@dreamtree-org/integrations

中文文档
0.6.5发布于 2026/11/13MITTypeScriptESM + CJS近 30 天下载 2845

Shared third-party integrations for every Dreamtree product — OAuth, sealed per-company connections, Google Drive and Calendar, Zoho Books, Slack, SMTP and IMAP, and private S3/Spaces object storage.

原始使用文档
文档目录21 节
  1. About the Company
  2. Author & Maintainer
  3. 💚 Sponsor & Support
  4. ☕ Make a one-tap donation via UPI
  5. 🤝 Become a Sponsor
  6. Features
  7. Installation
  8. Design rules
  9. Google Drive
  10. Connections and OAuth state
  11. Google Calendar
  12. Slack
  13. Zoho Books
  14. Mail: SMTP and IMAP
  15. Razorpay
  16. Storage: DigitalOcean Spaces
  17. OAuth for providers without an SDK
  18. Development
  19. Contributing
  20. License
  21. Support

@dreamtree-org/integrations

Replaces the third-party integration code that was copied into each Pro product (infrastructure/googleDriveClient.js in taskpro, utils/googleDrive.js in venturepro, lib/zoho.js in procurementpro, …). One package, one subpath per provider. Each provider's SDK is an optional peer dependency, so an app installs only the SDKs for the providers it imports. The exception is nodemailer, a regular dependency: SMTP sending works with no extra install.

npm version License: MIT Website

About the Company

@dreamtree-org/integrations is built and maintained under Dreamtree-Org — a team crafting thoughtful, developer-first software for the modern web. We believe great products start with a great foundation, and that's exactly what this package aims to be: a polished, open, and dependable integration layer for teams everywhere.

🌐 Website: www.dreamtreeglobal.com

Author & Maintainer

Partha Preetham Krishna — Dreamtree-Org 📧 [email protected]

Have an idea, a bug, or just want to say hi? Reach out anytime — feedback from developers like you is what keeps this package growing.

💚 Sponsor & Support

@dreamtree-org/integrations is free and open source, built in the open and powered entirely by passion and late nights. Every feature you rely on — every OAuth flow, provider client, and webhook signature check — represents hours of careful craft so that your product can talk to Google, Zoho, Slack, mail and Razorpay without re-writing the plumbing.

If this package has saved you time, sparked an idea, or spared you another hand-rolled API client, please consider supporting its continued development. Your sponsorship keeps the updates coming, the bugs squashed, and the docs sharp — for the whole community. Even the smallest contribution is a huge encouragement. 🙏

☕ Make a one-tap donation via UPI

📷 Scan the QR with any UPI app (Google Pay, PhonePe, Paytm, BHIM, …) to pay instantly:

Scan to pay Dreamtree-Org via UPI

UPI ID: dhrugantha.llp@kotak  •  Payee: Dreamtree-Org

📱 On mobile, you can also tap the button below — it opens your UPI app directly. On desktop, scan the QR above or copy the UPI ID into your payment app.

Pay via UPI

🤝 Become a Sponsor

Want to back the project long-term or as a company? You can sponsor the package directly:

Every star ⭐, share, and contribution helps more than you know. Thank you for being part of the journey!

Features

ImportWhatNeeds
@dreamtree-org/integrationsIntegrationError, OAuth 2.0 client over fetch, OAuth state store, sealed connection store, memory/knex adapters—
@dreamtree-org/integrations/googleGoogle OAuth (createGoogleAuth), Drive (createDriveClient) and Calendar + Meet (createCalendarClient)googleapis
@dreamtree-org/integrations/zohoZoho OAuth, region-aware (createZohoAuth), and Zoho Books v3 (createZohoBooksClient)—
@dreamtree-org/integrations/mailSMTP sending (createSmtpMailer) and IMAP reading (createImapReader), with host, header and password guardsnodemailer; imapflow + mailparser for IMAP
@dreamtree-org/integrations/slackSlack OAuth v2 bot install (createSlackAuth) and Web API (createSlackClient)—
@dreamtree-org/integrations/razorpayRazorpay Payments, Invoicing, Subscriptions, Partner OAuth, Webhooks & Signatures—
@dreamtree-org/integrations/storagePrivate object storage on DigitalOcean Spaces or any S3-compatible store (createSpacesStorage), with byte ranges and key guards@aws-sdk/client-s3

