@alter-ai/alter-sdk 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -17,40 +17,70 @@ 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 {
27
- policyError;
28
- constructor(message, policyError, details) {
26
+ var ReAuthRequiredError = class extends BackendError {
27
+ constructor(message, details) {
29
28
  super(message, details);
30
- this.name = "PolicyViolationError";
31
- this.policyError = policyError;
29
+ this.name = "ReAuthRequiredError";
32
30
  }
33
31
  };
34
- var ConnectionNotFoundError = class extends TokenRetrievalError {
32
+ var ConnectionExpiredError = class extends ReAuthRequiredError {
35
33
  constructor(message, details) {
36
34
  super(message, details);
37
- this.name = "ConnectionNotFoundError";
35
+ this.name = "ConnectionExpiredError";
38
36
  }
39
37
  };
40
- var TokenExpiredError = class extends TokenRetrievalError {
38
+ var ConnectionRevokedError = class extends ReAuthRequiredError {
41
39
  connectionId;
42
40
  constructor(message, connectionId, details) {
43
41
  super(message, details);
44
- this.name = "TokenExpiredError";
42
+ this.name = "ConnectionRevokedError";
45
43
  this.connectionId = connectionId;
46
44
  }
47
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 {
59
+ policyError;
60
+ constructor(message, policyError, details) {
61
+ super(message, details);
62
+ this.name = "PolicyViolationError";
63
+ this.policyError = policyError;
64
+ }
65
+ };
48
66
  var ConnectFlowError = class extends AlterSDKError {
49
67
  constructor(message, details) {
50
68
  super(message, details);
51
69
  this.name = "ConnectFlowError";
52
70
  }
53
71
  };
72
+ var ConnectDeniedError = class extends ConnectFlowError {
73
+ constructor(message, details) {
74
+ super(message, details);
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
+ };
54
84
  var ConnectTimeoutError = class extends ConnectFlowError {
55
85
  constructor(message, details) {
56
86
  super(message, details);
@@ -67,6 +97,16 @@ var ProviderAPIError = class extends AlterSDKError {
67
97
  this.responseBody = responseBody;
68
98
  }
69
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
+ };
70
110
  var NetworkError = class extends AlterSDKError {
71
111
  constructor(message, details) {
72
112
  super(message, details);
@@ -106,6 +146,8 @@ var TokenResponse = class _TokenResponse {
106
146
  injectionFormat;
107
147
  /** Extra credentials for multi-part auth (e.g. AWS SigV4 secret_key, region) */
108
148
  additionalCredentials;
149
+ /** Additional injection rules for multi-header or query param auth */
150
+ additionalInjections;
109
151
  constructor(data) {
110
152
  this.tokenType = data.token_type ?? "Bearer";
111
153
  this.expiresIn = data.expires_in ?? null;
@@ -121,6 +163,7 @@ var TokenResponse = class _TokenResponse {
121
163
  } else {
122
164
  this.additionalCredentials = null;
123
165
  }
166
+ this.additionalInjections = data.additional_injections ?? null;
124
167
  Object.freeze(this);
125
168
  }
126
169
  /**
@@ -166,7 +209,8 @@ var TokenResponse = class _TokenResponse {
166
209
  expires_in: this.expiresIn,
167
210
  expires_at: this.expiresAt?.toISOString() ?? null,
168
211
  scopes: this.scopes,
169
- connection_id: this.connectionId
212
+ connection_id: this.connectionId,
213
+ provider_id: this.providerId
170
214
  };
171
215
  }
172
216
  /**
@@ -183,22 +227,24 @@ var TokenResponse = class _TokenResponse {
183
227
  }
184
228
  };
185
229
  var ConnectionInfo = class {
186
- id;
230
+ connectionId;
187
231
  providerId;
188
232
  scopes;
189
233
  accountIdentifier;
190
234
  accountDisplayName;
191
235
  status;
236
+ scopeMismatch;
192
237
  expiresAt;
193
238
  createdAt;
194
239
  lastUsedAt;
195
240
  constructor(data) {
196
- this.id = data.id;
241
+ this.connectionId = data.connection_id;
197
242
  this.providerId = data.provider_id;
198
243
  this.scopes = data.scopes ?? [];
199
244
  this.accountIdentifier = data.account_identifier ?? null;
200
245
  this.accountDisplayName = data.account_display_name ?? null;
201
246
  this.status = data.status;
247
+ this.scopeMismatch = data.scope_mismatch ?? false;
202
248
  this.expiresAt = data.expires_at ?? null;
203
249
  this.createdAt = data.created_at;
204
250
  this.lastUsedAt = data.last_used_at ?? null;
@@ -206,19 +252,20 @@ var ConnectionInfo = class {
206
252
  }
207
253
  toJSON() {
208
254
  return {
209
- id: this.id,
255
+ connection_id: this.connectionId,
210
256
  provider_id: this.providerId,
211
257
  scopes: this.scopes,
212
258
  account_identifier: this.accountIdentifier,
213
259
  account_display_name: this.accountDisplayName,
214
260
  status: this.status,
261
+ scope_mismatch: this.scopeMismatch,
215
262
  expires_at: this.expiresAt,
216
263
  created_at: this.createdAt,
217
264
  last_used_at: this.lastUsedAt
218
265
  };
219
266
  }
220
267
  toString() {
221
- return `ConnectionInfo(id=${this.id}, provider=${this.providerId}, status=${this.status})`;
268
+ return `ConnectionInfo(connection_id=${this.connectionId}, provider=${this.providerId}, status=${this.status})`;
222
269
  }
223
270
  };
224
271
  var ConnectSession = class {
@@ -265,11 +312,23 @@ var ConnectResult = class {
265
312
  providerId;
266
313
  accountIdentifier;
267
314
  scopes;
315
+ connectionPolicy;
268
316
  constructor(data) {
269
317
  this.connectionId = data.connection_id;
270
318
  this.providerId = data.provider_id;
271
319
  this.accountIdentifier = data.account_identifier ?? null;
272
320
  this.scopes = data.scopes ?? [];
321
+ const rawPolicy = data.connection_policy;
322
+ if (rawPolicy) {
323
+ const raw = rawPolicy;
324
+ this.connectionPolicy = Object.freeze({
325
+ expiresAt: raw["expires_at"] !== void 0 ? raw["expires_at"] : rawPolicy.expiresAt ?? null,
326
+ createdBy: raw["created_by"] !== void 0 ? raw["created_by"] : rawPolicy.createdBy ?? null,
327
+ createdAt: raw["created_at"] !== void 0 ? raw["created_at"] : rawPolicy.createdAt ?? null
328
+ });
329
+ } else {
330
+ this.connectionPolicy = null;
331
+ }
273
332
  Object.freeze(this);
274
333
  }
275
334
  toJSON() {
@@ -277,7 +336,12 @@ var ConnectResult = class {
277
336
  connection_id: this.connectionId,
278
337
  provider_id: this.providerId,
279
338
  account_identifier: this.accountIdentifier,
280
- scopes: this.scopes
339
+ scopes: this.scopes,
340
+ connection_policy: this.connectionPolicy ? {
341
+ expires_at: this.connectionPolicy.expiresAt ?? null,
342
+ created_by: this.connectionPolicy.createdBy ?? null,
343
+ created_at: this.connectionPolicy.createdAt ?? null
344
+ } : null
281
345
  };
282
346
  }
283
347
  toString() {
@@ -291,7 +355,8 @@ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
291
355
  "x-api-key",
292
356
  "x-auth-token",
293
357
  "x-amz-date",
294
- "x-amz-content-sha256"
358
+ "x-amz-content-sha256",
359
+ "x-amz-security-token"
295
360
  ]);
296
361
  var APICallAuditLog = class {
297
362
  connectionId;
@@ -370,6 +435,7 @@ import { createHash, createHmac } from "crypto";
370
435
  var ALGORITHM = "AWS4-HMAC-SHA256";
371
436
  var AWS_HOST_RE = /^(?<service>[a-z0-9-]+)\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.amazonaws\.com$/;
372
437
  var S3_VIRTUAL_HOST_RE = /^[^.]+\.s3\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.amazonaws\.com$/;
438
+ var VPCE_HOST_RE = /^vpce-[a-z0-9-]+\.(?<service>[a-z0-9-]+)\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.vpce\.amazonaws\.com$/;
373
439
  function hmacSha256(key, message) {
374
440
  return createHmac("sha256", key).update(message, "utf8").digest();
375
441
  }
@@ -392,6 +458,10 @@ function detectServiceAndRegion(hostname) {
392
458
  if (s3m?.groups) {
393
459
  return { service: "s3", region: s3m.groups.region };
394
460
  }
461
+ const vpcem = VPCE_HOST_RE.exec(lower);
462
+ if (vpcem?.groups) {
463
+ return { service: vpcem.groups.service, region: vpcem.groups.region };
464
+ }
395
465
  return { service: null, region: null };
396
466
  }
397
467
  function deriveSigningKey(secretKey, dateStamp, region, service) {
@@ -426,18 +496,21 @@ function canonicalQueryString(query) {
426
496
  ]);
427
497
  }
428
498
  }
429
- sorted.sort((a, b) => {
499
+ const sigv4Encode = (s) => encodeURIComponent(s).replace(
500
+ /[!'()*]/g,
501
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
502
+ );
503
+ const encoded = sorted.map(
504
+ ([k, v]) => [sigv4Encode(k), sigv4Encode(v)]
505
+ );
506
+ encoded.sort((a, b) => {
430
507
  if (a[0] < b[0]) return -1;
431
508
  if (a[0] > b[0]) return 1;
432
509
  if (a[1] < b[1]) return -1;
433
510
  if (a[1] > b[1]) return 1;
434
511
  return 0;
435
512
  });
436
- const sigv4Encode = (s) => encodeURIComponent(s).replace(
437
- /[!'()*]/g,
438
- (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
439
- );
440
- return sorted.map(([k, v]) => `${sigv4Encode(k)}=${sigv4Encode(v)}`).join("&");
513
+ return encoded.map(([k, v]) => `${k}=${v}`).join("&");
441
514
  }
442
515
  function canonicalHeadersAndSigned(headers) {
443
516
  const canonical = {};
@@ -456,11 +529,9 @@ function signAwsRequest(opts) {
456
529
  const parsed = new URL(opts.url);
457
530
  const hostname = parsed.hostname;
458
531
  let { region, service } = opts;
459
- if (region == null || service == null) {
460
- const detected = detectServiceAndRegion(hostname);
461
- if (region == null) region = detected.region;
462
- if (service == null) service = detected.service;
463
- }
532
+ const detected = detectServiceAndRegion(hostname);
533
+ if (detected.region != null) region = detected.region;
534
+ if (detected.service != null) service = detected.service;
464
535
  if (!region) {
465
536
  throw new Error(
466
537
  `Cannot determine AWS region from URL '${opts.url}'. Pass region explicitly via additional_credentials.`
@@ -543,10 +614,11 @@ function _extractAdditionalCredentials(token) {
543
614
  return _additionalCredsStore.get(token);
544
615
  }
545
616
  var _fetch;
546
- var SDK_VERSION = "0.4.0";
617
+ var SDK_VERSION = "0.6.0";
547
618
  var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
548
619
  var HTTP_FORBIDDEN = 403;
549
620
  var HTTP_NOT_FOUND = 404;
621
+ var HTTP_GONE = 410;
550
622
  var HTTP_BAD_REQUEST = 400;
551
623
  var HTTP_UNAUTHORIZED = 401;
552
624
  var HTTP_BAD_GATEWAY = 502;
@@ -602,6 +674,9 @@ var HttpClient = class {
602
674
  };
603
675
  if (options?.body !== void 0) {
604
676
  init.body = options.body;
677
+ if (!mergedHeaders["Content-Type"]) {
678
+ mergedHeaders["Content-Type"] = "application/json";
679
+ }
605
680
  } else if (options?.json !== void 0) {
606
681
  init.body = JSON.stringify(options.json);
607
682
  mergedHeaders["Content-Type"] = "application/json";
@@ -638,6 +713,8 @@ var AlterVault = class _AlterVault {
638
713
  #closed = false;
639
714
  /** Pending audit log promises (fire-and-forget) */
640
715
  #auditPromises = /* @__PURE__ */ new Set();
716
+ /** JWT identity resolution: callable that returns user token */
717
+ #userTokenGetter = null;
641
718
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
642
719
  // Public readonly properties (frozen by Object.freeze)
643
720
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -673,6 +750,7 @@ var AlterVault = class _AlterVault {
673
750
  this.#actorName = options.actorName;
674
751
  this.#actorVersion = options.actorVersion;
675
752
  this.#clientType = options.clientType;
753
+ this.#userTokenGetter = options.userTokenGetter ?? null;
676
754
  this.#framework = options.framework;
677
755
  if (!_fetch) {
678
756
  _fetch = globalThis.fetch;
@@ -847,6 +925,24 @@ ${contentHash}`;
847
925
  }
848
926
  return false;
849
927
  }
928
+ /**
929
+ * Resolve a value_source to the actual value for injection.
930
+ *
931
+ * @param valueSource - "token" or "additional_credentials.<field>"
932
+ * @param accessToken - The main credential value
933
+ * @param additionalCreds - Additional credentials from vault
934
+ * @returns The resolved value, or null if not available
935
+ */
936
+ static #resolveInjectionValue(valueSource, accessToken, additionalCreds) {
937
+ if (valueSource === "token") {
938
+ return accessToken;
939
+ }
940
+ if (valueSource.startsWith("additional_credentials.")) {
941
+ const field = valueSource.slice("additional_credentials.".length);
942
+ return additionalCreds?.[field] ?? null;
943
+ }
944
+ return null;
945
+ }
850
946
  static async #safeParseJson(response) {
851
947
  try {
852
948
  return await response.clone().json();
@@ -868,12 +964,25 @@ ${contentHash}`;
868
964
  }
869
965
  if (response.status === HTTP_FORBIDDEN) {
870
966
  const errorData = await _AlterVault.#safeParseJson(response);
967
+ if (errorData.error === "connection_expired") {
968
+ throw new ConnectionExpiredError(
969
+ errorData.message ?? "Connection expired per TTL policy",
970
+ errorData.details
971
+ );
972
+ }
871
973
  throw new PolicyViolationError(
872
974
  errorData.message ?? "Access denied by policy",
873
975
  errorData.error,
874
976
  errorData.details
875
977
  );
876
978
  }
979
+ if (response.status === HTTP_GONE) {
980
+ const errorData = await _AlterVault.#safeParseJson(response);
981
+ throw new ConnectionDeletedError(
982
+ errorData.message ?? "Connection has been deleted. A new connection_id will be issued on re-authorization.",
983
+ errorData
984
+ );
985
+ }
877
986
  if (response.status === HTTP_NOT_FOUND) {
878
987
  const errorData = await _AlterVault.#safeParseJson(response);
879
988
  throw new ConnectionNotFoundError(
@@ -883,35 +992,35 @@ ${contentHash}`;
883
992
  }
884
993
  if (response.status === HTTP_BAD_REQUEST || response.status === HTTP_BAD_GATEWAY) {
885
994
  const errorData = await _AlterVault.#safeParseJson(response);
886
- if (JSON.stringify(errorData).toLowerCase().includes("token_expired")) {
887
- throw new TokenExpiredError(
888
- errorData.message ?? "Token expired and refresh failed",
995
+ if (errorData.error === "connection_revoked") {
996
+ throw new ConnectionRevokedError(
997
+ errorData.message ?? "Connection has been revoked. User must re-authorize.",
889
998
  errorData.connection_id,
890
999
  errorData
891
1000
  );
892
1001
  }
893
- throw new TokenRetrievalError(
1002
+ throw new BackendError(
894
1003
  errorData.message ?? `Backend error ${response.status}`,
895
1004
  errorData
896
1005
  );
897
1006
  }
898
1007
  if (response.status === HTTP_UNAUTHORIZED) {
899
1008
  const errorData = await _AlterVault.#safeParseJson(response);
900
- throw new TokenRetrievalError(
1009
+ throw new BackendError(
901
1010
  errorData.message ?? "Unauthorized \u2014 check your API key",
902
1011
  errorData
903
1012
  );
904
1013
  }
905
1014
  if (response.status === HTTP_INTERNAL_SERVER_ERROR || response.status === HTTP_SERVICE_UNAVAILABLE) {
906
1015
  const errorData = await _AlterVault.#safeParseJson(response);
907
- throw new TokenRetrievalError(
1016
+ throw new BackendError(
908
1017
  errorData.message ?? `Backend unavailable (HTTP ${response.status})`,
909
1018
  errorData
910
1019
  );
911
1020
  }
912
1021
  if (response.status >= HTTP_CLIENT_ERROR_START) {
913
1022
  const errorData = await _AlterVault.#safeParseJson(response);
914
- throw new TokenRetrievalError(
1023
+ throw new BackendError(
915
1024
  errorData.message ?? `Unexpected backend error (HTTP ${response.status})`,
916
1025
  errorData
917
1026
  );
@@ -923,24 +1032,52 @@ ${contentHash}`;
923
1032
  * This is a private method. Tokens are NEVER exposed to developers.
924
1033
  * Use request() instead, which handles tokens internally.
925
1034
  */
926
- async #getToken(connectionId, reason, requestMetadata, runId, threadId, toolCallId) {
1035
+ async #getUserToken() {
1036
+ if (!this.#userTokenGetter) {
1037
+ throw new AlterSDKError(
1038
+ "userTokenGetter is required for provider-based resolution. Pass userTokenGetter to AlterVault constructor."
1039
+ );
1040
+ }
1041
+ const result = this.#userTokenGetter();
1042
+ const token = result instanceof Promise ? await result : result;
1043
+ if (!token || typeof token !== "string") {
1044
+ throw new AlterSDKError("userTokenGetter must return a non-empty string");
1045
+ }
1046
+ return token;
1047
+ }
1048
+ async #getToken(connectionId, reason, requestMetadata, runId, threadId, toolCallId, provider, account) {
927
1049
  const actorHeaders = this.#getActorRequestHeaders(
928
1050
  runId,
929
1051
  threadId,
930
1052
  toolCallId
931
1053
  );
932
1054
  let response;
933
- const tokenBody = {
934
- connection_id: connectionId,
935
- reason: reason ?? null,
936
- request: requestMetadata ?? null
937
- };
1055
+ let tokenBody;
1056
+ if (provider) {
1057
+ const userToken = await this.#getUserToken();
1058
+ tokenBody = {
1059
+ provider_id: provider,
1060
+ user_token: userToken,
1061
+ reason: reason ?? null,
1062
+ request: requestMetadata ?? null
1063
+ };
1064
+ if (account) {
1065
+ tokenBody.account = account;
1066
+ }
1067
+ } else {
1068
+ tokenBody = {
1069
+ connection_id: connectionId,
1070
+ reason: reason ?? null,
1071
+ request: requestMetadata ?? null
1072
+ };
1073
+ }
938
1074
  const tokenPath = "/sdk/token";
939
- const hmacHeaders = this.#computeHmacHeaders("POST", tokenPath, JSON.stringify(tokenBody));
1075
+ const tokenBodyStr = JSON.stringify(tokenBody);
1076
+ const hmacHeaders = this.#computeHmacHeaders("POST", tokenPath, tokenBodyStr);
940
1077
  try {
941
1078
  response = await this.#alterClient.post(tokenPath, {
942
- json: tokenBody,
943
- headers: { ...actorHeaders, ...hmacHeaders }
1079
+ body: tokenBodyStr,
1080
+ headers: { ...actorHeaders, ...hmacHeaders, "Content-Type": "application/json" }
944
1081
  });
945
1082
  } catch (error) {
946
1083
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
@@ -955,7 +1092,7 @@ ${contentHash}`;
955
1092
  { base_url: this.baseUrl }
956
1093
  );
957
1094
  }
958
- throw new TokenRetrievalError(
1095
+ throw new BackendError(
959
1096
  `Failed to retrieve token: ${error instanceof Error ? error.message : String(error)}`,
960
1097
  { connection_id: connectionId, error: String(error) }
961
1098
  );
@@ -964,15 +1101,16 @@ ${contentHash}`;
964
1101
  await this.#handleErrorResponse(response);
965
1102
  const tokenData = await response.json();
966
1103
  const typedData = tokenData;
1104
+ const scopeMismatch = typedData.scope_mismatch ?? false;
967
1105
  const tokenResponse = new TokenResponse(typedData);
968
1106
  if (!/^[A-Za-z][A-Za-z0-9-]*$/.test(tokenResponse.injectionHeader)) {
969
- throw new TokenRetrievalError(
1107
+ throw new BackendError(
970
1108
  `Backend returned invalid injection_header: ${tokenResponse.injectionHeader}`,
971
1109
  { connectionId: String(connectionId) }
972
1110
  );
973
1111
  }
974
1112
  if (/[\r\n\x00]/.test(tokenResponse.injectionFormat)) {
975
- throw new TokenRetrievalError(
1113
+ throw new BackendError(
976
1114
  `Backend returned invalid injection_format (contains control characters)`,
977
1115
  { connectionId: String(connectionId) }
978
1116
  );
@@ -981,7 +1119,7 @@ ${contentHash}`;
981
1119
  if (typedData.additional_credentials) {
982
1120
  _storeAdditionalCredentials(tokenResponse, typedData.additional_credentials);
983
1121
  }
984
- return tokenResponse;
1122
+ return { tokenResponse, scopeMismatch };
985
1123
  }
986
1124
  /**
987
1125
  * Log an API call to the backend audit endpoint (INTERNAL).
@@ -1011,10 +1149,11 @@ ${contentHash}`;
1011
1149
  const actorHeaders = this.#getActorRequestHeaders(params.runId);
1012
1150
  const auditPath = "/sdk/oauth/audit/api-call";
1013
1151
  const auditBody = sanitized;
1014
- const auditHmac = this.#computeHmacHeaders("POST", auditPath, JSON.stringify(auditBody));
1152
+ const auditBodyStr = JSON.stringify(auditBody);
1153
+ const auditHmac = this.#computeHmacHeaders("POST", auditPath, auditBodyStr);
1015
1154
  const response = await this.#alterClient.post(auditPath, {
1016
- json: auditBody,
1017
- headers: { ...actorHeaders, ...auditHmac }
1155
+ body: auditBodyStr,
1156
+ headers: { ...actorHeaders, ...auditHmac, "Content-Type": "application/json" }
1018
1157
  });
1019
1158
  this.#cacheActorIdFromResponse(response);
1020
1159
  if (!response.ok) {
@@ -1086,12 +1225,22 @@ ${contentHash}`;
1086
1225
  "SDK instance has been closed. Create a new AlterVault instance to make requests."
1087
1226
  );
1088
1227
  }
1228
+ const provider = options?.provider;
1229
+ const account = options?.account;
1230
+ if (!connectionId && !provider) {
1231
+ throw new AlterSDKError("Provide connectionId or options.provider");
1232
+ }
1233
+ if (connectionId && provider) {
1234
+ throw new AlterSDKError("Cannot provide both connectionId and options.provider");
1235
+ }
1236
+ const effectiveConnectionId = connectionId ?? null;
1237
+ let currentUrl = url;
1089
1238
  const runId = options?.runId ?? randomUUID();
1090
1239
  const methodStr = String(method).toUpperCase();
1091
- const urlLower = url.toLowerCase();
1240
+ const urlLower = currentUrl.toLowerCase();
1092
1241
  if (!ALLOWED_URL_SCHEMES.some((scheme) => urlLower.startsWith(scheme))) {
1093
1242
  throw new AlterSDKError(
1094
- `URL must start with https:// or http://, got: ${url.slice(0, 50)}`
1243
+ `URL must start with https:// or http://, got: ${currentUrl.slice(0, 50)}`
1095
1244
  );
1096
1245
  }
1097
1246
  if (options?.pathParams && Object.keys(options.pathParams).length > 0) {
@@ -1100,7 +1249,7 @@ ${contentHash}`;
1100
1249
  encodedParams[key] = encodeURIComponent(String(value));
1101
1250
  }
1102
1251
  try {
1103
- let resolvedUrl = url;
1252
+ let resolvedUrl = currentUrl;
1104
1253
  for (const [key, value] of Object.entries(encodedParams)) {
1105
1254
  const placeholder = `{${key}}`;
1106
1255
  if (!resolvedUrl.includes(placeholder)) {
@@ -1111,26 +1260,28 @@ ${contentHash}`;
1111
1260
  const remaining = resolvedUrl.match(/\{(\w+)\}/);
1112
1261
  if (remaining) {
1113
1262
  throw new AlterSDKError(
1114
- `Invalid URL template or missing path_params: '${remaining[1]}'. URL: ${url}, path_params: ${JSON.stringify(options.pathParams)}`
1263
+ `Invalid URL template or missing path_params: '${remaining[1]}'. URL: ${currentUrl}, path_params: ${JSON.stringify(options.pathParams)}`
1115
1264
  );
1116
1265
  }
1117
- url = resolvedUrl;
1266
+ currentUrl = resolvedUrl;
1118
1267
  } catch (error) {
1119
1268
  if (error instanceof AlterSDKError) {
1120
1269
  throw error;
1121
1270
  }
1122
1271
  throw new AlterSDKError(
1123
- `Invalid URL template or missing path_params: ${error instanceof Error ? error.message : String(error)}. URL: ${url}, path_params: ${JSON.stringify(options.pathParams)}`
1272
+ `Invalid URL template or missing path_params: ${error instanceof Error ? error.message : String(error)}. URL: ${currentUrl}, path_params: ${JSON.stringify(options.pathParams)}`
1124
1273
  );
1125
1274
  }
1126
1275
  }
1127
- const tokenResponse = await this.#getToken(
1128
- connectionId,
1276
+ const { tokenResponse, scopeMismatch } = await this.#getToken(
1277
+ effectiveConnectionId,
1129
1278
  options?.reason,
1130
- { method: methodStr, url },
1279
+ { method: methodStr, url: currentUrl },
1131
1280
  runId,
1132
1281
  options?.threadId,
1133
- options?.toolCallId
1282
+ options?.toolCallId,
1283
+ provider,
1284
+ account
1134
1285
  );
1135
1286
  const requestHeaders = options?.extraHeaders ? { ...options.extraHeaders } : {};
1136
1287
  const accessToken = _extractAccessToken(tokenResponse);
@@ -1147,14 +1298,14 @@ ${contentHash}`;
1147
1298
  }
1148
1299
  }
1149
1300
  if (!additionalCreds.secret_key) {
1150
- throw new TokenRetrievalError(
1301
+ throw new BackendError(
1151
1302
  "AWS SigV4 credential is missing secret_key in additional_credentials. Re-store the credential with both Access Key ID and Secret Access Key.",
1152
- { connection_id: connectionId }
1303
+ { connection_id: effectiveConnectionId }
1153
1304
  );
1154
1305
  }
1155
1306
  const awsHeaders = signAwsRequest({
1156
1307
  method: methodStr,
1157
- url,
1308
+ url: currentUrl,
1158
1309
  headers: requestHeaders,
1159
1310
  body: sigv4BodyStr,
1160
1311
  accessKeyId,
@@ -1173,6 +1324,25 @@ ${contentHash}`;
1173
1324
  }
1174
1325
  requestHeaders[tokenResponse.injectionHeader] = tokenResponse.injectionFormat.replace("{token}", accessToken);
1175
1326
  }
1327
+ const auditUrl = currentUrl;
1328
+ if (tokenResponse.additionalInjections && !isSigV4) {
1329
+ for (const rule of tokenResponse.additionalInjections) {
1330
+ const value = _AlterVault.#resolveInjectionValue(
1331
+ rule.value_source,
1332
+ accessToken,
1333
+ additionalCreds
1334
+ );
1335
+ if (!value) continue;
1336
+ if (!/^[A-Za-z][A-Za-z0-9\-_]*$/.test(rule.key)) continue;
1337
+ if (rule.target === "header") {
1338
+ requestHeaders[rule.key] = value;
1339
+ } else if (rule.target === "query_param") {
1340
+ const urlObj = new URL(currentUrl);
1341
+ urlObj.searchParams.set(rule.key, value);
1342
+ currentUrl = urlObj.toString();
1343
+ }
1344
+ }
1345
+ }
1176
1346
  if (!requestHeaders["User-Agent"]) {
1177
1347
  requestHeaders["User-Agent"] = SDK_USER_AGENT;
1178
1348
  }
@@ -1180,13 +1350,13 @@ ${contentHash}`;
1180
1350
  let response;
1181
1351
  try {
1182
1352
  if (isSigV4 && sigv4BodyStr != null) {
1183
- response = await this.#providerClient.request(methodStr, url, {
1353
+ response = await this.#providerClient.request(methodStr, currentUrl, {
1184
1354
  body: sigv4BodyStr,
1185
1355
  headers: requestHeaders,
1186
1356
  params: options?.queryParams
1187
1357
  });
1188
1358
  } else {
1189
- response = await this.#providerClient.request(methodStr, url, {
1359
+ response = await this.#providerClient.request(methodStr, currentUrl, {
1190
1360
  json: options?.json,
1191
1361
  headers: requestHeaders,
1192
1362
  params: options?.queryParams
@@ -1197,18 +1367,18 @@ ${contentHash}`;
1197
1367
  throw new TimeoutError(
1198
1368
  `Provider API request timed out: ${error instanceof Error ? error.message : String(error)}`,
1199
1369
  {
1200
- connection_id: connectionId,
1370
+ connection_id: effectiveConnectionId,
1201
1371
  method: methodStr,
1202
- url
1372
+ url: currentUrl
1203
1373
  }
1204
1374
  );
1205
1375
  }
1206
1376
  throw new NetworkError(
1207
1377
  `Failed to call provider API: ${error instanceof Error ? error.message : String(error)}`,
1208
1378
  {
1209
- connection_id: connectionId,
1379
+ connection_id: effectiveConnectionId,
1210
1380
  method: methodStr,
1211
- url,
1381
+ url: currentUrl,
1212
1382
  error: String(error)
1213
1383
  }
1214
1384
  );
@@ -1216,6 +1386,13 @@ ${contentHash}`;
1216
1386
  const latencyMs = Date.now() - startTime;
1217
1387
  const sigv4Sensitive = /* @__PURE__ */ new Set(["authorization", "x-amz-date", "x-amz-content-sha256"]);
1218
1388
  const stripHeaders = isSigV4 ? sigv4Sensitive : /* @__PURE__ */ new Set([injectionHeaderLower]);
1389
+ if (tokenResponse.additionalInjections && !isSigV4) {
1390
+ for (const rule of tokenResponse.additionalInjections) {
1391
+ if (rule.target === "header") {
1392
+ stripHeaders.add(rule.key.toLowerCase());
1393
+ }
1394
+ }
1395
+ }
1219
1396
  const auditHeaders = {};
1220
1397
  for (const [key, value] of Object.entries(requestHeaders)) {
1221
1398
  if (!stripHeaders.has(key.toLowerCase())) {
@@ -1229,9 +1406,9 @@ ${contentHash}`;
1229
1406
  });
1230
1407
  this.#scheduleAuditLog({
1231
1408
  connectionId: tokenResponse.connectionId,
1232
- providerId: tokenResponse.providerId || connectionId,
1409
+ providerId: tokenResponse.providerId || effectiveConnectionId || "",
1233
1410
  method: methodStr,
1234
- url,
1411
+ url: auditUrl,
1235
1412
  requestHeaders: auditHeaders,
1236
1413
  requestBody: options?.json ?? null,
1237
1414
  responseStatus: response.status,
@@ -1244,14 +1421,29 @@ ${contentHash}`;
1244
1421
  toolCallId: options?.toolCallId ?? null
1245
1422
  });
1246
1423
  if (response.status >= HTTP_CLIENT_ERROR_START) {
1424
+ if (response.status === HTTP_FORBIDDEN && scopeMismatch) {
1425
+ throw new ScopeReauthRequiredError(
1426
+ "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.",
1427
+ tokenResponse.connectionId,
1428
+ tokenResponse.providerId,
1429
+ response.status,
1430
+ responseBody,
1431
+ {
1432
+ connection_id: tokenResponse.connectionId,
1433
+ provider_id: tokenResponse.providerId,
1434
+ method: methodStr,
1435
+ url: currentUrl
1436
+ }
1437
+ );
1438
+ }
1247
1439
  throw new ProviderAPIError(
1248
1440
  `Provider API returned error ${response.status}`,
1249
1441
  response.status,
1250
1442
  responseBody,
1251
1443
  {
1252
- connection_id: connectionId,
1444
+ connection_id: effectiveConnectionId,
1253
1445
  method: methodStr,
1254
- url
1446
+ url: currentUrl
1255
1447
  }
1256
1448
  );
1257
1449
  }
@@ -1276,12 +1468,23 @@ ${contentHash}`;
1276
1468
  limit: options?.limit ?? 100,
1277
1469
  offset: options?.offset ?? 0
1278
1470
  };
1471
+ if (this.#userTokenGetter) {
1472
+ try {
1473
+ listBody.user_token = await this.#getUserToken();
1474
+ } catch (err) {
1475
+ console.warn(
1476
+ "user_token_getter failed in listConnections, falling back to un-scoped listing:",
1477
+ err instanceof Error ? err.message : String(err)
1478
+ );
1479
+ }
1480
+ }
1279
1481
  const listPath = "/sdk/oauth/connections/list";
1280
- const listHmac = this.#computeHmacHeaders("POST", listPath, JSON.stringify(listBody));
1482
+ const listBodyStr = JSON.stringify(listBody);
1483
+ const listHmac = this.#computeHmacHeaders("POST", listPath, listBodyStr);
1281
1484
  try {
1282
1485
  response = await this.#alterClient.post(listPath, {
1283
- json: listBody,
1284
- headers: { ...actorHeaders, ...listHmac }
1486
+ body: listBodyStr,
1487
+ headers: { ...actorHeaders, ...listHmac, "Content-Type": "application/json" }
1285
1488
  });
