@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.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -24,24 +34,33 @@ __export(index_exports, {
24
34
  ActorType: () => ActorType,
25
35
  AlterSDKError: () => AlterSDKError,
26
36
  AlterVault: () => AlterVault,
37
+ BackendError: () => BackendError,
38
+ ConnectConfigError: () => ConnectConfigError,
39
+ ConnectDeniedError: () => ConnectDeniedError,
40
+ ConnectFlowError: () => ConnectFlowError,
41
+ ConnectResult: () => ConnectResult,
27
42
  ConnectSession: () => ConnectSession,
43
+ ConnectTimeoutError: () => ConnectTimeoutError,
44
+ ConnectionDeletedError: () => ConnectionDeletedError,
45
+ ConnectionExpiredError: () => ConnectionExpiredError,
28
46
  ConnectionInfo: () => ConnectionInfo,
29
47
  ConnectionListResult: () => ConnectionListResult,
30
48
  ConnectionNotFoundError: () => ConnectionNotFoundError,
49
+ ConnectionRevokedError: () => ConnectionRevokedError,
31
50
  HttpMethod: () => HttpMethod,
32
51
  NetworkError: () => NetworkError,
33
52
  PolicyViolationError: () => PolicyViolationError,
34
53
  Provider: () => Provider,
35
54
  ProviderAPIError: () => ProviderAPIError,
55
+ ReAuthRequiredError: () => ReAuthRequiredError,
56
+ ScopeReauthRequiredError: () => ScopeReauthRequiredError,
36
57
  TimeoutError: () => TimeoutError,
37
- TokenExpiredError: () => TokenExpiredError,
38
- TokenResponse: () => TokenResponse,
39
- TokenRetrievalError: () => TokenRetrievalError
58
+ TokenResponse: () => TokenResponse
40
59
  });
41
60
  module.exports = __toCommonJS(index_exports);
42
61
 
43
62
  // src/client.ts
44
- var import_node_crypto = require("crypto");
63
+ var import_node_crypto2 = require("crypto");
45
64
 
46
65
  // src/exceptions.ts
47
66
  var AlterSDKError = class extends Error {
@@ -59,13 +78,45 @@ var AlterSDKError = class extends Error {
59
78
  return this.message;
60
79
  }
61
80
  };
