@alter-ai/alter-sdk 0.3.1 → 0.5.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/client.ts
2
- import { createHash, createHmac, randomUUID } from "crypto";
2
+ import { createHash as createHash2, createHmac as createHmac2, randomUUID } from "crypto";
3
3
 
4
4
  // src/exceptions.ts
5
5
  var AlterSDKError = class extends Error {
@@ -17,13 +17,45 @@ var AlterSDKError = class extends Error {
17
17
  return this.message;
18
18
  }
19
19
  };
20
- var TokenRetrievalError = class extends AlterSDKError {
20
+ var BackendError = class extends AlterSDKError {
21
21
  constructor(message, details) {
22
22
  super(message, details);
23
- this.name = "TokenRetrievalError";
23
+ this.name = "BackendError";
24
24
  }
25
25
  };
26
- var PolicyViolationError = class extends TokenRetrievalError {
26
+ var ReAuthRequiredError = class extends BackendError {
27
+ constructor(message, details) {
28
+ super(message, details);
29
+ this.name = "ReAuthRequiredError";
30
+ }
31
+ };
32
+ var ConnectionExpiredError = class extends ReAuthRequiredError {
33
+ constructor(message, details) {
34
+ super(message, details);
35
+ this.name = "ConnectionExpiredError";
36
+ }
37
+ };
38
+ var ConnectionRevokedError = class extends ReAuthRequiredError {
39
+ connectionId;
40
+ constructor(message, connectionId, details) {
41
+ super(message, details);
42
+ this.name = "ConnectionRevokedError";
43
+ this.connectionId = connectionId;
44
+ }
45
+ };
46
+ var ConnectionDeletedError = class extends ReAuthRequiredError {
47
+ constructor(message, details) {
48
+ super(message, details);
49
+ this.name = "ConnectionDeletedError";
50
+ }
51
+ };
52
+ var ConnectionNotFoundError = class extends BackendError {
53
+ constructor(message, details) {
54
+ super(message, details);
55
+ this.name = "ConnectionNotFoundError";
56
+ }
57
+ };
58
+ var PolicyViolationError = class extends BackendError {
27
59
  policyError;
28
60
  constructor(message, policyError, details) {
29
61
  super(message, details);
@@ -31,18 +63,28 @@ var PolicyViolationError = class extends TokenRetrievalError {
31
63
  this.policyError = policyError;
32
64
  }
33
65
  };
34
- var ConnectionNotFoundError = class extends TokenRetrievalError {
66
+ var ConnectFlowError = class extends AlterSDKError {
35
67
  constructor(message, details) {
36
68
  super(message, details);
37
- this.name = "ConnectionNotFoundError";
69
+ this.name = "ConnectFlowError";
38
70
  }
39
71
  };
40
- var TokenExpiredError = class extends TokenRetrievalError {
41
- connectionId;
42
- constructor(message, connectionId, details) {
72
+ var ConnectDeniedError = class extends ConnectFlowError {
73
+ constructor(message, details) {
43
74
  super(message, details);
44
- this.name = "TokenExpiredError";
45
- this.connectionId = connectionId;
75
+ this.name = "ConnectDeniedError";
76
+ }
77
+ };
78
+ var ConnectConfigError = class extends ConnectFlowError {
79
+ constructor(message, details) {
80
+ super(message, details);
81
+ this.name = "ConnectConfigError";
82
+ }
83
+ };
84
+ var ConnectTimeoutError = class extends ConnectFlowError {
85
+ constructor(message, details) {
86
+ super(message, details);
87
+ this.name = "ConnectTimeoutError";
46
88
  }
47
89
  };
48
90
  var ProviderAPIError = class extends AlterSDKError {
@@ -55,6 +97,16 @@ var ProviderAPIError = class extends AlterSDKError {
55
97
  this.responseBody = responseBody;
56
98
  }
57
99
  };
100
+ var ScopeReauthRequiredError = class extends ProviderAPIError {
101
+ connectionId;
102
+ providerId;
103
+ constructor(message, connectionId, providerId, statusCode, responseBody, details) {
104
+ super(message, statusCode, responseBody, details);
105
+ this.name = "ScopeReauthRequiredError";
106
+ this.connectionId = connectionId;
107
+ this.providerId = providerId;
108
+ }
109
+ };
58
110
  var NetworkError = class extends AlterSDKError {
59
111
  constructor(message, details) {
60
112
  super(message, details);
@@ -92,6 +144,10 @@ var TokenResponse = class _TokenResponse {
92
144
  injectionHeader;
93
145
  /** Header value format with {token} placeholder (e.g., "Bearer {token}", "{token}") */
94
146
  injectionFormat;
147
+ /** Extra credentials for multi-part auth (e.g. AWS SigV4 secret_key, region) */
148
+ additionalCredentials;
149
+ /** Additional injection rules for multi-header or query param auth */
150
+ additionalInjections;
95
151
  constructor(data) {
96
152
  this.tokenType = data.token_type ?? "Bearer";
97
153
  this.expiresIn = data.expires_in ?? null;
@@ -101,6 +157,13 @@ var TokenResponse = class _TokenResponse {
101
157
  this.providerId = data.provider_id ?? "";
102
158
  this.injectionHeader = data.injection_header ?? "Authorization";
103
159
  this.injectionFormat = data.injection_format ?? "Bearer {token}";
160
+ if (data.additional_credentials) {
161
+ const { secret_key: _, ...safeCredentials } = data.additional_credentials;
162
+ this.additionalCredentials = Object.keys(safeCredentials).length > 0 ? safeCredentials : null;
163
+ } else {
164
+ this.additionalCredentials = null;
165
+ }
166
+ this.additionalInjections = data.additional_injections ?? null;
104
167
  Object.freeze(this);
105
168
  }
106
169
  /**
@@ -169,6 +232,7 @@ var ConnectionInfo = class {
169
232
  accountIdentifier;
170
233
  accountDisplayName;
171
234
  status;
235
+ scopeMismatch;
172
236
  expiresAt;
173
237
  createdAt;
174
238
  lastUsedAt;
@@ -179,6 +243,7 @@ var ConnectionInfo = class {
179
243
  this.accountIdentifier = data.account_identifier ?? null;
180
244
  this.accountDisplayName = data.account_display_name ?? null;
181
245
  this.status = data.status;
246
+ this.scopeMismatch = data.scope_mismatch ?? false;
182
247
  this.expiresAt = data.expires_at ?? null;
183
248
  this.createdAt = data.created_at;
184
249
  this.lastUsedAt = data.last_used_at ?? null;
@@ -192,6 +257,7 @@ var ConnectionInfo = class {
192
257
  account_identifier: this.accountIdentifier,
193
258
  account_display_name: this.accountDisplayName,
194
259
  status: this.status,
260
+ scope_mismatch: this.scopeMismatch,
195
261
  expires_at: this.expiresAt,
196
262
  created_at: this.createdAt,
197
263
  last_used_at: this.lastUsedAt
@@ -240,12 +306,41 @@ var ConnectionListResult = class {
240
306
  Object.freeze(this);
241
307
  }
242
308
  };
309
+ var ConnectResult = class {
310
+ connectionId;
311
+ providerId;
312
+ accountIdentifier;
313
+ scopes;
314
+ connectionPolicy;
315
+ constructor(data) {
316
+ this.connectionId = data.connection_id;
317
+ this.providerId = data.provider_id;
318
+ this.accountIdentifier = data.account_identifier ?? null;
319
+ this.scopes = data.scopes ?? [];
320
+ this.connectionPolicy = data.connection_policy ?? null;
321
+ Object.freeze(this);
322
+ }
323
+ toJSON() {
324
+ return {
325
+ connection_id: this.connectionId,
326
+ provider_id: this.providerId,
327
+ account_identifier: this.accountIdentifier,
328
+ scopes: this.scopes,
329
+ connection_policy: this.connectionPolicy
330
+ };
331
+ }
332
+ toString() {
333
+ return `ConnectResult(connection_id=${this.connectionId}, provider=${this.providerId})`;
334
+ }
335
+ };
243
336
  var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
244
337
  "authorization",
245
338
  "cookie",
246
339
  "set-cookie",
247
340
  "x-api-key",
248
- "x-auth-token"
341
+ "x-auth-token",
342
+ "x-amz-date",
343
+ "x-amz-content-sha256"
249
344
  ]);
250
345
  var APICallAuditLog = class {
251
346
  connectionId;
@@ -319,11 +414,171 @@ var APICallAuditLog = class {
319
414
  }
320
415
  };
321
416
 
417
+ // src/aws-sig-v4.ts
418
+ import { createHash, createHmac } from "crypto";
419
+ var ALGORITHM = "AWS4-HMAC-SHA256";
420
+ var AWS_HOST_RE = /^(?<service>[a-z0-9-]+)\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.amazonaws\.com$/;
421
+ var S3_VIRTUAL_HOST_RE = /^[^.]+\.s3\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.amazonaws\.com$/;
422
+ function hmacSha256(key, message) {
423
+ return createHmac("sha256", key).update(message, "utf8").digest();
424
+ }
425
+ function sha256Hex(data) {
426
+ const hash = createHash("sha256");
427
+ if (typeof data === "string") {
428
+ hash.update(data, "utf8");
429
+ } else {
430
+ hash.update(data);
431
+ }
432
+ return hash.digest("hex");
433
+ }
434
+ function detectServiceAndRegion(hostname) {
435
+ const lower = hostname.toLowerCase();
436
+ const m = AWS_HOST_RE.exec(lower);
437
+ if (m?.groups) {
438
+ return { service: m.groups.service, region: m.groups.region };
439
+ }
440
+ const s3m = S3_VIRTUAL_HOST_RE.exec(lower);
441
+ if (s3m?.groups) {
442
+ return { service: "s3", region: s3m.groups.region };
443
+ }
444
+ return { service: null, region: null };
445
+ }
446
+ function deriveSigningKey(secretKey, dateStamp, region, service) {
447
+ const kDate = hmacSha256(Buffer.from("AWS4" + secretKey, "utf8"), dateStamp);
448
+ const kRegion = hmacSha256(kDate, region);
449
+ const kService = hmacSha256(kRegion, service);
450
+ const kSigning = hmacSha256(kService, "aws4_request");
451
+ return kSigning;
452
+ }
453
+ function canonicalUri(path) {
454
+ if (!path) return "/";
455
+ if (!path.startsWith("/")) path = "/" + path;
456
+ const segments = path.split("/");
457
+ return segments.map(
458
+ (seg) => encodeURIComponent(seg).replace(
459
+ /[!'()*]/g,
460
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
461
+ )
462
+ ).join("/");
463
+ }
464
+ function canonicalQueryString(query) {
465
+ if (!query) return "";
466
+ const sorted = [];
467
+ for (const pair of query.split("&")) {
468
+ const eqIdx = pair.indexOf("=");
469
+ if (eqIdx === -1) {
470
+ sorted.push([decodeURIComponent(pair), ""]);
471
+ } else {
472
+ sorted.push([
473
+ decodeURIComponent(pair.slice(0, eqIdx)),
474
+ decodeURIComponent(pair.slice(eqIdx + 1))
475
+ ]);
476
+ }
477
+ }
478
+ sorted.sort((a, b) => {
479
+ if (a[0] < b[0]) return -1;
480
+ if (a[0] > b[0]) return 1;
481
+ if (a[1] < b[1]) return -1;
482
+ if (a[1] > b[1]) return 1;
483
+ return 0;
484
+ });
485
+ const sigv4Encode = (s) => encodeURIComponent(s).replace(
486
+ /[!'()*]/g,
487
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
488
+ );
489
+ return sorted.map(([k, v]) => `${sigv4Encode(k)}=${sigv4Encode(v)}`).join("&");
490
+ }
491
+ function canonicalHeadersAndSigned(headers) {
492
+ const canonical = {};
493
+ for (const [name, value] of Object.entries(headers)) {
494
+ const lowerName = name.toLowerCase().trim();
495
+ const trimmedValue = value.replace(/\s+/g, " ").trim();
496
+ canonical[lowerName] = trimmedValue;
497
+ }
498
+ const sortedNames = Object.keys(canonical).sort();
499
+ const canonicalStr = sortedNames.map((name) => `${name}:${canonical[name]}
500
+ `).join("");
501
+ const signedStr = sortedNames.join(";");
502
+ return { canonicalHeaders: canonicalStr, signedHeaders: signedStr };
503
+ }
504
+ function signAwsRequest(opts) {
505
+ const parsed = new URL(opts.url);
506
+ const hostname = parsed.hostname;
507
+ let { region, service } = opts;
508
+ if (region == null || service == null) {
509
+ const detected = detectServiceAndRegion(hostname);
510
+ if (region == null) region = detected.region;
511
+ if (service == null) service = detected.service;
512
+ }
513
+ if (!region) {
514
+ throw new Error(
515
+ `Cannot determine AWS region from URL '${opts.url}'. Pass region explicitly via additional_credentials.`
516
+ );
517
+ }
518
+ if (!service) {
519
+ throw new Error(
520
+ `Cannot determine AWS service from URL '${opts.url}'. Pass service explicitly via additional_credentials.`
521
+ );
522
+ }
523
+ const timestamp = opts.timestamp ?? /* @__PURE__ */ new Date();
524
+ const amzDate = timestamp.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
525
+ const dateStamp = amzDate.slice(0, 8);
526
+ const bodyBytes = opts.body != null ? typeof opts.body === "string" ? Buffer.from(opts.body, "utf8") : opts.body : Buffer.alloc(0);
527
+ const payloadHash = sha256Hex(bodyBytes);
528
+ const headersToSign = {};
529
+ const hasHost = Object.keys(opts.headers).some(
530
+ (k) => k.toLowerCase() === "host"
531
+ );
532
+ if (!hasHost) {
533
+ const port = parsed.port;
534
+ headersToSign["host"] = port && port !== "80" && port !== "443" ? `${hostname}:${port}` : hostname;
535
+ } else {
536
+ for (const [k, v] of Object.entries(opts.headers)) {
537
+ if (k.toLowerCase() === "host") {
538
+ headersToSign["host"] = v;
539
+ break;
540
+ }
541
+ }
542
+ }
543
+ headersToSign["x-amz-date"] = amzDate;
544
+ headersToSign["x-amz-content-sha256"] = payloadHash;
545
+ const canonicalUriStr = canonicalUri(parsed.pathname);
546
+ const canonicalQs = canonicalQueryString(parsed.search.replace(/^\?/, ""));
547
+ const { canonicalHeaders, signedHeaders } = canonicalHeadersAndSigned(headersToSign);
548
+ const canonicalRequest = [
549
+ opts.method.toUpperCase(),
550
+ canonicalUriStr,
551
+ canonicalQs,
552
+ canonicalHeaders,
553
+ signedHeaders,
554
+ payloadHash
555
+ ].join("\n");
556
+ const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
557
+ const stringToSign = [
558
+ ALGORITHM,
559
+ amzDate,
560
+ credentialScope,
561
+ sha256Hex(canonicalRequest)
562
+ ].join("\n");
563
+ const signingKey = deriveSigningKey(opts.secretKey, dateStamp, region, service);
564
+ const signature = createHmac("sha256", signingKey).update(stringToSign, "utf8").digest("hex");
565
+ const authorization = `${ALGORITHM} Credential=${opts.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
566
+ return {
567
+ Authorization: authorization,
568
+ "x-amz-date": amzDate,
569
+ "x-amz-content-sha256": payloadHash
570
+ };
571
+ }
572
+
322
573
  // src/client.ts
323
574
  var _tokenStore = /* @__PURE__ */ new WeakMap();
575
+ var _additionalCredsStore = /* @__PURE__ */ new WeakMap();
324
576
  function _storeAccessToken(token, accessToken) {
325
577
  _tokenStore.set(token, accessToken);
326
578
  }
579
+ function _storeAdditionalCredentials(token, creds) {
580
+ _additionalCredsStore.set(token, creds);
581
+ }
327
582
  function _extractAccessToken(token) {
328
583
  const value = _tokenStore.get(token);
329
584
  if (value === void 0) {
@@ -333,11 +588,15 @@ function _extractAccessToken(token) {
333
588
  }
334
589
  return value;
335
590
  }
591
+ function _extractAdditionalCredentials(token) {
592
+ return _additionalCredsStore.get(token);
593
+ }
336
594
  var _fetch;
337
- var SDK_VERSION = "0.3.0";
595
+ var SDK_VERSION = "0.5.0";
338
596
  var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
339
597
  var HTTP_FORBIDDEN = 403;
340
598
  var HTTP_NOT_FOUND = 404;
599
+ var HTTP_GONE = 410;
341
600
  var HTTP_BAD_REQUEST = 400;
342
601
  var HTTP_UNAUTHORIZED = 401;
343
602
  var HTTP_BAD_GATEWAY = 502;
@@ -391,7 +650,12 @@ var HttpClient = class {
391
650
  headers: mergedHeaders,
392
651
  signal: controller.signal
393
652
  };
394
- if (options?.json !== void 0) {
653
+ if (options?.body !== void 0) {
654
+ init.body = options.body;
655
+ if (!mergedHeaders["Content-Type"]) {
656
+ mergedHeaders["Content-Type"] = "application/json";
657
+ }
658
+ } else if (options?.json !== void 0) {
395
659
  init.body = JSON.stringify(options.json);
396
660
  mergedHeaders["Content-Type"] = "application/json";
397
661
  }
@@ -427,6 +691,8 @@ var AlterVault = class _AlterVault {
427
691
  #closed = false;
428
692
  /** Pending audit log promises (fire-and-forget) */
429
693
  #auditPromises = /* @__PURE__ */ new Set();
694
+ /** JWT identity resolution: callable that returns user token */
695
+ #userTokenGetter = null;
430
696
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
431
697
  // Public readonly properties (frozen by Object.freeze)
432
698
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -454,7 +720,7 @@ var AlterVault = class _AlterVault {
454
720
  for (const [name, value] of actorStrings) {
455
721
  _AlterVault.#validateActorString(value, name);
456
722
  }
457
- this.#hmacKey = createHmac("sha256", options.apiKey).update("alter-signing-v1").digest();
723
+ this.#hmacKey = createHmac2("sha256", options.apiKey).update("alter-signing-v1").digest();
458
724
  this.baseUrl = (process.env.ALTER_BASE_URL ?? "https://backend.alterai.dev").replace(/\/+$/, "");
459
725
  const timeoutMs = options.timeout ?? 3e4;
460
726
  this.#actorType = options.actorType;
@@ -462,6 +728,7 @@ var AlterVault = class _AlterVault {
462
728
  this.#actorName = options.actorName;
463
729
  this.#actorVersion = options.actorVersion;
464
730
  this.#clientType = options.clientType;
731
+ this.#userTokenGetter = options.userTokenGetter ?? null;
465
732
  this.#framework = options.framework;
466
733
  if (!_fetch) {
467
734
  _fetch = globalThis.fetch;
@@ -558,12 +825,12 @@ var AlterVault = class _AlterVault {
558
825
  */
559
826
  #computeHmacHeaders(method, path, body) {
560
827
  const timestamp = String(Math.floor(Date.now() / 1e3));
561
- const contentHash = createHash("sha256").update(body ?? "").digest("hex");
828
+ const contentHash = createHash2("sha256").update(body ?? "").digest("hex");
562
829
  const stringToSign = `${method.toUpperCase()}
563
830
  ${path}
564
831
  ${timestamp}
565
832
  ${contentHash}`;
566
- const signature = createHmac("sha256", this.#hmacKey).update(stringToSign).digest("hex");
833
+ const signature = createHmac2("sha256", this.#hmacKey).update(stringToSign).digest("hex");
567
834
  return {
568
835
  "X-Alter-Timestamp": timestamp,
569
836
  "X-Alter-Content-SHA256": contentHash,
@@ -636,6 +903,24 @@ ${contentHash}`;
636
903
  }
637
904
  return false;
638
905
  }
906
+ /**
907
+ * Resolve a value_source to the actual value for injection.
908
+ *
909
+ * @param valueSource - "token" or "additional_credentials.<field>"
910
+ * @param accessToken - The main credential value
911
+ * @param additionalCreds - Additional credentials from vault
912
+ * @returns The resolved value, or null if not available
913
+ */
914
+ static #resolveInjectionValue(valueSource, accessToken, additionalCreds) {
915
+ if (valueSource === "token") {
916
+ return accessToken;
917
+ }
918
+ if (valueSource.startsWith("additional_credentials.")) {
919
+ const field = valueSource.slice("additional_credentials.".length);
920
+ return additionalCreds?.[field] ?? null;
921
+ }
922
+ return null;
923
+ }
639
924
  static async #safeParseJson(response) {
640
925
  try {
641
926
  return await response.clone().json();
@@ -657,12 +942,25 @@ ${contentHash}`;
657
942
  }
658
943
  if (response.status === HTTP_FORBIDDEN) {
659
944
  const errorData = await _AlterVault.#safeParseJson(response);
945
+ if (errorData.error === "connection_expired") {
946
+ throw new ConnectionExpiredError(
947
+ errorData.message ?? "Connection expired per TTL policy",
948
+ errorData.details
949
+ );
950
+ }
660
951
  throw new PolicyViolationError(
661
952
  errorData.message ?? "Access denied by policy",
662
953
  errorData.error,
663
954
  errorData.details
664
955
  );
665
956
  }
957
+ if (response.status === HTTP_GONE) {
958
+ const errorData = await _AlterVault.#safeParseJson(response);
959
+ throw new ConnectionDeletedError(
960
+ errorData.message ?? "Connection has been deleted. A new connection_id will be issued on re-authorization.",
961
+ errorData
962
+ );
963
+ }
666
964
  if (response.status === HTTP_NOT_FOUND) {
667
965
  const errorData = await _AlterVault.#safeParseJson(response);
668
966
  throw new ConnectionNotFoundError(
@@ -672,35 +970,35 @@ ${contentHash}`;
672
970
  }
673
971
  if (response.status === HTTP_BAD_REQUEST || response.status === HTTP_BAD_GATEWAY) {
674
972
  const errorData = await _AlterVault.#safeParseJson(response);
675
- if (JSON.stringify(errorData).toLowerCase().includes("token_expired")) {
676
- throw new TokenExpiredError(
677
- errorData.message ?? "Token expired and refresh failed",
973
+ if (errorData.error === "connection_revoked") {
974
+ throw new ConnectionRevokedError(
975
+ errorData.message ?? "Connection has been revoked. User must re-authorize.",
678
976
  errorData.connection_id,
679
977
  errorData
680
978
  );
681
979
  }
682
- throw new TokenRetrievalError(
980
+ throw new BackendError(
683
981
  errorData.message ?? `Backend error ${response.status}`,
684
982
  errorData
685
983
  );
686
984
  }
687
985
  if (response.status === HTTP_UNAUTHORIZED) {
688
986
  const errorData = await _AlterVault.#safeParseJson(response);
689
- throw new TokenRetrievalError(
987
+ throw new BackendError(
690
988
  errorData.message ?? "Unauthorized \u2014 check your API key",
691
989
  errorData
692
990
  );
693
991
  }
694
992
  if (response.status === HTTP_INTERNAL_SERVER_ERROR || response.status === HTTP_SERVICE_UNAVAILABLE) {
695
993
  const errorData = await _AlterVault.#safeParseJson(response);
696
- throw new TokenRetrievalError(
994
+ throw new BackendError(
697
995
  errorData.message ?? `Backend unavailable (HTTP ${response.status})`,
698
996
  errorData
699
997
  );
700
998
  }
701
999
  if (response.status >= HTTP_CLIENT_ERROR_START) {
702
1000
  const errorData = await _AlterVault.#safeParseJson(response);
703
- throw new TokenRetrievalError(
1001
+ throw new BackendError(
704
1002
  errorData.message ?? `Unexpected backend error (HTTP ${response.status})`,
705
1003
  errorData
706
1004
  );
@@ -712,24 +1010,52 @@ ${contentHash}`;
712
1010
  * This is a private method. Tokens are NEVER exposed to developers.
713
1011
  * Use request() instead, which handles tokens internally.
714
1012
  */
715
- async #getToken(connectionId, reason, requestMetadata, runId, threadId, toolCallId) {
1013
+ async #getUserToken() {
1014
+ if (!this.#userTokenGetter) {
1015
+ throw new AlterSDKError(
1016
+ "userTokenGetter is required for provider-based resolution. Pass userTokenGetter to AlterVault constructor."
1017
+ );
1018
+ }
1019
+ const result = this.#userTokenGetter();
1020
+ const token = result instanceof Promise ? await result : result;
1021
+ if (!token || typeof token !== "string") {
1022
+ throw new AlterSDKError("userTokenGetter must return a non-empty string");
1023
+ }
1024
+ return token;
1025
+ }
1026
+ async #getToken(connectionId, reason, requestMetadata, runId, threadId, toolCallId, provider, account) {
716
1027
  const actorHeaders = this.#getActorRequestHeaders(
717
1028
  runId,
718
1029
  threadId,
719
1030
  toolCallId
720
1031
  );
721
1032
  let response;
722
- const tokenBody = {
723
- connection_id: connectionId,
724
- reason: reason ?? null,
725
- request: requestMetadata ?? null
726
- };
1033
+ let tokenBody;
1034
+ if (provider) {
1035
+ const userToken = await this.#getUserToken();
1036
+ tokenBody = {
1037
+ provider_id: provider,
1038
+ user_token: userToken,
1039
+ reason: reason ?? null,
1040
+ request: requestMetadata ?? null
1041
+ };
1042
+ if (account) {
1043
+ tokenBody.account = account;
1044
+ }
1045
+ } else {
1046
+ tokenBody = {
1047
+ connection_id: connectionId,
1048
+ reason: reason ?? null,
1049
+ request: requestMetadata ?? null
1050
+ };
1051
+ }
727
1052
  const tokenPath = "/sdk/token";
728
- const hmacHeaders = this.#computeHmacHeaders("POST", tokenPath, JSON.stringify(tokenBody));
1053
+ const tokenBodyStr = JSON.stringify(tokenBody);
1054
+ const hmacHeaders = this.#computeHmacHeaders("POST", tokenPath, tokenBodyStr);
729
1055
  try {
730
1056
  response = await this.#alterClient.post(tokenPath, {
731
- json: tokenBody,
732
- headers: { ...actorHeaders, ...hmacHeaders }
1057
+ body: tokenBodyStr,
1058
+ headers: { ...actorHeaders, ...hmacHeaders, "Content-Type": "application/json" }
733
1059
  });
734
1060
  } catch (error) {
735
1061
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
@@ -744,7 +1070,7 @@ ${contentHash}`;
744
1070
  { base_url: this.baseUrl }
745
1071
  );
746
1072
  }
747
- throw new TokenRetrievalError(
1073
+ throw new BackendError(
748
1074
  `Failed to retrieve token: ${error instanceof Error ? error.message : String(error)}`,
749
1075
  { connection_id: connectionId, error: String(error) }
750
1076
  );
@@ -753,21 +1079,25 @@ ${contentHash}`;
753
1079
  await this.#handleErrorResponse(response);
754
1080
  const tokenData = await response.json();
755
1081
  const typedData = tokenData;
1082
+ const scopeMismatch = typedData.scope_mismatch ?? false;
756
1083
  const tokenResponse = new TokenResponse(typedData);
757
1084
  if (!/^[A-Za-z][A-Za-z0-9-]*$/.test(tokenResponse.injectionHeader)) {
758
- throw new TokenRetrievalError(
1085
+ throw new BackendError(
759
1086
  `Backend returned invalid injection_header: ${tokenResponse.injectionHeader}`,
760
1087
  { connectionId: String(connectionId) }
761
1088
  );
762
1089
  }
763
1090
  if (/[\r\n\x00]/.test(tokenResponse.injectionFormat)) {
764
- throw new TokenRetrievalError(
1091
+ throw new BackendError(
765
1092
  `Backend returned invalid injection_format (contains control characters)`,
766
1093
  { connectionId: String(connectionId) }
767
1094
  );
768
1095
  }
769
1096
  _storeAccessToken(tokenResponse, typedData.access_token);
770
- return tokenResponse;
1097
+ if (typedData.additional_credentials) {
1098
+ _storeAdditionalCredentials(tokenResponse, typedData.additional_credentials);
1099
+ }
1100
+ return { tokenResponse, scopeMismatch };
771
1101
  }
772
1102
  /**
773
1103
  * Log an API call to the backend audit endpoint (INTERNAL).
@@ -797,10 +1127,11 @@ ${contentHash}`;
797
1127
  const actorHeaders = this.#getActorRequestHeaders(params.runId);
798
1128
  const auditPath = "/sdk/oauth/audit/api-call";
799
1129
  const auditBody = sanitized;
800
- const auditHmac = this.#computeHmacHeaders("POST", auditPath, JSON.stringify(auditBody));
1130
+ const auditBodyStr = JSON.stringify(auditBody);
1131
+ const auditHmac = this.#computeHmacHeaders("POST", auditPath, auditBodyStr);
801
1132
  const response = await this.#alterClient.post(auditPath, {
802
- json: auditBody,
803
- headers: { ...actorHeaders, ...auditHmac }
1133
+ body: auditBodyStr,
1134
+ headers: { ...actorHeaders, ...auditHmac, "Content-Type": "application/json" }
804
1135
  });
805
1136
  this.#cacheActorIdFromResponse(response);
806
1137
  if (!response.ok) {
@@ -872,12 +1203,22 @@ ${contentHash}`;
872
1203
  "SDK instance has been closed. Create a new AlterVault instance to make requests."
873
1204
  );
874
1205
  }
1206
+ const provider = options?.provider;
1207
+ const account = options?.account;
1208
+ if (!connectionId && !provider) {
1209
+ throw new AlterSDKError("Provide connectionId or options.provider");
1210
+ }
1211
+ if (connectionId && provider) {
1212
+ throw new AlterSDKError("Cannot provide both connectionId and options.provider");
1213
+ }
1214
+ const effectiveConnectionId = connectionId ?? null;
1215
+ let currentUrl = url;
875
1216
  const runId = options?.runId ?? randomUUID();
876
1217
  const methodStr = String(method).toUpperCase();
877
- const urlLower = url.toLowerCase();
1218
+ const urlLower = currentUrl.toLowerCase();
878
1219
  if (!ALLOWED_URL_SCHEMES.some((scheme) => urlLower.startsWith(scheme))) {
879
1220
  throw new AlterSDKError(
880
- `URL must start with https:// or http://, got: ${url.slice(0, 50)}`
1221
+ `URL must start with https:// or http://, got: ${currentUrl.slice(0, 50)}`
881
1222
  );
882
1223
  }
883
1224
  if (options?.pathParams && Object.keys(options.pathParams).length > 0) {
@@ -886,7 +1227,7 @@ ${contentHash}`;
886
1227
  encodedParams[key] = encodeURIComponent(String(value));
887
1228
  }
888
1229
  try {
889
- let resolvedUrl = url;
1230
+ let resolvedUrl = currentUrl;
890
1231
  for (const [key, value] of Object.entries(encodedParams)) {
891
1232
  const placeholder = `{${key}}`;
892
1233
  if (!resolvedUrl.includes(placeholder)) {
@@ -897,74 +1238,142 @@ ${contentHash}`;
897
1238
  const remaining = resolvedUrl.match(/\{(\w+)\}/);
898
1239
  if (remaining) {
899
1240
  throw new AlterSDKError(
900
- `Invalid URL template or missing path_params: '${remaining[1]}'. URL: ${url}, path_params: ${JSON.stringify(options.pathParams)}`
1241
+ `Invalid URL template or missing path_params: '${remaining[1]}'. URL: ${currentUrl}, path_params: ${JSON.stringify(options.pathParams)}`
901
1242
  );
902
1243
  }
903
- url = resolvedUrl;
1244
+ currentUrl = resolvedUrl;
904
1245
  } catch (error) {
905
1246
  if (error instanceof AlterSDKError) {
906
1247
  throw error;
907
1248
  }
908
1249
  throw new AlterSDKError(
909
- `Invalid URL template or missing path_params: ${error instanceof Error ? error.message : String(error)}. URL: ${url}, path_params: ${JSON.stringify(options.pathParams)}`
1250
+ `Invalid URL template or missing path_params: ${error instanceof Error ? error.message : String(error)}. URL: ${currentUrl}, path_params: ${JSON.stringify(options.pathParams)}`
910
1251
  );
911
1252
  }
912
1253
  }
913
- const tokenResponse = await this.#getToken(
914
- connectionId,
1254
+ const { tokenResponse, scopeMismatch } = await this.#getToken(
1255
+ effectiveConnectionId,
915
1256
  options?.reason,
916
- { method: methodStr, url },
1257
+ { method: methodStr, url: currentUrl },
917
1258
  runId,
918
1259
  options?.threadId,
919
- options?.toolCallId
1260
+ options?.toolCallId,
1261
+ provider,
1262
+ account
920
1263
  );
921
- const injectionHeaderLower = tokenResponse.injectionHeader.toLowerCase();
922
- if (options?.extraHeaders && Object.keys(options.extraHeaders).some(
923
- (k) => k.toLowerCase() === injectionHeaderLower
924
- )) {
925
- console.warn(
926
- `extraHeaders contains '${tokenResponse.injectionHeader}' which will be overwritten with the auto-injected credential`
927
- );
928
- }
929
1264
  const requestHeaders = options?.extraHeaders ? { ...options.extraHeaders } : {};
930
1265
  const accessToken = _extractAccessToken(tokenResponse);
931
- requestHeaders[tokenResponse.injectionHeader] = tokenResponse.injectionFormat.replace("{token}", accessToken);
1266
+ const injectionHeaderLower = tokenResponse.injectionHeader.toLowerCase();
1267
+ const additionalCreds = _extractAdditionalCredentials(tokenResponse);
1268
+ const isSigV4 = tokenResponse.injectionFormat.startsWith("AWS4-HMAC-SHA256") && additionalCreds != null;
1269
+ let sigv4BodyStr = null;
1270
+ if (isSigV4) {
1271
+ const accessKeyId = accessToken;
1272
+ if (options?.json != null) {
1273
+ sigv4BodyStr = JSON.stringify(options.json);
1274
+ if (!requestHeaders["Content-Type"]) {
1275
+ requestHeaders["Content-Type"] = "application/json";
1276
+ }
1277
+ }
1278
+ if (!additionalCreds.secret_key) {
1279
+ throw new BackendError(
1280
+ "AWS SigV4 credential is missing secret_key in additional_credentials. Re-store the credential with both Access Key ID and Secret Access Key.",
1281
+ { connection_id: effectiveConnectionId }
1282
+ );
1283
+ }
1284
+ const awsHeaders = signAwsRequest({
1285
+ method: methodStr,
1286
+ url: currentUrl,
1287
+ headers: requestHeaders,
1288
+ body: sigv4BodyStr,
1289
+ accessKeyId,
1290
+ secretKey: additionalCreds.secret_key,
1291
+ region: additionalCreds.region ?? null,
1292
+ service: additionalCreds.service ?? null
1293
+ });
1294
+ Object.assign(requestHeaders, awsHeaders);
1295
+ } else {
1296
+ if (options?.extraHeaders && Object.keys(options.extraHeaders).some(
1297
+ (k) => k.toLowerCase() === injectionHeaderLower
1298
+ )) {
1299
+ console.warn(
1300
+ `extraHeaders contains '${tokenResponse.injectionHeader}' which will be overwritten with the auto-injected credential`
1301
+ );
1302
+ }
1303
+ requestHeaders[tokenResponse.injectionHeader] = tokenResponse.injectionFormat.replace("{token}", accessToken);
1304
+ }
1305
+ const auditUrl = currentUrl;
1306
+ if (tokenResponse.additionalInjections && !isSigV4) {
1307
+ for (const rule of tokenResponse.additionalInjections) {
1308
+ const value = _AlterVault.#resolveInjectionValue(
1309
+ rule.value_source,
1310
+ accessToken,
1311
+ additionalCreds
1312
+ );
1313
+ if (!value) continue;
1314
+ if (!/^[A-Za-z][A-Za-z0-9\-_]*$/.test(rule.key)) continue;
1315
+ if (rule.target === "header") {
1316
+ requestHeaders[rule.key] = value;
1317
+ } else if (rule.target === "query_param") {
1318
+ const urlObj = new URL(currentUrl);
1319
+ urlObj.searchParams.set(rule.key, value);
1320
+ currentUrl = urlObj.toString();
1321
+ }
1322
+ }
1323
+ }
932
1324
  if (!requestHeaders["User-Agent"]) {
933
1325
  requestHeaders["User-Agent"] = SDK_USER_AGENT;
934
1326
  }
935
1327
  const startTime = Date.now();
936
1328
  let response;
937
1329
  try {
938
- response = await this.#providerClient.request(methodStr, url, {
939
- json: options?.json,
940
- headers: requestHeaders,
941
- params: options?.queryParams
942
- });
1330
+ if (isSigV4 && sigv4BodyStr != null) {
1331
+ response = await this.#providerClient.request(methodStr, currentUrl, {
1332
+ body: sigv4BodyStr,
1333
+ headers: requestHeaders,
1334
+ params: options?.queryParams
1335
+ });
1336
+ } else {
1337
+ response = await this.#providerClient.request(methodStr, currentUrl, {
1338
+ json: options?.json,
1339
+ headers: requestHeaders,
1340
+ params: options?.queryParams
1341
+ });
1342
+ }
943
1343
  } catch (error) {
944
1344
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
945
1345
  throw new TimeoutError(
946
1346
  `Provider API request timed out: ${error instanceof Error ? error.message : String(error)}`,
947
1347
  {
948
- connection_id: connectionId,
1348
+ connection_id: effectiveConnectionId,
949
1349
  method: methodStr,
950
- url
1350
+ url: currentUrl
951
1351
  }
952
1352
  );
953
1353
  }
954
1354
  throw new NetworkError(
955
1355
  `Failed to call provider API: ${error instanceof Error ? error.message : String(error)}`,
956
1356
  {
957
- connection_id: connectionId,
1357
+ connection_id: effectiveConnectionId,
958
1358
  method: methodStr,
959
- url,
1359
+ url: currentUrl,
960
1360
  error: String(error)
961
1361
  }
962
1362
  );
963
1363
  }
