@alter-ai/alter-sdk 0.2.0 → 0.2.2

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,7 +1,5 @@
1
1
  # Alter SDK for TypeScript / Node.js
2
2
 
3
- > **Version**: 0.4.0
4
-
5
3
  Official TypeScript SDK for [Alter Vault](https://alterai.dev) — OAuth token management with policy enforcement.
6
4
 
7
5
  ## Features
@@ -10,11 +8,9 @@ Official TypeScript SDK for [Alter Vault](https://alterai.dev) — OAuth token m
10
8
  - **Single Entry Point**: One method (`vault.request()`) for all provider APIs
11
9
  - **Type-Safe Enums**: `Provider` and `HttpMethod` enums with autocomplete
12
10
  - **URL Templating**: Path parameter substitution with automatic URL encoding
13
- - **Automatic Audit Logging**: All API calls logged as fire-and-forget background tasks
11
+ - **Automatic Audit Logging**: All API calls logged with request metadata (HTTP method and URL) for full audit trail
14
12
  - **Real-time Policy Enforcement**: Every token request checked against current policies
15
13
  - **Automatic Token Refresh**: Tokens refreshed transparently by the backend
16
- - **Dual HTTP Client Security**: Separate clients prevent credential leakage to providers
17
- - **Security Hardened (v0.4.0)**: Frozen `HttpClient` prototype, `WeakMap` token storage, unexported module-private token accessors, and hardened `fetch` capture prevent rogue agent token extraction
18
14
  - **Actor Tracking**: First-class support for AI agent and MCP server observability
19
15
  - **Native Promises**: Built on native `fetch` — no heavy dependencies
20
16
 
@@ -95,17 +91,70 @@ const response = await vault.request(
95
91
 
96
92
  ```typescript
97
93
  const response = await vault.request(
98
- Provider.GOOGLE,
99
- HttpMethod.GET,
100
- "https://api.example.com/v1/items",
94
+ "notion",
95
+ HttpMethod.POST,
96
+ "https://api.notion.com/v1/databases/{db_id}/query",
101
97
  {
102
98
  user: { user_id: "alice" },
103
- queryParams: { fields: "name,price", limit: "10" },
99
+ pathParams: { db_id: "abc123" },
104
100
  extraHeaders: { "Notion-Version": "2022-06-28" },
101
+ json: { page_size: 10 },
105
102
  },
106
103
  );
107
104
  ```
108
105
 
106
+ ### Connection Management
107
+
108
+ #### List Connections
109
+
110
+ Retrieve OAuth connections for your app, optionally filtered by provider:
111
+
112
+ ```typescript
113
+ const result = await vault.listConnections();
114
+ for (const conn of result.connections) {
115
+ console.log(`${conn.providerId}: ${conn.accountDisplayName} (${conn.status})`);
116
+ }
117
+
118
+ // Filter by provider with pagination
119
+ const googleConns = await vault.listConnections({
120
+ providerId: "google",
121
+ limit: 10,
122
+ offset: 0,
123
+ });
124
+ console.log(`Total: ${googleConns.total}, Has more: ${googleConns.hasMore}`);
125
+ ```
126
+
127
+ | Parameter | Type | Default | Description |
128
+ |-----------|------|---------|-------------|
129
+ | `providerId` | `string` | - | Filter by provider (e.g., `"google"`) |
130
+ | `limit` | `number` | `100` | Max connections to return |
131
+ | `offset` | `number` | `0` | Pagination offset |
132
+
133
+ Returns `ConnectionListResult` with: `connections` (`ConnectionInfo[]`), `total`, `limit`, `offset`, `hasMore`.
134
+
135
+ #### Create Connect Session
136
+
137
+ Generate a session URL for end-users to authenticate with OAuth providers:
138
+
139
+ ```typescript
140
+ const session = await vault.createConnectSession({
141
+ endUser: { id: "alice" },
142
+ allowedProviders: ["google", "github"],
143
+ returnUrl: "https://myapp.com/callback",
144
+ });
145
+ console.log(`Connect URL: ${session.connectUrl}`);
146
+ console.log(`Expires in: ${session.expiresIn}s`);
147
+ ```
148
+
149
+ | Parameter | Type | Default | Description |
150
+ |-----------|------|---------|-------------|
151
+ | `endUser` | `{ id: string }` | *required* | End user identity |
152
+ | `attributes` | `Record<string, unknown>` | - | Connection attributes |
153
+ | `allowedProviders` | `string[]` | - | Restrict to specific providers |
154
+ | `returnUrl` | `string` | - | Redirect URL after OAuth flow |
155
+
156
+ Returns `ConnectSession` with: `sessionToken`, `connectUrl`, `expiresIn`, `expiresAt`.
157
+
109
158
  ### AI Agent Actor Tracking
110
159
 
111
160
  ```typescript
@@ -126,17 +175,55 @@ const response = await vault.request(
126
175
  user: { user_id: "alice" },
127
176
  runId: "550e8400-e29b-41d4-a716-446655440000",
128
177
  threadId: "thread-xyz",
178
+ toolCallId: "call_abc_123",
129
179
  },
130
180
  );
131
181
  ```
132
182
 
183
+ ### Multi-Agent Deployments
184
+
185
+ Each agent must create its own `AlterVault` instance with a unique actor identity. Do not share a single instance across agents.
186
+
187
+ ```typescript
188
+ // Each agent gets its own vault instance
189
+ const emailAgent = new AlterVault({
190
+ apiKey: "alter_key_...",
191
+ actorType: "ai_agent",
192
+ actorIdentifier: "email-assistant-v2",
193
+ actorName: "Email Assistant",
194
+ });
195
+
196
+ const calendarAgent = new AlterVault({
197
+ apiKey: "alter_key_...",
198
+ actorType: "ai_agent",
199
+ actorIdentifier: "calendar-agent-v1",
200
+ actorName: "Calendar Agent",
201
+ });
202
+
203
+ // Audit logs and policies are tracked per agent
204
+ const user = { user_id: "alice" };
205
+ await emailAgent.request(
206
+ Provider.GOOGLE, HttpMethod.GET,
207
+ "https://gmail.googleapis.com/gmail/v1/users/me/messages",
208
+ { user },
209
+ );
210
+ await calendarAgent.request(
211
+ Provider.GOOGLE, HttpMethod.GET,
212
+ "https://www.googleapis.com/calendar/v3/calendars/primary/events",
213
+ { user },
214
+ );
215
+
216
+ // Clean up each instance
217
+ await emailAgent.close();
218
+ await calendarAgent.close();
219
+ ```
220
+
133
221
  ## Configuration
134
222
 
135
223
  ```typescript
136
224
  const vault = new AlterVault({
137
225
  apiKey: "alter_key_...", // Required: Alter Vault API key
138
226
  baseUrl: "https://api.alter.com", // Optional: Custom API URL
139
- enableAuditLogging: true, // Optional: Enable audit logs (default: true)
140
227
  timeout: 30000, // Optional: HTTP timeout in ms (default: 30000)
141
228
  // Actor tracking (optional)
142
229
  actorType: "ai_agent", // "ai_agent" or "mcp_server"
@@ -150,14 +237,14 @@ const vault = new AlterVault({
150
237
 
151
238
  ## Error Handling
152
239
 
153
- > **Note:** Input validation errors (invalid `apiKey`, invalid `actorType`, SSRF URL scheme violations, missing `pathParams`) throw `AlterSDKError`.
240
+ > **Note:** Input validation errors (invalid `apiKey`, invalid `actorType`, invalid URL scheme, missing `pathParams`) throw `AlterSDKError`.
154
241
 
155
242
  ```typescript
156
243
  import {
157
244
  AlterVault,
158
245
  Provider,
159
246
  HttpMethod,
160
- AlterSDKError, // Base error (including validation: apiKey, actorType, SSRF, pathParams)
247
+ AlterSDKError, // Base error (including validation: apiKey, actorType, URL scheme, pathParams)
161
248
  PolicyViolationError,
162
249
  ConnectionNotFoundError,
163
250
  TokenExpiredError,
@@ -206,22 +293,20 @@ try {
206
293
  // Calling request() after close() throws AlterSDKError
207
294
  ```
208
295
 
209
- ## Architecture
296
+ ## Supported Providers
210
297
 
211
- See [ALTER_TYPESCRIPT_SDK_ARCHITECTURE.md](./ALTER_TYPESCRIPT_SDK_ARCHITECTURE.md) for details on:
212
- - Dual HTTP client security model
213
- - Zero token exposure design
214
- - No SDK-side caching (real-time policy enforcement)
215
- - SSRF prevention with case-insensitive URL scheme validation
216
- - Actor tracking and audit logging
298
+ ```typescript
299
+ import { Provider } from "@alter-ai/alter-sdk";
217
300
 
218
- ## Development
301
+ Provider.GOOGLE // "google"
302
+ Provider.GITHUB // "github"
303
+ Provider.SLACK // "slack"
304
+ Provider.MICROSOFT // "microsoft"
305
+ Provider.SALESFORCE // "salesforce"
306
+ Provider.SENTRY // "sentry"
219
307
 
220
- ```bash
221
- npm install
222
- npm run test # Run tests (70 tests)
223
- npm run typecheck # Type check
224
- npm run build # Build for distribution
308
+ // Strings also work for any provider
309
+ await vault.request("notion", HttpMethod.GET, url, { user });
225
310
  ```
226
311
 
227
312
  ## Requirements
package/dist/index.cjs CHANGED
@@ -23,6 +23,9 @@ __export(index_exports, {
23
23
  APICallAuditLog: () => APICallAuditLog,
24
24
  AlterSDKError: () => AlterSDKError,
25
25
  AlterVault: () => AlterVault,
26
+ ConnectSession: () => ConnectSession,
27
+ ConnectionInfo: () => ConnectionInfo,
28
+ ConnectionListResult: () => ConnectionListResult,
26
29
  ConnectionNotFoundError: () => ConnectionNotFoundError,
27
30
  HttpMethod: () => HttpMethod,
28
31
  NetworkError: () => NetworkError,
@@ -182,6 +185,87 @@ var TokenResponse = class _TokenResponse {
182
185
  return this.toString();
183
186
  }
184
187
  };
188
+ var ConnectionInfo = class {
189
+ id;
190
+ providerId;
191
+ attributes;
192
+ scopes;
193
+ accountIdentifier;
194
+ accountDisplayName;
195
+ status;
196
+ expiresAt;
197
+ createdAt;
198
+ lastUsedAt;
199
+ constructor(data) {
200
+ this.id = data.id;
201
+ this.providerId = data.provider_id;
202
+ this.attributes = data.attributes ?? {};
203
+ this.scopes = data.scopes ?? [];
204
+ this.accountIdentifier = data.account_identifier ?? null;
205
+ this.accountDisplayName = data.account_display_name ?? null;
206
+ this.status = data.status;
207
+ this.expiresAt = data.expires_at ?? null;
208
+ this.createdAt = data.created_at;
209
+ this.lastUsedAt = data.last_used_at ?? null;
210
+ Object.freeze(this);
211
+ }
212
+ toJSON() {
213
+ return {
214
+ id: this.id,
215
+ provider_id: this.providerId,
216
+ attributes: this.attributes,
217
+ scopes: this.scopes,
218
+ account_identifier: this.accountIdentifier,
219
+ account_display_name: this.accountDisplayName,
220
+ status: this.status,
221
+ expires_at: this.expiresAt,
222
+ created_at: this.createdAt,
223
+ last_used_at: this.lastUsedAt
224
+ };
225
+ }
226
+ toString() {
227
+ return `ConnectionInfo(id=${this.id}, provider=${this.providerId}, status=${this.status})`;
228
+ }
229
+ };
230
+ var ConnectSession = class {
231
+ sessionToken;
232
+ connectUrl;
233
+ expiresIn;
234
+ expiresAt;
235
+ constructor(data) {
236
+ this.sessionToken = data.session_token;
237
+ this.connectUrl = data.connect_url;
238
+ this.expiresIn = data.expires_in;
239
+ this.expiresAt = data.expires_at;
240
+ Object.freeze(this);
241
+ }
242
+ toJSON() {
243
+ return {
244
+ session_token: this.sessionToken,
245
+ connect_url: this.connectUrl,
246
+ expires_in: this.expiresIn,
247
+ expires_at: this.expiresAt
248
+ };
249
+ }
250
+ toString() {
251
+ return `ConnectSession(url=${this.connectUrl}, expires_in=${this.expiresIn})`;
252
+ }
253
+ };
254
+ var ConnectionListResult = class {
255
+ connections;
256
+ total;
257
+ limit;
258
+ offset;
259
+ hasMore;
260
+ constructor(data) {
261
+ this.connections = data.connections;
262
+ this.total = data.total;
263
+ this.limit = data.limit;
264
+ this.offset = data.offset;
265
+ this.hasMore = data.has_more;
266
+ Object.freeze(this);
267
+ }
268
+ };
185
269
  var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
186
270
  "authorization",
187
271
  "cookie",
@@ -276,7 +360,7 @@ function _extractAccessToken(token) {
276
360
  return value;
277
361
  }
278
362
  var _fetch;
279
- var SDK_VERSION = "0.2.0";
363
+ var SDK_VERSION = "0.2.2";
280
364
  var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
281
365
  var VALID_ACTOR_TYPES = ["ai_agent", "mcp_server"];
282
366
  var HTTP_FORBIDDEN = 403;
@@ -372,7 +456,6 @@ var AlterVault = class _AlterVault {
372
456
  // Public readonly properties (frozen by Object.freeze)
373
457
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
374
458
  baseUrl;
375
- enableAuditLogging;
376
459
  /** Actor tracking configuration (readonly, not secret) */
377
460
  #actorType;
378
461
  #actorIdentifier;
@@ -400,7 +483,6 @@ var AlterVault = class _AlterVault {
400
483
  /\/+$/,
401
484
  ""
402
485
  );
403
- this.enableAuditLogging = options.enableAuditLogging ?? true;
404
486
  const timeoutMs = options.timeout ?? 3e4;
405
487
  this.#actorType = options.actorType;
406
488
  this.#actorIdentifier = options.actorIdentifier;
@@ -629,7 +711,7 @@ var AlterVault = class _AlterVault {
629
711
  * This is a private method. Tokens are NEVER exposed to developers.
630
712
  * Use request() instead, which handles tokens internally.
631
713
  */
632
- async #getToken(providerId, attributes, reason, runId, threadId, toolCallId) {
714
+ async #getToken(providerId, attributes, reason, requestMetadata, runId, threadId, toolCallId) {
633
715
  const actorHeaders = this.#getActorRequestHeaders(
634
716
  runId,
635
717
  threadId,
@@ -641,7 +723,8 @@ var AlterVault = class _AlterVault {
641
723
  json: {
642
724
  provider_id: providerId,
643
725
  attributes,
644
- reason: reason ?? null
726
+ reason: reason ?? null,
727
+ request: requestMetadata ?? null
645
728
  },
646
729
  headers: actorHeaders
647
730
  });
@@ -678,9 +761,6 @@ var AlterVault = class _AlterVault {
678
761
  * if audit logging fails.
679
762
  */
680
763
  async #logApiCall(params) {
681
- if (!this.enableAuditLogging) {
682
- return;
683
- }
684
764
  try {
685
765
  const auditLog = new APICallAuditLog({
686
766
  connectionId: params.connectionId,
@@ -765,7 +845,7 @@ var AlterVault = class _AlterVault {
765
845
  * 1. Fetches an OAuth token from Alter backend (never exposed)
766
846
  * 2. Injects the token as a Bearer header
767
847
  * 3. Calls the provider API
768
- * 4. Logs the call for audit (if enabled, fire-and-forget)
848
+ * 4. Logs the call for audit (fire-and-forget)
769
849
  * 5. Returns the raw response
770
850
  */
771
851
  async request(provider, method, url, options) {
@@ -821,6 +901,7 @@ var AlterVault = class _AlterVault {
821
901
  providerStr,
822
902
  options.user,
823
903
  options.reason,
904
+ { method: methodStr, url },
824
905
  options.runId,
825
906
  options.threadId,
826
907
  options.toolCallId
@@ -901,6 +982,112 @@ var AlterVault = class _AlterVault {
901
982
  }
902
983
  return response;
903
984
  }
985
+ /**
986
+ * List OAuth connections for this app.
987
+ *
988
+ * Returns paginated connection metadata (no tokens).
989
+ * Useful for discovering which services a user has connected.
990
+ */
991
+ async listConnections(options) {
992
+ if (this.#closed) {
993
+ throw new AlterSDKError(
994
+ "SDK instance has been closed. Create a new AlterVault instance to make requests."
995
+ );
996
+ }
997
+ const actorHeaders = this.#getActorRequestHeaders();
998
+ let response;
999
+ try {
1000
+ response = await this.#alterClient.post("/oauth/connections/list", {
1001
+ json: {
1002
+ provider_id: options?.providerId ?? null,
1003
+ limit: options?.limit ?? 100,
1004
+ offset: options?.offset ?? 0
1005
+ },
1006
+ headers: actorHeaders
1007
+ });
1008
+ } catch (error) {
1009
+ if (_AlterVault.#isTimeoutOrAbortError(error)) {
1010
+ throw new TimeoutError(
1011
+ `Request to Alter Vault backend timed out: ${error instanceof Error ? error.message : String(error)}`,
1012
+ { base_url: this.baseUrl }
1013
+ );
1014
+ }
1015
+ if (error instanceof TypeError) {
1016
+ throw new NetworkError(
1017
+ `Failed to connect to Alter Vault backend: ${error.message}`,
1018
+ { base_url: this.baseUrl }
1019
+ );
1020
+ }
1021
+ throw new AlterSDKError(
1022
+ `Failed to list connections: ${error instanceof Error ? error.message : String(error)}`
1023
+ );
1024
+ }
1025
+ this.#cacheActorIdFromResponse(response);
1026
+ await this.#handleErrorResponse(response);
1027
+ const data = await response.json();
1028
+ const connections = data.connections.map(
1029
+ (c) => new ConnectionInfo(
1030
+ c
1031
+ )
1032
+ );
1033
+ return new ConnectionListResult({
1034
+ connections,
1035
+ total: data.total,
1036
+ limit: data.limit,
1037
+ offset: data.offset,
1038
+ has_more: data.has_more
1039
+ });
1040
+ }
1041
+ /**
1042
+ * Create a Connect session for initiating OAuth flows.
1043
+ *
1044
+ * Returns a URL the user can open in their browser to authorize access.
1045
+ */
1046
+ async createConnectSession(options) {
1047
+ if (this.#closed) {
1048
+ throw new AlterSDKError(
1049
+ "SDK instance has been closed. Create a new AlterVault instance to make requests."
1050
+ );
1051
+ }
1052
+ if (!options.endUser?.id) {
1053
+ throw new AlterSDKError("endUser.id is required");
1054
+ }
1055
+ const actorHeaders = this.#getActorRequestHeaders();
1056
+ let response;
1057
+ try {
1058
+ response = await this.#alterClient.post("/oauth/connect/session", {
1059
+ json: {
1060
+ end_user: options.endUser,
1061
+ attributes: options.attributes ?? null,
1062
+ allowed_providers: options.allowedProviders ?? null,
1063
+ return_url: options.returnUrl ?? null,
1064
+ allowed_origin: options.allowedOrigin ?? null,
1065
+ metadata: options.metadata ?? null
1066
+ },
1067
+ headers: actorHeaders
1068
+ });
1069
+ } catch (error) {
1070
+ if (_AlterVault.#isTimeoutOrAbortError(error)) {
1071
+ throw new TimeoutError(
1072
+ `Request to Alter Vault backend timed out: ${error instanceof Error ? error.message : String(error)}`,
1073
+ { base_url: this.baseUrl }
1074
+ );
1075
+ }
1076
+ if (error instanceof TypeError) {
1077
+ throw new NetworkError(
1078
+ `Failed to connect to Alter Vault backend: ${error.message}`,
1079
+ { base_url: this.baseUrl }
1080
+ );
1081
+ }
1082
+ throw new AlterSDKError(
1083
+ `Failed to create connect session: ${error instanceof Error ? error.message : String(error)}`
1084
+ );
1085
+ }
1086
+ this.#cacheActorIdFromResponse(response);
1087
+ await this.#handleErrorResponse(response);
1088
+ const data = await response.json();
1089
+ return new ConnectSession(data);
1090
+ }
904
1091
  /**
905
1092
  * Close HTTP clients and release resources.
906
1093
  * Waits for any pending audit tasks before closing.
@@ -949,6 +1136,9 @@ var HttpMethod = /* @__PURE__ */ ((HttpMethod2) => {
949
1136
  APICallAuditLog,
950
1137
  AlterSDKError,
951
1138
  AlterVault,
1139
+ ConnectSession,
1140
+ ConnectionInfo,
1141
+ ConnectionListResult,
952
1142
  ConnectionNotFoundError,
953
1143
  HttpMethod,
954
1144
  NetworkError,