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

@alter-ai/alter-sdk

v0.11.0

Published

Official TypeScript SDK for Alter Vault — OAuth token management with policy enforcement

Readme

Alter SDK for TypeScript / Node.js

Official TypeScript SDK for Alter Vault — Credential management for agents with policy enforcement.

Runtime: Node.js 20+. The SDK uses Node-only APIs (node:crypto, dynamic import of the optional open peer dependency, process.env, server-side HTTP) and is not supported in browsers, Deno, Bun, or Cloudflare Workers without a Node compatibility layer. Browser-side OAuth should use the Alter Connect widget instead.

Features

  • Zero Token Exposure: Tokens are never exposed to developers — injected automatically
  • Single Entry Point: One method (vault.request()) for all provider APIs
  • Type-Safe Enums: HttpMethod enums with autocomplete
  • URL Templating: Path parameter substitution with automatic URL encoding
  • Automatic Audit Logging: All API calls logged with request metadata (HTTP method and URL) for full audit trail
  • Real-time Policy Enforcement: Every token request checked against current policies
  • Automatic Token Refresh: Tokens refreshed transparently by the backend
  • API Key and Custom Credential Support: Handles OAuth tokens, API keys, and custom credential formats automatically
  • Caller Tracking: First-class support for AI agent and backend service observability
  • Signed Requests: All SDK-to-backend requests are cryptographically signed for integrity, authenticity, and replay protection
  • Native Promises: Built on native fetch — no heavy dependencies

Installation

npm install @alter-ai/alter-sdk

Quick Start

import { AlterVault, HttpMethod } from "@alter-ai/alter-sdk";

const vault = new AlterVault({
  apiKey: "alter_key_...",
  caller: "my-agent",
});

// Make API request — token injected automatically, never exposed
const response = await vault.request(
  HttpMethod.GET,
  "https://api.example.com/v1/resource",
  {
    grantId: "GRANT_ID",  // from Alter Connect (see below)
    queryParams: { maxResults: "10" },
  },
);

const data = await response.json();
console.log(data);

await vault.close();

Where does grantId come from?

OAuth grants (per-user, from end user action):

  1. Your end user completes OAuth via Alter Connect (frontend widget) or vault.connect() (headless)
  2. The onSuccess callback returns a grantId (UUID) — one per user per account
  3. You save it in your database, mapped to your user
  4. You pass it to vault.request() when making API calls

Managed secrets (per-service, from developer action):

  1. You store credentials in the Developer Portal under Managed Secrets
  2. The portal returns a grantId — one per stored credential, shared across your backend
  3. Use the same vault.request() — credentials are injected automatically
// You can also discover grantIds programmatically:
const result = await vault.listGrants({ providerId: "google" });
for (const grant of result.grants) {
  console.log(`${grant.grantId}: ${grant.accountDisplayName}`);
}

Usage

The request() method returns the raw Response object. The token is injected automatically and never exposed. The backend token response includes grantId and providerId for audit correlation.

Simple GET Request

const response = await vault.request(
  HttpMethod.GET,
  "https://api.example.com/v1/resource",
  { grantId },
);

POST with JSON Body

const response = await vault.request(
  HttpMethod.POST,
  "https://api.example.com/v1/items",
  {
    grantId,
    json: { name: "New Item", price: 99.99 },
    reason: "Creating new item",
  },
);

URL Path Templating

const response = await vault.request(
  HttpMethod.PUT,
  "https://api.example.com/v1/items/{item_id}",
  {
    grantId,
    pathParams: { item_id: "123" },
    json: { price: 89.99 },
  },
);

Query Parameters and Extra Headers

const response = await vault.request(
  HttpMethod.POST,
  "https://api.example.com/v1/databases/{db_id}/query",
  {
    grantId,
    pathParams: { db_id: "abc123" },
    extraHeaders: { "X-API-Version": "2022-06-28" },
    json: { page_size: 10 },
  },
);