964
1364
  const latencyMs = Date.now() - startTime;
1365
+ const sigv4Sensitive = /* @__PURE__ */ new Set(["authorization", "x-amz-date", "x-amz-content-sha256"]);
1366
+ const stripHeaders = isSigV4 ? sigv4Sensitive : /* @__PURE__ */ new Set([injectionHeaderLower]);
1367
+ if (tokenResponse.additionalInjections && !isSigV4) {
1368
+ for (const rule of tokenResponse.additionalInjections) {
1369
+ if (rule.target === "header") {
1370
+ stripHeaders.add(rule.key.toLowerCase());
1371
+ }
1372
+ }
1373
+ }
965
1374
  const auditHeaders = {};
966
1375
  for (const [key, value] of Object.entries(requestHeaders)) {
967
- if (key.toLowerCase() !== injectionHeaderLower) {
1376
+ if (!stripHeaders.has(key.toLowerCase())) {
968
1377
  auditHeaders[key] = value;
969
1378
  }
970
1379
  }
@@ -975,9 +1384,9 @@ ${contentHash}`;
975
1384
  });
976
1385
  this.#scheduleAuditLog({
977
1386
  connectionId: tokenResponse.connectionId,
978
- providerId: tokenResponse.providerId || connectionId,
1387
+ providerId: tokenResponse.providerId || effectiveConnectionId || "",
979
1388
  method: methodStr,
980
- url,
1389
+ url: auditUrl,
981
1390
  requestHeaders: auditHeaders,
982
1391
  requestBody: options?.json ?? null,
983
1392
  responseStatus: response.status,
@@ -990,14 +1399,29 @@ ${contentHash}`;
990
1399
  toolCallId: options?.toolCallId ?? null
991
1400
  });