62
- var TokenRetrievalError = class extends AlterSDKError {
81
+ var BackendError = class extends AlterSDKError {
82
+ constructor(message, details) {
83
+ super(message, details);
84
+ this.name = "BackendError";
85
+ }
86
+ };
87
+ var ReAuthRequiredError = class extends BackendError {
88
+ constructor(message, details) {
89
+ super(message, details);
90
+ this.name = "ReAuthRequiredError";
91
+ }
92
+ };
93
+ var ConnectionExpiredError = class extends ReAuthRequiredError {
94
+ constructor(message, details) {
95
+ super(message, details);
96
+ this.name = "ConnectionExpiredError";
97
+ }
98
+ };
99
+ var ConnectionRevokedError = class extends ReAuthRequiredError {
100
+ connectionId;
101
+ constructor(message, connectionId, details) {
102
+ super(message, details);
103
+ this.name = "ConnectionRevokedError";
104
+ this.connectionId = connectionId;
105
+ }
106
+ };
107
+ var ConnectionDeletedError = class extends ReAuthRequiredError {
108
+ constructor(message, details) {
109
+ super(message, details);
110
+ this.name = "ConnectionDeletedError";
111
+ }
112
+ };
113
+ var ConnectionNotFoundError = class extends BackendError {
63
114
  constructor(message, details) {
64
115
  super(message, details);
65
- this.name = "TokenRetrievalError";
116
+ this.name = "ConnectionNotFoundError";
66
117
  }
67
118
  };
68
- var PolicyViolationError = class extends TokenRetrievalError {
119
+ var PolicyViolationError = class extends BackendError {
69
120
  policyError;
70
121
  constructor(message, policyError, details) {
71
122
  super(message, details);
@@ -73,18 +124,28 @@ var PolicyViolationError = class extends TokenRetrievalError {
73
124
  this.policyError = policyError;
74
125
  }
75
126
  };
76
- var ConnectionNotFoundError = class extends TokenRetrievalError {
127
+ var ConnectFlowError = class extends AlterSDKError {
77
128
  constructor(message, details) {
78
129
  super(message, details);
79
- this.name = "ConnectionNotFoundError";
130
+ this.name = "ConnectFlowError";
80
131
  }
81
132
  };
82
- var TokenExpiredError = class extends TokenRetrievalError {
83
- connectionId;
84
- constructor(message, connectionId, details) {
133
+ var ConnectDeniedError = class extends ConnectFlowError {
134
+ constructor(message, details) {
85
135
  super(message, details);
86
- this.name = "TokenExpiredError";
87
- this.connectionId = connectionId;
136
+ this.name = "ConnectDeniedError";
137
+ }
138
+ };
139
+ var ConnectConfigError = class extends ConnectFlowError {
140
+ constructor(message, details) {
141
+ super(message, details);
142
+ this.name = "ConnectConfigError";
143
+ }
144
+ };
145
+ var ConnectTimeoutError = class extends ConnectFlowError {
146
+ constructor(message, details) {
147
+ super(message, details);
148
+ this.name = "ConnectTimeoutError";
88
149
  }
89
150
  };
90
151
  var ProviderAPIError = class extends AlterSDKError {
@@ -97,6 +158,16 @@ var ProviderAPIError = class extends AlterSDKError {
97
158
  this.responseBody = responseBody;
98
159
  }
99
160
  };
161
+ var ScopeReauthRequiredError = class extends ProviderAPIError {
162
+ connectionId;
163
+ providerId;
164
+ constructor(message, connectionId, providerId, statusCode, responseBody, details) {
165
+ super(message, statusCode, responseBody, details);
166
+ this.name = "ScopeReauthRequiredError";
167
+ this.connectionId = connectionId;
168
+ this.providerId = providerId;
169
+ }
170
+ };
100
171
  var NetworkError = class extends AlterSDKError {
101
172
  constructor(message, details) {
102
173
  super(message, details);
@@ -134,6 +205,10 @@ var TokenResponse = class _TokenResponse {
134
205
  injectionHeader;
135
206
  /** Header value format with {token} placeholder (e.g., "Bearer {token}", "{token}") */
136
207
  injectionFormat;
208
+ /** Extra credentials for multi-part auth (e.g. AWS SigV4 secret_key, region) */
209
+ additionalCredentials;
210
+ /** Additional injection rules for multi-header or query param auth */
211
+ additionalInjections;
137
212
  constructor(data) {
138
213
  this.tokenType = data.token_type ?? "Bearer";
139
214
  this.expiresIn = data.expires_in ?? null;
@@ -143,6 +218,13 @@ var TokenResponse = class _TokenResponse {
143
218
  this.providerId = data.provider_id ?? "";
144
219
  this.injectionHeader = data.injection_header ?? "Authorization";
145
220
  this.injectionFormat = data.injection_format ?? "Bearer {token}";
221
+ if (data.additional_credentials) {
222
+ const { secret_key: _, ...safeCredentials } = data.additional_credentials;
223
+ this.additionalCredentials = Object.keys(safeCredentials).length > 0 ? safeCredentials : null;
224
+ } else {
225
+ this.additionalCredentials = null;
226
+ }
227
+ this.additionalInjections = data.additional_injections ?? null;
146
228
  Object.freeze(this);
147
229
  }
148
230
  /**
@@ -211,6 +293,7 @@ var ConnectionInfo = class {
211
293
  accountIdentifier;
212
294
  accountDisplayName;
213
295
  status;
296
+ scopeMismatch;
214
297
  expiresAt;
215
298
  createdAt;
216
299
  lastUsedAt;
@@ -221,6 +304,7 @@ var ConnectionInfo = class {
221
304
  this.accountIdentifier = data.account_identifier ?? null;
222
305
  this.accountDisplayName = data.account_display_name ?? null;
223
306
  this.status = data.status;
307
+ this.scopeMismatch = data.scope_mismatch ?? false;
224
308
  this.expiresAt = data.expires_at ?? null;
225
309
  this.createdAt = data.created_at;
226
310
  this.lastUsedAt = data.last_used_at ?? null;
@@ -234,6 +318,7 @@ var ConnectionInfo = class {
234
318
  account_identifier: this.accountIdentifier,
235
319
  account_display_name: this.accountDisplayName,
236
320
  status: this.status,
321
+ scope_mismatch: this.scopeMismatch,
237
322
  expires_at: this.expiresAt,
238
323
  created_at: this.createdAt,
239
324
  last_used_at: this.lastUsedAt
@@ -282,12 +367,41 @@ var ConnectionListResult = class {
282
367
  Object.freeze(this);
283
368
  }
284
369
  };
370
+ var ConnectResult = class {
371
+ connectionId;
372
+ providerId;
373
+ accountIdentifier;
374
+ scopes;
375
+ connectionPolicy;
376
+ constructor(data) {
377
+ this.connectionId = data.connection_id;
378
+ this.providerId = data.provider_id;
379
+ this.accountIdentifier = data.account_identifier ?? null;
380
+ this.scopes = data.scopes ?? [];
381
+ this.connectionPolicy = data.connection_policy ?? null;
382
+ Object.freeze(this);
383
+ }
384
+ toJSON() {
385
+ return {
386
+ connection_id: this.connectionId,
387
+ provider_id: this.providerId,
388
+ account_identifier: this.accountIdentifier,
389
+ scopes: this.scopes,
390
+ connection_policy: this.connectionPolicy
391
+ };
392
+ }
393
+ toString() {
394
+ return `ConnectResult(connection_id=${this.connectionId}, provider=${this.providerId})`;
395
+ }
396
+ };
285
397
  var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
286
398
  "authorization",
287
399
  "cookie",
288
400
  "set-cookie",
289
401
  "x-api-key",
290
- "x-auth-token"
402
+ "x-auth-token",
403
+ "x-amz-date",
404
+ "x-amz-content-sha256"
291
405
  ]);
292
406
  var APICallAuditLog = class {
293
407
  connectionId;
@@ -361,11 +475,171 @@ var APICallAuditLog = class {
361
475
  }
362
476
  };
363
477
 
478
+ // src/aws-sig-v4.ts
479
+ var import_node_crypto = require("crypto");
480
+ var ALGORITHM = "AWS4-HMAC-SHA256";
481
+ var AWS_HOST_RE = /^(?<service>[a-z0-9-]+)\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.amazonaws\.com$/;
482
+ var S3_VIRTUAL_HOST_RE = /^[^.]+\.s3\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.amazonaws\.com$/;
483
+ function hmacSha256(key, message) {
484
+ return (0, import_node_crypto.createHmac)("sha256", key).update(message, "utf8").digest();
485
+ }
486
+ function sha256Hex(data) {
487
+ const hash = (0, import_node_crypto.createHash)("sha256");
488
+ if (typeof data === "string") {
489
+ hash.update(data, "utf8");
490
+ } else {
491
+ hash.update(data);
492
+ }
493
+ return hash.digest("hex");
494
+ }
495
+ function detectServiceAndRegion(hostname) {
496
+ const lower = hostname.toLowerCase();
497
+ const m = AWS_HOST_RE.exec(lower);
498
+ if (m?.groups) {
499
+ return { service: m.groups.service, region: m.groups.region };
500
+ }
501
+ const s3m = S3_VIRTUAL_HOST_RE.exec(lower);
502
+ if (s3m?.groups) {
503
+ return { service: "s3", region: s3m.groups.region };
504
+ }
505
+ return { service: null, region: null };
506
+ }
507
+ function deriveSigningKey(secretKey, dateStamp, region, service) {
508
+ const kDate = hmacSha256(Buffer.from("AWS4" + secretKey, "utf8"), dateStamp);
509
+ const kRegion = hmacSha256(kDate, region);
510
+ const kService = hmacSha256(kRegion, service);
511
+ const kSigning = hmacSha256(kService, "aws4_request");
512
+ return kSigning;
513
+ }
514
+ function canonicalUri(path) {
515
+ if (!path) return "/";
516
+ if (!path.startsWith("/")) path = "/" + path;
517
+ const segments = path.split("/");
518
+ return segments.map(
519
+ (seg) => encodeURIComponent(seg).replace(
520
+ /[!'()*]/g,
521
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
522
+ )
523
+ ).join("/");
524
+ }
525
+ function canonicalQueryString(query) {
526
+ if (!query) return "";
527
+ const sorted = [];
528
+ for (const pair of query.split("&")) {
529
+ const eqIdx = pair.indexOf("=");
530
+ if (eqIdx === -1) {
531
+ sorted.push([decodeURIComponent(pair), ""]);
532
+ } else {
533
+ sorted.push([
534
+ decodeURIComponent(pair.slice(0, eqIdx)),
535
+ decodeURIComponent(pair.slice(eqIdx + 1))
536
+ ]);
537
+ }
538
+ }
539
+ sorted.sort((a, b) => {
540
+ if (a[0] < b[0]) return -1;
541
+ if (a[0] > b[0]) return 1;
542
+ if (a[1] < b[1]) return -1;
543
+ if (a[1] > b[1]) return 1;
544
+ return 0;
545
+ });
546
+ const sigv4Encode = (s) => encodeURIComponent(s).replace(
547
+ /[!'()*]/g,
548
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
549
+ );
550
+ return sorted.map(([k, v]) => `${sigv4Encode(k)}=${sigv4Encode(v)}`).join("&");
551
+ }
552
+ function canonicalHeadersAndSigned(headers) {
553
+ const canonical = {};
554
+ for (const [name, value] of Object.entries(headers)) {
555
+ const lowerName = name.toLowerCase().trim();
556
+ const trimmedValue = value.replace(/\s+/g, " ").trim();
557
+ canonical[lowerName] = trimmedValue;
558
+ }
559
+ const sortedNames = Object.keys(canonical).sort();
560
+ const canonicalStr = sortedNames.map((name) => `${name}:${canonical[name]}
561
+ `).join("");
562
+ const signedStr = sortedNames.join(";");
563
+ return { canonicalHeaders: canonicalStr, signedHeaders: signedStr };
564
+ }
565
+ function signAwsRequest(opts) {
566
+ const parsed = new URL(opts.url);
567
+ const hostname = parsed.hostname;
568
+ let { region, service } = opts;
569
+ if (region == null || service == null) {
570
+ const detected = detectServiceAndRegion(hostname);
571
+ if (region == null) region = detected.region;
572
+ if (service == null) service = detected.service;
573
+ }
574
+ if (!region) {
575
+ throw new Error(
576
+ `Cannot determine AWS region from URL '${opts.url}'. Pass region explicitly via additional_credentials.`
577
+ );
578
+ }
579
+ if (!service) {
580
+ throw new Error(
581
+ `Cannot determine AWS service from URL '${opts.url}'. Pass service explicitly via additional_credentials.`
582
+ );
583
+ }
584
+ const timestamp = opts.timestamp ?? /* @__PURE__ */ new Date();
585
+ const amzDate = timestamp.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
586
+ const dateStamp = amzDate.slice(0, 8);
587
+ const bodyBytes = opts.body != null ? typeof opts.body === "string" ? Buffer.from(opts.body, "utf8") : opts.body : Buffer.alloc(0);
588
+ const payloadHash = sha256Hex(bodyBytes);
589
+ const headersToSign = {};
590
+ const hasHost = Object.keys(opts.headers).some(
591
+ (k) => k.toLowerCase() === "host"
592
+ );
593
+ if (!hasHost) {
594
+ const port = parsed.port;
595
+ headersToSign["host"] = port && port !== "80" && port !== "443" ? `${hostname}:${port}` : hostname;
596
+ } else {
597
+ for (const [k, v] of Object.entries(opts.headers)) {
598
+ if (k.toLowerCase() === "host") {
599
+ headersToSign["host"] = v;
600
+ break;
601
+ }
602
+ }
603
+ }
604
+ headersToSign["x-amz-date"] = amzDate;
605
+ headersToSign["x-amz-content-sha256"] = payloadHash;
606
+ const canonicalUriStr = canonicalUri(parsed.pathname);
607
+ const canonicalQs = canonicalQueryString(parsed.search.replace(/^\?/, ""));
608
+ const { canonicalHeaders, signedHeaders } = canonicalHeadersAndSigned(headersToSign);
609
+ const canonicalRequest = [
610
+ opts.method.toUpperCase(),
611
+ canonicalUriStr,
612
+ canonicalQs,
613
+ canonicalHeaders,
614
+ signedHeaders,
615
+ payloadHash
616
+ ].join("\n");
617
+ const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
618
+ const stringToSign = [
619
+ ALGORITHM,
620
+ amzDate,
621
+ credentialScope,
622
+ sha256Hex(canonicalRequest)
623
+ ].join("\n");
624
+ const signingKey = deriveSigningKey(opts.secretKey, dateStamp, region, service);
625
+ const signature = (0, import_node_crypto.createHmac)("sha256", signingKey).update(stringToSign, "utf8").digest("hex");
626
+ const authorization = `${ALGORITHM} Credential=${opts.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
627
+ return {
628
+ Authorization: authorization,
629
+ "x-amz-date": amzDate,
630
+ "x-amz-content-sha256": payloadHash
631
+ };
632
+ }
633
+
364
634
  // src/client.ts
365
635
  var _tokenStore = /* @__PURE__ */ new WeakMap();
636
+ var _additionalCredsStore = /* @__PURE__ */ new WeakMap();
366
637
  function _storeAccessToken(token, accessToken) {
367
638
  _tokenStore.set(token, accessToken);
368
639
  }
640
+ function _storeAdditionalCredentials(token, creds) {
641
+ _additionalCredsStore.set(token, creds);
642
+ }
369
643
  function _extractAccessToken(token) {
370
644
  const value = _tokenStore.get(token);
371
645
  if (value === void 0) {
@@ -375,11 +649,15 @@ function _extractAccessToken(token) {
375
649
  }
376
650
  return value;
377
651
  }
652
+ function _extractAdditionalCredentials(token) {
653
+ return _additionalCredsStore.get(token);
654
+ }
378
655
  var _fetch;
379
- var SDK_VERSION = "0.3.0";
656
+ var SDK_VERSION = "0.5.0";
380
657
  var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
381
658
  var HTTP_FORBIDDEN = 403;
382
659
  var HTTP_NOT_FOUND = 404;
660
+ var HTTP_GONE = 410;
383
661
  var HTTP_BAD_REQUEST = 400;
384
662
  var HTTP_UNAUTHORIZED = 401;
385
663
  var HTTP_BAD_GATEWAY = 502;
@@ -433,7 +711,12 @@ var HttpClient = class {
433
711
  headers: mergedHeaders,
434
712
  signal: controller.signal
435
713
  };
436
- if (options?.json !== void 0) {
714
+ if (options?.body !== void 0) {
715
+ init.body = options.body;
716
+ if (!mergedHeaders["Content-Type"]) {
717
+ mergedHeaders["Content-Type"] = "application/json";
718
+ }
719
+ } else if (options?.json !== void 0) {
437
720
  init.body = JSON.stringify(options.json);
438
721
  mergedHeaders["Content-Type"] = "application/json";
439
722
  }
@@ -469,6 +752,8 @@ var AlterVault = class _AlterVault {
469
752
  #closed = false;
470
753
  /** Pending audit log promises (fire-and-forget) */
471
754
  #auditPromises = /* @__PURE__ */ new Set();
755
+ /** JWT identity resolution: callable that returns user token */
756
+ #userTokenGetter = null;
472
757
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
473
758
  // Public readonly properties (frozen by Object.freeze)
474
759
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -496,7 +781,7 @@ var AlterVault = class _AlterVault {
496
781
  for (const [name, value] of actorStrings) {
497
782
  _AlterVault.#validateActorString(value, name);
498
783
  }
499
- this.#hmacKey = (0, import_node_crypto.createHmac)("sha256", options.apiKey).update("alter-signing-v1").digest();
784
+ this.#hmacKey = (0, import_node_crypto2.createHmac)("sha256", options.apiKey).update("alter-signing-v1").digest();
500
785
  this.baseUrl = (process.env.ALTER_BASE_URL ?? "https://backend.alterai.dev").replace(/\/+$/, "");
501
786
  const timeoutMs = options.timeout ?? 3e4;
502
787
  this.#actorType = options.actorType;
@@ -504,6 +789,7 @@ var AlterVault = class _AlterVault {
504
789
  this.#actorName = options.actorName;
505
790
  this.#actorVersion = options.actorVersion;
506
791
  this.#clientType = options.clientType;
792
+ this.#userTokenGetter = options.userTokenGetter ?? null;
507
793
  this.#framework = options.framework;
508
794
  if (!_fetch) {
509
795
  _fetch = globalThis.fetch;
@@ -600,12 +886,12 @@ var AlterVault = class _AlterVault {
600
886
  */
601
887
  #computeHmacHeaders(method, path, body) {
602
888
  const timestamp = String(Math.floor(Date.now() / 1e3));
603
- const contentHash = (0, import_node_crypto.createHash)("sha256").update(body ?? "").digest("hex");
889
+ const contentHash = (0, import_node_crypto2.createHash)("sha256").update(body ?? "").digest("hex");
604
890
  const stringToSign = `${method.toUpperCase()}
605
891
  ${path}
606
892
  ${timestamp}
607
893
  ${contentHash}`;
608
- const signature = (0, import_node_crypto.createHmac)("sha256", this.#hmacKey).update(stringToSign).digest("hex");
894
+ const signature = (0, import_node_crypto2.createHmac)("sha256", this.#hmacKey).update(stringToSign).digest("hex");
609
895
  return {
610
896
  "X-Alter-Timestamp": timestamp,
611
897
  "X-Alter-Content-SHA256": contentHash,
@@ -678,6 +964,24 @@ ${contentHash}`;
678
964
  }
679
965
  return false;
680
966
  }
967
+ /**
968
+ * Resolve a value_source to the actual value for injection.
969
+ *
970
+ * @param valueSource - "token" or "additional_credentials.<field>"
971
+ * @param accessToken - The main credential value
972
+ * @param additionalCreds - Additional credentials from vault
973
+ * @returns The resolved value, or null if not available
974
+ */
975
+ static #resolveInjectionValue(valueSource, accessToken, additionalCreds) {
976
+ if (valueSource === "token") {
977
+ return accessToken;
978
+ }
979
+ if (valueSource.startsWith("additional_credentials.")) {
980
+ const field = valueSource.slice("additional_credentials.".length);
981
+ return additionalCreds?.[field] ?? null;
982
+ }
983
+ return null;
984
+ }
681
985
  static async #safeParseJson(response) {
682
986
  try {
683
987
  return await response.clone().json();
@@ -699,12 +1003,25 @@ ${contentHash}`;
699
1003
  }
700
1004
  if (response.status === HTTP_FORBIDDEN) {
701
1005
  const errorData = await _AlterVault.#safeParseJson(response);
1006
+ if (errorData.error === "connection_expired") {
1007
+ throw new ConnectionExpiredError(
1008
+ errorData.message ?? "Connection expired per TTL policy",
1009
+ errorData.details
1010
+ );
1011
+ }
702
1012
  throw new PolicyViolationError(
703
1013
  errorData.message ?? "Access denied by policy",
704
1014
  errorData.error,
705
1015
  errorData.details
706
1016
  );
707
1017
  }
1018
+ if (response.status === HTTP_GONE) {
1019
+ const errorData = await _AlterVault.#safeParseJson(response);
1020
+ throw new ConnectionDeletedError(
1021
+ errorData.message ?? "Connection has been deleted. A new connection_id will be issued on re-authorization.",
1022
+ errorData
1023
+ );
1024
+ }
708
1025
  if (response.status === HTTP_NOT_FOUND) {
709
1026
  const errorData = await _AlterVault.#safeParseJson(response);
710
1027
  throw new ConnectionNotFoundError(
@@ -714,35 +1031,35 @@ ${contentHash}`;
714
1031
  }
715
1032
  if (response.status === HTTP_BAD_REQUEST || response.status === HTTP_BAD_GATEWAY) {
716
1033
  const errorData = await _AlterVault.#safeParseJson(response);
717
- if (JSON.stringify(errorData).toLowerCase().includes("token_expired")) {
718
- throw new TokenExpiredError(
719
- errorData.message ?? "Token expired and refresh failed",
1034
+ if (errorData.error === "connection_revoked") {
1035
+ throw new ConnectionRevokedError(
1036
+ errorData.message ?? "Connection has been revoked. User must re-authorize.",
720
1037
  errorData.connection_id,
721
1038
  errorData
722
1039
  );
723
1040
  }
724
- throw new TokenRetrievalError(
1041
+ throw new BackendError(
725
1042
  errorData.message ?? `Backend error ${response.status}`,
726
1043
  errorData
727
1044
  );
728
1045
  }
729
1046
  if (response.status === HTTP_UNAUTHORIZED) {
730
1047
  const errorData = await _AlterVault.#safeParseJson(response);
731
- throw new TokenRetrievalError(
1048
+ throw new BackendError(
732
1049
  errorData.message ?? "Unauthorized \u2014 check your API key",
733
1050
  errorData
734
1051
  );
735
1052
  }
736
1053
  if (response.status === HTTP_INTERNAL_SERVER_ERROR || response.status === HTTP_SERVICE_UNAVAILABLE) {
737
1054
  const errorData = await _AlterVault.#safeParseJson(response);
738
- throw new TokenRetrievalError(
1055
+ throw new BackendError(
739
1056
  errorData.message ?? `Backend unavailable (HTTP ${response.status})`,
740
1057
  errorData
741
1058
  );
742
1059
  }
743
1060
  if (response.status >= HTTP_CLIENT_ERROR_START) {
744
1061
  const errorData = await _AlterVault.#safeParseJson(response);
745
- throw new TokenRetrievalError(
1062
+ throw new BackendError(
746
1063
  errorData.message ?? `Unexpected backend error (HTTP ${response.status})`,
747
1064
  errorData
748
1065
  );
@@ -754,24 +1071,52 @@ ${contentHash}`;
754
1071
  * This is a private method. Tokens are NEVER exposed to developers.
755
1072
  * Use request() instead, which handles tokens internally.
756
1073
  */
757
- async #getToken(connectionId, reason, requestMetadata, runId, threadId, toolCallId) {
1074
+ async #getUserToken() {
1075
+ if (!this.#userTokenGetter) {
1076
+ throw new AlterSDKError(
1077
+ "userTokenGetter is required for provider-based resolution. Pass userTokenGetter to AlterVault constructor."
1078
+ );
1079
+ }
1080
+ const result = this.#userTokenGetter();
1081
+ const token = result instanceof Promise ? await result : result;
1082
+ if (!token || typeof token !== "string") {
1083
+ throw new AlterSDKError("userTokenGetter must return a non-empty string");
1084
+ }
1085
+ return token;
1086
+ }
1087
+ async #getToken(connectionId, reason, requestMetadata, runId, threadId, toolCallId, provider, account) {
758
1088
  const actorHeaders = this.#getActorRequestHeaders(
759
1089
  runId,
760
1090
  threadId,
761
1091
  toolCallId
762
1092
  );
763
1093
  let response;
764
- const tokenBody = {
765
- connection_id: connectionId,
766
- reason: reason ?? null,
767
- request: requestMetadata ?? null
768
- };
1094
+ let tokenBody;
1095
+ if (provider) {
1096
+ const userToken = await this.#getUserToken();
1097
+ tokenBody = {
1098
+ provider_id: provider,
1099
+ user_token: userToken,
1100
+ reason: reason ?? null,
1101
+ request: requestMetadata ?? null
1102
+ };
1103
+ if (account) {
1104
+ tokenBody.account = account;
1105
+ }
1106
+ } else {
1107
+ tokenBody = {
1108
+ connection_id: connectionId,
1109
+ reason: reason ?? null,
1110
+ request: requestMetadata ?? null
1111
+ };
1112
+ }
769
1113
  const tokenPath = "/sdk/token";
770
- const hmacHeaders = this.#computeHmacHeaders("POST", tokenPath, JSON.stringify(tokenBody));
1114
+ const tokenBodyStr = JSON.stringify(tokenBody);
1115
+ const hmacHeaders = this.#computeHmacHeaders("POST", tokenPath, tokenBodyStr);
771
1116
  try {
772
1117
  response = await this.#alterClient.post(tokenPath, {
773
- json: tokenBody,
774
- headers: { ...actorHeaders, ...hmacHeaders }
1118
+ body: tokenBodyStr,
1119
+ headers: { ...actorHeaders, ...hmacHeaders, "Content-Type": "application/json" }
775
1120
  });
776
1121
  } catch (error) {
777
1122
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
@@ -786,7 +1131,7 @@ ${contentHash}`;
786
1131
  { base_url: this.baseUrl }
787
1132
  );
788
1133
  }
789
- throw new TokenRetrievalError(
1134
+ throw new BackendError(
790
1135
  `Failed to retrieve token: ${error instanceof Error ? error.message : String(error)}`,
791
1136
  { connection_id: connectionId, error: String(error) }
792
1137
  );
@@ -795,21 +1140,25 @@ ${contentHash}`;
795
1140
  await this.#handleErrorResponse(response);
796
1141
  const tokenData = await response.json();
797
1142
  const typedData = tokenData;
1143
+ const scopeMismatch = typedData.scope_mismatch ?? false;
798
1144
  const tokenResponse = new TokenResponse(typedData);
799
1145
  if (!/^[A-Za-z][A-Za-z0-9-]*$/.test(tokenResponse.injectionHeader)) {
800
- throw new TokenRetrievalError(
1146
+ throw new BackendError(
801
1147
  `Backend returned invalid injection_header: ${tokenResponse.injectionHeader}`,
802
1148
  { connectionId: String(connectionId) }
803
1149
  );
804
1150
  }
805
1151
  if (/[\r\n\x00]/.test(tokenResponse.injectionFormat)) {
806
- throw new TokenRetrievalError(
1152
+ throw new BackendError(
807
1153
  `Backend returned invalid injection_format (contains control characters)`,
808
1154
  { connectionId: String(connectionId) }
809
1155
  );
810
1156
  }
811
1157
  _storeAccessToken(tokenResponse, typedData.access_token);
812
- return tokenResponse;
1158
+ if (typedData.additional_credentials) {
1159
+ _storeAdditionalCredentials(tokenResponse, typedData.additional_credentials);
1160
+ }
1161
+ return { tokenResponse, scopeMismatch };
813
1162
  }
814
1163
  /**
815
1164
  * Log an API call to the backend audit endpoint (INTERNAL).
@@ -839,10 +1188,11 @@ ${contentHash}`;
839
1188
  const actorHeaders = this.#getActorRequestHeaders(params.runId);
840
1189
  const auditPath = "/sdk/oauth/audit/api-call";
841
1190
  const auditBody = sanitized;
842
- const auditHmac = this.#computeHmacHeaders("POST", auditPath, JSON.stringify(auditBody));
1191
+ const auditBodyStr = JSON.stringify(auditBody);
1192
+ const auditHmac = this.#computeHmacHeaders("POST", auditPath, auditBodyStr);
843
1193
  const response = await this.#alterClient.post(auditPath, {
844
- json: auditBody,
845
- headers: { ...actorHeaders, ...auditHmac }
1194
+ body: auditBodyStr,
1195
+ headers: { ...actorHeaders, ...auditHmac, "Content-Type": "application/json" }
846
1196
  });
847
1197
  this.#cacheActorIdFromResponse(response);
848
1198
  if (!response.ok) {
@@ -914,12 +1264,22 @@ ${contentHash}`;
914
1264
  "SDK instance has been closed. Create a new AlterVault instance to make requests."
915
1265
  );
916
1266
  }
917
- const runId = options?.runId ?? (0, import_node_crypto.randomUUID)();
1267
+ const provider = options?.provider;
1268
+ const account = options?.account;
1269
+ if (!connectionId && !provider) {
1270
+ throw new AlterSDKError("Provide connectionId or options.provider");
1271
+ }
1272
+ if (connectionId && provider) {
1273
+ throw new AlterSDKError("Cannot provide both connectionId and options.provider");
1274
+ }
1275
+ const effectiveConnectionId = connectionId ?? null;
1276
+ let currentUrl = url;
1277
+ const runId = options?.runId ?? (0, import_node_crypto2.randomUUID)();
918
1278
  const methodStr = String(method).toUpperCase();
919
- const urlLower = url.toLowerCase();
1279
+ const urlLower = currentUrl.toLowerCase();
920
1280
  if (!ALLOWED_URL_SCHEMES.some((scheme) => urlLower.startsWith(scheme))) {
921
1281
  throw new AlterSDKError(
922
- `URL must start with https:// or http://, got: ${url.slice(0, 50)}`
1282
+ `URL must start with https:// or http://, got: ${currentUrl.slice(0, 50)}`
923
1283
  );
924
1284
  }
925
1285
  if (options?.pathParams && Object.keys(options.pathParams).length > 0) {
@@ -928,7 +1288,7 @@ ${contentHash}`;
928
1288
  encodedParams[key] = encodeURIComponent(String(value));
929
1289
  }
930
1290
  try {
931
- let resolvedUrl = url;
1291
+ let resolvedUrl = currentUrl;
932
1292
  for (const [key, value] of Object.entries(encodedParams)) {
933
1293
  const placeholder = `{${key}}`;
934
1294
  if (!resolvedUrl.includes(placeholder)) {
@@ -939,74 +1299,142 @@ ${contentHash}`;
939
1299
  const remaining = resolvedUrl.match(/\{(\w+)\}/);
940
1300
  if (remaining) {
941
1301
  throw new AlterSDKError(
942
- `Invalid URL template or missing path_params: '${remaining[1]}'. URL: ${url}, path_params: ${JSON.stringify(options.pathParams)}`
1302
+ `Invalid URL template or missing path_params: '${remaining[1]}'. URL: ${currentUrl}, path_params: ${JSON.stringify(options.pathParams)}`
943
1303
  );
944
1304
  }
945
- url = resolvedUrl;
1305
+ currentUrl = resolvedUrl;
946
1306
  } catch (error) {
947
1307
  if (error instanceof AlterSDKError) {
948
1308
  throw error;
949
1309
  }
950
1310
  throw new AlterSDKError(
951
- `Invalid URL template or missing path_params: ${error instanceof Error ? error.message : String(error)}. URL: ${url}, path_params: ${JSON.stringify(options.pathParams)}`
1311
+ `Invalid URL template or missing path_params: ${error instanceof Error ? error.message : String(error)}. URL: ${currentUrl}, path_params: ${JSON.stringify(options.pathParams)}`
952
1312
  );
953
1313
  }
954
1314
  }
955
- const tokenResponse = await this.#getToken(
956
- connectionId,
1315
+ const { tokenResponse, scopeMismatch } = await this.#getToken(
1316
+ effectiveConnectionId,
957
1317
  options?.reason,
958
- { method: methodStr, url },
1318
+ { method: methodStr, url: currentUrl },
959
1319
  runId,
960
1320
  options?.threadId,
961
- options?.toolCallId
1321
+ options?.toolCallId,
1322
+ provider,
1323
+ account
962
1324
  );
963
- const injectionHeaderLower = tokenResponse.injectionHeader.toLowerCase();
964
- if (options?.extraHeaders && Object.keys(options.extraHeaders).some(
965
- (k) => k.toLowerCase() === injectionHeaderLower
966
- )) {
967
- console.warn(
968
- `extraHeaders contains '${tokenResponse.injectionHeader}' which will be overwritten with the auto-injected credential`
969
- );
970
- }
971
1325
  const requestHeaders = options?.extraHeaders ? { ...options.extraHeaders } : {};
972
1326
  const accessToken = _extractAccessToken(tokenResponse);
973
- requestHeaders[tokenResponse.injectionHeader] = tokenResponse.injectionFormat.replace("{token}", accessToken);
1327
+ const injectionHeaderLower = tokenResponse.injectionHeader.toLowerCase();
1328
+ const additionalCreds = _extractAdditionalCredentials(tokenResponse);
1329
+ const isSigV4 = tokenResponse.injectionFormat.startsWith("AWS4-HMAC-SHA256") && additionalCreds != null;
1330
+ let sigv4BodyStr = null;
1331
+ if (isSigV4) {
1332
+ const accessKeyId = accessToken;
1333
+ if (options?.json != null) {
1334
+ sigv4BodyStr = JSON.stringify(options.json);
1335
+ if (!requestHeaders["Content-Type"]) {
1336
+ requestHeaders["Content-Type"] = "application/json";
1337
+ }
1338
+ }
1339
+ if (!additionalCreds.secret_key) {
1340
+ throw new BackendError(
1341
+ "AWS SigV4 credential is missing secret_key in additional_credentials. Re-store the credential with both Access Key ID and Secret Access Key.",
1342
+ { connection_id: effectiveConnectionId }
1343
+ );
1344
+ }
1345
+ const awsHeaders = signAwsRequest({
1346
+ method: methodStr,
1347
+ url: currentUrl,
1348
+ headers: requestHeaders,
1349
+ body: sigv4BodyStr,
1350
+ accessKeyId,
1351
+ secretKey: additionalCreds.secret_key,
1352
+ region: additionalCreds.region ?? null,
1353
+ service: additionalCreds.service ?? null
1354
+ });
1355
+ Object.assign(requestHeaders, awsHeaders);
1356
+ } else {
1357
+ if (options?.extraHeaders && Object.keys(options.extraHeaders).some(
1358
+ (k) => k.toLowerCase() === injectionHeaderLower
1359
+ )) {
1360
+ console.warn(
1361
+ `extraHeaders contains '${tokenResponse.injectionHeader}' which will be overwritten with the auto-injected credential`
1362
+ );
1363
+ }
1364
+ requestHeaders[tokenResponse.injectionHeader] = tokenResponse.injectionFormat.replace("{token}", accessToken);
1365
+ }
1366
+ const auditUrl = currentUrl;
1367
+ if (tokenResponse.additionalInjections && !isSigV4) {
1368
+ for (const rule of tokenResponse.additionalInjections) {
1369
+ const value = _AlterVault.#resolveInjectionValue(
1370
+ rule.value_source,
1371
+ accessToken,
1372
+ additionalCreds
1373
+ );
1374
+ if (!value) continue;
1375
+ if (!/^[A-Za-z][A-Za-z0-9\-_]*$/.test(rule.key)) continue;
1376
+ if (rule.target === "header") {
1377
+ requestHeaders[rule.key] = value;
1378
+ } else if (rule.target === "query_param") {
1379
+ const urlObj = new URL(currentUrl);
1380
+ urlObj.searchParams.set(rule.key, value);
1381
+ currentUrl = urlObj.toString();
1382
+ }
1383
+ }
1384
+ }
974
1385
  if (!requestHeaders["User-Agent"]) {
975
1386
  requestHeaders["User-Agent"] = SDK_USER_AGENT;
976
1387
  }
977
1388
  const startTime = Date.now();
978
1389
  let response;
979
1390
  try {
980
- response = await this.#providerClient.request(methodStr, url, {
981
- json: options?.json,
982
- headers: requestHeaders,
983
- params: options?.queryParams
984
- });
1391
+ if (isSigV4 && sigv4BodyStr != null) {
1392
+ response = await this.#providerClient.request(methodStr, currentUrl, {
1393
+ body: sigv4BodyStr,
1394
+ headers: requestHeaders,
1395
+ params: options?.queryParams
1396
+ });
1397
+ } else {
1398
+ response = await this.#providerClient.request(methodStr, currentUrl, {
1399
+ json: options?.json,
1400
+ headers: requestHeaders,
1401
+ params: options?.queryParams
1402
+ });
1403
+ }
985
1404
  } catch (error) {
986
1405
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
987
1406
  throw new TimeoutError(
988
1407
  `Provider API request timed out: ${error instanceof Error ? error.message : String(error)}`,
989
1408
  {
990
- connection_id: connectionId,
1409
+ connection_id: effectiveConnectionId,
991
1410
  method: methodStr,
992
- url
1411
+ url: currentUrl
993
1412
  }
994
1413
  );
995
1414
  }
996
1415
  throw new NetworkError(
997
1416
  `Failed to call provider API: ${error instanceof Error ? error.message : String(error)}`,
998
1417
  {
999
- connection_id: connectionId,
1418
+ connection_id: effectiveConnectionId,
1000
1419
  method: methodStr,
1001
- url,
1420
+ url: currentUrl,
1002
1421
  error: String(error)
1003
1422
  }
1004
1423
  );
1005
1424
  }