Using Managed Secrets

For your own APIs with API keys or service tokens (no OAuth flow needed):

const vault = new AlterVault({
  apiKey: "alter_key_...",
  caller: "my-service",
});

const response = await vault.request(
  HttpMethod.GET,
  "https://api.example.com/v1/data",
  { grantId: "MANAGED_SECRET_GRANT_ID" },  // from Developer Portal
);

await vault.close();

The credential is injected automatically as the configured header type (Bearer, API Key, Basic Auth).

Grant Management

List Grants

Retrieve OAuth grants for your app, optionally filtered by provider:

const result = await vault.listGrants();
for (const grant of result.grants) {
  console.log(`${grant.providerId}: ${grant.accountDisplayName} (${grant.status})`);
}

// Filter by provider with pagination
const filteredGrants = await vault.listGrants({
  providerId: "google",
  limit: 10,
  offset: 0,
});
console.log(`Total: ${filteredGrants.total}, Has more: ${filteredGrants.hasMore}`);

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | providerId | string | - | Filter by provider (e.g., "google") | | limit | number | 100 | Max grants to return | | offset | number | 0 | Pagination offset |

Returns GrantListResult with: grants (GrantInfo[]), total, limit, offset, hasMore.

Each GrantInfo has:

| Field | Type | Description | |-------|------|-------------| | grantId | string | Unique grant identifier (UUID). Not .id — always use .grantId | | providerId | string | Provider slug (e.g., "google", "slack") | | scopes | string[] | Granted OAuth scopes | | accountIdentifier | string \| null | Provider account email or username | | accountDisplayName | string \| null | Human-readable account name | | status | string | Grant status (e.g., "active") | | scopeMismatch | boolean | true if granted scopes don't match requested scopes | | expiresAt | string \| null | Expiry timestamp (if a grant policy TTL was set) | | createdAt | string | When the grant was created | | lastUsedAt | string \| null | When the grant was last used for an API call |

Create Connect Session

Generate a session URL for end-users to authenticate with OAuth providers:

const session = await vault.createConnectSession({
  allowedProviders: ["google", "github"],
  returnUrl: "https://myapp.com/callback",
});
console.log(`Connect URL: ${session.connectUrl}`);
console.log(`Expires in: ${session.expiresIn}s`);

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | allowedProviders | string[] | - | Restrict to specific providers | | returnUrl | string | - | Redirect URL after OAuth flow |

Returns ConnectSession with: sessionToken, connectUrl, expiresIn, expiresAt.

Headless Connect (from code)

For CLI tools, scripts, and server-side applications -- opens the browser, waits for the user to complete OAuth, and returns the result:

const results = await vault.connect({
  providers: ["google"],
  grantPolicy: {  // optional TTL bounds
    maxTtlSeconds: 86400,
    defaultTtlSeconds: 3600,
  },
  timeout: 300,        // max wait in seconds (default: 5 min)
  openBrowser: true,   // set false to print URL instead
});
for (const result of results) {
  console.log(`Connected: ${result.grantId} (${result.providerId})`);
  console.log(`Account: ${result.accountIdentifier}`);
}

// Now use the grantId with vault.request()
const response = await vault.request(
  HttpMethod.GET,
  "https://api.example.com/v1/resource",
  { grantId: results[0].grantId },
);

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | providers | string[] | - | Restrict to specific providers | | timeout | number | 300 | Max seconds to wait for completion | | pollInterval | number | 2 | Seconds between status checks | | grantPolicy | { maxTtlSeconds?: number; defaultTtlSeconds?: number } | - | TTL bounds (optional) | | openBrowser | boolean | true | Open browser automatically |

Returns ConnectResult[] — one per connected provider. Each has: grantId, providerId, accountIdentifier, scopes, and optionally grantPolicy (if a TTL was set).

