npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dreamtree-org/integrations

v0.4.1

Published

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

Readme

@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.

| Import | What | Needs | |---|---|---| | @dreamtree-org/integrations | IntegrationError, OAuth 2.0 client over fetch, OAuth state store, sealed connection store, memory/knex adapters | — | | @dreamtree-org/integrations/google | Google OAuth (createGoogleAuth), Drive (createDriveClient) and Calendar + Meet (createCalendarClient) | googleapis | | @dreamtree-org/integrations/zoho | Zoho OAuth, region-aware (createZohoAuth), and Zoho Books v3 (createZohoBooksClient) | — | | @dreamtree-org/integrations/mail | SMTP sending (createSmtpMailer) and IMAP reading (createImapReader), with host, header and password guards | nodemailer; imapflow + mailparser for IMAP | | @dreamtree-org/integrations/slack | Slack OAuth v2 bot install (createSlackAuth) and Web API (createSlackClient) | — |

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.

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}.{js,cjs}