992
1401
  if (response.status >= HTTP_CLIENT_ERROR_START) {
1402
+ if (response.status === HTTP_FORBIDDEN && scopeMismatch) {
1403
+ throw new ScopeReauthRequiredError(
1404
+ "Provider API returned 403 and the connection's scopes don't match the provider config. The user needs to re-authorize to grant updated permissions.",
1405
+ tokenResponse.connectionId,
1406
+ tokenResponse.providerId,
1407
+ response.status,
1408
+ responseBody,
1409
+ {
1410
+ connection_id: tokenResponse.connectionId,
1411
+ provider_id: tokenResponse.providerId,
1412
+ method: methodStr,
1413
+ url: currentUrl
1414
+ }
1415
+ );
1416
+ }
993
1417
  throw new ProviderAPIError(
994
1418
  `Provider API returned error ${response.status}`,
995
1419
  response.status,
996
1420
  responseBody,
997
1421
  {
998
- connection_id: connectionId,
1422
+ connection_id: effectiveConnectionId,
999
1423
  method: methodStr,
1000
- url
1424
+ url: currentUrl
1001
1425
  }
1002
1426
  );
1003
1427
  }
@@ -1022,12 +1446,23 @@ ${contentHash}`;
1022
1446
  limit: options?.limit ?? 100,
1023
1447
  offset: options?.offset ?? 0
1024
1448
  };
1449
+ if (this.#userTokenGetter) {
1450
+ try {
1451
+ listBody.user_token = await this.#getUserToken();
1452
+ } catch (err) {
1453
+ console.warn(
1454
+ "user_token_getter failed in listConnections, falling back to un-scoped listing:",
1455
+ err instanceof Error ? err.message : String(err)
1456
+ );
1457
+ }
1458
+ }
1025
1459
  const listPath = "/sdk/oauth/connections/list";
1026
- const listHmac = this.#computeHmacHeaders("POST", listPath, JSON.stringify(listBody));
1460
+ const listBodyStr = JSON.stringify(listBody);
1461
+ const listHmac = this.#computeHmacHeaders("POST", listPath, listBodyStr);
1027
1462
  try {
1028
1463
  response = await this.#alterClient.post(listPath, {
1029
- json: listBody,
1030
- headers: { ...actorHeaders, ...listHmac }
1464
+ body: listBodyStr,
1465
+ headers: { ...actorHeaders, ...listHmac, "Content-Type": "application/json" }
1031
1466
  });
1032
1467
  } catch (error) {
1033
1468
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
@@ -1073,24 +1508,37 @@ ${contentHash}`;
1073
1508
  "SDK instance has been closed. Create a new AlterVault instance to make requests."