Throws ConnectTimeoutError if the user doesn't complete in time, ConnectDeniedError if the user denies authorization, ConnectConfigError if the OAuth app is misconfigured.

Caller and Context Tracking

The caller identifies the SDK instance (set once in the constructor). The context provides per-request metadata for audit attribution:

import { AlterVault, HttpMethod } from "@alter-ai/alter-sdk";

const vault = new AlterVault({
  apiKey: "alter_key_...",
  caller: "email-assistant-v2",
});

const response = await vault.request(
  HttpMethod.GET,
  "https://api.example.com/v1/resource",
  {
    grantId,
    context: { tool: "read_calendar", agent: "my-agent" },
  },
);

The context is a free-form key-value map that appears in audit logs. Use it to track which tool, agent, or framework triggered the request.

The SDK validates the context before sending — keys and values must be strings, the object must have at most 20 keys, no key longer than 64 chars, no value longer than 512 chars, and the JSON-encoded payload must fit in 4 KB. Violations throw AlterValueError so a malformed context never silently disappears from the audit trail.

Multi-Agent Deployments

Each agent must create its own AlterVault instance with a unique caller identity. Do not share a single instance across agents.

// Each agent gets its own vault instance
const emailAgent = new AlterVault({
  apiKey: "alter_key_...",
  caller: "email-assistant-v2",
});

const calendarAgent = new AlterVault({
  apiKey: "alter_key_...",
  caller: "calendar-agent-v1",
});

// Audit logs and policies are tracked per caller
await emailAgent.request(
  HttpMethod.GET,
  "https://api.example.com/v1/messages",
  { grantId: emailGrantId },
);
await calendarAgent.request(
  HttpMethod.GET,
  "https://api.example.com/v1/events",
  { grantId: calendarGrantId },
);

// Clean up each instance
await emailAgent.close();
await calendarAgent.close();

Configuration

import { AlterVault, CallerType } from "@alter-ai/alter-sdk";

const vault = new AlterVault({
  apiKey: "alter_key_...",              // Required: Alter Vault API key
  caller: "my-agent",                  // Optional: Caller identifier for audit attribution
  callerType: CallerType.AGENT,        // Optional: "agent" (default, Agents tab) or "service" (hidden)
  timeout: 30000,                       // Optional: HTTP timeout in ms (default: 30000)
  userTokenGetter: () => getUserJwt(),  // Optional: JWT getter for identity resolution
});

End-User Authentication

For identity-based grant resolution, authenticate end users via the configured IDP:

// Trigger browser-based IDP login
const auth = await vault.authenticate({ timeout: 300_000 });

console.log(auth.userInfo); // { sub: "user-123", email: "[email protected]", ... }

// Subsequent requests automatically use identity resolution
const response = await vault.request(
  HttpMethod.GET,
  "https://api.example.com/v1/resource",
  { provider: "google" },  // no grantId — resolved via user identity
);

The authenticate() method opens the IDP login page in the user's browser, polls for completion, and returns an AuthResult with the user's token and profile info. After authentication, request() calls with provider automatically resolve to the correct grant for that user.

Error Handling

The SDK provides a typed exception hierarchy so you can handle each failure mode precisely:

AlterSDKError (base)
├── AlterValueError                  // SDK rejected your input — fix your code
├── BackendError                     // Generic backend error
│   ├── ReAuthRequiredError          // User must re-authorize via Alter Connect
│   │   ├── GrantExpiredError        // 403 — grant TTL elapsed
│   │   ├── CredentialRevokedError   // 400 — underlying auth permanently broken (revoked, invalid_grant)
│   │   ├── GrantRevokedError        // 400 — grant revoked
│   │   └── GrantDeletedError        // 410 — user disconnected via Wallet (new ID on re-auth)
│   ├── GrantNotFoundError           // 404 — wrong grant_id
│   ├── PolicyViolationError         // 403 — policy denied (business hours, IP, etc.)
│   └── TokenRefreshInProgressError  // 409 — another request is refreshing the token; retry after backoff
├── ConnectFlowError                 // Headless connect() failed
│   ├── ConnectDeniedError           // User clicked Deny
│   ├── ConnectConfigError           // OAuth app misconfigured
│   └── ConnectTimeoutError          // User didn't complete in time
├── ProviderAPIError                 // Provider returned 4xx/5xx
│   └── ScopeReauthRequiredError    // 403 + scope mismatch — user must re-authorize
└── NetworkError                     // Backend or provider unreachable
    └── TimeoutError                 // Request timed out (safe to retry)

