@alter-ai/alter-sdk 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Alter SDK for TypeScript / Node.js
2
2
 
3
- Official TypeScript SDK for [Alter Vault](https://alterai.dev) Credential management for agents with policy enforcement.
3
+ Official TypeScript SDK for [Alter Vault](https://alterai.dev) -Credential management for agents with policy enforcement.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Zero Token Exposure**: Tokens are never exposed to developers injected automatically
7
+ - **Zero Token Exposure**: Tokens are never exposed to developers -injected automatically
8
8
  - **Single Entry Point**: One method (`vault.request()`) for all provider APIs
9
9
  - **Type-Safe Enums**: `HttpMethod` enums with autocomplete
10
10
  - **URL Templating**: Path parameter substitution with automatic URL encoding
@@ -12,10 +12,9 @@ Official TypeScript SDK for [Alter Vault](https://alterai.dev) — Credential ma
12
12
  - **Real-time Policy Enforcement**: Every token request checked against current policies
13
13
  - **Automatic Token Refresh**: Tokens refreshed transparently by the backend
14
14
  - **API Key and Custom Credential Support**: Handles OAuth tokens, API keys, and custom credential formats automatically
15
- - **AWS SigV4 Support**: Automatic AWS Signature Version 4 signing for S3, Bedrock, DynamoDB, and other AWS services (no AWS SDK required)
16
15
  - **Actor Tracking**: First-class support for AI agent and MCP server observability
17
- - **HMAC Request Signing**: All SDK-to-backend requests are signed with a derived HMAC-SHA256 key for integrity, authenticity, and replay protection
18
- - **Native Promises**: Built on native `fetch` no heavy dependencies
16
+ - **HMAC Request Signing**: All SDK-to-backend requests are cryptographically signed for integrity, authenticity, and replay protection
17
+ - **Native Promises**: Built on native `fetch` -no heavy dependencies
19
18
 
20
19
  ## Installation
21
20
 
@@ -34,11 +33,11 @@ const vault = new AlterVault({
34
33
  actorIdentifier: "my-agent",
35
34
  });
36
35
 
37
- // Make API request token injected automatically, never exposed
36
+ // Make API request -token injected automatically, never exposed
38
37
  const response = await vault.request(
39
38
  "CONNECTION_ID", // from Alter Connect (see below)
40
39
  HttpMethod.GET,
41
- "https://www.googleapis.com/calendar/v3/calendars/primary/events",
40
+ "https://api.example.com/v1/resource",
42
41
  {
43
42
  queryParams: { maxResults: "10" },
44
43
  },
@@ -52,22 +51,22 @@ await vault.close();
52
51
 
53
52
  ### Where does `connectionId` come from?
54
53
 
55
- **OAuth connections:**
56
- 1. User completes OAuth via [Alter Connect](https://docs.alterai.dev/sdks/javascript/quickstart) (frontend widget)
57
- 2. The `onSuccess` callback returns a `connectionId` (UUID)
54
+ **OAuth connections** (per-user, from end user action):
55
+ 1. Your **end user** completes OAuth via [Alter Connect](https://docs.alterai.dev/sdks/javascript/quickstart) (frontend widget) or `vault.connect()` (headless)
56
+ 2. The `onSuccess` callback returns a `connectionId` (UUID) -one per user per account
58
57
  3. You save it in your database, mapped to your user
59
58
  4. You pass it to `vault.request()` when making API calls
60
59
 
61
- **Managed secrets** (API keys, service tokens):
62
- 1. Store credentials in the [Developer Portal](https://portal.alterai.dev) under **Managed Secrets**
63
- 2. Copy the `connectionId` returned
64
- 3. Use the same `vault.request()` credentials are injected automatically
60
+ **Managed secrets** (per-service, from developer action):
61
+ 1. **You** store credentials in the [Developer Portal](https://portal.alterai.dev) under **Managed Secrets**
62
+ 2. The portal returns a `connectionId` -one per stored credential, shared across your backend
63
+ 3. Use the same `vault.request()` -credentials are injected automatically
65
64
 
66
65
  ```typescript
67
66
  // You can also discover connectionIds programmatically:
68
67
  const result = await vault.listConnections({ providerId: "google" });
69
68
  for (const conn of result.connections) {
70
- console.log(`${conn.id}: ${conn.accountDisplayName}`);
69
+ console.log(`${conn.connectionId}: ${conn.accountDisplayName}`);
71
70
  }
72
71
  ```
73
72
 
@@ -81,7 +80,7 @@ The `request()` method returns the raw `Response` object. The token is injected
81
80
  const response = await vault.request(
82
81
  connectionId,
83
82
  HttpMethod.GET,
84
- "https://www.googleapis.com/calendar/v3/calendars/primary/events",
83
+ "https://api.example.com/v1/resource",
85
84
  );
86
85
  ```
87
86
 
@@ -185,7 +184,6 @@ Generate a session URL for end-users to authenticate with OAuth providers:
185
184
 
186
185
  ```typescript
187
186
  const session = await vault.createConnectSession({
188
- endUser: { id: "alice" },
189
187
  allowedProviders: ["google", "github"],
190
188
  returnUrl: "https://myapp.com/callback",
191
189
  });
@@ -195,7 +193,6 @@ console.log(`Expires in: ${session.expiresIn}s`);
195
193
 
196
194
  | Parameter | Type | Default | Description |
197
195
  |-----------|------|---------|-------------|
198
- | `endUser` | `{ id: string }` | *required* | End user identity |
199
196
  | `allowedProviders` | `string[]` | - | Restrict to specific providers |
200
197
  | `returnUrl` | `string` | - | Redirect URL after OAuth flow |
201
198
 
@@ -207,8 +204,11 @@ For CLI tools, scripts, and server-side applications -- opens the browser, waits
207
204
 
208
205
  ```typescript
209
206
  const results = await vault.connect({
210
- endUser: { id: "alice" },
211
207
  providers: ["google"],
208
+ connectionPolicy: { // optional TTL bounds
209
+ maxTtlSeconds: 86400,
210
+ defaultTtlSeconds: 3600,
211
+ },
212
212
  timeout: 300, // max wait in seconds (default: 5 min)
213
213
  openBrowser: true, // set false to print URL instead
214
214
  });
@@ -221,21 +221,21 @@ for (const result of results) {
221
221
  const response = await vault.request(
222
222
  results[0].connectionId,
223
223
  HttpMethod.GET,
224
- "https://www.googleapis.com/calendar/v3/calendars/primary/events",
224
+ "https://api.example.com/v1/resource",
225
225
  );
226
226
  ```
227
227
 
228
228
  | Parameter | Type | Default | Description |
229
229
  |-----------|------|---------|-------------|
230
- | `endUser` | `{ id: string }` | *required* | End user identity |
231
230
  | `providers` | `string[]` | - | Restrict to specific providers |
232
231
  | `timeout` | `number` | `300` | Max seconds to wait for completion |
233
232
  | `pollInterval` | `number` | `2` | Seconds between status checks |
233
+ | `connectionPolicy` | `{ maxTtlSeconds?: number; defaultTtlSeconds?: number }` | - | TTL bounds (optional) |
234
234
  | `openBrowser` | `boolean` | `true` | Open browser automatically |
235
235
 
236
- Returns `ConnectResult[]` one per connected provider. Each has: `connectionId`, `providerId`, `accountIdentifier`, `scopes`.
236
+ Returns `ConnectResult[]` -one per connected provider. Each has: `connectionId`, `providerId`, `accountIdentifier`, `scopes`, and optionally `connectionPolicy` (if a TTL was set).
237
237
 
238
- Throws `ConnectTimeoutError` if the user doesn't complete in time, `ConnectFlowError` if denied.
238
+ Throws `ConnectTimeoutError` if the user doesn't complete in time, `ConnectDeniedError` if the user denies authorization, `ConnectConfigError` if the OAuth app is misconfigured.
239
239
 
240
240
  ### AI Agent Actor Tracking
241
241
 
@@ -254,7 +254,7 @@ const vault = new AlterVault({
254
254
  const response = await vault.request(
255
255
  connectionId,
256
256
  HttpMethod.GET,
257
- "https://www.googleapis.com/calendar/v3/calendars/primary/events",
257
+ "https://api.example.com/v1/resource",
258
258
  {
259
259
  runId: "550e8400-e29b-41d4-a716-446655440000", // auto-generated UUID if omitted
260
260
  threadId: "thread-xyz",
@@ -289,12 +289,12 @@ const calendarAgent = new AlterVault({
289
289
  await emailAgent.request(
290
290
  gmailConnectionId, // from Alter Connect
291
291
  HttpMethod.GET,
292
- "https://gmail.googleapis.com/gmail/v1/users/me/messages",
292
+ "https://api.example.com/v1/messages",
293
293
  );
294
294
  await calendarAgent.request(
295
295
  calendarConnectionId, // from Alter Connect
296
296
  HttpMethod.GET,
297
- "https://www.googleapis.com/calendar/v3/calendars/primary/events",
297
+ "https://api.example.com/v1/resource",
298
298
  );
299
299
 
300
300
  // Clean up each instance
@@ -321,47 +321,108 @@ const vault = new AlterVault({
321
321
 
322
322
  ## Error Handling
323
323
 
324
+ The SDK provides a typed exception hierarchy so you can handle each failure mode precisely:
325
+
326
+ ```
327
+ AlterSDKError (base)
328
+ ├── BackendError // Generic backend error
329
+ │ ├── ReAuthRequiredError // User must re-authorize via Alter Connect
330
+ │ │ ├── ConnectionExpiredError // 403 — connection TTL elapsed
331
+ │ │ ├── ConnectionRevokedError // 400 — auth permanently broken (revoked, invalid_grant)
332
+ │ │ └── ConnectionDeletedError // 410 — user disconnected via Wallet (new ID on re-auth)
333
+ │ ├── ConnectionNotFoundError // 404 — wrong connection_id
334
+ │ └── PolicyViolationError // 403 — policy denied (business hours, IP, etc.)
335
+ ├── ConnectFlowError // Headless connect() failed
336
+ │ ├── ConnectDeniedError // User clicked Deny
337
+ │ ├── ConnectConfigError // OAuth app misconfigured
338
+ │ └── ConnectTimeoutError // User didn't complete in time
339
+ ├── ProviderAPIError // Provider returned 4xx/5xx
340
+ │ └── ScopeReauthRequiredError // 403 + scope mismatch — user must re-authorize
341
+ └── NetworkError // Backend or provider unreachable
342
+ └── TimeoutError // Request timed out (safe to retry)
343
+ ```
344
+
324
345
  > **Note:** Input validation errors (invalid `apiKey`, invalid/missing `actorType`, missing `actorIdentifier`, invalid URL scheme, missing `pathParams`) throw `AlterSDKError`.
325
346
 
326
347
  ```typescript
327
348
  import {
328
349
  AlterVault,
329
350
  HttpMethod,
330
- AlterSDKError, // Base error (including validation: apiKey, actorType, actorIdentifier, URL scheme, pathParams)
331
- PolicyViolationError,
332
- ConnectionNotFoundError,
333
- TokenExpiredError,
334
- TokenRetrievalError,
351
+ AlterSDKError, // Base error (including validation errors)
352
+ BackendError, // Generic backend error
353
+ ReAuthRequiredError, // Parent for all re-auth errors
354
+ ConnectionDeletedError, // User disconnected via Wallet — new ID on re-auth (410)
355
+ ConnectionExpiredError, // TTL expired — user must re-authorize (403)
356
+ ConnectionNotFoundError, // Wrong connection_id — check for typos (404)
357
+ ConnectionRevokedError, // Auth permanently broken — revoked/invalid_grant (400)
358
+ PolicyViolationError, // Policy denied — business hours, IP, etc. (403)
335
359
  ConnectFlowError, // Headless connect() failed (denied, provider error)
360
+ ConnectDeniedError, // User denied authorization
361
+ ConnectConfigError, // OAuth app misconfigured
336
362
  ConnectTimeoutError, // Headless connect() timed out
337
- NetworkError,
338
- TimeoutError,
339
- ProviderAPIError,
363
+ NetworkError, // Backend or provider unreachable
364
+ TimeoutError, // Request timed out (subclass of NetworkError)
365
+ ProviderAPIError, // Provider API returned error (4xx/5xx)
366
+ ScopeReauthRequiredError, // 403 + scope mismatch (subclass of ProviderAPIError)
340
367
  } from "@alter-ai/alter-sdk";
341
368
 
342
369
  try {
343
370
  const response = await vault.request(
344
371
  connectionId,
345
372
  HttpMethod.GET,
346
- "https://www.googleapis.com/calendar/v3/calendars/primary/events",
373
+ "https://api.example.com/v1/resource",
347
374
  );
348
375
  } catch (error) {
349
- if (error instanceof PolicyViolationError) {
350
- console.error(`Policy denied: ${error.message}`);
376
+ // --- Connection unusable — user must re-authorize via Alter Connect ---
377
+ if (error instanceof ConnectionExpiredError) {
378
+ // TTL set during Connect flow has elapsed
379
+ console.error("Connection expired — prompt user to re-authorize");
380
+ } else if (error instanceof ConnectionRevokedError) {
381
+ // Auth permanently broken — user revoked at provider, refresh token
382
+ // expired, or token refresh permanently failed (invalid_grant)
383
+ console.error("Connection revoked — prompt user to re-authorize");
384
+ } else if (error instanceof ConnectionDeletedError) {
385
+ // User disconnected via Wallet — re-auth generates a NEW connectionId
386
+ console.error("Connection deleted — prompt user to re-connect, store the new ID");
351
387
  } else if (error instanceof ConnectionNotFoundError) {
352
- console.error("No OAuth connection — user needs to authenticate");
353
- } else if (error instanceof TokenExpiredError) {
354
- console.error(`Token expired for connection: ${error.connectionId}`);
355
- } else if (error instanceof TimeoutError) {
356
- console.error(`Request timed out safe to retry: ${error.message}`);
388
+ // No connection with this ID exists check for typos or stale references
389
+ console.error("Connection not found verify your connectionId");
390
+
391
+ // --- Policy restrictions may resolve on its own ---
392
+ } else if (error instanceof PolicyViolationError) {
393
+ // Business hours, IP allowlist, or other policy denial
394
+ console.error(`Policy denied: ${error.message} (reason: ${error.policyError})`);
395
+
396
+ // --- Transient / infrastructure errors — safe to retry ---
357
397
  } else if (error instanceof NetworkError) {
358
- console.error(`Network issue: ${error.message}`);
398
+ // TimeoutError is a subclass, so this catches both
399
+ console.error(`Network issue — retry with backoff: ${error.message}`);
400
+
401
+ // --- Provider errors ---
402
+ } else if (error instanceof ScopeReauthRequiredError) {
403
+ console.error(`Scope mismatch on ${error.connectionId} - user needs to re-authorize`);
404
+ // Create a new Connect session so the user can grant updated scopes
359
405
  } else if (error instanceof ProviderAPIError) {
360
406
  console.error(`Provider error ${error.statusCode}: ${error.responseBody}`);
407
+ } else {
408
+ throw error;
361
409
  }
362
410
  }
363
411
  ```
364
412
 
413
+ ### Re-authorization and Connection IDs
414
+
415
+ When a user re-authorizes through Alter Connect, the **same `connectionId` is preserved** in most cases. The existing connection record is updated in place with fresh tokens. You do **not** need to update your stored `connectionId`.
416
+
417
+ The exception is `ConnectionDeletedError` — the user disconnected via the Wallet, so re-authorization creates a new connection with a **new `connectionId`**. Store the new ID from the `ConnectResult`.
418
+
419
+ | Exception | Same `connectionId` after re-auth? |
420
+ |---|---|
421
+ | `ConnectionExpiredError` | Yes |
422
+ | `ConnectionRevokedError` | Yes |
423
+ | `ConnectionDeletedError` | **No** — new ID generated |
424
+ | `ConnectionNotFoundError` | N/A — ID never existed |
425
+
365
426
  ## Resource Management
366
427
 
367
428
  ```typescript
@@ -377,19 +438,51 @@ try {
377
438
  await vault.close(); // Waits for pending audit logs, cleans up timers
378
439
  }
379
440
 
380
- // close() is idempotent safe to call multiple times
441
+ // close() is idempotent -safe to call multiple times
381
442
  // Calling request() after close() throws AlterSDKError
382
443
  ```
383
444
 
384
445
  ## Supported Providers
385
446
 
386
- The SDK works with any OAuth provider configured in your Alter Vault dashboard. The first parameter to `vault.request()` is the `connection_id` (UUID) returned when a user connects via Alter Connect.
447
+ The SDK includes type-safe `Provider` enums for all 66 supported providers. Use them for filtering connections or as documentation -- `request()` takes a `connectionId` string, not a provider enum.
387
448
 
388
449
  ```typescript
389
- // Use the connection_id from Alter Connect
390
- await vault.request("550e8400-e29b-41d4-a716-446655440000", HttpMethod.GET, url);
450
+ import { Provider } from "@alter-ai/alter-sdk";
451
+
452
+ // Custom providers (full OAuth implementations)
453
+ Provider.GOOGLE // "google"
454
+ Provider.GITHUB // "github"
455
+ Provider.SLACK // "slack"
456
+ Provider.SENTRY // "sentry"
457
+
458
+ // Config-driven providers (63 total) -- examples:
459
+ Provider.HUBSPOT // "hubspot"
460
+ Provider.SALESFORCE // "salesforce"
461
+ Provider.STRIPE // "stripe"
462
+ Provider.MICROSOFT // "microsoft"
463
+ Provider.DISCORD // "discord"
464
+ Provider.SPOTIFY // "spotify"
465
+ Provider.LINKEDIN // "linkedin"
466
+ Provider.DROPBOX // "dropbox"
467
+ Provider.FIGMA // "figma"
468
+ Provider.CALENDLY // "calendly"
469
+ Provider.CLICKUP // "clickup"
470
+ // ... and 52 more (see Provider enum for full list)
471
+
472
+ // Usage: filter connections by provider
473
+ const result = await vault.listConnections({ providerId: Provider.HUBSPOT });
474
+
475
+ // Usage: make requests with connectionId
476
+ await vault.request(connectionId, HttpMethod.GET, url);
391
477
  ```
392
478
 
479
+ <details>
480
+ <summary>All 66 providers</summary>
481
+
482
+ 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
483
+
484
+ </details>
485
+
393
486
  ## Requirements
394
487
 
395
488
  - Node.js 18+ (uses native `fetch`)