@alter-ai/alter-sdk 0.2.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/dist/index.js ADDED
@@ -0,0 +1,923 @@
1
+ // src/exceptions.ts
2
+ var AlterSDKError = class extends Error {
3
+ details;
4
+ constructor(message, details) {
5
+ super(message);
6
+ this.name = "AlterSDKError";
7
+ this.details = details ?? {};
8
+ Object.setPrototypeOf(this, new.target.prototype);
9
+ }
10
+ toString() {
11
+ if (Object.keys(this.details).length > 0) {
12
+ return `${this.message} (details: ${JSON.stringify(this.details)})`;
13
+ }
14
+ return this.message;
15
+ }
16
+ };
17
+ var TokenRetrievalError = class extends AlterSDKError {
18
+ constructor(message, details) {
19
+ super(message, details);
20
+ this.name = "TokenRetrievalError";
21
+ }
22
+ };
23
+ var PolicyViolationError = class extends TokenRetrievalError {
24
+ policyError;
25
+ constructor(message, policyError, details) {
26
+ super(message, details);
27
+ this.name = "PolicyViolationError";
28
+ this.policyError = policyError;
29
+ }
30
+ };
31
+ var ConnectionNotFoundError = class extends TokenRetrievalError {
32
+ constructor(message, details) {
33
+ super(message, details);
34
+ this.name = "ConnectionNotFoundError";
35
+ }
36
+ };
37
+ var TokenExpiredError = class extends TokenRetrievalError {
38
+ connectionId;
39
+ constructor(message, connectionId, details) {
40
+ super(message, details);
41
+ this.name = "TokenExpiredError";
42
+ this.connectionId = connectionId;
43
+ }
44
+ };
45
+ var ProviderAPIError = class extends AlterSDKError {
46
+ statusCode;
47
+ responseBody;
48
+ constructor(message, statusCode, responseBody, details) {
49
+ super(message, details);
50
+ this.name = "ProviderAPIError";
51
+ this.statusCode = statusCode;
52
+ this.responseBody = responseBody;
53
+ }
54
+ };
55
+ var NetworkError = class extends AlterSDKError {
56
+ constructor(message, details) {
57
+ super(message, details);
58
+ this.name = "NetworkError";
59
+ }
60
+ };
61
+ var TimeoutError = class extends NetworkError {
62
+ constructor(message, details) {
63
+ super(message, details);
64
+ this.name = "TimeoutError";
65
+ }
66
+ };
67
+
68
+ // src/models.ts
69
+ var TokenResponse = class _TokenResponse {
70
+ /** Token type (usually "Bearer") */
71
+ tokenType;
72
+ /** Seconds until token expires */
73
+ expiresIn;
74
+ /** Absolute expiration time */
75
+ expiresAt;
76
+ /** OAuth scopes granted */
77
+ scopes;
78
+ /** Connection ID that provided this token */
79
+ connectionId;
80
+ constructor(data) {
81
+ this.tokenType = data.token_type ?? "Bearer";
82
+ this.expiresIn = data.expires_in ?? null;
83
+ this.expiresAt = data.expires_at ? _TokenResponse.parseExpiresAt(data.expires_at) : null;
84
+ this.scopes = data.scopes ?? [];
85
+ this.connectionId = data.connection_id;
86
+ Object.freeze(this);
87
+ }
88
+ /**
89
+ * Parse expires_at from ISO string.
90
+ */
91
+ static parseExpiresAt(value) {
92
+ const dt = new Date(value);
93
+ if (isNaN(dt.getTime())) {
94
+ throw new Error(`Invalid expires_at value: ${value}`);
95
+ }
96
+ return dt;
97
+ }
98
+ /**
99
+ * Check if token is expired.
100
+ *
101
+ * @param bufferSeconds - Consider token expired N seconds before actual expiry.
102
+ * Useful for preventing race conditions.
103
+ * @returns True if token is expired or will expire within bufferSeconds.
104
+ */
105
+ isExpired(bufferSeconds = 0) {
106
+ if (this.expiresAt === null) {
107
+ return false;
108
+ }
109
+ const now = Date.now();
110
+ return this.expiresAt.getTime() <= now + bufferSeconds * 1e3;
111
+ }
112
+ /**
113
+ * Check if token should be refreshed soon.
114
+ *
115
+ * @param bufferSeconds - Consider token needing refresh N seconds before expiry.
116
+ * Default 5 minutes (300 seconds).
117
+ * @returns True if token will expire within bufferSeconds.
118
+ */
119
+ needsRefresh(bufferSeconds = 300) {
120
+ return this.isExpired(bufferSeconds);
121
+ }
122
+ /**
123
+ * Custom JSON serialization — EXCLUDES access_token for security.
124
+ */
125
+ toJSON() {
126
+ return {
127
+ token_type: this.tokenType,
128
+ expires_in: this.expiresIn,
129
+ expires_at: this.expiresAt?.toISOString() ?? null,
130
+ scopes: this.scopes,
131
+ connection_id: this.connectionId
132
+ };
133
+ }
134
+ /**
135
+ * Custom string representation — EXCLUDES access_token for security.
136
+ */
137
+ toString() {
138
+ return `TokenResponse(connection_id=${this.connectionId}, token_type=${this.tokenType}, scopes=[${this.scopes.join(", ")}])`;
139
+ }
140
+ /**
141
+ * Custom Node.js inspect output — EXCLUDES access_token for security.
142
+ */
143
+ [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
144
+ return this.toString();
145
+ }
146
+ };
147
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
148
+ "authorization",
149
+ "cookie",
150
+ "set-cookie",
151
+ "x-api-key",
152
+ "x-auth-token"
153
+ ]);
154
+ var APICallAuditLog = class {
155
+ connectionId;
156
+ providerId;
157
+ method;
158
+ url;
159
+ requestHeaders;
160
+ requestBody;
161
+ responseStatus;
162
+ responseHeaders;
163
+ responseBody;
164
+ latencyMs;
165
+ /** Client-side timestamp (excluded from sanitize() output, like Python SDK) */
166
+ timestamp;
167
+ reason;
168
+ /** Execution run ID for actor tracking */
169
+ runId;
170
+ /** Conversation thread ID for actor tracking */
171
+ threadId;
172
+ /** Tool invocation ID for actor tracking */
173
+ toolCallId;
174
+ constructor(data) {
175
+ this.connectionId = data.connectionId;
176
+ this.providerId = data.providerId;
177
+ this.method = data.method;
178
+ this.url = data.url;
179
+ this.requestHeaders = data.requestHeaders ?? null;
180
+ this.requestBody = data.requestBody ?? null;
181
+ this.responseStatus = data.responseStatus;
182
+ this.responseHeaders = data.responseHeaders ?? null;
183
+ this.responseBody = data.responseBody ?? null;
184
+ this.latencyMs = data.latencyMs;
185
+ this.timestamp = /* @__PURE__ */ new Date();
186
+ this.reason = data.reason ?? null;
187
+ this.runId = data.runId ?? null;
188
+ this.threadId = data.threadId ?? null;
189
+ this.toolCallId = data.toolCallId ?? null;
190
+ }
191
+ /**
192
+ * Sanitize sensitive data before sending.
193
+ *
194
+ * Removes Authorization headers, cookies, etc.
195
+ */
196
+ sanitize() {
197
+ const sanitized = {
198
+ connection_id: this.connectionId,
199
+ provider_id: this.providerId,
200
+ method: this.method,
201
+ url: this.url,
202
+ request_headers: this.requestHeaders ? this.filterSensitiveHeaders(this.requestHeaders) : null,
203
+ request_body: this.requestBody,
204
+ response_status: this.responseStatus,
205
+ response_headers: this.responseHeaders ? this.filterSensitiveHeaders(this.responseHeaders) : null,
206
+ response_body: this.responseBody,
207
+ latency_ms: this.latencyMs,
208
+ reason: this.reason,
209
+ run_id: this.runId,
210
+ thread_id: this.threadId,
211
+ tool_call_id: this.toolCallId
212
+ };
213
+ return sanitized;
214
+ }
215
+ filterSensitiveHeaders(headers) {
216
+ const filtered = {};
217
+ for (const [key, value] of Object.entries(headers)) {
218
+ if (!SENSITIVE_HEADERS.has(key.toLowerCase())) {
219
+ filtered[key] = value;
220
+ }
221
+ }
222
+ return filtered;
223
+ }
224
+ };
225
+
226
+ // src/client.ts
227
+ var _tokenStore = /* @__PURE__ */ new WeakMap();
228
+ function _storeAccessToken(token, accessToken) {
229
+ _tokenStore.set(token, accessToken);
230
+ }
231
+ function _extractAccessToken(token) {
232
+ const value = _tokenStore.get(token);
233
+ if (value === void 0) {
234
+ throw new Error(
235
+ "Token data unavailable \u2014 TokenResponse may have been garbage collected"
236
+ );
237
+ }
238
+ return value;
239
+ }
240
+ var _fetch;
241
+ var SDK_VERSION = "0.2.0";
242
+ var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
243
+ var VALID_ACTOR_TYPES = ["ai_agent", "mcp_server"];
244
+ var HTTP_FORBIDDEN = 403;
245
+ var HTTP_NOT_FOUND = 404;
246
+ var HTTP_BAD_REQUEST = 400;
247
+ var HTTP_UNAUTHORIZED = 401;
248
+ var HTTP_BAD_GATEWAY = 502;
249
+ var HTTP_INTERNAL_SERVER_ERROR = 500;
250
+ var HTTP_SERVICE_UNAVAILABLE = 503;
251
+ var MAX_BODY_SIZE_BYTES = 1e4;
252
+ var HTTP_CLIENT_ERROR_START = 400;
253
+ var MAX_ACTOR_STRING_LENGTH = 255;
254
+ var MAX_TRACKING_ID_LENGTH = 255;
255
+ var SAFE_HEADER_PATTERN = /^[\x20-\x7E]+$/;
256
+ var ALLOWED_URL_SCHEMES = ["https://", "http://"];
257
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
258
+ var HttpClient = class {
259
+ #baseUrl;
260
+ #defaultHeaders;
261
+ #timeoutMs;
262
+ constructor(options) {
263
+ this.#baseUrl = options.baseUrl ?? "";
264
+ this.#defaultHeaders = { ...options.headers };
265
+ this.#timeoutMs = options.timeout;
266
+ Object.freeze(this);
267
+ }
268
+ /** Get default headers (returns a copy). */
269
+ get headers() {
270
+ return { ...this.#defaultHeaders };
271
+ }
272
+ /**
273
+ * Execute an HTTP request.
274
+ */
275
+ async request(method, url, options) {
276
+ let fullUrl = this.#baseUrl ? `${this.#baseUrl}${url}` : url;
277
+ if (options?.params && Object.keys(options.params).length > 0) {
278
+ const stringParams = {};
279
+ for (const [key, value] of Object.entries(options.params)) {
280
+ stringParams[key] = String(value);
281
+ }
282
+ const searchParams = new URLSearchParams(stringParams);
283
+ const separator = fullUrl.includes("?") ? "&" : "?";
284
+ fullUrl = `${fullUrl}${separator}${searchParams.toString()}`;
285
+ }
286
+ const mergedHeaders = {
287
+ ...this.#defaultHeaders,
288
+ ...options?.headers
289
+ };
290
+ const controller = new AbortController();
291
+ const timeoutId = setTimeout(() => {
292
+ controller.abort(new DOMException("The operation timed out.", "TimeoutError"));
293
+ }, this.#timeoutMs);
294
+ const init = {
295
+ method,
296
+ headers: mergedHeaders,
297
+ signal: controller.signal
298
+ };
299
+ if (options?.json !== void 0) {
300
+ init.body = JSON.stringify(options.json);
301
+ mergedHeaders["Content-Type"] = "application/json";
302
+ }
303
+ const fetchFn = _fetch ?? globalThis.fetch;
304
+ try {
305
+ return await fetchFn(fullUrl, init);
306
+ } finally {
307
+ clearTimeout(timeoutId);
308
+ }
309
+ }
310
+ /**
311
+ * Execute a POST request.
312
+ */
313
+ async post(url, options) {
314
+ return this.request("POST", url, options);
315
+ }
316
+ };
317
+ Object.freeze(HttpClient.prototype);
318
+ var AlterVault = class _AlterVault {
319
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
320
+ // SECURITY LAYER 4: ES2022 private fields — truly private at runtime.
321
+ // These are NOT accessible via (obj as any), Object.keys(), or prototype.
322
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
323
+ /** HTTP Client for Alter Backend (has x-api-key) */
324
+ #alterClient;
325
+ /** HTTP Client for External Provider APIs (NO x-api-key) */
326
+ #providerClient;
327
+ /** Cached actor_id from backend response (avoids DB lookup) */
328
+ #cachedActorId = null;
329
+ /** Whether close() has been called */
330
+ #closed = false;
331
+ /** Pending audit log promises (fire-and-forget) */
332
+ #auditPromises = /* @__PURE__ */ new Set();
333
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
334
+ // Public readonly properties (frozen by Object.freeze)
335
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
336
+ baseUrl;
337
+ enableAuditLogging;
338
+ /** Actor tracking configuration (readonly, not secret) */
339
+ #actorType;
340
+ #actorIdentifier;
341
+ #actorName;
342
+ #actorVersion;
343
+ #clientType;
344
+ #framework;
345
+ constructor(options) {
346
+ _AlterVault.#validateInitParams(
347
+ options.apiKey,
348
+ options.actorType,
349
+ options.actorIdentifier
350
+ );
351
+ const actorStrings = [
352
+ ["actorIdentifier", options.actorIdentifier],
353
+ ["actorName", options.actorName],
354
+ ["actorVersion", options.actorVersion],
355
+ ["clientType", options.clientType],
356
+ ["framework", options.framework]
357
+ ];
358
+ for (const [name, value] of actorStrings) {
359
+ _AlterVault.#validateActorString(value, name);
360
+ }
361
+ this.baseUrl = (options.baseUrl ?? "https://api.alter.com").replace(
362
+ /\/+$/,
363
+ ""
364
+ );
365
+ this.enableAuditLogging = options.enableAuditLogging ?? true;
366
+ const timeoutMs = options.timeout ?? 3e4;
367
+ this.#actorType = options.actorType;
368
+ this.#actorIdentifier = options.actorIdentifier;
369
+ this.#actorName = options.actorName;
370
+ this.#actorVersion = options.actorVersion;
371
+ this.#clientType = options.clientType;
372
+ this.#framework = options.framework;
373
+ if (!_fetch) {
374
+ _fetch = globalThis.fetch;
375
+ }
376
+ const alterHeaders = this.#buildAlterClientHeaders(options.apiKey);
377
+ this.#alterClient = new HttpClient({
378
+ baseUrl: this.baseUrl,
379
+ headers: alterHeaders,
380
+ timeout: timeoutMs
381
+ });
382
+ this.#providerClient = new HttpClient({
383
+ headers: {
384
+ "User-Agent": SDK_USER_AGENT
385
+ },
386
+ timeout: timeoutMs
387
+ });
388
+ Object.freeze(this);
389
+ }
390
+ /**
391
+ * Validate an actor string parameter for length and safe characters.
392
+ */
393
+ static #validateActorString(value, name, maxLength = MAX_ACTOR_STRING_LENGTH) {
394
+ if (value === void 0) {
395
+ return;
396
+ }
397
+ if (value.length > maxLength) {
398
+ throw new AlterSDKError(
399
+ `${name} exceeds max length (${maxLength}): ${value.length}`
400
+ );
401
+ }
402
+ if (!SAFE_HEADER_PATTERN.test(value)) {
403
+ throw new AlterSDKError(
404
+ `${name} contains invalid characters (only printable ASCII allowed)`
405
+ );
406
+ }
407
+ }
408
+ /**
409
+ * Validate constructor parameters. Throws Error on invalid input.
410
+ */
411
+ static #validateInitParams(apiKey, actorType, actorIdentifier) {
412
+ if (!apiKey) {
413
+ throw new AlterSDKError("api_key is required");
414
+ }
415
+ if (!apiKey.startsWith("alter_key_")) {
416
+ throw new AlterSDKError("api_key must start with 'alter_key_'");
417
+ }
418
+ if (actorType && !VALID_ACTOR_TYPES.includes(actorType)) {
419
+ throw new AlterSDKError("actor_type must be 'ai_agent' or 'mcp_server'");
420
+ }
421
+ if (actorType && !actorIdentifier) {
422
+ throw new AlterSDKError(
423
+ "actor_identifier is required when actor_type is set"
424
+ );
425
+ }
426
+ }
427
+ /**
428
+ * Build default headers for the Alter backend HTTP client.
429
+ *
430
+ * SECURITY: apiKey is passed as parameter, NOT read from this.
431
+ */
432
+ #buildAlterClientHeaders(apiKey) {
433
+ const headers = {
434
+ "x-api-key": apiKey,
435
+ "User-Agent": SDK_USER_AGENT
436
+ };
437
+ const actorHeaders = {
438
+ "X-Alter-Actor-Type": this.#actorType,
439
+ "X-Alter-Actor-Identifier": this.#actorIdentifier,
440
+ "X-Alter-Actor-Name": this.#actorName,
441
+ "X-Alter-Actor-Version": this.#actorVersion,
442
+ "X-Alter-Client-Type": this.#clientType,
443
+ "X-Alter-Framework": this.#framework
444
+ };
445
+ for (const [key, value] of Object.entries(actorHeaders)) {
446
+ if (value) {
447
+ headers[key] = value;
448
+ }
449
+ }
450
+ return headers;
451
+ }
452
+ /**
453
+ * Build per-request actor headers for instance tracking.
454
+ */
455
+ #getActorRequestHeaders(runId, threadId, toolCallId) {
456
+ const headers = {};
457
+ if (this.#cachedActorId) {
458
+ headers["X-Alter-Actor-ID"] = this.#cachedActorId;
459
+ }
460
+ if (runId) {
461
+ if (UUID_PATTERN.test(runId)) {
462
+ headers["X-Alter-Run-ID"] = runId;
463
+ } else {
464
+ console.warn(`Invalid run_id (must be UUID), skipping: ${runId}`);
465
+ }
466
+ }
467
+ if (threadId) {
468
+ if (threadId.length <= MAX_TRACKING_ID_LENGTH && SAFE_HEADER_PATTERN.test(threadId)) {
469
+ headers["X-Alter-Thread-ID"] = threadId;
470
+ } else {
471
+ console.warn(
472
+ "Invalid thread_id (too long or invalid chars), skipping"
473
+ );
474
+ }
475
+ }
476
+ if (toolCallId) {
477
+ if (toolCallId.length <= MAX_TRACKING_ID_LENGTH && SAFE_HEADER_PATTERN.test(toolCallId)) {
478
+ headers["X-Alter-Tool-Call-ID"] = toolCallId;
479
+ } else {
480
+ console.warn(
481
+ "Invalid tool_call_id (too long or invalid chars), skipping"
482
+ );
483
+ }
484
+ }
485
+ return headers;
486
+ }
487
+ /**
488
+ * Cache actor_id from response header for subsequent requests.
489
+ */
490
+ #cacheActorIdFromResponse(response) {
491
+ const actorId = response.headers.get("X-Alter-Actor-ID");
492
+ if (actorId && !this.#cachedActorId) {
493
+ if (!UUID_PATTERN.test(actorId)) {
494
+ console.warn(
495
+ `Invalid actor_id in response header (not a UUID), ignoring: ${actorId}`
496
+ );
497
+ return;
498
+ }
499
+ this.#cachedActorId = actorId;
500
+ } else if (actorId && this.#cachedActorId && actorId !== this.#cachedActorId) {
501
+ console.warn(
502
+ `Backend returned different actor_id (${actorId}) than cached (${this.#cachedActorId}), ignoring`
503
+ );
504
+ }
505
+ }
506
+ /**
507
+ * Check if an error is a timeout or abort error.
508
+ */
509
+ static #isTimeoutOrAbortError(error) {
510
+ if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
511
+ return true;
512
+ }
513
+ if (typeof DOMException !== "undefined" && error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError")) {
514
+ return true;
515
+ }
516
+ return false;
517
+ }
518
+ static async #safeParseJson(response) {
519
+ try {
520
+ return await response.clone().json();
521
+ } catch {
522
+ try {
523
+ const text = await response.clone().text();
524
+ return { message: text.slice(0, 500) || "Unknown error" };
525
+ } catch {
526
+ return { message: "Unknown error" };
527
+ }
528
+ }
529
+ }
530
+ /**
531
+ * Handle HTTP error responses from backend.
532
+ */
533
+ async #handleErrorResponse(response) {
534
+ if (response.ok) {
535
+ return;
536
+ }
537
+ if (response.status === HTTP_FORBIDDEN) {
538
+ const errorData = await _AlterVault.#safeParseJson(response);
539
+ throw new PolicyViolationError(
540
+ errorData.message ?? "Access denied by policy",
541
+ errorData.error,
542
+ errorData.details
543
+ );
544
+ }
545
+ if (response.status === HTTP_NOT_FOUND) {
546
+ const errorData = await _AlterVault.#safeParseJson(response);
547
+ throw new ConnectionNotFoundError(
548
+ errorData.message ?? "OAuth connection not found for these attributes",
549
+ errorData
550
+ );
551
+ }
552
+ if (response.status === HTTP_BAD_REQUEST || response.status === HTTP_BAD_GATEWAY) {
553
+ const errorData = await _AlterVault.#safeParseJson(response);
554
+ if (JSON.stringify(errorData).toLowerCase().includes("token_expired")) {
555
+ throw new TokenExpiredError(
556
+ errorData.message ?? "Token expired and refresh failed",
557
+ errorData.connection_id,
558
+ errorData
559
+ );
560
+ }
561
+ throw new TokenRetrievalError(
562
+ errorData.message ?? `Backend error ${response.status}`,
563
+ errorData
564
+ );
565
+ }
566
+ if (response.status === HTTP_UNAUTHORIZED) {
567
+ const errorData = await _AlterVault.#safeParseJson(response);
568
+ throw new TokenRetrievalError(
569
+ errorData.message ?? "Unauthorized \u2014 check your API key",
570
+ errorData
571
+ );
572
+ }
573
+ if (response.status === HTTP_INTERNAL_SERVER_ERROR || response.status === HTTP_SERVICE_UNAVAILABLE) {
574
+ const errorData = await _AlterVault.#safeParseJson(response);
575
+ throw new TokenRetrievalError(
576
+ errorData.message ?? `Backend unavailable (HTTP ${response.status})`,
577
+ errorData
578
+ );
579
+ }
580
+ if (response.status >= HTTP_CLIENT_ERROR_START) {
581
+ const errorData = await _AlterVault.#safeParseJson(response);
582
+ throw new TokenRetrievalError(
583
+ errorData.message ?? `Unexpected backend error (HTTP ${response.status})`,
584
+ errorData
585
+ );
586
+ }
587
+ }
588
+ /**
589
+ * Retrieve OAuth access token for a provider and user (INTERNAL USE ONLY).
590
+ *
591
+ * This is a private method. Tokens are NEVER exposed to developers.
592
+ * Use request() instead, which handles tokens internally.
593
+ */
594
+ async #getToken(providerId, attributes, reason, runId, threadId, toolCallId) {
595
+ const actorHeaders = this.#getActorRequestHeaders(
596
+ runId,
597
+ threadId,
598
+ toolCallId
599
+ );
600
+ let response;
601
+ try {
602
+ response = await this.#alterClient.post("/oauth/token", {
603
+ json: {
604
+ provider_id: providerId,
605
+ attributes,
606
+ reason: reason ?? null
607
+ },
608
+ headers: actorHeaders
609
+ });
610
+ } catch (error) {
611
+ if (_AlterVault.#isTimeoutOrAbortError(error)) {
612
+ throw new TimeoutError(
613
+ `Request to Alter Vault backend timed out: ${error instanceof Error ? error.message : String(error)}`,
614
+ { base_url: this.baseUrl }
615
+ );
616
+ }
617
+ if (error instanceof TypeError) {
618
+ throw new NetworkError(
619
+ `Failed to connect to Alter Vault backend: ${error.message}`,
620
+ { base_url: this.baseUrl }
621
+ );
622
+ }
623
+ throw new TokenRetrievalError(
624
+ `Failed to retrieve token: ${error instanceof Error ? error.message : String(error)}`,
625
+ { provider_id: providerId, error: String(error) }
626
+ );
627
+ }
628
+ this.#cacheActorIdFromResponse(response);
629
+ await this.#handleErrorResponse(response);
630
+ const tokenData = await response.json();
631
+ const typedData = tokenData;
632
+ const tokenResponse = new TokenResponse(typedData);
633
+ _storeAccessToken(tokenResponse, typedData.access_token);
634
+ return tokenResponse;
635
+ }
636
+ /**
637
+ * Log an API call to the backend audit endpoint (INTERNAL).
638
+ *
639
+ * This method NEVER throws exceptions to avoid crashing the application
640
+ * if audit logging fails.
641
+ */
642
+ async #logApiCall(params) {
643
+ if (!this.enableAuditLogging) {
644
+ return;
645
+ }
646
+ try {
647
+ const auditLog = new APICallAuditLog({
648
+ connectionId: params.connectionId,
649
+ providerId: params.providerId,
650
+ method: params.method,
651
+ url: params.url,
652
+ requestHeaders: params.requestHeaders,
653
+ requestBody: params.requestBody,
654
+ responseStatus: params.responseStatus,
655
+ responseHeaders: params.responseHeaders,
656
+ responseBody: params.responseBody,
657
+ latencyMs: params.latencyMs,
658
+ reason: params.reason,
659
+ runId: params.runId,
660
+ threadId: params.threadId,
661
+ toolCallId: params.toolCallId
662
+ });
663
+ const sanitized = auditLog.sanitize();
664
+ const actorHeaders = this.#getActorRequestHeaders();
665
+ const response = await this.#alterClient.post("/oauth/audit/api-call", {
666
+ json: sanitized,
667
+ headers: actorHeaders
668
+ });
669
+ this.#cacheActorIdFromResponse(response);
670
+ if (!response.ok) {
671
+ console.warn(
672
+ `Audit log failed with status ${response.status} (non-fatal)`
673
+ );
674
+ }
675
+ } catch (error) {
676
+ console.warn(
677
+ `Failed to log API call (non-fatal): ${error instanceof Error ? error.message : String(error)}`
678
+ );
679
+ }
680
+ }
681
+ /**
682
+ * Extract response body for audit logging without decoding large/binary payloads.
683
+ */
684
+ static async #safeResponseBody(response, maxBytes = MAX_BODY_SIZE_BYTES) {
685
+ try {
686
+ const cloned = response.clone();
687
+ const buffer = await cloned.arrayBuffer();
688
+ const contentLength = buffer.byteLength;
689
+ if (contentLength === 0) {
690
+ return "";
691
+ }
692
+ if (contentLength > maxBytes) {
693
+ return `<response too large: ${contentLength} bytes>`;
694
+ }
695
+ try {
696
+ const decoder = new TextDecoder("utf-8", { fatal: true });
697
+ const text = decoder.decode(buffer);
698
+ if (text.length > maxBytes) {
699
+ return text.slice(0, maxBytes) + "...";
700
+ }
701
+ return text;
702
+ } catch {
703
+ return `<binary response: ${contentLength} bytes>`;
704
+ }
705
+ } catch {
706
+ return "";
707
+ }
708
+ }
709
+ /**
710
+ * Schedule audit logging as a fire-and-forget background task.
711
+ */
712
+ #scheduleAuditLog(params) {
713
+ try {
714
+ const promise = this.#logApiCall(params).catch(() => {
715
+ });
716
+ this.#auditPromises.add(promise);
717
+ promise.finally(() => {
718
+ this.#auditPromises.delete(promise);
719
+ });
720
+ } catch {
721
+ }
722
+ }
723
+ /**
724
+ * Execute an HTTP request to a provider API with automatic token injection.
725
+ *
726
+ * This is the single entry point for all provider API calls. The SDK:
727
+ * 1. Fetches an OAuth token from Alter backend (never exposed)
728
+ * 2. Injects the token as a Bearer header
729
+ * 3. Calls the provider API
730
+ * 4. Logs the call for audit (if enabled, fire-and-forget)
731
+ * 5. Returns the raw response
732
+ */
733
+ async request(provider, method, url, options) {
734
+ if (this.#closed) {
735
+ throw new AlterSDKError(
736
+ "SDK instance has been closed. Create a new AlterVault instance to make requests."
737
+ );
738
+ }
739
+ const providerStr = String(provider);
740
+ const methodStr = String(method).toUpperCase();
741
+ const urlLower = url.toLowerCase();
742
+ if (!ALLOWED_URL_SCHEMES.some((scheme) => urlLower.startsWith(scheme))) {
743
+ throw new AlterSDKError(
744
+ `URL must start with https:// or http://, got: ${url.slice(0, 50)}`
745
+ );
746
+ }
747
+ if (options.pathParams && Object.keys(options.pathParams).length > 0) {
748
+ const encodedParams = {};
749
+ for (const [key, value] of Object.entries(options.pathParams)) {
750
+ encodedParams[key] = encodeURIComponent(String(value));
751
+ }
752
+ try {
753
+ let resolvedUrl = url;
754
+ for (const [key, value] of Object.entries(encodedParams)) {
755
+ const placeholder = `{${key}}`;
756
+ if (!resolvedUrl.includes(placeholder)) {
757
+ continue;
758
+ }
759
+ resolvedUrl = resolvedUrl.replaceAll(placeholder, value);
760
+ }
761
+ const remaining = resolvedUrl.match(/\{(\w+)\}/);
762
+ if (remaining) {
763
+ throw new AlterSDKError(
764
+ `Invalid URL template or missing path_params: '${remaining[1]}'. URL: ${url}, path_params: ${JSON.stringify(options.pathParams)}`
765
+ );
766
+ }
767
+ url = resolvedUrl;
768
+ } catch (error) {
769
+ if (error instanceof AlterSDKError) {
770
+ throw error;
771
+ }
772
+ throw new AlterSDKError(
773
+ `Invalid URL template or missing path_params: ${error instanceof Error ? error.message : String(error)}. URL: ${url}, path_params: ${JSON.stringify(options.pathParams)}`
774
+ );
775
+ }
776
+ }
777
+ if (options.extraHeaders && "Authorization" in options.extraHeaders) {
778
+ console.warn(
779
+ "extraHeaders contains 'Authorization' which will be overwritten with the auto-injected Bearer token"
780
+ );
781
+ }
782
+ const tokenResponse = await this.#getToken(
783
+ providerStr,
784
+ options.user,
785
+ options.reason,
786
+ options.runId,
787
+ options.threadId,
788
+ options.toolCallId
789
+ );
790
+ const requestHeaders = options.extraHeaders ? { ...options.extraHeaders } : {};
791
+ requestHeaders["Authorization"] = `Bearer ${_extractAccessToken(tokenResponse)}`;
792
+ if (!requestHeaders["User-Agent"]) {
793
+ requestHeaders["User-Agent"] = SDK_USER_AGENT;
794
+ }
795
+ const startTime = Date.now();
796
+ let response;
797
+ try {
798
+ response = await this.#providerClient.request(methodStr, url, {
799
+ json: options.json,
800
+ headers: requestHeaders,
801
+ params: options.queryParams
802
+ });
803
+ } catch (error) {
804
+ if (_AlterVault.#isTimeoutOrAbortError(error)) {
805
+ throw new TimeoutError(
806
+ `Provider API request timed out: ${error instanceof Error ? error.message : String(error)}`,
807
+ {
808
+ provider: providerStr,
809
+ method: methodStr,
810
+ url
811
+ }
812
+ );
813
+ }
814
+ throw new NetworkError(
815
+ `Failed to call provider API: ${error instanceof Error ? error.message : String(error)}`,
816
+ {
817
+ provider: providerStr,
818
+ method: methodStr,
819
+ url,
820
+ error: String(error)
821
+ }
822
+ );
823
+ }
824
+ const latencyMs = Date.now() - startTime;
825
+ const auditHeaders = {};
826
+ for (const [key, value] of Object.entries(requestHeaders)) {
827
+ if (key.toLowerCase() !== "authorization") {
828
+ auditHeaders[key] = value;
829
+ }
830
+ }
831
+ const responseBody = await _AlterVault.#safeResponseBody(response);
832
+ const responseHeaders = {};
833
+ response.headers.forEach((value, key) => {
834
+ responseHeaders[key] = value;
835
+ });
836
+ this.#scheduleAuditLog({
837
+ connectionId: tokenResponse.connectionId,
838
+ providerId: providerStr,
839
+ method: methodStr,
840
+ url,
841
+ requestHeaders: auditHeaders,
842
+ requestBody: options.json ?? null,
843
+ responseStatus: response.status,
844
+ responseHeaders,
845
+ responseBody,
846
+ latencyMs,
847
+ reason: options.reason ?? null,
848
+ runId: options.runId ?? null,
849
+ threadId: options.threadId ?? null,
850
+ toolCallId: options.toolCallId ?? null
851
+ });
852
+ if (response.status >= HTTP_CLIENT_ERROR_START) {
853
+ throw new ProviderAPIError(
854
+ `Provider API returned error ${response.status}`,
855
+ response.status,
856
+ responseBody,
857
+ {
858
+ provider: providerStr,
859
+ method: methodStr,
860
+ url
861
+ }
862
+ );
863
+ }
864
+ return response;
865
+ }
866
+ /**
867
+ * Close HTTP clients and release resources.
868
+ * Waits for any pending audit tasks before closing.
869
+ */
870
+ async close() {
871
+ if (this.#closed) {
872
+ return;
873
+ }
874
+ this.#closed = true;
875
+ if (this.#auditPromises.size > 0) {
876
+ await Promise.allSettled([...this.#auditPromises]);
877
+ }
878
+ }
879
+ /**
880
+ * Async dispose support for `await using vault = new AlterVault(...)`.
881
+ * Requires TypeScript 5.2+ and Node.js 18+.
882
+ */
883
+ async [Symbol.asyncDispose]() {
884
+ await this.close();
885
+ }
886
+ };
887
+ Object.freeze(AlterVault.prototype);
888
+
889
+ // src/providers/enums.ts
890
+ var Provider = /* @__PURE__ */ ((Provider2) => {
891
+ Provider2["GOOGLE"] = "google";
892
+ Provider2["GITHUB"] = "github";
893
+ Provider2["SLACK"] = "slack";
894
+ Provider2["MICROSOFT"] = "microsoft";
895
+ Provider2["SALESFORCE"] = "salesforce";
896
+ Provider2["SENTRY"] = "sentry";
897
+ return Provider2;
898
+ })(Provider || {});
899
+ var HttpMethod = /* @__PURE__ */ ((HttpMethod2) => {
900
+ HttpMethod2["GET"] = "GET";
901
+ HttpMethod2["POST"] = "POST";
902
+ HttpMethod2["PUT"] = "PUT";
903
+ HttpMethod2["PATCH"] = "PATCH";
904
+ HttpMethod2["DELETE"] = "DELETE";
905
+ HttpMethod2["HEAD"] = "HEAD";
906
+ HttpMethod2["OPTIONS"] = "OPTIONS";
907
+ return HttpMethod2;
908
+ })(HttpMethod || {});
909
+ export {
910
+ APICallAuditLog,
911
+ AlterSDKError,
912
+ AlterVault,
913
+ ConnectionNotFoundError,
914
+ HttpMethod,
915
+ NetworkError,
916
+ PolicyViolationError,
917
+ Provider,
918
+ ProviderAPIError,
919
+ TimeoutError,
920
+ TokenExpiredError,
921
+ TokenResponse,
922
+ TokenRetrievalError
923
+ };