1074
1509
  );
1075
1510
  }
1076
- if (!options.endUser?.id) {
1077
- throw new AlterSDKError("endUser.id is required");
1078
- }
1079
1511
  const actorHeaders = this.#getActorRequestHeaders();
1080
1512
  let response;
1081
1513
  const sessionBody = {
1082
- end_user: options.endUser,
1083
1514
  allowed_providers: options.allowedProviders ?? null,
1084
1515
  return_url: options.returnUrl ?? null,
1085
1516
  allowed_origin: options.allowedOrigin ?? null,
1086
1517
  metadata: options.metadata ?? null
1087
1518
  };
1519
+ if (options.connectionPolicy) {
1520
+ sessionBody.connection_policy = {
1521
+ max_ttl_seconds: options.connectionPolicy.maxTtlSeconds ?? null,
1522
+ default_ttl_seconds: options.connectionPolicy.defaultTtlSeconds ?? null
1523
+ };
1524
+ }
1525
+ if (this.#userTokenGetter) {
1526
+ try {
1527
+ sessionBody.user_token = await this.#getUserToken();
1528
+ } catch (err) {
1529
+ console.warn(
1530
+ "userTokenGetter failed in createConnectSession, session will not have identity tagging:",
1531
+ err instanceof Error ? err.message : String(err)
1532
+ );
1533
+ }
1534
+ }
1088
1535
  const sessionPath = "/sdk/oauth/connect/session";
