@dreamtree-org/合并
替换复制到每个 Pro 产品的第三方集成代码
(单位:千美元)infrastructure/googleDriveClient.js 在任务pro, utils/googleDrive.js 内
冒险者, lib/zoho.js (......) 每包一个子路径
供养. 每个供应商的SDK是 可选同伴依赖,所以一个应用程序
只为其进口的供应商安装SDK。 例外的是
nodemailer,一个正则依赖: SMTP发送的作品没有额外的安装.
关于公司
@dreamtree-org/ 整合的建设和维护 梦幻之树 ——一个团队为现代网络设计了深思熟虑,开发者第一的软件. 我们相信,伟大的产品从一个伟大的基础开始, 而这正是这个一揽子计划的目标: 一个精致的,开放的,和可靠的整合层 对于各地的团队.
🌐 网站: www.dreamtreeglobal.com (中文(简体) )
作者维护者( V)
普雷塔姆·克里希纳 —— 说 梦幻之树 📧 [email protected] 互联网档案馆的存檔,存档日期2013-12-21
有想法,一个虫子, 还是只是想打个招呼? 随时伸出援手——像你这样的开发者的回馈是这个软件包的不断增长.
++ 赞助和支持
@dreamtree-org/整合为 自由和开放源码完全由激情和深夜驱动 您所依赖的每个功能——每个 OAuth 流量, 提供者客户端, 以及 Webhook 签名检查—— 代表了数小时的仔细操作, 以便 产品可以和Google,Zoho,Slack,邮件和Razorpay交谈,而无需重写管道.
如果此软件包节省了您的时间, 引发了一个想法, 或者省去了另一个手卷的 API 客户端, 请考虑支持它的持续开发 . 您的赞助让更新不断出现, 虫子被压倒, 和文件尖锐的—— 整个社区. 甚至最小的贡献也是巨大的鼓励
通过UPI进行一塔普捐赠
QQ 用任何 UPI 应用程序扫描 QR (Google Pay, PhonePe, Paytm, BHIM,.) 即时支付:
UPI身份证:
dhrugantha.llp@kotak二. 支助 收款人 : 梦幻之树QQ 在手机上,您也可以从下面的按钮上点击——它直接打开了您的UPI应用程序. 在桌面上, 扫描上面的 QR 或将 UPI ID 复制到您的支付应用程序中 .
成为赞助者
要长期支持项目 还是作为一个公司? 您可以直接赞助软件包 :
- 📦 无 :
@dreamtree-org/integrations- 明星、分享和赞助软件包 - ✉️ 公司赞助和伙伴关系: [email protected] 互联网档案馆的存檔,存档日期2013-12-21
每个明星 分享和贡献 都比你知道的多 谢谢你参与这次旅程!
特征
| 导入 | 什麽 | 需求 |
|---|---|---|
@dreamtree-org/integrations | IntegrationError, OAuth 2.0 客户端结束 fetch, OAuth 状态商店, 已封接的连接商店, 内存/ knex 适配器 | —— 说 |
@dreamtree-org/integrations/google | 谷歌 OAuth (英语:createGoogleAuth),驱动(createDriveClient)和日历+相会(createCalendarClient(中文(简体) ) | googleapis |
@dreamtree-org/integrations/zoho | 区域认识的Zoho OAuth(createZohoAuth),而"佐霍书社"v3(createZohoBooksClient(中文(简体) ) | —— 说 |
@dreamtree-org/integrations/mail | SMTP 发送( S)createSmtpMailer)和IMAP读取(createImapReader),包含主机,信头和密码守护器 | nodemailer· ; imapflow + 键 mailparser 用于 IMAP |
@dreamtree-org/integrations/slack | Slack OAuth v2 bot 安装 (createSlackAuth)和网络API(createSlackClient(中文(简体) ) | —— 说 |
@dreamtree-org/integrations/razorpay | Razorpay 付款、发票、订阅、合作伙伴OAuth、Webhooks和签名 | —— 说 |
@dreamtree-org/integrations/storage | 数字海洋空间或任何S3兼容商店的私人物体存储(createSpacesStorage),有字节范围和关键守卫 | @aws-sdk/client-s3 |
安装
npm install @dreamtree-org/integrations
# plus only the provider SDKs you import, e.g. googleapis or @aws-sdk/client-s3 (nodemailer is included)
设计规则
- 供货商的客户不储存任何东西. 他们把全权证书当作投入
(单位:千美元)
drive.forToken(refreshToken)从来没有读过数据库,所以应用程序 注册 / 测试双模式继续工作 . - 一个错误类型 . 每个客户都扔
IntegrationError带有code数字 :AUTH_REQUIRED, (中文(简体) ),FORBIDDEN, (中文(简体) ),NOT_FOUND, (中文(简体) ),RATE_LIMITED, (中文(简体) ),RETRYABLE, (中文(简体) ),NOT_CONFIGURED或者说UNKNOWN。检查它与IntegrationError.is(err)没有instanceof(每个子路径都是自己捆绑的). - 证书以租赁方式密封. 连通店收你的
@dreamtree-org/tenancy特勤服务tenancy.secrets或者说createSecretService()) (中文(简体) ). AAD 将环境、公司、行、供应商捆绑在一起 和字段,所以一个密封值复制到另一个公司或字段不会打开. - 儒取为懒. 通过配置作为函数; 应用靴子不包含
供应商的证书,以及
status()名称 缺少. - 无硬码主机 . 重定向URI通过每个呼叫,从 请求的来源 。 Google 只接受在 OAuth 客户端注册的 URI, 因此,该列表是主机可以完成登录的限度.
谷歌驱动器
const { createGoogleAuth, createDriveClient, BASE_SCOPES, DRIVE_FILE_SCOPE } = require('@dreamtree-org/integrations/google');
const auth = createGoogleAuth({
provider: 'google-drive',
scopes: [...BASE_SCOPES, DRIVE_FILE_SCOPE],
config: () => ({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
});
const drive = createDriveClient({ auth, appProperties: { app: 'taskpro' } });
// The redirect URI comes from the request, so one build serves every host
// (localhost, stage, prod). Behind a proxy, set Express `trust proxy`.
const redirectUri = `${req.protocol}://${req.get('host')}/api/integrations/google-drive/callback`;
auth.status(); // { ok, hint }
const url = auth.buildAuthUrl({ state, redirectUri }); // offline access, prompt=consent
const tokens = await auth.exchangeCode(code, { redirectUri }); // same URI; { refreshToken, idToken, scopes, ... }
const who = await auth.verifyIdToken(tokens.idToken); // { sub, email, name }
const d = drive.forToken(refreshToken); // a fresh OAuth client per operation
const leaf = await d.ensureFolder({ rootId, segments: ['PRO-4', 'BRIT-1'] });
const file = await d.upload({ name, parentId: leaf.id, mimeType, buffer });
const bytes = await d.download(file.id); // Buffer; downloadWithType() adds mimeType
await d.rename(file.id, 'new name');
await d.remove(file.id);
连接和 OAuth 状态
const { createConnectionStore, createStateStore, knexAdapters, createIntegrationTables } = require('@dreamtree-org/integrations');
await createIntegrationTables(knex); // integration_connections + integration_oauth_states
const adapters = knexAdapters(knex); // or knexAdapters(() => currentCompanyKnex())
const connections = createConnectionStore({ adapter: adapters.connections, secrets: tenancy.secrets });
const states = createStateStore({ adapter: adapters.states });
// connect
const state = await states.issue({ companyId, provider: 'google-drive', userId });
res.redirect(auth.buildAuthUrl({ state, redirectUri }));
// callback
const { companyId } = await states.consume(req.query.state, { provider: 'google-drive' }); // one-time, 10 min
const tokens = await auth.exchangeCode(req.query.code, { redirectUri });
await connections.save({
companyId, provider: 'google-drive',
account: await auth.verifyIdToken(tokens.idToken),
scopes: tokens.scopes,
credentials: { refreshToken: tokens.refreshToken }, // undefined keeps, null clears
});
// use
const conn = await connections.open({ companyId, provider: 'google-drive' });
try {
await drive.forToken(conn.credentials.refreshToken).upload({ ... });
} catch (err) {
await connections.markError({ companyId, provider: 'google-drive' }, err); // AUTH_REQUIRED → status "revoked"
throw err;
}
connections.get() 永远不返回证书,只返回
secrets: { refreshToken: { configured: true, keyId } }所以这是安全的
发送到客户端.
已经有自己的连接表的 Apps 可以保存它们并通过一个
其自己的适配器({ find, insert, update, remove }),或仅使用
供应商客户.
谷歌日历
const { createGoogleAuth, createCalendarClient, BASE_SCOPES, CALENDAR_APP_CREATED_SCOPE } = require('@dreamtree-org/integrations/google');
const auth = createGoogleAuth({ provider: 'google-calendar', scopes: [...BASE_SCOPES, CALENDAR_APP_CREATED_SCOPE], config });
const calendar = createCalendarClient({ auth });
const c = calendar.forToken(refreshToken);
const cal = (await c.getCalendar(storedCalendarId)) || (await c.createCalendar({ name: 'TaskPro' }));
const ev = await c.createEvent({ calendarId: cal.id, event: { summary, start, end, attendees }, withMeet: true });
ev.meetUrl; // the Meet link
await c.patchEvent({ calendarId: cal.id, eventId: ev.id, event: { summary: 'Moved' } }); // keeps the conference
await c.deleteEvent({ calendarId: cal.id, eventId: ev.id }); // 404/410 = already gone, fine
const busy = await c.listEvents({ timeMin, timeMax }); // recurring expanded, all pages
- 范围.
calendar.app.created是最小的特权: 应用程序创建 一个日历,管理它上的事件。 它不能阅读日历列表,所以 以存储的 ID 查看日历 。 使用calendar.events只有工作时 使用用户自己的日历. - 参见链接. 每一次带有相遇链接的事件都会得到新的随机请求ID, 因为再利用的id会让同一次会议讨论两个事件.
偷懒
const { createSlackAuth, createSlackClient } = require('@dreamtree-org/integrations/slack');
const auth = createSlackAuth({ config: () => ({ clientId, clientSecret }) }); // bot scopes: chat:write, channels:read, …
res.redirect(auth.buildAuthUrl({ state, redirectUri }));
const install = await auth.exchangeCode(code, { redirectUri }); // { botToken, teamId, teamName, botUserId, scopes }
const slack = createSlackClient().forToken(install.botToken);
const channels = await slack.listChannels(); // public + private the bot is in, all pages
const who = await slack.lookupUserByEmail('[email protected]'); // null when not a member
await slack.postMessage({ channel, text: who ? `<@${who.id}> done` : 'Ann: done', blocks });
- 只有瓶装的. 只归还了机器人标志 安装用户的 代号被丢掉,所以没有像一个人.
- 错误. 黑答 HTTP 200 用
{ ok: false }对于大多数失败, 所以客户端检查ok失败投出IntegrationError与斯拉克的 自有字符串err.slackError数字 :token_revoked编号 :invalid_auth→AUTH_REQUIREDmissing_scope→FORBIDDENchannel_not_found→NOT_FOUNDratelimited→RATE_LIMITED,与err.retryAfterMs
- 复问. 电话不是默认重审的,因为呼叫者通常
从工作重试。 过
retries: 2等待一个短Retry-After改为内线.
佐霍书社
const { createZohoAuth, createZohoBooksClient } = require('@dreamtree-org/integrations/zoho');
const auth = createZohoAuth({ config: () => ({ clientId, clientSecret }) }); // scope ZohoBooks.fullaccess.all
const books = createZohoBooksClient({ auth });
// connect: redirect to the company's data center
res.redirect(auth.buildAuthUrl({ state, redirectUri, dataCenter: 'in' }));
// callback: Zoho appends ?code&accounts-server — the server is validated against
// the known Zoho hosts before the client secret is sent anywhere.
const t = await auth.exchangeCode(req.query.code, { redirectUri, accountsServer: req.query['accounts-server'] });
// store t.refreshToken (sealed), t.accountsServer, t.apiDomain
// use
const b = books.forConnection({
refreshToken, accountsServer, apiDomain, organizationId,
accessToken, expiresAt, // optional: a stored, still-valid token
onTokens: (fresh) => saveAccessToken(fresh), // Zoho caps token minting — persist each one
});
const orgs = await b.organizations();
const { contact } = await b.post('contacts', { contact_name: 'Acme' });
const items = await b.listAll('items'); // follows page_context.has_more_page
- 访问令牌在到期前被缓存并刷新了60 s. 401部队1 刷新一次再试一次 并联通话共享一次刷新.
- 429(与
Retry-After), Books代码为43, 5xx和连接重审. 每个请求都有30分超时. - 错误是
IntegrationError,带有 Books 自己的代码err.zohoCode。 。 。 .invalid_code, (中文(简体) ),invalid_grant和第二个401 ×AUTH_REQUIRED· ; 代码 57 –FORBIDDEN; 43 / 429 +RATE_LIMITED。 。 。 .
邮件: SMTP 和 IMAP
const { createSmtpMailer, createImapReader, smtpAccountFromSettings } = require('@dreamtree-org/integrations/mail');
const mailer = createSmtpMailer();
const account = smtpAccountFromSettings(resolvedSettings); // tenancy's smtp.* family → { host, port, username, password, from }
await mailer.verify(account); // logs in, sends nothing
const r = await mailer.send(account, { from: account.from, to: '[email protected]', subject: 'Hi', text: '…', html: '…' });
// r.ok, r.errorCode ('AUTH_FAILED' | 'RECIPIENT_REFUSED' | …), r.code (IntegrationError code), r.response (scrubbed)
const reader = createImapReader();
const res = await reader.fetch(
{ host: 'imap.gmail.com', port: 993, username, password, allowedHosts: ['imap.gmail.com'] },
{ sinceUid: mailbox.lastUid, uidValidity: mailbox.uidValidity, maxMessages: 50 },
);
// res.messages (parsed), res.highestUid (store it), res.more, res.errorCode === 'UIDVALIDITY_CHANGED' → reset the mark
- 永不掷出. 邮件呼叫返回结果
{ ok, errorCode, code, message }· ;sendBatch因此,一个死继电器,邮箱或拒绝 收件人不能中止工人勾选 . - 宿卫. IP 字节、单一标签名称和非邮件端口总是
拒绝。 过
allowedHosts缩小范围. - TLS政策,没有逃生舱门. Port 465 从一开始就使用 TLS; 所有其他 端口必须使用STARTLS升级,所以密码从不以清文本行走. 验证证书, TLS 1.2 是最小值 .
- 秘诀. 服务器响应被擦除密码: 按键, 无 空格,和 AUTH LOGIN 和 AUTH PLAIN 所使用的基数64。 平流日志为 关闭, 因为 IMAP 对话包含 LOGIN 密码 .
- 入入道场. 检查每个地址、主题和标题的CR/LF和 发送前折叠.
- IMAP 边界 . 阅读受计数,大小和期限的限制. 一个信息
太大了,或不会解析回 作为其信封
oversized: true, (中文(简体) ), 所以水印还在进步.AUTH_FAILED财务报告和审定财务报表APP_PASSWORD_REQUIRED为: 单独的代码,因为谷歌报告 既是AUTHENTICATIONFAILED 和他们 需要不同的修正.
剃刀纸
const {
createRazorpayAuth,
createRazorpayClient,
verifyPaymentSignature,
verifyWebhookSignature,
} = require('@dreamtree-org/integrations/razorpay');
// 1. Direct Merchant Key Auth (standard)
const rzp = createRazorpayClient().forCredentials({
keyId: process.env.RAZORPAY_KEY_ID,
keySecret: process.env.RAZORPAY_KEY_SECRET,
});
// Create Order for checkout
const order = await rzp.orders.create({
amount: 49900, // in paise (₹499.00)
currency: 'INR',
receipt: 'rcpt_order_123',
notes: { companyId: 'cmp_1' },
});
// Capture or Refund Payment
await rzp.payments.capture(paymentId, 49900, 'INR');
await rzp.payments.refund(paymentId, { amount: 10000 });
// Verify Checkout Payment Signature
const isValid = verifyPaymentSignature(
{ orderId: req.body.razorpay_order_id, paymentId: req.body.razorpay_payment_id, signature: req.body.razorpay_signature },
process.env.RAZORPAY_KEY_SECRET
);
// Verify Webhook Signature (raw body string or Buffer)
const isWebhookValid = verifyWebhookSignature(req.rawBody, req.headers['x-razorpay-signature'], process.env.RAZORPAY_WEBHOOK_SECRET);
// 2. Partner OAuth Flow (connect merchant accounts)
const auth = createRazorpayAuth({
config: () => ({ clientId: process.env.RAZORPAY_CLIENT_ID, clientSecret: process.env.RAZORPAY_CLIENT_SECRET }),
});
const url = auth.buildAuthUrl({ state, redirectUri });
const tokens = await auth.exchangeCode(code, { redirectUri }); // { accessToken, refreshToken, razorpayAccountId, ... }
// Use Partner Access Token
const partnerClient = createRazorpayClient().forToken(tokens.accessToken);
const merchantOrder = await partnerClient.orders.create({ amount: 10000, currency: 'INR' });
- 双重认证. 支持标准密钥 ID & 密钥 (HTTP Basic Auth) 和 Partner OAuth Bearer 令牌.
- 资源覆盖. 提供专用手柄用于
orders, (中文(简体) ),payments, (中文(简体) ),paymentLinks, (中文(简体) ),invoices, (中文(简体) ),plans, (中文(简体) ),subscriptions, (中文(简体) ),customers,以及refunds。 。 。 . - 时常签名. Webhooks和取出签名通过时间安全比较检查,以防止时间攻击.
- 自动重试 . 429个费率限额(荣誉)
Retry-After),网络出错,而上游5xx进行回转.
存储:数字海洋空间
文件的私人对象存储程序只有在它授权查看器后才服务 (上载,广告创意,文档). 与任何S3兼容的商店(Spaces, (原始内容存档于2018-09-29). Minio, R2.
const { createSpacesStorage, spacesConfigFromEnv } = require('@dreamtree-org/integrations/storage');
const store = createSpacesStorage(spacesConfigFromEnv(process.env));
await store.put('creatives/42/ad.mp4', bytes, { contentType: 'video/mp4' });
const size = await store.size('creatives/42/ad.mp4');
const body = await store.stream('creatives/42/ad.mp4', { start: 0, end: 99 }); // a Readable; pipe it to res
await store.remove('creatives/42/ad.mp4');
| 变量 | 含义 |
|---|---|
SPACES_BUCKET, (中文(简体) ), SPACES_KEY, (中文(简体) ), SPACES_SECRET | 要求。 最好只用这桶钥匙. |
SPACES_REGION | 默认 blr1。 。 。 . |
SPACES_ENDPOINT | 默认 https://<region>.digitaloceanspaces.com。 。 。 . |
SPACES_FOLDER | 每个密钥的前缀,所以应用程序和环境可以共享一桶(例如. myapp-stage) (中文(简体) ). |
SPACES_FORCE_PATH_STYLE | 1 给米尼欧的. |
- 每个对象都用
ACL: 'private';没有公开的选择. 提供文件 在授权检查后通过您的应用程序. - 键是相对路径. 空密钥, 线索
/,或一个., (中文(简体) ),..或空块 扔出数(status: 400在任何请求之前. - 丢失对象拒绝
IntegrationErrorNOT_FOUND(单位:千美元)status: 404页:1 失败被归类为每个提供者(AUTH_REQUIRED, (中文(简体) ),FORBIDDEN, (中文(简体) ),RETRYABLE(......) (中文(简体) ). @aws-sdk/client-s3是一个可选的对等点, 被装入到第一次使用中。 过client(任何东西) 与send(command)在测试中注入假药.
无 SDK 供应商的操作权限
const { createOAuthClient } = require('@dreamtree-org/integrations');
const zoho = createOAuthClient({
provider: 'zoho-books',
config: () => ({
authorizeUrl: 'https://accounts.zoho.in/oauth/v2/auth',
tokenUrl: 'https://accounts.zoho.in/oauth/v2/token',
clientId, clientSecret, redirectUri,
scopes: ['ZohoBooks.fullaccess.all'], scopeSeparator: ',',
authorizeParams: { access_type: 'offline', prompt: 'consent' },
}),
});
const tokens = await zoho.exchangeCode(code); // tokens.raw.api_domain kept
const fresh = await zoho.refresh(refreshToken); // invalid_grant → AUTH_REQUIRED
发展
npm install
npm test # node --test
npm run typecheck
npm run build # dist/{index,google,zoho,mail,slack,razorpay}.{js,cjs}
捐款
- 叉子存储器
- 创建您的特性分支( E)
git checkout -b feature/amazing-feature(中文(简体) ) - 提交更改( E)
git commit -m 'Add some amazing feature'(中文(简体) ) - 推向分行(
git push origin feature/amazing-feature(中文(简体) ) - 打开拉请求
许可证
这个项目根据麻省理工学院许可证颁发——见 自由主义 详细文件.
支助
由 Q 制作 普雷塔姆·克里希纳 下级 梦幻之树 • 妇女 www.dreamtreeglobal.com (中文(简体) )