Installation

npm install @dreamtree-org/integrations
# plus only the provider SDKs you import, e.g. googleapis or @aws-sdk/client-s3 (nodemailer is included)

Design rules

  • Provider clients don't store anything. They take credentials as input (drive.forToken(refreshToken)) and never read a database, so the app's registry / test-double pattern keeps working.
  • One error type. Every client throws IntegrationError with a code: AUTH_REQUIRED, FORBIDDEN, NOT_FOUND, RATE_LIMITED, RETRYABLE, NOT_CONFIGURED or UNKNOWN. Check it with IntegrationError.is(err), not instanceof (each subpath is bundled on its own).
  • Credentials are sealed with tenancy. The connection store takes your @dreamtree-org/tenancy secret service (tenancy.secrets or createSecretService()). The AAD binds environment, company, row, provider and field, so a sealed value copied to another company or field won't open.
  • Config is lazy. Pass config as a function; the app boots without the provider's credentials, and status() names what is missing.
  • No hard-coded host. Redirect URIs are passed per call, built from the request's origin. Google only accepts URIs registered on the OAuth client, so that list is what limits which hosts can complete a sign-in.

Google Drive

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);

Connections and OAuth state

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() never returns credentials, only secrets: { refreshToken: { configured: true, keyId } }, so it is safe to send to a client.

Apps that already have their own connection tables can keep them and pass an adapter of their own ({ find, insert, update, remove }), or use only the provider clients.

Google Calendar

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
  • Scopes. calendar.app.created is the least privilege: the app creates one calendar and manages events on it. It can't read the calendar list, so look the calendar up by its stored id. Use calendar.events only to work with the user's own calendars.
  • Meet links. Each event with a Meet link gets a fresh random request id, because a reused id would put the same meeting on two events.

Slack

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 });
  • Bot token only. Only the bot token is returned. The installing user's token is dropped, so nothing acts as a person.
  • Errors. Slack answers HTTP 200 with { ok: false } for most failures, so the client checks ok. Failures throw IntegrationError with Slack's own string on err.slackError:
    • token_revoked / invalid_auth → AUTH_REQUIRED
    • missing_scope → FORBIDDEN
    • channel_not_found → NOT_FOUND
    • ratelimited → RATE_LIMITED, with err.retryAfterMs
  • Retries. Calls are not retried by default, because callers usually retry from a job. Pass retries: 2 to wait out a short Retry-After inline instead.

Zoho Books

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
  • The access token is cached and refreshed 60 s before expiry. A 401 forces one refresh and one retry. Concurrent calls share one refresh.
  • 429 (with Retry-After), Books code 43, 5xx and connection resets are retried. Every request has a 30 s timeout.
  • Errors are IntegrationError, with Books' own code on err.zohoCode. invalid_code, invalid_grant and a second 401 → AUTH_REQUIRED; code 57 → FORBIDDEN; 43 / 429 → RATE_LIMITED.

Mail: SMTP and 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
  • Never throws. Mail calls return outcomes ({ ok, errorCode, code, message }; sendBatch gives one per message), so one dead relay, mailbox or refused recipient cannot abort a worker tick.
  • Host guard. IP literals, single-label names and non-mail ports are always refused. Pass allowedHosts to narrow further.
  • TLS policy, no escape hatch. Port 465 uses TLS from the start; every other port must upgrade with STARTTLS, so the password never travels in cleartext. Certificates are verified, and TLS 1.2 is the minimum.
  • Secrets. Server responses are scrubbed of the password: as typed, without spaces, and in base64 as used by AUTH LOGIN and AUTH PLAIN. imapflow logging is off, because the IMAP conversation contains the LOGIN password.
  • Injection. Every address, subject and header is checked for CR/LF and folding before sending.
  • IMAP bounds. Reads are limited by count, size and a deadline. A message that is too large or will not parse comes back as its envelope (oversized: true), so the watermark still advances. AUTH_FAILED and APP_PASSWORD_REQUIRED are separate codes, because Google reports both as AUTHENTICATIONFAILED and they need different fixes.