1006
1425
  const latencyMs = Date.now() - startTime;
1426
+ const sigv4Sensitive = /* @__PURE__ */ new Set(["authorization", "x-amz-date", "x-amz-content-sha256"]);
1427
+ const stripHeaders = isSigV4 ? sigv4Sensitive : /* @__PURE__ */ new Set([injectionHeaderLower]);
1428
+ if (tokenResponse.additionalInjections && !isSigV4) {
1429
+ for (const rule of tokenResponse.additionalInjections) {
1430
+ if (rule.target === "header") {
1431
+ stripHeaders.add(rule.key.toLowerCase());
1432
+ }
1433
+ }
1434
+ }
1007
1435
  const auditHeaders = {};
1008
1436
  for (const [key, value] of Object.entries(requestHeaders)) {
1009
- if (key.toLowerCase() !== injectionHeaderLower) {
1437
+ if (!stripHeaders.has(key.toLowerCase())) {
1010
1438
  auditHeaders[key] = value;
1011
1439
  }
1012
1440
  }
@@ -1017,9 +1445,9 @@ ${contentHash}`;
1017
1445
  });
1018
1446
  this.#scheduleAuditLog({
1019
1447
  connectionId: tokenResponse.connectionId,
1020
- providerId: tokenResponse.providerId || connectionId,
1448
+ providerId: tokenResponse.providerId || effectiveConnectionId || "",
1021
1449
  method: methodStr,
1022
- url,
1450
+ url: auditUrl,
1023
1451
  requestHeaders: auditHeaders,
1024
1452
  requestBody: options?.json ?? null,
1025
1453
  responseStatus: response.status,
@@ -1032,14 +1460,29 @@ ${contentHash}`;
1032
1460
  toolCallId: options?.toolCallId ?? null
1033
1461
  });