AlterValueError is thrown for input validation failures the developer must fix in their own code (e.g., a malformed context object). It signals a programming bug, not a runtime/network/backend failure. Other input errors (invalid apiKey, invalid URL scheme, missing pathParams) still throw AlterSDKError.

import {
  AlterVault,
  HttpMethod,
  AlterSDKError,              // Base error
  AlterValueError,            // SDK rejected input — fix your code
  BackendError,               // Generic backend error
  ReAuthRequiredError,        // Parent for all re-auth errors
  GrantDeletedError,          // User disconnected via Wallet — new ID on re-auth (410)
  GrantExpiredError,          // TTL expired — user must re-authorize (403)
  GrantNotFoundError,         // Wrong grant_id — check for typos (404)
  CredentialRevokedError,     // Underlying auth permanently broken — revoked/invalid_grant (400)
  GrantRevokedError,          // Grant revoked (400)
  PolicyViolationError,       // Policy denied — business hours, IP, etc. (403)
  TokenRefreshInProgressError,  // Another request is refreshing the token; retry after backoff (409)
  ConnectFlowError,           // Headless connect() failed (denied, provider error)
  ConnectDeniedError,         // User denied authorization
  ConnectConfigError,         // OAuth app misconfigured
  ConnectTimeoutError,        // Headless connect() timed out
  NetworkError,               // Backend or provider unreachable
  TimeoutError,               // Request timed out (subclass of NetworkError)
  ProviderAPIError,           // Provider API returned error (4xx/5xx)
  ScopeReauthRequiredError,   // 403 + scope mismatch (subclass of ProviderAPIError)
} from "@alter-ai/alter-sdk";

try {
  const response = await vault.request(
    HttpMethod.GET,
    "https://api.example.com/v1/resource",
    { grantId },
  );
} catch (error) {
  // --- Grant unusable — user must re-authorize via Alter Connect ---
  if (error instanceof GrantExpiredError) {
    // TTL set during Connect flow has elapsed
    console.error("Grant expired — prompt user to re-authorize");
  } else if (error instanceof CredentialRevokedError) {
    // Underlying auth permanently broken — user revoked at provider, refresh token
    // expired, or token refresh permanently failed (invalid_grant)
    console.error("Connection revoked — prompt user to re-authorize");
  } else if (error instanceof GrantRevokedError) {
    // Grant itself was revoked
    console.error("Grant revoked — prompt user to re-authorize");
  } else if (error instanceof GrantDeletedError) {
    // User disconnected via Wallet — re-auth generates a NEW grantId
    console.error("Grant deleted — prompt user to re-connect, store the new ID");
  } else if (error instanceof GrantNotFoundError) {
    // No grant with this ID exists — check for typos or stale references
    console.error("Grant not found — verify your grantId");

  // --- Policy restrictions — may resolve on its own ---
  } else if (error instanceof PolicyViolationError) {
    // Business hours, IP allowlist, or other policy denial
    console.error(`Policy denied: ${error.message} (reason: ${error.policyError})`);

  // --- Transient / infrastructure errors — safe to retry ---
  } else if (error instanceof TokenRefreshInProgressError) {
    // Another request is currently refreshing the token (backend holds a Redis lock).
    // Wait briefly and retry — the refresh will complete within a few seconds.
    console.error(`Token refresh in progress for ${error.grantId} — retry after backoff`);
  } else if (error instanceof NetworkError) {
    // TimeoutError is a subclass, so this catches both
    console.error(`Network issue — retry with backoff: ${error.message}`);

  // --- Provider errors ---
  } else if (error instanceof ScopeReauthRequiredError) {
    console.error(`Scope mismatch on ${error.grantId} - user needs to re-authorize`);
    // Create a new Connect session so the user can grant updated scopes
  } else if (error instanceof ProviderAPIError) {
    console.error(`Provider error ${error.statusCode}: ${error.responseBody}`);
  } else {
    throw error;
  }
}