Razorpay

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' });
  • Dual authentication. Supports standard Key ID & Key Secret (HTTP Basic Auth) and Partner OAuth Bearer tokens.
  • Resource coverage. Provides dedicated handles for orders, payments, paymentLinks, invoices, plans, subscriptions, customers, and refunds.
  • Constant-time signatures. Webhooks and checkout signatures are checked with timing-safe comparisons to prevent timing attacks.
  • Automatic retries. 429 rate limits (honoring Retry-After), network errors, and upstream 5xx are retried with backoff.

Storage: DigitalOcean Spaces

Private object storage for files an app serves only after it authorises the viewer (uploads, ad creatives, documents). Works with any S3-compatible store (Spaces, 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');
VariableMeaning
SPACES_BUCKET, SPACES_KEY, SPACES_SECRETRequired. Prefer a key limited to this one bucket.
SPACES_REGIONDefault blr1.
SPACES_ENDPOINTDefault https://<region>.digitaloceanspaces.com.
SPACES_FOLDERPrefix for every key, so apps and environments can share one bucket (e.g. myapp-stage).
SPACES_FORCE_PATH_STYLE1 for MinIO.
  • Every object is written with ACL: 'private'; there is no public option. Serve files through your app after an authorisation check.
  • Keys are relative paths. An empty key, a leading /, or a ., .. or empty segment throws (status: 400) before any request.
  • A missing object rejects with IntegrationError NOT_FOUND (status: 404); other failures are classified like every provider (AUTH_REQUIRED, FORBIDDEN, RETRYABLE…).
  • @aws-sdk/client-s3 is an optional peer, loaded on first use. Pass client (anything with send(command)) to inject a fake in tests.

OAuth for providers without an 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

Development

npm install
npm test          # node --test
npm run typecheck
npm run build     # dist/{index,google,zoho,mail,slack,razorpay}.{js,cjs}

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support


Made with ❤️ by Partha Preetham Krishna under Dreamtree-Org · www.dreamtreeglobal.com

版本与发布记录

@dreamtree-org/integrations 共发布 17 个版本,首次发布于 2026/10/21;过去 12 个月发布了 17 个版本。

版本发布日期距今
0.6.5latest2026/11/13今天
0.6.62026/11/13今天
0.6.72026/11/13今天
0.6.82026/11/13今天
0.6.02026/11/085 天前
0.5.52026/11/0310 天前

其他发布通道:stage → 0.5.0。

关于 @dreamtree-org/integrations 的常见问题

@dreamtree-org/integrations 是什么?

Shared third-party integrations for every Dreamtree product — OAuth, sealed per-company connections, Google Drive and Calendar, Zoho Books, Slack, SMTP and IMAP, and private S3/Spaces object storage.

如何安装 @dreamtree-org/integrations?

在项目目录运行 npm install @dreamtree-org/integrations,或使用 pnpm add / yarn add / bun add。

@dreamtree-org/integrations 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@dreamtree-org/integrations 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@dreamtree-org/integrations 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18.17。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@dreamtree-org/integrations 可以商用吗?

@dreamtree-org/integrations 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@dreamtree-org/integrations 的下载量有多少?

根据 npm 官方下载统计,@dreamtree-org/integrations 在 2026/10/13 – 2026/11/11 这 30 天内被下载 2,845 次,日均约 95 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@dreamtree-org/integrations 还在维护吗?

最新版本 0.6.5 发布于 2026/11/13(今天);至今共发布 17 个版本,其中过去 12 个月发布了 17 个;首次发布于 2026/10/21。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。