1286
1489
  } catch (error) {
1287
1490
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
@@ -1327,24 +1530,37 @@ ${contentHash}`;
1327
1530
  "SDK instance has been closed. Create a new AlterVault instance to make requests."
1328
1531
  );
1329
1532
  }
1330
- if (!options.endUser?.id) {
1331
- throw new AlterSDKError("endUser.id is required");
1332
- }
1333
1533
  const actorHeaders = this.#getActorRequestHeaders();
1334
1534
  let response;
1335
1535
  const sessionBody = {
1336
- end_user: options.endUser,
1337
1536
  allowed_providers: options.allowedProviders ?? null,
1338
1537
  return_url: options.returnUrl ?? null,
1339
1538
  allowed_origin: options.allowedOrigin ?? null,
1340
1539
  metadata: options.metadata ?? null
1341
1540
  };
1541
+ if (options.connectionPolicy) {
1542
+ sessionBody.connection_policy = {
1543
+ max_ttl_seconds: options.connectionPolicy.maxTtlSeconds ?? null,
1544
+ default_ttl_seconds: options.connectionPolicy.defaultTtlSeconds ?? null
1545
+ };
1546
+ }
1547
+ if (this.#userTokenGetter) {
1548
+ try {
1549
+ sessionBody.user_token = await this.#getUserToken();
1550
+ } catch (err) {
1551
+ console.warn(
1552
+ "userTokenGetter failed in createConnectSession, session will not have identity tagging:",
1553
+ err instanceof Error ? err.message : String(err)
1554
+ );
1555
+ }
1556
+ }
1342
1557
  const sessionPath = "/sdk/oauth/connect/session";
1343
- const sessionHmac = this.#computeHmacHeaders("POST", sessionPath, JSON.stringify(sessionBody));
1558
+ const sessionBodyStr = JSON.stringify(sessionBody);
1559
+ const sessionHmac = this.#computeHmacHeaders("POST", sessionPath, sessionBodyStr);
1344
1560
  try {
1345
1561
  response = await this.#alterClient.post(sessionPath, {
1346
- json: sessionBody,
1347
- headers: { ...actorHeaders, ...sessionHmac }
1562
+ body: sessionBodyStr,
1563
+ headers: { ...actorHeaders, ...sessionHmac, "Content-Type": "application/json" }
1348
1564
  });
1349
1565
  } catch (error) {
1350
1566
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
@@ -1375,7 +1591,7 @@ ${contentHash}`;
1375
1591
  * server-side applications. It creates a Connect session, opens the
1376
1592
  * browser, and polls until the user completes OAuth.
1377
1593
  *
1378
- * @param options - Connect options (endUser is required)
1594
+ * @param options - Connect options
1379
1595
  * @returns Array of ConnectResult objects (one per connected provider)
1380
1596
  * @throws ConnectTimeoutError if the user doesn't complete within timeout
1381
1597
  * @throws ConnectFlowError if the user denies or provider returns error
@@ -1391,8 +1607,8 @@ ${contentHash}`;
1391
1607
  const pollInterval = options.pollInterval ?? 2;
1392
1608
  const openBrowser = options.openBrowser ?? true;
1393
1609
  const session = await this.createConnectSession({
1394
- endUser: options.endUser,
1395
- allowedProviders: options.providers
1610
+ allowedProviders: options.providers,
1611
+ connectionPolicy: options.connectionPolicy
1396
1612
  });
1397
1613
  if (openBrowser) {
1398
1614
  try {
@@ -1429,18 +1645,28 @@ ${contentHash}`;
1429
1645
  connection_id: conn.connection_id ?? "",
1430
1646
  provider_id: conn.provider_id ?? "",
1431
1647
  account_identifier: conn.account_identifier ?? null,
1432
- scopes: conn.scopes ?? []
1648
+ scopes: conn.scopes ?? [],
1649
+ connection_policy: conn.connection_policy ?? null
1433
1650
  })