1034
1462
  if (response.status >= HTTP_CLIENT_ERROR_START) {
1463
+ if (response.status === HTTP_FORBIDDEN && scopeMismatch) {
1464
+ throw new ScopeReauthRequiredError(
1465
+ "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.",
1466
+ tokenResponse.connectionId,
1467
+ tokenResponse.providerId,
1468
+ response.status,
1469
+ responseBody,
1470
+ {
1471
+ connection_id: tokenResponse.connectionId,
1472
+ provider_id: tokenResponse.providerId,
1473
+ method: methodStr,
1474
+ url: currentUrl
1475
+ }
1476
+ );
1477
+ }
1035
1478
  throw new ProviderAPIError(
1036
1479
  `Provider API returned error ${response.status}`,
1037
1480
  response.status,
1038
1481
  responseBody,
1039
1482
  {
1040
- connection_id: connectionId,
1483
+ connection_id: effectiveConnectionId,
1041
1484
  method: methodStr,
1042
- url
1485
+ url: currentUrl
1043
1486
  }
1044
1487
  );
1045
1488
  }
@@ -1064,12 +1507,23 @@ ${contentHash}`;
1064
1507
  limit: options?.limit ?? 100,
1065
1508
  offset: options?.offset ?? 0
1066
1509
  };
1510
+ if (this.#userTokenGetter) {
1511
+ try {
1512
+ listBody.user_token = await this.#getUserToken();
1513
+ } catch (err) {
1514
+ console.warn(
1515
+ "user_token_getter failed in listConnections, falling back to un-scoped listing:",
1516
+ err instanceof Error ? err.message : String(err)
1517
+ );
1518
+ }
1519
+ }
1067
1520
  const listPath = "/sdk/oauth/connections/list";
1068
- const listHmac = this.#computeHmacHeaders("POST", listPath, JSON.stringify(listBody));
1521
+ const listBodyStr = JSON.stringify(listBody);
1522
+ const listHmac = this.#computeHmacHeaders("POST", listPath, listBodyStr);
1069
1523
  try {
1070
1524
  response = await this.#alterClient.post(listPath, {
1071
- json: listBody,
1072
- headers: { ...actorHeaders, ...listHmac }
1525
+ body: listBodyStr,
1526
+ headers: { ...actorHeaders, ...listHmac, "Content-Type": "application/json" }
1073
1527
  });
1074
1528
  } catch (error) {
1075
1529
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
@@ -1115,24 +1569,37 @@ ${contentHash}`;
1115
1569
  "SDK instance has been closed. Create a new AlterVault instance to make requests."