Re-authorization and Grant IDs

When a user re-authorizes through Alter Connect, the same grantId is preserved in most cases. The existing grant record is updated in place with fresh tokens. You do not need to update your stored grantId.

The exception is GrantDeletedError — the user disconnected via the Wallet, so re-authorization creates a new grant with a new grantId. Store the new ID from the ConnectResult.

| Exception | Same grantId after re-auth? | |---|---| | GrantExpiredError | Yes | | CredentialRevokedError | Yes | | GrantRevokedError | Yes | | GrantDeletedError | No — new ID generated | | GrantNotFoundError | N/A — ID never existed |

Resource Management

// Manual cleanup (recommended)
const vault = new AlterVault({
  apiKey: "alter_key_...",
  caller: "my-service",
});
try {
  const response = await vault.request(...);
} finally {
  await vault.close(); // Waits for pending audit logs, cleans up timers
}

// close() is idempotent — safe to call multiple times
// Calling request() after close() throws AlterSDKError

Supported Providers

The SDK includes type-safe Provider enums for all 66 supported providers. Use them for filtering grants or as documentation -- request() takes a grantId string, not a provider enum.

import { Provider } from "@alter-ai/alter-sdk";

// Custom providers (full OAuth implementations)
Provider.GOOGLE       // "google"
Provider.GITHUB       // "github"
Provider.SLACK        // "slack"
Provider.SENTRY       // "sentry"

// Config-driven providers (63 total) -- examples:
Provider.HUBSPOT      // "hubspot"
Provider.SALESFORCE   // "salesforce"
Provider.STRIPE       // "stripe"
Provider.MICROSOFT    // "microsoft"
Provider.DISCORD      // "discord"
Provider.SPOTIFY      // "spotify"
Provider.LINKEDIN     // "linkedin"
Provider.DROPBOX      // "dropbox"
Provider.FIGMA        // "figma"
Provider.CALENDLY     // "calendly"
Provider.CLICKUP      // "clickup"
// ... and 52 more (see Provider enum for full list)

// Usage: filter grants by provider
const result = await vault.listGrants({ providerId: Provider.HUBSPOT });

// Usage: make requests with grantId
await vault.request(HttpMethod.GET, url, { grantId });

Acuity Scheduling, Adobe, Aircall, Airtable, Apollo, Asana, Atlassian, Attio, Autodesk, Basecamp, Bitbucket, Bitly, Box, Brex, Cal.com, Calendly, Canva, ClickUp, Close, Constant Contact, Contentful, Deel, Dialpad, DigitalOcean, Discord, DocuSign, Dropbox, eBay, Eventbrite, Facebook, Figma, GitHub, Google, HubSpot, Instagram, Linear, LinkedIn, Mailchimp, Mercury, Microsoft, Miro, Monday, Notion, Outreach, PagerDuty, PayPal, Pinterest, Pipedrive, QuickBooks, Ramp, Reddit, RingCentral, Salesforce, Sentry, Slack, Snapchat, Spotify, Square, Squarespace, Stripe, TikTok, Todoist, Twitter, Typeform, Webex, Webflow

Requirements

  • Node.js 18+ (uses native fetch)
  • TypeScript 5.0+

License

MIT License