@alter-ai/alter-sdk 0.3.1 → 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/README.md CHANGED
@@ -12,6 +12,7 @@ Official TypeScript SDK for [Alter Vault](https://alterai.dev) — Credential ma
12
12
  - **Real-time Policy Enforcement**: Every token request checked against current policies
13
13
  - **Automatic Token Refresh**: Tokens refreshed transparently by the backend
14
14
  - **API Key and Custom Credential Support**: Handles OAuth tokens, API keys, and custom credential formats automatically
15
+ - **AWS SigV4 Support**: Automatic AWS Signature Version 4 signing for S3, Bedrock, DynamoDB, and other AWS services (no AWS SDK required)
15
16
  - **Actor Tracking**: First-class support for AI agent and MCP server observability
16
17
  - **HMAC Request Signing**: All SDK-to-backend requests are signed with a derived HMAC-SHA256 key for integrity, authenticity, and replay protection
17
18
  - **Native Promises**: Built on native `fetch` — no heavy dependencies
@@ -200,6 +201,42 @@ console.log(`Expires in: ${session.expiresIn}s`);
200
201
 
201
202
  Returns `ConnectSession` with: `sessionToken`, `connectUrl`, `expiresIn`, `expiresAt`.
202
203
 
204
+ #### Headless Connect (from code)
205
+
206
+ For CLI tools, scripts, and server-side applications -- opens the browser, waits for the user to complete OAuth, and returns the result:
207
+
208
+ ```typescript
209
+ const results = await vault.connect({
210
+ endUser: { id: "alice" },
211
+ providers: ["google"],
212
+ timeout: 300, // max wait in seconds (default: 5 min)
213
+ openBrowser: true, // set false to print URL instead
214
+ });
215
+ for (const result of results) {
216
+ console.log(`Connected: ${result.connectionId} (${result.providerId})`);
217
+ console.log(`Account: ${result.accountIdentifier}`);
218
+ }
219
+
220
+ // Now use the connectionId with vault.request()
221
+ const response = await vault.request(
222
+ results[0].connectionId,
223
+ HttpMethod.GET,
224
+ "https://www.googleapis.com/calendar/v3/calendars/primary/events",
225
+ );
226
+ ```
227
+
228
+ | Parameter | Type | Default | Description |
229
+ |-----------|------|---------|-------------|
230
+ | `endUser` | `{ id: string }` | *required* | End user identity |
231
+ | `providers` | `string[]` | - | Restrict to specific providers |
232
+ | `timeout` | `number` | `300` | Max seconds to wait for completion |
233
+ | `pollInterval` | `number` | `2` | Seconds between status checks |
234
+ | `openBrowser` | `boolean` | `true` | Open browser automatically |
235
+
236
+ Returns `ConnectResult[]` — one per connected provider. Each has: `connectionId`, `providerId`, `accountIdentifier`, `scopes`.
237
+
238
+ Throws `ConnectTimeoutError` if the user doesn't complete in time, `ConnectFlowError` if denied.
239
+
203
240
  ### AI Agent Actor Tracking
204
241
 
205
242
  ```typescript
@@ -295,6 +332,8 @@ import {
295
332
  ConnectionNotFoundError,
296
333
  TokenExpiredError,
297
334
  TokenRetrievalError,
335
+ ConnectFlowError, // Headless connect() failed (denied, provider error)
336
+ ConnectTimeoutError, // Headless connect() timed out
298
337
  NetworkError,
299
338
  TimeoutError,
300
339
  ProviderAPIError,
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,7 +34,10 @@ __export(index_exports, {
24
34
  ActorType: () => ActorType,
25
35
  AlterSDKError: () => AlterSDKError,
26
36
  AlterVault: () => AlterVault,
37
+ ConnectFlowError: () => ConnectFlowError,
38
+ ConnectResult: () => ConnectResult,
27
39
  ConnectSession: () => ConnectSession,
40
+ ConnectTimeoutError: () => ConnectTimeoutError,
28
41
  ConnectionInfo: () => ConnectionInfo,
29
42
  ConnectionListResult: () => ConnectionListResult,
30
43
  ConnectionNotFoundError: () => ConnectionNotFoundError,
@@ -41,7 +54,7 @@ __export(index_exports, {
41
54
  module.exports = __toCommonJS(index_exports);
42
55
 
43
56
  // src/client.ts
44
- var import_node_crypto = require("crypto");
57
+ var import_node_crypto2 = require("crypto");
45
58
 
46
59
  // src/exceptions.ts
47
60
  var AlterSDKError = class extends Error {
@@ -87,6 +100,18 @@ var TokenExpiredError = class extends TokenRetrievalError {
87
100
  this.connectionId = connectionId;
88
101
  }
89
102
  };
103
+ var ConnectFlowError = class extends AlterSDKError {
104
+ constructor(message, details) {
105
+ super(message, details);
106
+ this.name = "ConnectFlowError";
107
+ }
108
+ };
109
+ var ConnectTimeoutError = class extends ConnectFlowError {
110
+ constructor(message, details) {
111
+ super(message, details);
112
+ this.name = "ConnectTimeoutError";
113
+ }
114
+ };
90
115
  var ProviderAPIError = class extends AlterSDKError {
91
116
  statusCode;
92
117
  responseBody;
@@ -134,6 +159,8 @@ var TokenResponse = class _TokenResponse {
134
159
  injectionHeader;
135
160
  /** Header value format with {token} placeholder (e.g., "Bearer {token}", "{token}") */
136
161
  injectionFormat;
162
+ /** Extra credentials for multi-part auth (e.g. AWS SigV4 secret_key, region) */
163
+ additionalCredentials;
137
164
  constructor(data) {
138
165
  this.tokenType = data.token_type ?? "Bearer";
139
166
  this.expiresIn = data.expires_in ?? null;
@@ -143,6 +170,12 @@ var TokenResponse = class _TokenResponse {
143
170
  this.providerId = data.provider_id ?? "";
144
171
  this.injectionHeader = data.injection_header ?? "Authorization";
145
172
  this.injectionFormat = data.injection_format ?? "Bearer {token}";
173
+ if (data.additional_credentials) {
174
+ const { secret_key: _, ...safeCredentials } = data.additional_credentials;
175
+ this.additionalCredentials = Object.keys(safeCredentials).length > 0 ? safeCredentials : null;
176
+ } else {
177
+ this.additionalCredentials = null;
178
+ }
146
179
  Object.freeze(this);
147
180
  }
148
181
  /**
@@ -282,12 +315,38 @@ var ConnectionListResult = class {
282
315
  Object.freeze(this);
283
316
  }
284
317
  };
318
+ var ConnectResult = class {
319
+ connectionId;
320
+ providerId;
321
+ accountIdentifier;
322
+ scopes;
323
+ constructor(data) {
324
+ this.connectionId = data.connection_id;
325
+ this.providerId = data.provider_id;
326
+ this.accountIdentifier = data.account_identifier ?? null;
327
+ this.scopes = data.scopes ?? [];
328
+ Object.freeze(this);
329
+ }
330
+ toJSON() {
331
+ return {
332
+ connection_id: this.connectionId,
333
+ provider_id: this.providerId,
334
+ account_identifier: this.accountIdentifier,
335
+ scopes: this.scopes
336
+ };
337
+ }
338
+ toString() {
339
+ return `ConnectResult(connection_id=${this.connectionId}, provider=${this.providerId})`;
340
+ }
341
+ };
285
342
  var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
286
343
  "authorization",
287
344
  "cookie",
288
345
  "set-cookie",
289
346
  "x-api-key",
290
- "x-auth-token"
347
+ "x-auth-token",
348
+ "x-amz-date",
349
+ "x-amz-content-sha256"
291
350
  ]);
292
351
  var APICallAuditLog = class {
293
352
  connectionId;
@@ -361,11 +420,171 @@ var APICallAuditLog = class {
361
420
  }
362
421
  };
363
422
 
423
+ // src/aws-sig-v4.ts
424
+ var import_node_crypto = require("crypto");
425
+ var ALGORITHM = "AWS4-HMAC-SHA256";
426
+ var AWS_HOST_RE = /^(?<service>[a-z0-9-]+)\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.amazonaws\.com$/;
427
+ var S3_VIRTUAL_HOST_RE = /^[^.]+\.s3\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.amazonaws\.com$/;
428
+ function hmacSha256(key, message) {
429
+ return (0, import_node_crypto.createHmac)("sha256", key).update(message, "utf8").digest();
430
+ }
431
+ function sha256Hex(data) {
432
+ const hash = (0, import_node_crypto.createHash)("sha256");
433
+ if (typeof data === "string") {
434
+ hash.update(data, "utf8");
435
+ } else {
436
+ hash.update(data);
437
+ }
438
+ return hash.digest("hex");
439
+ }
440
+ function detectServiceAndRegion(hostname) {
441
+ const lower = hostname.toLowerCase();
442
+ const m = AWS_HOST_RE.exec(lower);
443
+ if (m?.groups) {
444
+ return { service: m.groups.service, region: m.groups.region };
445
+ }
446
+ const s3m = S3_VIRTUAL_HOST_RE.exec(lower);
447
+ if (s3m?.groups) {
448
+ return { service: "s3", region: s3m.groups.region };
449
+ }
450
+ return { service: null, region: null };
451
+ }
452
+ function deriveSigningKey(secretKey, dateStamp, region, service) {
453
+ const kDate = hmacSha256(Buffer.from("AWS4" + secretKey, "utf8"), dateStamp);
454
+ const kRegion = hmacSha256(kDate, region);
455
+ const kService = hmacSha256(kRegion, service);
456
+ const kSigning = hmacSha256(kService, "aws4_request");
457
+ return kSigning;
458
+ }
459
+ function canonicalUri(path) {
460
+ if (!path) return "/";
461
+ if (!path.startsWith("/")) path = "/" + path;
462
+ const segments = path.split("/");
463
+ return segments.map(
464
+ (seg) => encodeURIComponent(seg).replace(
465
+ /[!'()*]/g,
466
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
467
+ )
468
+ ).join("/");
469
+ }
470
+ function canonicalQueryString(query) {
471
+ if (!query) return "";
472
+ const sorted = [];
473
+ for (const pair of query.split("&")) {
474
+ const eqIdx = pair.indexOf("=");
475
+ if (eqIdx === -1) {
476
+ sorted.push([decodeURIComponent(pair), ""]);
477
+ } else {
478
+ sorted.push([
479
+ decodeURIComponent(pair.slice(0, eqIdx)),
480
+ decodeURIComponent(pair.slice(eqIdx + 1))
481
+ ]);
482
+ }
483
+ }
484
+ sorted.sort((a, b) => {
485
+ if (a[0] < b[0]) return -1;
486
+ if (a[0] > b[0]) return 1;
487
+ if (a[1] < b[1]) return -1;
488
+ if (a[1] > b[1]) return 1;
489
+ return 0;
490
+ });
491
+ const sigv4Encode = (s) => encodeURIComponent(s).replace(
492
+ /[!'()*]/g,
493
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
494
+ );
495
+ return sorted.map(([k, v]) => `${sigv4Encode(k)}=${sigv4Encode(v)}`).join("&");
496
+ }
497
+ function canonicalHeadersAndSigned(headers) {
498
+ const canonical = {};
499
+ for (const [name, value] of Object.entries(headers)) {
500
+ const lowerName = name.toLowerCase().trim();
501
+ const trimmedValue = value.replace(/\s+/g, " ").trim();
502
+ canonical[lowerName] = trimmedValue;
503
+ }
504
+ const sortedNames = Object.keys(canonical).sort();
505
+ const canonicalStr = sortedNames.map((name) => `${name}:${canonical[name]}
506
+ `).join("");
507
+ const signedStr = sortedNames.join(";");
508
+ return { canonicalHeaders: canonicalStr, signedHeaders: signedStr };
509
+ }
510
+ function signAwsRequest(opts) {
511
+ const parsed = new URL(opts.url);
512
+ const hostname = parsed.hostname;
513
+ let { region, service } = opts;
514
+ if (region == null || service == null) {
515
+ const detected = detectServiceAndRegion(hostname);
516
+ if (region == null) region = detected.region;
517
+ if (service == null) service = detected.service;
518
+ }
519
+ if (!region) {
520
+ throw new Error(
521
+ `Cannot determine AWS region from URL '${opts.url}'. Pass region explicitly via additional_credentials.`
522
+ );
523
+ }
524
+ if (!service) {
525
+ throw new Error(
526
+ `Cannot determine AWS service from URL '${opts.url}'. Pass service explicitly via additional_credentials.`
527
+ );
528
+ }
529
+ const timestamp = opts.timestamp ?? /* @__PURE__ */ new Date();
530
+ const amzDate = timestamp.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
531
+ const dateStamp = amzDate.slice(0, 8);
532
+ const bodyBytes = opts.body != null ? typeof opts.body === "string" ? Buffer.from(opts.body, "utf8") : opts.body : Buffer.alloc(0);
533
+ const payloadHash = sha256Hex(bodyBytes);
534
+ const headersToSign = {};
535
+ const hasHost = Object.keys(opts.headers).some(
536
+ (k) => k.toLowerCase() === "host"
537
+ );
538
+ if (!hasHost) {
539
+ const port = parsed.port;
540
+ headersToSign["host"] = port && port !== "80" && port !== "443" ? `${hostname}:${port}` : hostname;
541
+ } else {
542
+ for (const [k, v] of Object.entries(opts.headers)) {
543
+ if (k.toLowerCase() === "host") {
544
+ headersToSign["host"] = v;
545
+ break;
546
+ }
547
+ }
548
+ }
549
+ headersToSign["x-amz-date"] = amzDate;
550
+ headersToSign["x-amz-content-sha256"] = payloadHash;
551
+ const canonicalUriStr = canonicalUri(parsed.pathname);
552
+ const canonicalQs = canonicalQueryString(parsed.search.replace(/^\?/, ""));
553
+ const { canonicalHeaders, signedHeaders } = canonicalHeadersAndSigned(headersToSign);
554
+ const canonicalRequest = [
555
+ opts.method.toUpperCase(),
556
+ canonicalUriStr,
557
+ canonicalQs,
558
+ canonicalHeaders,
559
+ signedHeaders,
560
+ payloadHash
561
+ ].join("\n");
562
+ const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
563
+ const stringToSign = [
564
+ ALGORITHM,
565
+ amzDate,
566
+ credentialScope,
567
+ sha256Hex(canonicalRequest)
568
+ ].join("\n");
569
+ const signingKey = deriveSigningKey(opts.secretKey, dateStamp, region, service);
570
+ const signature = (0, import_node_crypto.createHmac)("sha256", signingKey).update(stringToSign, "utf8").digest("hex");
571
+ const authorization = `${ALGORITHM} Credential=${opts.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
572
+ return {
573
+ Authorization: authorization,
574
+ "x-amz-date": amzDate,
575
+ "x-amz-content-sha256": payloadHash
576
+ };
577
+ }
578
+
364
579
  // src/client.ts
365
580
  var _tokenStore = /* @__PURE__ */ new WeakMap();
581
+ var _additionalCredsStore = /* @__PURE__ */ new WeakMap();
366
582
  function _storeAccessToken(token, accessToken) {
367
583
  _tokenStore.set(token, accessToken);
368
584
  }
585
+ function _storeAdditionalCredentials(token, creds) {
586
+ _additionalCredsStore.set(token, creds);
587
+ }
369
588
  function _extractAccessToken(token) {
370
589
  const value = _tokenStore.get(token);
371
590
  if (value === void 0) {
@@ -375,8 +594,11 @@ function _extractAccessToken(token) {
375
594
  }
376
595
  return value;
377
596
  }
597
+ function _extractAdditionalCredentials(token) {
598
+ return _additionalCredsStore.get(token);
599
+ }
378
600
  var _fetch;
379
- var SDK_VERSION = "0.3.0";
601
+ var SDK_VERSION = "0.4.0";
380
602
  var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
381
603
  var HTTP_FORBIDDEN = 403;
382
604
  var HTTP_NOT_FOUND = 404;
@@ -433,7 +655,9 @@ var HttpClient = class {
433
655
  headers: mergedHeaders,
434
656
  signal: controller.signal
435
657
  };
436
- if (options?.json !== void 0) {
658
+ if (options?.body !== void 0) {
659
+ init.body = options.body;
660
+ } else if (options?.json !== void 0) {
437
661
  init.body = JSON.stringify(options.json);
438
662
  mergedHeaders["Content-Type"] = "application/json";
439
663
  }
@@ -496,7 +720,7 @@ var AlterVault = class _AlterVault {
496
720
  for (const [name, value] of actorStrings) {
497
721
  _AlterVault.#validateActorString(value, name);
498
722
  }
499
- this.#hmacKey = (0, import_node_crypto.createHmac)("sha256", options.apiKey).update("alter-signing-v1").digest();
723
+ this.#hmacKey = (0, import_node_crypto2.createHmac)("sha256", options.apiKey).update("alter-signing-v1").digest();
500
724
  this.baseUrl = (process.env.ALTER_BASE_URL ?? "https://backend.alterai.dev").replace(/\/+$/, "");
501
725
  const timeoutMs = options.timeout ?? 3e4;
502
726
  this.#actorType = options.actorType;
@@ -600,12 +824,12 @@ var AlterVault = class _AlterVault {
600
824
  */
601
825
  #computeHmacHeaders(method, path, body) {
602
826
  const timestamp = String(Math.floor(Date.now() / 1e3));
603
- const contentHash = (0, import_node_crypto.createHash)("sha256").update(body ?? "").digest("hex");
827
+ const contentHash = (0, import_node_crypto2.createHash)("sha256").update(body ?? "").digest("hex");
604
828
  const stringToSign = `${method.toUpperCase()}
605
829
  ${path}
606
830
  ${timestamp}
607
831
  ${contentHash}`;
608
- const signature = (0, import_node_crypto.createHmac)("sha256", this.#hmacKey).update(stringToSign).digest("hex");
832
+ const signature = (0, import_node_crypto2.createHmac)("sha256", this.#hmacKey).update(stringToSign).digest("hex");
609
833
  return {
610
834
  "X-Alter-Timestamp": timestamp,
611
835
  "X-Alter-Content-SHA256": contentHash,
@@ -809,6 +1033,9 @@ ${contentHash}`;
809
1033
  );
810
1034
  }
811
1035
  _storeAccessToken(tokenResponse, typedData.access_token);
1036
+ if (typedData.additional_credentials) {
1037
+ _storeAdditionalCredentials(tokenResponse, typedData.additional_credentials);
1038
+ }
812
1039
  return tokenResponse;
813
1040
  }
814
1041
  /**
@@ -914,7 +1141,7 @@ ${contentHash}`;
914
1141
  "SDK instance has been closed. Create a new AlterVault instance to make requests."
915
1142
  );
916
1143
  }
917
- const runId = options?.runId ?? (0, import_node_crypto.randomUUID)();
1144
+ const runId = options?.runId ?? (0, import_node_crypto2.randomUUID)();
918
1145
  const methodStr = String(method).toUpperCase();
919
1146
  const urlLower = url.toLowerCase();
920
1147
  if (!ALLOWED_URL_SCHEMES.some((scheme) => urlLower.startsWith(scheme))) {
@@ -960,28 +1187,66 @@ ${contentHash}`;
960
1187
  options?.threadId,
961
1188
  options?.toolCallId
962
1189
  );
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
1190
  const requestHeaders = options?.extraHeaders ? { ...options.extraHeaders } : {};
972
1191
  const accessToken = _extractAccessToken(tokenResponse);
973
- requestHeaders[tokenResponse.injectionHeader] = tokenResponse.injectionFormat.replace("{token}", accessToken);
1192
+ const injectionHeaderLower = tokenResponse.injectionHeader.toLowerCase();
1193
+ const additionalCreds = _extractAdditionalCredentials(tokenResponse);
1194
+ const isSigV4 = tokenResponse.injectionFormat.startsWith("AWS4-HMAC-SHA256") && additionalCreds != null;
1195
+ let sigv4BodyStr = null;
1196
+ if (isSigV4) {
1197
+ const accessKeyId = accessToken;
1198
+ if (options?.json != null) {
1199
+ sigv4BodyStr = JSON.stringify(options.json);
1200
+ if (!requestHeaders["Content-Type"]) {
1201
+ requestHeaders["Content-Type"] = "application/json";
1202
+ }
1203
+ }
1204
+ if (!additionalCreds.secret_key) {
1205
+ throw new TokenRetrievalError(
1206
+ "AWS SigV4 credential is missing secret_key in additional_credentials. Re-store the credential with both Access Key ID and Secret Access Key.",
1207
+ { connection_id: connectionId }
1208
+ );
1209
+ }
1210
+ const awsHeaders = signAwsRequest({
1211
+ method: methodStr,
1212
+ url,
1213
+ headers: requestHeaders,
1214
+ body: sigv4BodyStr,
1215
+ accessKeyId,
1216
+ secretKey: additionalCreds.secret_key,
1217
+ region: additionalCreds.region ?? null,
1218
+ service: additionalCreds.service ?? null
1219
+ });
1220
+ Object.assign(requestHeaders, awsHeaders);
1221
+ } else {
1222
+ if (options?.extraHeaders && Object.keys(options.extraHeaders).some(
1223
+ (k) => k.toLowerCase() === injectionHeaderLower
1224
+ )) {
1225
+ console.warn(
1226
+ `extraHeaders contains '${tokenResponse.injectionHeader}' which will be overwritten with the auto-injected credential`
1227
+ );
1228
+ }
1229
+ requestHeaders[tokenResponse.injectionHeader] = tokenResponse.injectionFormat.replace("{token}", accessToken);
1230
+ }
974
1231
  if (!requestHeaders["User-Agent"]) {
975
1232
  requestHeaders["User-Agent"] = SDK_USER_AGENT;
976
1233
  }
977
1234
  const startTime = Date.now();
978
1235
  let response;
979
1236
  try {
980
- response = await this.#providerClient.request(methodStr, url, {
981
- json: options?.json,
982
- headers: requestHeaders,
983
- params: options?.queryParams
984
- });
1237
+ if (isSigV4 && sigv4BodyStr != null) {
1238
+ response = await this.#providerClient.request(methodStr, url, {
1239
+ body: sigv4BodyStr,
1240
+ headers: requestHeaders,
1241
+ params: options?.queryParams
1242
+ });
1243
+ } else {
1244
+ response = await this.#providerClient.request(methodStr, url, {
1245
+ json: options?.json,
1246
+ headers: requestHeaders,
1247
+ params: options?.queryParams
1248
+ });
1249
+ }
985
1250
  } catch (error) {
986
1251
  if (_AlterVault.#isTimeoutOrAbortError(error)) {
987
1252
  throw new TimeoutError(
@@ -1004,9 +1269,11 @@ ${contentHash}`;
1004
1269
  );
1005
1270
  }
1006
1271
  const latencyMs = Date.now() - startTime;
1272
+ const sigv4Sensitive = /* @__PURE__ */ new Set(["authorization", "x-amz-date", "x-amz-content-sha256"]);
1273
+ const stripHeaders = isSigV4 ? sigv4Sensitive : /* @__PURE__ */ new Set([injectionHeaderLower]);
1007
1274
  const auditHeaders = {};
1008
1275
  for (const [key, value] of Object.entries(requestHeaders)) {
1009
- if (key.toLowerCase() !== injectionHeaderLower) {
1276
+ if (!stripHeaders.has(key.toLowerCase())) {
1010
1277
  auditHeaders[key] = value;
1011
1278
  }
1012
1279
  }
@@ -1156,6 +1423,132 @@ ${contentHash}`;
1156
1423
  const data = await response.json();
1157
1424
  return new ConnectSession(data);
1158
1425
  }
1426
+ /**
1427
+ * Open OAuth in the user's browser and wait for completion.
1428
+ *
1429
+ * This is the headless connect flow for CLI tools, scripts, and
1430
+ * server-side applications. It creates a Connect session, opens the
1431
+ * browser, and polls until the user completes OAuth.
1432
+ *
1433
+ * @param options - Connect options (endUser is required)
1434
+ * @returns Array of ConnectResult objects (one per connected provider)
1435
+ * @throws ConnectTimeoutError if the user doesn't complete within timeout
1436
+ * @throws ConnectFlowError if the user denies or provider returns error
1437
+ * @throws AlterSDKError if SDK is closed or session creation fails
1438
+ */
1439
+ async connect(options) {
1440
+ if (this.#closed) {
1441
+ throw new AlterSDKError(
1442
+ "SDK instance has been closed. Create a new AlterVault instance to make requests."
1443
+ );
1444
+ }
1445
+ const timeout = options.timeout ?? 300;
1446
+ const pollInterval = options.pollInterval ?? 2;
1447
+ const openBrowser = options.openBrowser ?? true;
1448
+ const session = await this.createConnectSession({
1449
+ endUser: options.endUser,
1450
+ allowedProviders: options.providers
1451
+ });
1452
+ if (openBrowser) {
1453
+ try {
1454
+ const openModule = await import("open");
1455
+ const openFn = openModule.default;
1456
+ if (openFn) {
1457
+ await openFn(session.connectUrl);
1458
+ } else {
1459
+ console.log(
1460
+ `Open this URL to authorize: ${session.connectUrl}`
1461
+ );
1462
+ }
1463
+ } catch {
1464
+ console.log(
1465
+ `Open this URL to authorize: ${session.connectUrl}`
1466
+ );
1467
+ }
1468
+ } else {
1469
+ console.log(
1470
+ `Open this URL to authorize: ${session.connectUrl}`
1471
+ );
1472
+ }
1473
+ const deadline = Date.now() + timeout * 1e3;
1474
+ while (Date.now() < deadline) {
1475
+ await new Promise(
1476
+ (resolve) => setTimeout(resolve, pollInterval * 1e3)
1477
+ );
1478
+ const pollResult = await this.#pollSession(session.sessionToken);
1479
+ const pollStatus = pollResult.status;
1480
+ if (pollStatus === "completed") {
1481
+ const connectionsData = pollResult.connections ?? [];
1482
+ return connectionsData.map(
1483
+ (conn) => new ConnectResult({
1484
+ connection_id: conn.connection_id ?? "",
1485
+ provider_id: conn.provider_id ?? "",
1486
+ account_identifier: conn.account_identifier ?? null,
1487
+ scopes: conn.scopes ?? []
1488
+ })
1489
+ );
1490
+ }
1491
+ if (pollStatus === "error") {
1492
+ const err = pollResult.error;
1493
+ throw new ConnectFlowError(
1494
+ err?.error_message ?? "OAuth flow failed",
1495
+ {
1496
+ error_code: err?.error_code ?? "unknown_error"
1497
+ }
1498
+ );
1499
+ }
1500
+ if (pollStatus === "expired") {
1501
+ throw new ConnectFlowError(
1502
+ "Connect session expired before OAuth was completed"
1503
+ );
1504
+ }
1505
+ }
1506
+ throw new ConnectTimeoutError(
1507
+ `OAuth flow did not complete within ${timeout} seconds. The user may not have finished authorizing in the browser.`,
1508
+ { timeout }
1509
+ );
1510
+ }
1511
+ /**
1512
+ * Poll the Connect session for completion status (INTERNAL).
1513
+ *
1514
+ * Makes an HMAC-signed POST to the poll endpoint.
1515
+ */
1516
+ async #pollSession(sessionToken) {
1517
+ const actorHeaders = this.#getActorRequestHeaders();
1518
+ const pollPath = "/sdk/oauth/connect/session/poll";
1519
+ const pollBody = { session_token: sessionToken };
1520
+ const pollHmac = this.#computeHmacHeaders(
1521
+ "POST",
1522
+ pollPath,
1523
+ JSON.stringify(pollBody)
1524
+ );
1525
+ let response;
1526
+ try {
1527
+ response = await this.#alterClient.post(pollPath, {
1528
+ json: pollBody,
1529
+ headers: { ...actorHeaders, ...pollHmac }
1530
+ });
1531
+ } catch (error) {
1532
+ if (_AlterVault.#isTimeoutOrAbortError(error)) {
1533
+ throw new TimeoutError(
1534
+ `Request to Alter Vault backend timed out: ${error instanceof Error ? error.message : String(error)}`,
1535
+ { base_url: this.baseUrl }
1536
+ );
1537
+ }
1538
+ if (error instanceof TypeError) {
1539
+ throw new NetworkError(
1540
+ `Failed to connect to Alter Vault backend: ${error.message}`,
1541
+ { base_url: this.baseUrl }
1542
+ );
1543
+ }
1544
+ throw new AlterSDKError(
1545
+ `Failed to poll connect session: ${error instanceof Error ? error.message : String(error)}`
1546
+ );
1547
+ }
1548
+ this.#cacheActorIdFromResponse(response);
1549
+ await this.#handleErrorResponse(response);
1550
+ return await response.json();
1551
+ }
1159
1552
  /**
1160
1553
  * Close HTTP clients and release resources.
1161
1554
  * Waits for any pending audit tasks before closing.
@@ -1203,7 +1596,10 @@ var HttpMethod = /* @__PURE__ */ ((HttpMethod2) => {
1203
1596
  ActorType,
1204
1597
  AlterSDKError,
1205
1598
  AlterVault,
1599
+ ConnectFlowError,
1600
+ ConnectResult,
1206
1601
  ConnectSession,
1602
+ ConnectTimeoutError,
1207
1603
  ConnectionInfo,
1208
1604
  ConnectionListResult,
1209
1605
  ConnectionNotFoundError,