1116
1570
  );
1117
1571
  }
1118
- if (!options.endUser?.id) {
1119
- throw new AlterSDKError("endUser.id is required");
1120
- }
1121
1572
  const actorHeaders = this.#getActorRequestHeaders();
1122
1573
  let response;
1123
1574
  const sessionBody = {
1124
- end_user: options.endUser,
1125
1575
  allowed_providers: options.allowedProviders ?? null,
1126
1576
  return_url: options.returnUrl ?? null,
1127
1577
  allowed_origin: options.allowedOrigin ?? null,
1128
1578
  metadata: options.metadata ?? null
1129
1579
  };
1580
+ if (options.connectionPolicy) {
1581
+ sessionBody.connection_policy = {
1582
+ max_ttl_seconds: options.connectionPolicy.maxTtlSeconds ?? null,
1583
+ default_ttl_seconds: options.connectionPolicy.defaultTtlSeconds ?? null
1584
+ };
1585
+ }
1586
+ if (this.#userTokenGetter) {
1587
+ try {
1588
+ sessionBody.user_token = await this.#getUserToken();
1589
+ } catch (err) {
1590
+ console.warn(
1591
+ "userTokenGetter failed in createConnectSession, session will not have identity tagging:",
1592
+ err instanceof Error ? err.message : String(err)
1593
+ );
1594
+ }
1595
+ }
1130
1596
  const sessionPath = "/sdk/oauth/connect/session";