1089
- const sessionHmac = this.#computeHmacHeaders("POST", sessionPath, JSON.stringify(sessionBody));
1536
+ const sessionBodyStr = JSON.stringify(sessionBody);
1537
+ const sessionHmac = this.#computeHmacHeaders("POST", sessionPath, sessionBodyStr);
1090
1538
  try {
1091
1539
  response = await this.#alterClient.post(sessionPath, {
1092
- json: sessionBody,
1093
- headers: { ...actorHeaders, ...sessionHmac }
1540
+ body: sessionBodyStr,
1541
+ headers: { ...actorHeaders, ...sessionHmac, "Content-Type": "application/json" }
1094
1542
  });
1095
1543
  } catch (error) {
1096
1544
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
@@ -1114,6 +1562,143 @@ ${contentHash}`;
1114
1562
  const data = await response.json();
1115
1563
  return new ConnectSession(data);
1116
1564
  }
1565
+ /**
1566
+ * Open OAuth in the user's browser and wait for completion.
1567
+ *
1568
+ * This is the headless connect flow for CLI tools, scripts, and
1569
+ * server-side applications. It creates a Connect session, opens the
1570
+ * browser, and polls until the user completes OAuth.
1571
+ *
1572
+ * @param options - Connect options
1573
+ * @returns Array of ConnectResult objects (one per connected provider)
1574
+ * @throws ConnectTimeoutError if the user doesn't complete within timeout
1575
+ * @throws ConnectFlowError if the user denies or provider returns error
1576
+ * @throws AlterSDKError if SDK is closed or session creation fails
1577
+ */
1578
+ async connect(options) {
1579
+ if (this.#closed) {
1580
+ throw new AlterSDKError(
1581
+ "SDK instance has been closed. Create a new AlterVault instance to make requests."
1582
+ );
1583
+ }
1584
+ const timeout = options.timeout ?? 300;
1585
+ const pollInterval = options.pollInterval ?? 2;
1586
+ const openBrowser = options.openBrowser ?? true;
1587
+ const session = await this.createConnectSession({
1588
+ allowedProviders: options.providers,
1589
+ connectionPolicy: options.connectionPolicy
1590
+ });
1591
+ if (openBrowser) {
1592
+ try {
1593
+ const openModule = await import("open");
1594
+ const openFn = openModule.default;
1595
+ if (openFn) {
1596
+ await openFn(session.connectUrl);
1597
+ } else {
1598
+ console.log(
1599
+ `Open this URL to authorize: ${session.connectUrl}`
1600
+ );
1601
+ }
1602
+ } catch {
1603
+ console.log(
1604
+ `Open this URL to authorize: ${session.connectUrl}`
1605
+ );
1606
+ }
1607
+ } else {
1608
+ console.log(
1609
+ `Open this URL to authorize: ${session.connectUrl}`
1610
+ );
1611
+ }
1612
+ const deadline = Date.now() + timeout * 1e3;
1613
+ while (Date.now() < deadline) {
1614
+ await new Promise(
1615
+ (resolve) => setTimeout(resolve, pollInterval * 1e3)
1616
+ );
1617
+ const pollResult = await this.#pollSession(session.sessionToken);
1618
+ const pollStatus = pollResult.status;
1619
+ if (pollStatus === "completed") {
1620
+ const connectionsData = pollResult.connections ?? [];
1621
+ return connectionsData.map(
1622
+ (conn) => new ConnectResult({
1623
+ connection_id: conn.connection_id ?? "",
1624
+ provider_id: conn.provider_id ?? "",
1625
+ account_identifier: conn.account_identifier ?? null,
1626
+ scopes: conn.scopes ?? [],
1627
+ connection_policy: conn.connection_policy ?? null
1628
+ })
1629
+ );
1630
+ }
1631
+ if (pollStatus === "error") {
1632
+ const err = pollResult.error;
1633
+ const errorCode = err?.error_code ?? "unknown_error";
1634
+ const errorMessage = err?.error_message ?? "OAuth flow failed";
1635
+ const errorDetails = { error_code: errorCode };
1636
+ if (errorCode === "connect_denied") {
1637
+ throw new ConnectDeniedError(errorMessage, errorDetails);
1638
+ }
1639
+ if ([
1640
+ "connect_config_error",
1641
+ "invalid_redirect_uri",
1642
+ "invalid_client",
1643
+ "unauthorized_client"
1644
+ ].includes(errorCode)) {
1645
+ throw new ConnectConfigError(errorMessage, errorDetails);
1646
+ }
1647
+ throw new ConnectFlowError(errorMessage, errorDetails);
1648
+ }
1649
+ if (pollStatus === "expired") {
1650
+ throw new ConnectFlowError(
1651
+ "Connect session expired before OAuth was completed"
1652
+ );
1653
+ }
1654
+ }
1655
+ throw new ConnectTimeoutError(
1656
+ `OAuth flow did not complete within ${timeout} seconds. The user may not have finished authorizing in the browser.`,
1657
+ { timeout }
1658
+ );
1659
+ }
1660
+ /**
1661
+ * Poll the Connect session for completion status (INTERNAL).
1662
+ *
1663
+ * Makes an HMAC-signed POST to the poll endpoint.
1664
+ */
1665
+ async #pollSession(sessionToken) {
1666
+ const actorHeaders = this.#getActorRequestHeaders();
1667
+ const pollPath = "/sdk/oauth/connect/session/poll";
1668
+ const pollBody = { session_token: sessionToken };
1669
+ const pollBodyStr = JSON.stringify(pollBody);
1670
+ const pollHmac = this.#computeHmacHeaders(
1671
+ "POST",
1672
+ pollPath,
1673
+ pollBodyStr
1674
+ );
1675
+ let response;
1676
+ try {
1677
+ response = await this.#alterClient.post(pollPath, {
1678
+ body: pollBodyStr,
1679
+ headers: { ...actorHeaders, ...pollHmac, "Content-Type": "application/json" }
1680
+ });
1681
+ } catch (error) {
1682
+ if (_AlterVault.#isTimeoutOrAbortError(error)) {
1683
+ throw new TimeoutError(
1684
+ `Request to Alter Vault backend timed out: ${error instanceof Error ? error.message : String(error)}`,
1685
+ { base_url: this.baseUrl }
1686
+ );
1687
+ }
1688
+ if (error instanceof TypeError) {
1689
+ throw new NetworkError(
1690
+ `Failed to connect to Alter Vault backend: ${error.message}`,
1691
+ { base_url: this.baseUrl }
1692
+ );
1693
+ }
1694
+ throw new AlterSDKError(
1695
+ `Failed to poll connect session: ${error instanceof Error ? error.message : String(error)}`
1696
+ );
1697
+ }
1698
+ this.#cacheActorIdFromResponse(response);
1699
+ await this.#handleErrorResponse(response);
1700
+ return await response.json();
1701
+ }
1117
1702
  /**
1118
1703
  * Close HTTP clients and release resources.
1119
1704
  * Waits for any pending audit tasks before closing.
@@ -1139,10 +1724,72 @@ Object.freeze(AlterVault.prototype);
1139
1724
 
1140
1725
  // src/providers/enums.ts
1141
1726
  var Provider = /* @__PURE__ */ ((Provider2) => {
1142
- Provider2["GOOGLE"] = "google";
1727
+ Provider2["ACUITY_SCHEDULING"] = "acuity-scheduling";
1728
+ Provider2["ADOBE"] = "adobe";
1729
+ Provider2["AIRCALL"] = "aircall";
1730
+ Provider2["AIRTABLE"] = "airtable";
1731
+ Provider2["APOLLO"] = "apollo";
1732
+ Provider2["ASANA"] = "asana";
1733
+ Provider2["ATLASSIAN"] = "atlassian";
1734
+ Provider2["ATTIO"] = "attio";
1735
+ Provider2["AUTODESK"] = "autodesk";
1736
+ Provider2["BASECAMP"] = "basecamp";
1737
+ Provider2["BITBUCKET"] = "bitbucket";
1738
+ Provider2["BITLY"] = "bitly";
1739
+ Provider2["BOX"] = "box";
1740
+ Provider2["BREX"] = "brex";
1741
+ Provider2["CALENDLY"] = "calendly";
1742
+ Provider2["CAL_COM"] = "cal-com";
1743
+ Provider2["CANVA"] = "canva";
1744
+ Provider2["CLICKUP"] = "clickup";
1745
+ Provider2["CLOSE"] = "close";
1746
+ Provider2["CONSTANT_CONTACT"] = "constant-contact";
1747
+ Provider2["CONTENTFUL"] = "contentful";
1748
+ Provider2["DEEL"] = "deel";
1749
+ Provider2["DIALPAD"] = "dialpad";
1750
+ Provider2["DIGITALOCEAN"] = "digitalocean";
1751
+ Provider2["DISCORD"] = "discord";
1752
+ Provider2["DOCUSIGN"] = "docusign";
1753
+ Provider2["DROPBOX"] = "dropbox";
1754
+ Provider2["EBAY"] = "ebay";
1755
+ Provider2["EVENTBRITE"] = "eventbrite";
1756
+ Provider2["FACEBOOK"] = "facebook";
1757
+ Provider2["FIGMA"] = "figma";
1143
1758
  Provider2["GITHUB"] = "github";
1144
- Provider2["SLACK"] = "slack";
1759
+ Provider2["GOOGLE"] = "google";
1760
+ Provider2["HUBSPOT"] = "hubspot";
1761
+ Provider2["INSTAGRAM"] = "instagram";
1762
+ Provider2["LINEAR"] = "linear";
1763
+ Provider2["LINKEDIN"] = "linkedin";
1764
+ Provider2["MAILCHIMP"] = "mailchimp";
1765
+ Provider2["MERCURY"] = "mercury";
1766
+ Provider2["MICROSOFT"] = "microsoft";
1767
+ Provider2["MIRO"] = "miro";
1768
+ Provider2["MONDAY"] = "monday";
1769
+ Provider2["NOTION"] = "notion";
1770
+ Provider2["OUTREACH"] = "outreach";
1771
+ Provider2["PAGERDUTY"] = "pagerduty";
1772
+ Provider2["PAYPAL"] = "paypal";
1773
+ Provider2["PINTEREST"] = "pinterest";
1774
+ Provider2["PIPEDRIVE"] = "pipedrive";
1775
+ Provider2["QUICKBOOKS"] = "quickbooks";
1776
+ Provider2["RAMP"] = "ramp";
1777
+ Provider2["REDDIT"] = "reddit";
1778
+ Provider2["RINGCENTRAL"] = "ringcentral";
1779
+ Provider2["SALESFORCE"] = "salesforce";
1145
1780
  Provider2["SENTRY"] = "sentry";
1781
+ Provider2["SLACK"] = "slack";
1782
+ Provider2["SNAPCHAT"] = "snapchat";
1783
+ Provider2["SPOTIFY"] = "spotify";
1784
+ Provider2["SQUARE"] = "square";
1785
+ Provider2["SQUARESPACE"] = "squarespace";
1786
+ Provider2["STRIPE"] = "stripe";
1787
+ Provider2["TIKTOK"] = "tiktok";
1788
+ Provider2["TODOIST"] = "todoist";
1789
+ Provider2["TWITTER"] = "twitter";
1790
+ Provider2["TYPEFORM"] = "typeform";
1791
+ Provider2["WEBEX"] = "webex";
1792
+ Provider2["WEBFLOW"] = "webflow";
1146
1793
  return Provider2;
1147
1794
  })(Provider || {});
1148
1795
  var HttpMethod = /* @__PURE__ */ ((HttpMethod2) => {
@@ -1160,17 +1807,26 @@ export {
1160
1807
  ActorType,
1161
1808
  AlterSDKError,
1162
1809
  AlterVault,
1810
+ BackendError,
1811
+ ConnectConfigError,
1812
+ ConnectDeniedError,
1813
+ ConnectFlowError,
1814
+ ConnectResult,
1163
1815
  ConnectSession,
1816
+ ConnectTimeoutError,
1817
+ ConnectionDeletedError,
1818
+ ConnectionExpiredError,
1164
1819
  ConnectionInfo,
1165
1820
  ConnectionListResult,
1166
1821
  ConnectionNotFoundError,
1822
+ ConnectionRevokedError,
1167
1823
  HttpMethod,
1168
1824
  NetworkError,
1169
1825
  PolicyViolationError,
1170
1826
  Provider,
1171
1827
  ProviderAPIError,
1828
+ ReAuthRequiredError,
1829
+ ScopeReauthRequiredError,
1172
1830
  TimeoutError,
1173
- TokenExpiredError,
1174
- TokenResponse,
1175
- TokenRetrievalError
1831
+ TokenResponse
1176
1832
  };