@alter-ai/alter-sdk 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/client.ts
2
+ import { createHash as createHash2, createHmac as createHmac2, randomUUID } from "crypto";
3
+
1
4
  // src/exceptions.ts
2
5
  var AlterSDKError = class extends Error {
3
6
  details;
@@ -42,6 +45,18 @@ var TokenExpiredError = class extends TokenRetrievalError {
42
45
  this.connectionId = connectionId;
43
46
  }
44
47
  };
48
+ var ConnectFlowError = class extends AlterSDKError {
49
+ constructor(message, details) {
50
+ super(message, details);
51
+ this.name = "ConnectFlowError";
52
+ }
53
+ };
54
+ var ConnectTimeoutError = class extends ConnectFlowError {
55
+ constructor(message, details) {
56
+ super(message, details);
57
+ this.name = "ConnectTimeoutError";
58
+ }
59
+ };
45
60
  var ProviderAPIError = class extends AlterSDKError {
46
61
  statusCode;
47
62
  responseBody;
@@ -66,6 +81,12 @@ var TimeoutError = class extends NetworkError {
66
81
  };
67
82
 
68
83
  // src/models.ts
84
+ var ActorType = /* @__PURE__ */ ((ActorType2) => {
85
+ ActorType2["BACKEND_SERVICE"] = "backend_service";
86
+ ActorType2["AI_AGENT"] = "ai_agent";
87
+ ActorType2["MCP_SERVER"] = "mcp_server";
88
+ return ActorType2;
89
+ })(ActorType || {});
69
90
  var TokenResponse = class _TokenResponse {
70
91
  /** Token type (usually "Bearer") */
71
92
  tokenType;
@@ -77,12 +98,29 @@ var TokenResponse = class _TokenResponse {
77
98
  scopes;
78
99
  /** Connection ID that provided this token */
79
100
  connectionId;
101
+ /** Provider ID (google, github, etc.) */
102
+ providerId;
103
+ /** HTTP header name for credential injection (e.g., "Authorization", "X-API-Key") */
104
+ injectionHeader;
105
+ /** Header value format with {token} placeholder (e.g., "Bearer {token}", "{token}") */
106
+ injectionFormat;
107
+ /** Extra credentials for multi-part auth (e.g. AWS SigV4 secret_key, region) */
108
+ additionalCredentials;
80
109
  constructor(data) {
81
110
  this.tokenType = data.token_type ?? "Bearer";
82
111
  this.expiresIn = data.expires_in ?? null;
83
112
  this.expiresAt = data.expires_at ? _TokenResponse.parseExpiresAt(data.expires_at) : null;
84
113
  this.scopes = data.scopes ?? [];
85
114
  this.connectionId = data.connection_id;
115
+ this.providerId = data.provider_id ?? "";
116
+ this.injectionHeader = data.injection_header ?? "Authorization";
117
+ this.injectionFormat = data.injection_format ?? "Bearer {token}";
118
+ if (data.additional_credentials) {
119
+ const { secret_key: _, ...safeCredentials } = data.additional_credentials;
120
+ this.additionalCredentials = Object.keys(safeCredentials).length > 0 ? safeCredentials : null;
121
+ } else {
122
+ this.additionalCredentials = null;
123
+ }
86
124
  Object.freeze(this);
87
125
  }
88
126
  /**
@@ -147,7 +185,6 @@ var TokenResponse = class _TokenResponse {
147
185
  var ConnectionInfo = class {
148
186
  id;
149
187
  providerId;
150
- attributes;
151
188
  scopes;
152
189
  accountIdentifier;
153
190
  accountDisplayName;
@@ -158,7 +195,6 @@ var ConnectionInfo = class {
158
195
  constructor(data) {
159
196
  this.id = data.id;
160
197
  this.providerId = data.provider_id;
161
- this.attributes = data.attributes ?? {};
162
198
  this.scopes = data.scopes ?? [];
163
199
  this.accountIdentifier = data.account_identifier ?? null;
164
200
  this.accountDisplayName = data.account_display_name ?? null;
@@ -172,7 +208,6 @@ var ConnectionInfo = class {
172
208
  return {
173
209
  id: this.id,
174
210
  provider_id: this.providerId,
175
- attributes: this.attributes,
176
211
  scopes: this.scopes,
177
212
  account_identifier: this.accountIdentifier,
178
213
  account_display_name: this.accountDisplayName,
@@ -225,12 +260,38 @@ var ConnectionListResult = class {
225
260
  Object.freeze(this);
226
261
  }
227
262
  };
263
+ var ConnectResult = class {
264
+ connectionId;
265
+ providerId;
266
+ accountIdentifier;
267
+ scopes;
268
+ constructor(data) {
269
+ this.connectionId = data.connection_id;
270
+ this.providerId = data.provider_id;
271
+ this.accountIdentifier = data.account_identifier ?? null;
272
+ this.scopes = data.scopes ?? [];
273
+ Object.freeze(this);
274
+ }
275
+ toJSON() {
276
+ return {
277
+ connection_id: this.connectionId,
278
+ provider_id: this.providerId,
279
+ account_identifier: this.accountIdentifier,
280
+ scopes: this.scopes
281
+ };
282
+ }
283
+ toString() {
284
+ return `ConnectResult(connection_id=${this.connectionId}, provider=${this.providerId})`;
285
+ }
286
+ };
228
287
  var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
229
288
  "authorization",
230
289
  "cookie",
231
290
  "set-cookie",
232
291
  "x-api-key",
233
- "x-auth-token"
292
+ "x-auth-token",
293
+ "x-amz-date",
294
+ "x-amz-content-sha256"
234
295
  ]);
235
296
  var APICallAuditLog = class {
236
297
  connectionId;
@@ -304,11 +365,171 @@ var APICallAuditLog = class {
304
365
  }
305
366
  };
306
367
 
368
+ // src/aws-sig-v4.ts
369
+ import { createHash, createHmac } from "crypto";
370
+ var ALGORITHM = "AWS4-HMAC-SHA256";
371
+ var AWS_HOST_RE = /^(?<service>[a-z0-9-]+)\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.amazonaws\.com$/;
372
+ var S3_VIRTUAL_HOST_RE = /^[^.]+\.s3\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.amazonaws\.com$/;
373
+ function hmacSha256(key, message) {
374
+ return createHmac("sha256", key).update(message, "utf8").digest();
375
+ }
376
+ function sha256Hex(data) {
377
+ const hash = createHash("sha256");
378
+ if (typeof data === "string") {
379
+ hash.update(data, "utf8");
380
+ } else {
381
+ hash.update(data);
382
+ }
383
+ return hash.digest("hex");
384
+ }
385
+ function detectServiceAndRegion(hostname) {
386
+ const lower = hostname.toLowerCase();
387
+ const m = AWS_HOST_RE.exec(lower);
388
+ if (m?.groups) {
389
+ return { service: m.groups.service, region: m.groups.region };
390
+ }
391
+ const s3m = S3_VIRTUAL_HOST_RE.exec(lower);
392
+ if (s3m?.groups) {
393
+ return { service: "s3", region: s3m.groups.region };
394
+ }
395
+ return { service: null, region: null };
396
+ }
397
+ function deriveSigningKey(secretKey, dateStamp, region, service) {
398
+ const kDate = hmacSha256(Buffer.from("AWS4" + secretKey, "utf8"), dateStamp);
399
+ const kRegion = hmacSha256(kDate, region);
400
+ const kService = hmacSha256(kRegion, service);
401
+ const kSigning = hmacSha256(kService, "aws4_request");
402
+ return kSigning;
403
+ }
404
+ function canonicalUri(path) {
405
+ if (!path) return "/";
406
+ if (!path.startsWith("/")) path = "/" + path;
407
+ const segments = path.split("/");
408
+ return segments.map(
409
+ (seg) => encodeURIComponent(seg).replace(
410
+ /[!'()*]/g,
411
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
412
+ )
413
+ ).join("/");
414
+ }
415
+ function canonicalQueryString(query) {
416
+ if (!query) return "";
417
+ const sorted = [];
418
+ for (const pair of query.split("&")) {
419
+ const eqIdx = pair.indexOf("=");
420
+ if (eqIdx === -1) {
421
+ sorted.push([decodeURIComponent(pair), ""]);
422
+ } else {
423
+ sorted.push([
424
+ decodeURIComponent(pair.slice(0, eqIdx)),
425
+ decodeURIComponent(pair.slice(eqIdx + 1))
426
+ ]);
427
+ }
428
+ }
429
+ sorted.sort((a, b) => {
430
+ if (a[0] < b[0]) return -1;
431
+ if (a[0] > b[0]) return 1;
432
+ if (a[1] < b[1]) return -1;
433
+ if (a[1] > b[1]) return 1;
434
+ return 0;
435
+ });
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("&");
441
+ }
442
+ function canonicalHeadersAndSigned(headers) {
443
+ const canonical = {};
444
+ for (const [name, value] of Object.entries(headers)) {
445
+ const lowerName = name.toLowerCase().trim();
446
+ const trimmedValue = value.replace(/\s+/g, " ").trim();
447
+ canonical[lowerName] = trimmedValue;
448
+ }
449
+ const sortedNames = Object.keys(canonical).sort();
450
+ const canonicalStr = sortedNames.map((name) => `${name}:${canonical[name]}
451
+ `).join("");
452
+ const signedStr = sortedNames.join(";");
453
+ return { canonicalHeaders: canonicalStr, signedHeaders: signedStr };
454
+ }
455
+ function signAwsRequest(opts) {
456
+ const parsed = new URL(opts.url);
457
+ const hostname = parsed.hostname;
458
+ 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
+ }
464
+ if (!region) {
465
+ throw new Error(
466
+ `Cannot determine AWS region from URL '${opts.url}'. Pass region explicitly via additional_credentials.`
467
+ );
468
+ }
469
+ if (!service) {
470
+ throw new Error(
471
+ `Cannot determine AWS service from URL '${opts.url}'. Pass service explicitly via additional_credentials.`
472
+ );
473
+ }
474
+ const timestamp = opts.timestamp ?? /* @__PURE__ */ new Date();
475
+ const amzDate = timestamp.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
476
+ const dateStamp = amzDate.slice(0, 8);
477
+ const bodyBytes = opts.body != null ? typeof opts.body === "string" ? Buffer.from(opts.body, "utf8") : opts.body : Buffer.alloc(0);
478
+ const payloadHash = sha256Hex(bodyBytes);
479
+ const headersToSign = {};
480
+ const hasHost = Object.keys(opts.headers).some(
481
+ (k) => k.toLowerCase() === "host"
482
+ );
483
+ if (!hasHost) {
484
+ const port = parsed.port;
485
+ headersToSign["host"] = port && port !== "80" && port !== "443" ? `${hostname}:${port}` : hostname;
486
+ } else {
487
+ for (const [k, v] of Object.entries(opts.headers)) {
488
+ if (k.toLowerCase() === "host") {
489
+ headersToSign["host"] = v;
490
+ break;
491
+ }
492
+ }
493
+ }
494
+ headersToSign["x-amz-date"] = amzDate;
495
+ headersToSign["x-amz-content-sha256"] = payloadHash;
496
+ const canonicalUriStr = canonicalUri(parsed.pathname);
497
+ const canonicalQs = canonicalQueryString(parsed.search.replace(/^\?/, ""));
498
+ const { canonicalHeaders, signedHeaders } = canonicalHeadersAndSigned(headersToSign);
499
+ const canonicalRequest = [
500
+ opts.method.toUpperCase(),
501
+ canonicalUriStr,
502
+ canonicalQs,
503
+ canonicalHeaders,
504
+ signedHeaders,
505
+ payloadHash
506
+ ].join("\n");
507
+ const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
508
+ const stringToSign = [
509
+ ALGORITHM,
510
+ amzDate,
511
+ credentialScope,
512
+ sha256Hex(canonicalRequest)
513
+ ].join("\n");
514
+ const signingKey = deriveSigningKey(opts.secretKey, dateStamp, region, service);
515
+ const signature = createHmac("sha256", signingKey).update(stringToSign, "utf8").digest("hex");
516
+ const authorization = `${ALGORITHM} Credential=${opts.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
517
+ return {
518
+ Authorization: authorization,
519
+ "x-amz-date": amzDate,
520
+ "x-amz-content-sha256": payloadHash
521
+ };
522
+ }
523
+
307
524
  // src/client.ts
308
525
  var _tokenStore = /* @__PURE__ */ new WeakMap();
526
+ var _additionalCredsStore = /* @__PURE__ */ new WeakMap();
309
527
  function _storeAccessToken(token, accessToken) {
310
528
  _tokenStore.set(token, accessToken);
311
529
  }
530
+ function _storeAdditionalCredentials(token, creds) {
531
+ _additionalCredsStore.set(token, creds);
532
+ }
312
533
  function _extractAccessToken(token) {
313
534
  const value = _tokenStore.get(token);
314
535
  if (value === void 0) {
@@ -318,10 +539,12 @@ function _extractAccessToken(token) {
318
539
  }
319
540
  return value;
320
541
  }
542
+ function _extractAdditionalCredentials(token) {
543
+ return _additionalCredsStore.get(token);
544
+ }
321
545
  var _fetch;
322
- var SDK_VERSION = "0.2.2";
546
+ var SDK_VERSION = "0.4.0";
323
547
  var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
324
- var VALID_ACTOR_TYPES = ["ai_agent", "mcp_server"];
325
548
  var HTTP_FORBIDDEN = 403;
326
549
  var HTTP_NOT_FOUND = 404;
327
550
  var HTTP_BAD_REQUEST = 400;
@@ -377,7 +600,9 @@ var HttpClient = class {
377
600
  headers: mergedHeaders,
378
601
  signal: controller.signal
379
602
  };
380
- if (options?.json !== void 0) {
603
+ if (options?.body !== void 0) {
604
+ init.body = options.body;
605
+ } else if (options?.json !== void 0) {
381
606
  init.body = JSON.stringify(options.json);
382
607
  mergedHeaders["Content-Type"] = "application/json";
383
608
  }
@@ -401,6 +626,8 @@ var AlterVault = class _AlterVault {
401
626
  // SECURITY LAYER 4: ES2022 private fields — truly private at runtime.
402
627
  // These are NOT accessible via (obj as any), Object.keys(), or prototype.
403
628
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
629
+ /** HMAC signing key (derived from API key using AWS SigV4 pattern, raw bytes) */
630
+ #hmacKey;
404
631
  /** HTTP Client for Alter Backend (has x-api-key) */
405
632
  #alterClient;
406
633
  /** HTTP Client for External Provider APIs (NO x-api-key) */
@@ -438,10 +665,8 @@ var AlterVault = class _AlterVault {
438
665
  for (const [name, value] of actorStrings) {
439
666
  _AlterVault.#validateActorString(value, name);
440
667
  }
441
- this.baseUrl = (options.baseUrl ?? "https://api.alter.com").replace(
442
- /\/+$/,
443
- ""
444
- );
668
+ this.#hmacKey = createHmac2("sha256", options.apiKey).update("alter-signing-v1").digest();
669
+ this.baseUrl = (process.env.ALTER_BASE_URL ?? "https://backend.alterai.dev").replace(/\/+$/, "");
445
670
  const timeoutMs = options.timeout ?? 3e4;
446
671
  this.#actorType = options.actorType;
447
672
  this.#actorIdentifier = options.actorIdentifier;
@@ -494,14 +719,20 @@ var AlterVault = class _AlterVault {
494
719
  if (!apiKey.startsWith("alter_key_")) {
495
720
  throw new AlterSDKError("api_key must start with 'alter_key_'");
496
721
  }
497
- if (actorType && !VALID_ACTOR_TYPES.includes(actorType)) {
498
- throw new AlterSDKError("actor_type must be 'ai_agent' or 'mcp_server'");
722
+ if (!actorType) {
723
+ throw new AlterSDKError(
724
+ "actor_type is required (use ActorType.AI_AGENT, ActorType.MCP_SERVER, or ActorType.BACKEND_SERVICE)"
725
+ );
499
726
  }
500
- if (actorType && !actorIdentifier) {
727
+ const validValues = Object.values(ActorType);
728
+ if (!validValues.includes(String(actorType))) {
501
729
  throw new AlterSDKError(
502
- "actor_identifier is required when actor_type is set"
730
+ `actor_type must be one of ${JSON.stringify(validValues.sort())}, got '${String(actorType)}'`
503
731
  );
504
732
  }
733
+ if (!actorIdentifier) {
734
+ throw new AlterSDKError("actor_identifier is required");
735
+ }
505
736
  }
506
737
  /**
507
738
  * Build default headers for the Alter backend HTTP client.
@@ -528,6 +759,28 @@ var AlterVault = class _AlterVault {
528
759
  }
529
760
  return headers;
530
761
  }
762
+ /**
763
+ * Compute HMAC-SHA256 signature headers for an Alter backend request.
764
+ *
765
+ * String-to-sign format: METHOD\nPATH_WITH_SORTED_QUERY\nTIMESTAMP\nCONTENT_HASH
766
+ *
767
+ * The path should include sorted query parameters if present (e.g. "/sdk/endpoint?a=1&b=2").
768
+ * Currently all SDK→backend calls are POSTs without query params, so the path is clean.
769
+ */
770
+ #computeHmacHeaders(method, path, body) {
771
+ const timestamp = String(Math.floor(Date.now() / 1e3));
772
+ const contentHash = createHash2("sha256").update(body ?? "").digest("hex");
773
+ const stringToSign = `${method.toUpperCase()}
774
+ ${path}
775
+ ${timestamp}
776
+ ${contentHash}`;
777
+ const signature = createHmac2("sha256", this.#hmacKey).update(stringToSign).digest("hex");
778
+ return {
779
+ "X-Alter-Timestamp": timestamp,
780
+ "X-Alter-Content-SHA256": contentHash,
781
+ "X-Alter-Signature": signature
782
+ };
783
+ }
531
784
  /**
532
785
  * Build per-request actor headers for instance tracking.
533
786
  */
@@ -624,7 +877,7 @@ var AlterVault = class _AlterVault {
624
877
  if (response.status === HTTP_NOT_FOUND) {
625
878
  const errorData = await _AlterVault.#safeParseJson(response);
626
879
  throw new ConnectionNotFoundError(
627
- errorData.message ?? "OAuth connection not found for these attributes",
880
+ errorData.message ?? "OAuth connection not found for the given connection_id",
628
881
  errorData
629
882
  );
630
883
  }
@@ -670,22 +923,24 @@ var AlterVault = class _AlterVault {
670
923
  * This is a private method. Tokens are NEVER exposed to developers.
671
924
  * Use request() instead, which handles tokens internally.
672
925
  */
673
- async #getToken(providerId, attributes, reason, requestMetadata, runId, threadId, toolCallId) {
926
+ async #getToken(connectionId, reason, requestMetadata, runId, threadId, toolCallId) {
674
927
  const actorHeaders = this.#getActorRequestHeaders(
675
928
  runId,
676
929
  threadId,
677
930
  toolCallId
678
931
  );
679
932
  let response;
933
+ const tokenBody = {
934
+ connection_id: connectionId,
935
+ reason: reason ?? null,
936
+ request: requestMetadata ?? null
937
+ };
938
+ const tokenPath = "/sdk/token";
939
+ const hmacHeaders = this.#computeHmacHeaders("POST", tokenPath, JSON.stringify(tokenBody));
680
940
  try {
681
- response = await this.#alterClient.post("/oauth/token", {
682
- json: {
683
- provider_id: providerId,
684
- attributes,
685
- reason: reason ?? null,
686
- request: requestMetadata ?? null
687
- },
688
- headers: actorHeaders
941
+ response = await this.#alterClient.post(tokenPath, {
942
+ json: tokenBody,
943
+ headers: { ...actorHeaders, ...hmacHeaders }
689
944
  });
690
945
  } catch (error) {
691
946
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
@@ -702,7 +957,7 @@ var AlterVault = class _AlterVault {
702
957
  }
703
958
  throw new TokenRetrievalError(
704
959
  `Failed to retrieve token: ${error instanceof Error ? error.message : String(error)}`,
705
- { provider_id: providerId, error: String(error) }
960
+ { connection_id: connectionId, error: String(error) }
706
961
  );
707
962
  }
708
963
  this.#cacheActorIdFromResponse(response);
@@ -710,7 +965,22 @@ var AlterVault = class _AlterVault {
710
965
  const tokenData = await response.json();
711
966
  const typedData = tokenData;
712
967
  const tokenResponse = new TokenResponse(typedData);
968
+ if (!/^[A-Za-z][A-Za-z0-9-]*$/.test(tokenResponse.injectionHeader)) {
969
+ throw new TokenRetrievalError(
970
+ `Backend returned invalid injection_header: ${tokenResponse.injectionHeader}`,
971
+ { connectionId: String(connectionId) }
972
+ );
973
+ }
974
+ if (/[\r\n\x00]/.test(tokenResponse.injectionFormat)) {
975
+ throw new TokenRetrievalError(
976
+ `Backend returned invalid injection_format (contains control characters)`,
977
+ { connectionId: String(connectionId) }
978
+ );
979
+ }
713
980
  _storeAccessToken(tokenResponse, typedData.access_token);
981
+ if (typedData.additional_credentials) {
982
+ _storeAdditionalCredentials(tokenResponse, typedData.additional_credentials);
983
+ }
714
984
  return tokenResponse;
715
985
  }
716
986
  /**
@@ -738,10 +1008,13 @@ var AlterVault = class _AlterVault {
738
1008
  toolCallId: params.toolCallId
739
1009
  });
740
1010
  const sanitized = auditLog.sanitize();
741
- const actorHeaders = this.#getActorRequestHeaders();
742
- const response = await this.#alterClient.post("/oauth/audit/api-call", {
743
- json: sanitized,
744
- headers: actorHeaders
1011
+ const actorHeaders = this.#getActorRequestHeaders(params.runId);
1012
+ const auditPath = "/sdk/oauth/audit/api-call";
1013
+ const auditBody = sanitized;
1014
+ const auditHmac = this.#computeHmacHeaders("POST", auditPath, JSON.stringify(auditBody));
1015
+ const response = await this.#alterClient.post(auditPath, {
1016
+ json: auditBody,
1017
+ headers: { ...actorHeaders, ...auditHmac }
745
1018
  });
746
1019
  this.#cacheActorIdFromResponse(response);
747
1020
  if (!response.ok) {
@@ -807,13 +1080,13 @@ var AlterVault = class _AlterVault {
807
1080
  * 4. Logs the call for audit (fire-and-forget)
808
1081
  * 5. Returns the raw response
809
1082
  */
810
- async request(provider, method, url, options) {
1083
+ async request(connectionId, method, url, options) {
811
1084
  if (this.#closed) {
812
1085
  throw new AlterSDKError(
813
1086
  "SDK instance has been closed. Create a new AlterVault instance to make requests."
814
1087
  );
815
1088
  }
816
- const providerStr = String(provider);
1089
+ const runId = options?.runId ?? randomUUID();
817
1090
  const methodStr = String(method).toUpperCase();
818
1091
  const urlLower = url.toLowerCase();
819
1092
  if (!ALLOWED_URL_SCHEMES.some((scheme) => urlLower.startsWith(scheme))) {
@@ -821,7 +1094,7 @@ var AlterVault = class _AlterVault {
821
1094
  `URL must start with https:// or http://, got: ${url.slice(0, 50)}`
822
1095
  );
823
1096
  }
824
- if (options.pathParams && Object.keys(options.pathParams).length > 0) {
1097
+ if (options?.pathParams && Object.keys(options.pathParams).length > 0) {
825
1098
  const encodedParams = {};
826
1099
  for (const [key, value] of Object.entries(options.pathParams)) {
827
1100
  encodedParams[key] = encodeURIComponent(String(value));
@@ -851,39 +1124,80 @@ var AlterVault = class _AlterVault {
851
1124
  );
852
1125
  }
853
1126
  }
854
- if (options.extraHeaders && "Authorization" in options.extraHeaders) {
855
- console.warn(
856
- "extraHeaders contains 'Authorization' which will be overwritten with the auto-injected Bearer token"
857
- );
858
- }
859
1127
  const tokenResponse = await this.#getToken(
860
- providerStr,
861
- options.user,
862
- options.reason,
1128
+ connectionId,
1129
+ options?.reason,
863
1130
  { method: methodStr, url },
864
- options.runId,
865
- options.threadId,
866
- options.toolCallId
1131
+ runId,
1132
+ options?.threadId,
1133
+ options?.toolCallId
867
1134
  );
868
- const requestHeaders = options.extraHeaders ? { ...options.extraHeaders } : {};
869
- requestHeaders["Authorization"] = `Bearer ${_extractAccessToken(tokenResponse)}`;
1135
+ const requestHeaders = options?.extraHeaders ? { ...options.extraHeaders } : {};
1136
+ const accessToken = _extractAccessToken(tokenResponse);
1137
+ const injectionHeaderLower = tokenResponse.injectionHeader.toLowerCase();
1138
+ const additionalCreds = _extractAdditionalCredentials(tokenResponse);
1139
+ const isSigV4 = tokenResponse.injectionFormat.startsWith("AWS4-HMAC-SHA256") && additionalCreds != null;
1140
+ let sigv4BodyStr = null;
1141
+ if (isSigV4) {
1142
+ const accessKeyId = accessToken;
1143
+ if (options?.json != null) {
1144
+ sigv4BodyStr = JSON.stringify(options.json);
1145
+ if (!requestHeaders["Content-Type"]) {
1146
+ requestHeaders["Content-Type"] = "application/json";
1147
+ }
1148
+ }
1149
+ if (!additionalCreds.secret_key) {
1150
+ throw new TokenRetrievalError(
1151
+ "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 }
1153
+ );
1154
+ }
1155
+ const awsHeaders = signAwsRequest({
1156
+ method: methodStr,
1157
+ url,
1158
+ headers: requestHeaders,
1159
+ body: sigv4BodyStr,
1160
+ accessKeyId,
1161
+ secretKey: additionalCreds.secret_key,
1162
+ region: additionalCreds.region ?? null,
1163
+ service: additionalCreds.service ?? null
1164
+ });
1165
+ Object.assign(requestHeaders, awsHeaders);
1166
+ } else {
1167
+ if (options?.extraHeaders && Object.keys(options.extraHeaders).some(
1168
+ (k) => k.toLowerCase() === injectionHeaderLower
1169
+ )) {
1170
+ console.warn(
1171
+ `extraHeaders contains '${tokenResponse.injectionHeader}' which will be overwritten with the auto-injected credential`
1172
+ );
1173
+ }
1174
+ requestHeaders[tokenResponse.injectionHeader] = tokenResponse.injectionFormat.replace("{token}", accessToken);
1175
+ }
870
1176
  if (!requestHeaders["User-Agent"]) {
871
1177
  requestHeaders["User-Agent"] = SDK_USER_AGENT;
872
1178
  }
873
1179
  const startTime = Date.now();
874
1180
  let response;
875
1181
  try {
876
- response = await this.#providerClient.request(methodStr, url, {
877
- json: options.json,
878
- headers: requestHeaders,
879
- params: options.queryParams
880
- });
1182
+ if (isSigV4 && sigv4BodyStr != null) {
1183
+ response = await this.#providerClient.request(methodStr, url, {
1184
+ body: sigv4BodyStr,
1185
+ headers: requestHeaders,
1186
+ params: options?.queryParams
1187
+ });
1188
+ } else {
1189
+ response = await this.#providerClient.request(methodStr, url, {
1190
+ json: options?.json,
1191
+ headers: requestHeaders,
1192
+ params: options?.queryParams
1193
+ });
1194
+ }
881
1195
  } catch (error) {
882
1196
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
883
1197
  throw new TimeoutError(
884
1198
  `Provider API request timed out: ${error instanceof Error ? error.message : String(error)}`,
885
1199
  {
886
- provider: providerStr,
1200
+ connection_id: connectionId,
887
1201
  method: methodStr,
888
1202
  url
889
1203
  }
@@ -892,7 +1206,7 @@ var AlterVault = class _AlterVault {
892
1206
  throw new NetworkError(
893
1207
  `Failed to call provider API: ${error instanceof Error ? error.message : String(error)}`,
894
1208
  {
895
- provider: providerStr,
1209
+ connection_id: connectionId,
896
1210
  method: methodStr,
897
1211
  url,
898
1212
  error: String(error)
@@ -900,9 +1214,11 @@ var AlterVault = class _AlterVault {
900
1214
  );
901
1215
  }
902
1216
  const latencyMs = Date.now() - startTime;
1217
+ const sigv4Sensitive = /* @__PURE__ */ new Set(["authorization", "x-amz-date", "x-amz-content-sha256"]);
1218
+ const stripHeaders = isSigV4 ? sigv4Sensitive : /* @__PURE__ */ new Set([injectionHeaderLower]);
903
1219
  const auditHeaders = {};
904
1220
  for (const [key, value] of Object.entries(requestHeaders)) {
905
- if (key.toLowerCase() !== "authorization") {
1221
+ if (!stripHeaders.has(key.toLowerCase())) {
906
1222
  auditHeaders[key] = value;
907
1223
  }
908
1224
  }
@@ -913,19 +1229,19 @@ var AlterVault = class _AlterVault {
913
1229
  });
914
1230
  this.#scheduleAuditLog({
915
1231
  connectionId: tokenResponse.connectionId,
916
- providerId: providerStr,
1232
+ providerId: tokenResponse.providerId || connectionId,
917
1233
  method: methodStr,
918
1234
  url,
919
1235
  requestHeaders: auditHeaders,
920
- requestBody: options.json ?? null,
1236
+ requestBody: options?.json ?? null,
921
1237
  responseStatus: response.status,
922
1238
  responseHeaders,
923
1239
  responseBody,
924
1240
  latencyMs,
925
- reason: options.reason ?? null,
926
- runId: options.runId ?? null,
927
- threadId: options.threadId ?? null,
928
- toolCallId: options.toolCallId ?? null
1241
+ reason: options?.reason ?? null,
1242
+ runId,
1243
+ threadId: options?.threadId ?? null,
1244
+ toolCallId: options?.toolCallId ?? null
929
1245
  });
930
1246
  if (response.status >= HTTP_CLIENT_ERROR_START) {
931
1247
  throw new ProviderAPIError(
@@ -933,7 +1249,7 @@ var AlterVault = class _AlterVault {
933
1249
  response.status,
934
1250
  responseBody,
935
1251
  {
936
- provider: providerStr,
1252
+ connection_id: connectionId,
937
1253
  method: methodStr,
938
1254
  url
939
1255
  }
@@ -955,14 +1271,17 @@ var AlterVault = class _AlterVault {
955
1271
  }
956
1272
  const actorHeaders = this.#getActorRequestHeaders();
957
1273
  let response;
1274
+ const listBody = {
1275
+ provider_id: options?.providerId ?? null,
1276
+ limit: options?.limit ?? 100,
1277
+ offset: options?.offset ?? 0
1278
+ };
1279
+ const listPath = "/sdk/oauth/connections/list";
1280
+ const listHmac = this.#computeHmacHeaders("POST", listPath, JSON.stringify(listBody));
958
1281
  try {
959
- response = await this.#alterClient.post("/oauth/connections/list", {
960
- json: {
961
- provider_id: options?.providerId ?? null,
962
- limit: options?.limit ?? 100,
963
- offset: options?.offset ?? 0
964
- },
965
- headers: actorHeaders
1282
+ response = await this.#alterClient.post(listPath, {
1283
+ json: listBody,
1284
+ headers: { ...actorHeaders, ...listHmac }
966
1285
  });
967
1286
  } catch (error) {
968
1287
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
@@ -1013,17 +1332,19 @@ var AlterVault = class _AlterVault {
1013
1332
  }
1014
1333
  const actorHeaders = this.#getActorRequestHeaders();
1015
1334
  let response;
1335
+ const sessionBody = {
1336
+ end_user: options.endUser,
1337
+ allowed_providers: options.allowedProviders ?? null,
1338
+ return_url: options.returnUrl ?? null,
1339
+ allowed_origin: options.allowedOrigin ?? null,
1340
+ metadata: options.metadata ?? null
1341
+ };
1342
+ const sessionPath = "/sdk/oauth/connect/session";
1343
+ const sessionHmac = this.#computeHmacHeaders("POST", sessionPath, JSON.stringify(sessionBody));
1016
1344
  try {
1017
- response = await this.#alterClient.post("/oauth/connect/session", {
1018
- json: {
1019
- end_user: options.endUser,
1020
- attributes: options.attributes ?? null,
1021
- allowed_providers: options.allowedProviders ?? null,
1022
- return_url: options.returnUrl ?? null,
1023
- allowed_origin: options.allowedOrigin ?? null,
1024
- metadata: options.metadata ?? null
1025
- },
1026
- headers: actorHeaders
1345
+ response = await this.#alterClient.post(sessionPath, {
1346
+ json: sessionBody,
1347
+ headers: { ...actorHeaders, ...sessionHmac }
1027
1348
  });
1028
1349
  } catch (error) {
1029
1350
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
@@ -1047,6 +1368,132 @@ var AlterVault = class _AlterVault {
1047
1368
  const data = await response.json();
1048
1369
  return new ConnectSession(data);
1049
1370
  }
1371
+ /**
1372
+ * Open OAuth in the user's browser and wait for completion.
1373
+ *
1374
+ * This is the headless connect flow for CLI tools, scripts, and
1375
+ * server-side applications. It creates a Connect session, opens the
1376
+ * browser, and polls until the user completes OAuth.
1377
+ *
1378
+ * @param options - Connect options (endUser is required)
1379
+ * @returns Array of ConnectResult objects (one per connected provider)
1380
+ * @throws ConnectTimeoutError if the user doesn't complete within timeout
1381
+ * @throws ConnectFlowError if the user denies or provider returns error
1382
+ * @throws AlterSDKError if SDK is closed or session creation fails
1383
+ */
1384
+ async connect(options) {
1385
+ if (this.#closed) {
1386
+ throw new AlterSDKError(
1387
+ "SDK instance has been closed. Create a new AlterVault instance to make requests."
1388
+ );
1389
+ }
1390
+ const timeout = options.timeout ?? 300;
1391
+ const pollInterval = options.pollInterval ?? 2;
1392
+ const openBrowser = options.openBrowser ?? true;
1393
+ const session = await this.createConnectSession({
1394
+ endUser: options.endUser,
1395
+ allowedProviders: options.providers
1396
+ });
1397
+ if (openBrowser) {
1398
+ try {
1399
+ const openModule = await import("open");
1400
+ const openFn = openModule.default;
1401
+ if (openFn) {
1402
+ await openFn(session.connectUrl);
1403
+ } else {
1404
+ console.log(
1405
+ `Open this URL to authorize: ${session.connectUrl}`
1406
+ );
1407
+ }
1408
+ } catch {
1409
+ console.log(
1410
+ `Open this URL to authorize: ${session.connectUrl}`
1411
+ );
1412
+ }
1413
+ } else {
1414
+ console.log(
1415
+ `Open this URL to authorize: ${session.connectUrl}`
1416
+ );
1417
+ }
1418
+ const deadline = Date.now() + timeout * 1e3;
1419
+ while (Date.now() < deadline) {
1420
+ await new Promise(
1421
+ (resolve) => setTimeout(resolve, pollInterval * 1e3)
1422
+ );
1423
+ const pollResult = await this.#pollSession(session.sessionToken);
1424
+ const pollStatus = pollResult.status;
1425
+ if (pollStatus === "completed") {
1426
+ const connectionsData = pollResult.connections ?? [];
1427
+ return connectionsData.map(
1428
+ (conn) => new ConnectResult({
1429
+ connection_id: conn.connection_id ?? "",
1430
+ provider_id: conn.provider_id ?? "",
1431
+ account_identifier: conn.account_identifier ?? null,
1432
+ scopes: conn.scopes ?? []
1433
+ })
1434
+ );
1435
+ }
1436
+ if (pollStatus === "error") {
1437
+ 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
+ );
1444
+ }
1445
+ if (pollStatus === "expired") {
1446
+ throw new ConnectFlowError(
1447
+ "Connect session expired before OAuth was completed"
1448
+ );
1449
+ }
1450
+ }
1451
+ throw new ConnectTimeoutError(
1452
+ `OAuth flow did not complete within ${timeout} seconds. The user may not have finished authorizing in the browser.`,
1453
+ { timeout }
1454
+ );
1455
+ }
1456
+ /**
1457
+ * Poll the Connect session for completion status (INTERNAL).
1458
+ *
1459
+ * Makes an HMAC-signed POST to the poll endpoint.
1460
+ */
1461
+ async #pollSession(sessionToken) {
1462
+ const actorHeaders = this.#getActorRequestHeaders();
1463
+ const pollPath = "/sdk/oauth/connect/session/poll";
1464
+ const pollBody = { session_token: sessionToken };
1465
+ const pollHmac = this.#computeHmacHeaders(
1466
+ "POST",
1467
+ pollPath,
1468
+ JSON.stringify(pollBody)
1469
+ );
1470
+ let response;
1471
+ try {
1472
+ response = await this.#alterClient.post(pollPath, {
1473
+ json: pollBody,
1474
+ headers: { ...actorHeaders, ...pollHmac }
1475
+ });
1476
+ } catch (error) {
1477
+ if (_AlterVault.#isTimeoutOrAbortError(error)) {
1478
+ throw new TimeoutError(
1479
+ `Request to Alter Vault backend timed out: ${error instanceof Error ? error.message : String(error)}`,
1480
+ { base_url: this.baseUrl }
1481
+ );
1482
+ }
1483
+ if (error instanceof TypeError) {
1484
+ throw new NetworkError(
1485
+ `Failed to connect to Alter Vault backend: ${error.message}`,
1486
+ { base_url: this.baseUrl }
1487
+ );
1488
+ }
1489
+ throw new AlterSDKError(
1490
+ `Failed to poll connect session: ${error instanceof Error ? error.message : String(error)}`
1491
+ );
1492
+ }
1493
+ this.#cacheActorIdFromResponse(response);
1494
+ await this.#handleErrorResponse(response);
1495
+ return await response.json();
1496
+ }
1050
1497
  /**
1051
1498
  * Close HTTP clients and release resources.
1052
1499
  * Waits for any pending audit tasks before closing.
@@ -1075,8 +1522,6 @@ var Provider = /* @__PURE__ */ ((Provider2) => {
1075
1522
  Provider2["GOOGLE"] = "google";
1076
1523
  Provider2["GITHUB"] = "github";
1077
1524
  Provider2["SLACK"] = "slack";
1078
- Provider2["MICROSOFT"] = "microsoft";
1079
- Provider2["SALESFORCE"] = "salesforce";
1080
1525
  Provider2["SENTRY"] = "sentry";
1081
1526
  return Provider2;
1082
1527
  })(Provider || {});
@@ -1092,9 +1537,13 @@ var HttpMethod = /* @__PURE__ */ ((HttpMethod2) => {
1092
1537
  })(HttpMethod || {});
1093
1538
  export {
1094
1539
  APICallAuditLog,
1540
+ ActorType,
1095
1541
  AlterSDKError,
1096
1542
  AlterVault,
1543
+ ConnectFlowError,
1544
+ ConnectResult,
1097
1545
  ConnectSession,
1546
+ ConnectTimeoutError,
1098
1547
  ConnectionInfo,
1099
1548
  ConnectionListResult,
1100
1549
  ConnectionNotFoundError,