1131
- const sessionHmac = this.#computeHmacHeaders("POST", sessionPath, JSON.stringify(sessionBody));
1597
+ const sessionBodyStr = JSON.stringify(sessionBody);
1598
+ const sessionHmac = this.#computeHmacHeaders("POST", sessionPath, sessionBodyStr);
1132
1599
  try {
1133
1600
  response = await this.#alterClient.post(sessionPath, {
1134
- json: sessionBody,
1135
- headers: { ...actorHeaders, ...sessionHmac }
1601
+ body: sessionBodyStr,
1602
+ headers: { ...actorHeaders, ...sessionHmac, "Content-Type": "application/json" }
1136
1603
  });
1137
1604
  } catch (error) {
1138
1605
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
@@ -1156,6 +1623,143 @@ ${contentHash}`;
1156
1623
  const data = await response.json();
1157
1624
  return new ConnectSession(data);
1158
1625
  }
1626
+ /**
1627
+ * Open OAuth in the user's browser and wait for completion.
1628
+ *
1629
+ * This is the headless connect flow for CLI tools, scripts, and
1630
+ * server-side applications. It creates a Connect session, opens the
1631
+ * browser, and polls until the user completes OAuth.
1632
+ *
1633
+ * @param options - Connect options
1634
+ * @returns Array of ConnectResult objects (one per connected provider)
1635
+ * @throws ConnectTimeoutError if the user doesn't complete within timeout
1636
+ * @throws ConnectFlowError if the user denies or provider returns error
1637
+ * @throws AlterSDKError if SDK is closed or session creation fails
1638
+ */
1639
+ async connect(options) {
1640
+ if (this.#closed) {
1641
+ throw new AlterSDKError(
1642
+ "SDK instance has been closed. Create a new AlterVault instance to make requests."
1643
+ );
1644
+ }
1645
+ const timeout = options.timeout ?? 300;
1646
+ const pollInterval = options.pollInterval ?? 2;
1647
+ const openBrowser = options.openBrowser ?? true;
1648
+ const session = await this.createConnectSession({
1649
+ allowedProviders: options.providers,
1650
+ connectionPolicy: options.connectionPolicy
1651
+ });
1652
+ if (openBrowser) {
1653
+ try {
1654
+ const openModule = await import("open");
1655
+ const openFn = openModule.default;
1656
+ if (openFn) {
1657
+ await openFn(session.connectUrl);
1658
+ } else {
1659
+ console.log(
1660
+ `Open this URL to authorize: ${session.connectUrl}`
1661
+ );
1662
+ }
1663
+ } catch {
1664
+ console.log(
1665
+ `Open this URL to authorize: ${session.connectUrl}`
1666
+ );
1667
+ }
1668
+ } else {
1669
+ console.log(
1670
+ `Open this URL to authorize: ${session.connectUrl}`
1671
+ );
1672
+ }
1673
+ const deadline = Date.now() + timeout * 1e3;
1674
+ while (Date.now() < deadline) {
1675
+ await new Promise(
1676
+ (resolve) => setTimeout(resolve, pollInterval * 1e3)
1677
+ );
1678
+ const pollResult = await this.#pollSession(session.sessionToken);
1679
+ const pollStatus = pollResult.status;
1680
+ if (pollStatus === "completed") {
1681
+ const connectionsData = pollResult.connections ?? [];
1682
+ return connectionsData.map(
1683
+ (conn) => new ConnectResult({
1684
+ connection_id: conn.connection_id ?? "",
1685
+ provider_id: conn.provider_id ?? "",
1686
+ account_identifier: conn.account_identifier ?? null,
1687
+ scopes: conn.scopes ?? [],
1688
+ connection_policy: conn.connection_policy ?? null
1689
+ })
1690
+ );
1691
+ }
1692
+ if (pollStatus === "error") {
1693
+ const err = pollResult.error;
1694
+ const errorCode = err?.error_code ?? "unknown_error";
1695
+ const errorMessage = err?.error_message ?? "OAuth flow failed";
1696
+ const errorDetails = { error_code: errorCode };
1697
+ if (errorCode === "connect_denied") {
1698
+ throw new ConnectDeniedError(errorMessage, errorDetails);
1699
+ }
1700
+ if ([
1701
+ "connect_config_error",
1702
+ "invalid_redirect_uri",
1703
+ "invalid_client",
1704
+ "unauthorized_client"
1705
+ ].includes(errorCode)) {
1706
+ throw new ConnectConfigError(errorMessage, errorDetails);
1707
+ }
1708
+ throw new ConnectFlowError(errorMessage, errorDetails);
1709
+ }
1710
+ if (pollStatus === "expired") {
1711
+ throw new ConnectFlowError(
1712
+ "Connect session expired before OAuth was completed"
1713
+ );
1714
+ }
1715
+ }
1716
+ throw new ConnectTimeoutError(
1717
+ `OAuth flow did not complete within ${timeout} seconds. The user may not have finished authorizing in the browser.`,
1718
+ { timeout }
1719
+ );
1720
+ }
1721
+ /**
1722
+ * Poll the Connect session for completion status (INTERNAL).
1723
+ *
1724
+ * Makes an HMAC-signed POST to the poll endpoint.
1725
+ */
1726
+ async #pollSession(sessionToken) {
1727
+ const actorHeaders = this.#getActorRequestHeaders();
1728
+ const pollPath = "/sdk/oauth/connect/session/poll";
1729
+ const pollBody = { session_token: sessionToken };
1730
+ const pollBodyStr = JSON.stringify(pollBody);
1731
+ const pollHmac = this.#computeHmacHeaders(
1732
+ "POST",
1733
+ pollPath,
1734
+ pollBodyStr
1735
+ );
1736
+ let response;
1737
+ try {
1738
+ response = await this.#alterClient.post(pollPath, {
1739
+ body: pollBodyStr,
1740
+ headers: { ...actorHeaders, ...pollHmac, "Content-Type": "application/json" }
1741
+ });
1742
+ } catch (error) {
1743
+ if (_AlterVault.#isTimeoutOrAbortError(error)) {
1744
+ throw new TimeoutError(
1745
+ `Request to Alter Vault backend timed out: ${error instanceof Error ? error.message : String(error)}`,
1746
+ { base_url: this.baseUrl }
1747
+ );
1748
+ }
1749
+ if (error instanceof TypeError) {
1750
+ throw new NetworkError(
1751
+ `Failed to connect to Alter Vault backend: ${error.message}`,
1752
+ { base_url: this.baseUrl }
1753
+ );
1754
+ }
1755
+ throw new AlterSDKError(
1756
+ `Failed to poll connect session: ${error instanceof Error ? error.message : String(error)}`
1757
+ );
1758
+ }
1759
+ this.#cacheActorIdFromResponse(response);
1760
+ await this.#handleErrorResponse(response);
1761
+ return await response.json();
1762
+ }
1159
1763
  /**
1160
1764
  * Close HTTP clients and release resources.
1161
1765
  * Waits for any pending audit tasks before closing.
@@ -1181,10 +1785,72 @@ Object.freeze(AlterVault.prototype);
1181
1785
 
1182
1786
  // src/providers/enums.ts
1183
1787
  var Provider = /* @__PURE__ */ ((Provider2) => {
1184
- Provider2["GOOGLE"] = "google";
1788
+ Provider2["ACUITY_SCHEDULING"] = "acuity-scheduling";
1789
+ Provider2["ADOBE"] = "adobe";
1790
+ Provider2["AIRCALL"] = "aircall";
1791
+ Provider2["AIRTABLE"] = "airtable";
1792
+ Provider2["APOLLO"] = "apollo";
1793
+ Provider2["ASANA"] = "asana";
1794
+ Provider2["ATLASSIAN"] = "atlassian";
1795
+ Provider2["ATTIO"] = "attio";
1796
+ Provider2["AUTODESK"] = "autodesk";
1797
+ Provider2["BASECAMP"] = "basecamp";
1798
+ Provider2["BITBUCKET"] = "bitbucket";
1799
+ Provider2["BITLY"] = "bitly";
1800
+ Provider2["BOX"] = "box";
1801
+ Provider2["BREX"] = "brex";
1802
+ Provider2["CALENDLY"] = "calendly";
1803
+ Provider2["CAL_COM"] = "cal-com";
1804
+ Provider2["CANVA"] = "canva";
1805
+ Provider2["CLICKUP"] = "clickup";
1806
+ Provider2["CLOSE"] = "close";
1807
+ Provider2["CONSTANT_CONTACT"] = "constant-contact";
1808
+ Provider2["CONTENTFUL"] = "contentful";
1809
+ Provider2["DEEL"] = "deel";
1810
+ Provider2["DIALPAD"] = "dialpad";
1811
+ Provider2["DIGITALOCEAN"] = "digitalocean";
1812
+ Provider2["DISCORD"] = "discord";
1813
+ Provider2["DOCUSIGN"] = "docusign";
1814
+ Provider2["DROPBOX"] = "dropbox";
1815
+ Provider2["EBAY"] = "ebay";
1816
+ Provider2["EVENTBRITE"] = "eventbrite";
1817
+ Provider2["FACEBOOK"] = "facebook";
1818
+ Provider2["FIGMA"] = "figma";
1185
1819
  Provider2["GITHUB"] = "github";
1186
- Provider2["SLACK"] = "slack";
1820
+ Provider2["GOOGLE"] = "google";
1821
+ Provider2["HUBSPOT"] = "hubspot";
1822
+ Provider2["INSTAGRAM"] = "instagram";
1823
+ Provider2["LINEAR"] = "linear";
1824
+ Provider2["LINKEDIN"] = "linkedin";
1825
+ Provider2["MAILCHIMP"] = "mailchimp";
1826
+ Provider2["MERCURY"] = "mercury";
1827
+ Provider2["MICROSOFT"] = "microsoft";
1828
+ Provider2["MIRO"] = "miro";
1829
+ Provider2["MONDAY"] = "monday";
1830
+ Provider2["NOTION"] = "notion";
1831
+ Provider2["OUTREACH"] = "outreach";
1832
+ Provider2["PAGERDUTY"] = "pagerduty";
1833
+ Provider2["PAYPAL"] = "paypal";
1834
+ Provider2["PINTEREST"] = "pinterest";
1835
+ Provider2["PIPEDRIVE"] = "pipedrive";
1836
+ Provider2["QUICKBOOKS"] = "quickbooks";
1837
+ Provider2["RAMP"] = "ramp";
1838
+ Provider2["REDDIT"] = "reddit";
1839
+ Provider2["RINGCENTRAL"] = "ringcentral";
1840
+ Provider2["SALESFORCE"] = "salesforce";
1187
1841
  Provider2["SENTRY"] = "sentry";
1842
+ Provider2["SLACK"] = "slack";
1843
+ Provider2["SNAPCHAT"] = "snapchat";
1844
+ Provider2["SPOTIFY"] = "spotify";
1845
+ Provider2["SQUARE"] = "square";
1846
+ Provider2["SQUARESPACE"] = "squarespace";
1847
+ Provider2["STRIPE"] = "stripe";
1848
+ Provider2["TIKTOK"] = "tiktok";
1849
+ Provider2["TODOIST"] = "todoist";
1850
+ Provider2["TWITTER"] = "twitter";
1851
+ Provider2["TYPEFORM"] = "typeform";
1852
+ Provider2["WEBEX"] = "webex";
1853
+ Provider2["WEBFLOW"] = "webflow";
1188
1854
  return Provider2;
1189
1855
  })(Provider || {});
1190
1856
  var HttpMethod = /* @__PURE__ */ ((HttpMethod2) => {
@@ -1203,17 +1869,26 @@ var HttpMethod = /* @__PURE__ */ ((HttpMethod2) => {
1203
1869
  ActorType,
1204
1870
  AlterSDKError,
1205
1871
  AlterVault,
1872
+ BackendError,
1873
+ ConnectConfigError,
1874
+ ConnectDeniedError,
1875
+ ConnectFlowError,
1876
+ ConnectResult,
1206
1877
  ConnectSession,
1878
+ ConnectTimeoutError,
1879
+ ConnectionDeletedError,
1880
+ ConnectionExpiredError,
1207
1881
  ConnectionInfo,
1208
1882
  ConnectionListResult,
1209
1883
  ConnectionNotFoundError,
1884
+ ConnectionRevokedError,
1210
1885
  HttpMethod,
1211
1886
  NetworkError,
1212
1887
  PolicyViolationError,
1213
1888
  Provider,
1214
1889
  ProviderAPIError,
1890
+ ReAuthRequiredError,
1891
+ ScopeReauthRequiredError,
1215
1892
  TimeoutError,
1216
- TokenExpiredError,
1217
- TokenResponse,
1218
- TokenRetrievalError
1893
+ TokenResponse
1219
1894
  });