1434
1651
  );
1435
1652
  }
1436
1653
  if (pollStatus === "error") {
1437
1654
  const err = pollResult.error;
1438
- throw new ConnectFlowError(
1439
- err?.error_message ?? "OAuth flow failed",
1440
- {
1441
- error_code: err?.error_code ?? "unknown_error"
1442
- }
1443
- );
1655
+ const errorCode = err?.error_code ?? "unknown_error";
1656
+ const errorMessage = err?.error_message ?? "OAuth flow failed";
1657
+ const errorDetails = { error_code: errorCode };
1658
+ if (errorCode === "connect_denied") {
1659
+ throw new ConnectDeniedError(errorMessage, errorDetails);
1660
+ }
1661
+ if ([
1662
+ "connect_config_error",
1663
+ "invalid_redirect_uri",
1664
+ "invalid_client",
1665
+ "unauthorized_client"
1666
+ ].includes(errorCode)) {
1667
+ throw new ConnectConfigError(errorMessage, errorDetails);
1668
+ }
1669
+ throw new ConnectFlowError(errorMessage, errorDetails);
1444
1670
  }
1445
1671
  if (pollStatus === "expired") {
1446
1672
  throw new ConnectFlowError(
@@ -1462,16 +1688,17 @@ ${contentHash}`;
1462
1688
  const actorHeaders = this.#getActorRequestHeaders();
1463
1689
  const pollPath = "/sdk/oauth/connect/session/poll";
1464
1690
  const pollBody = { session_token: sessionToken };
1691
+ const pollBodyStr = JSON.stringify(pollBody);
1465
1692
  const pollHmac = this.#computeHmacHeaders(
1466
1693
  "POST",
1467
1694
  pollPath,
1468
- JSON.stringify(pollBody)
1695
+ pollBodyStr
1469
1696
  );
1470
1697
  let response;
1471
1698
  try {
1472
1699
  response = await this.#alterClient.post(pollPath, {
1473
- json: pollBody,
1474
- headers: { ...actorHeaders, ...pollHmac }
1700
+ body: pollBodyStr,
1701
+ headers: { ...actorHeaders, ...pollHmac, "Content-Type": "application/json" }
1475
1702
  });
1476
1703
  } catch (error) {
1477
1704
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
@@ -1519,10 +1746,72 @@ Object.freeze(AlterVault.prototype);
1519
1746
 
1520
1747
  // src/providers/enums.ts
1521
1748
  var Provider = /* @__PURE__ */ ((Provider2) => {
1522
- Provider2["GOOGLE"] = "google";
1749
+ Provider2["ACUITY_SCHEDULING"] = "acuity-scheduling";
1750
+ Provider2["ADOBE"] = "adobe";
1751
+ Provider2["AIRCALL"] = "aircall";
1752
+ Provider2["AIRTABLE"] = "airtable";
1753
+ Provider2["APOLLO"] = "apollo";
1754
+ Provider2["ASANA"] = "asana";
1755
+ Provider2["ATLASSIAN"] = "atlassian";
1756
+ Provider2["ATTIO"] = "attio";
1757
+ Provider2["AUTODESK"] = "autodesk";
1758
+ Provider2["BASECAMP"] = "basecamp";
1759
+ Provider2["BITBUCKET"] = "bitbucket";
1760
+ Provider2["BITLY"] = "bitly";
1761
+ Provider2["BOX"] = "box";
1762
+ Provider2["BREX"] = "brex";
1763
+ Provider2["CALENDLY"] = "calendly";
1764
+ Provider2["CAL_COM"] = "cal-com";
1765
+ Provider2["CANVA"] = "canva";
1766
+ Provider2["CLICKUP"] = "clickup";
1767
+ Provider2["CLOSE"] = "close";
1768
+ Provider2["CONSTANT_CONTACT"] = "constant-contact";
1769
+ Provider2["CONTENTFUL"] = "contentful";
1770
+ Provider2["DEEL"] = "deel";
1771
+ Provider2["DIALPAD"] = "dialpad";
1772
+ Provider2["DIGITALOCEAN"] = "digitalocean";
1773
+ Provider2["DISCORD"] = "discord";
1774
+ Provider2["DOCUSIGN"] = "docusign";
1775
+ Provider2["DROPBOX"] = "dropbox";
1776
+ Provider2["EBAY"] = "ebay";
1777
+ Provider2["EVENTBRITE"] = "eventbrite";
1778
+ Provider2["FACEBOOK"] = "facebook";
1779
+ Provider2["FIGMA"] = "figma";
1523
1780
  Provider2["GITHUB"] = "github";
1524
- Provider2["SLACK"] = "slack";
1781
+ Provider2["GOOGLE"] = "google";
1782
+ Provider2["HUBSPOT"] = "hubspot";
1783
+ Provider2["INSTAGRAM"] = "instagram";
1784
+ Provider2["LINEAR"] = "linear";
1785
+ Provider2["LINKEDIN"] = "linkedin";
1786
+ Provider2["MAILCHIMP"] = "mailchimp";
1787
+ Provider2["MERCURY"] = "mercury";
1788
+ Provider2["MICROSOFT"] = "microsoft";
1789
+ Provider2["MIRO"] = "miro";
1790
+ Provider2["MONDAY"] = "monday";
1791
+ Provider2["NOTION"] = "notion";
1792
+ Provider2["OUTREACH"] = "outreach";
1793
+ Provider2["PAGERDUTY"] = "pagerduty";
1794
+ Provider2["PAYPAL"] = "paypal";
1795
+ Provider2["PINTEREST"] = "pinterest";
1796
+ Provider2["PIPEDRIVE"] = "pipedrive";
1797
+ Provider2["QUICKBOOKS"] = "quickbooks";
1798
+ Provider2["RAMP"] = "ramp";
1799
+ Provider2["REDDIT"] = "reddit";
1800
+ Provider2["RINGCENTRAL"] = "ringcentral";
1801
+ Provider2["SALESFORCE"] = "salesforce";
1525
1802
  Provider2["SENTRY"] = "sentry";
1803
+ Provider2["SLACK"] = "slack";
1804
+ Provider2["SNAPCHAT"] = "snapchat";
1805
+ Provider2["SPOTIFY"] = "spotify";
1806
+ Provider2["SQUARE"] = "square";
1807
+ Provider2["SQUARESPACE"] = "squarespace";
1808
+ Provider2["STRIPE"] = "stripe";
1809
+ Provider2["TIKTOK"] = "tiktok";
1810
+ Provider2["TODOIST"] = "todoist";
1811
+ Provider2["TWITTER"] = "twitter";
1812
+ Provider2["TYPEFORM"] = "typeform";
1813
+ Provider2["WEBEX"] = "webex";
1814
+ Provider2["WEBFLOW"] = "webflow";
1526
1815
  return Provider2;
1527
1816
  })(Provider || {});
1528
1817
  var HttpMethod = /* @__PURE__ */ ((HttpMethod2) => {
@@ -1540,20 +1829,26 @@ export {
1540
1829
  ActorType,
1541
1830
  AlterSDKError,
1542
1831
  AlterVault,
1832
+ BackendError,
1833
+ ConnectConfigError,
1834
+ ConnectDeniedError,
1543
1835
  ConnectFlowError,
1544
1836
  ConnectResult,
1545
1837
  ConnectSession,
1546
1838
  ConnectTimeoutError,
1839
+ ConnectionDeletedError,
1840
+ ConnectionExpiredError,
1547
1841
  ConnectionInfo,
1548
1842
  ConnectionListResult,
1549
1843
  ConnectionNotFoundError,
1844
+ ConnectionRevokedError,
1550
1845
  HttpMethod,
1551
1846
  NetworkError,
1552
1847
  PolicyViolationError,
1553
1848
  Provider,
1554
1849
  ProviderAPIError,
1850
+ ReAuthRequiredError,
1851
+ ScopeReauthRequiredError,
1555
1852
  TimeoutError,
1556
- TokenExpiredError,
1557
- TokenResponse,
1558
- TokenRetrievalError
1853
+ TokenResponse
1559
1854
  };