@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 +39 -0
- package/dist/index.cjs +419 -23
- package/dist/index.d.cts +77 -1
- package/dist/index.d.ts +77 -1
- package/dist/index.js +405 -22
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
|
-
import { createHash, createHmac, randomUUID } from "crypto";
|
|
2
|
+
import { createHash as createHash2, createHmac as createHmac2, randomUUID } from "crypto";
|
|
3
3
|
|
|
4
4
|
// src/exceptions.ts
|
|
5
5
|
var AlterSDKError = class extends Error {
|
|
@@ -45,6 +45,18 @@ var TokenExpiredError = class extends TokenRetrievalError {
|
|
|
45
45
|
this.connectionId = connectionId;
|
|
46
46
|
}
|
|
47
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
|
+
};
|
|
48
60
|
var ProviderAPIError = class extends AlterSDKError {
|
|
49
61
|
statusCode;
|
|
50
62
|
responseBody;
|
|
@@ -92,6 +104,8 @@ var TokenResponse = class _TokenResponse {
|
|
|
92
104
|
injectionHeader;
|
|
93
105
|
/** Header value format with {token} placeholder (e.g., "Bearer {token}", "{token}") */
|
|
94
106
|
injectionFormat;
|
|
107
|
+
/** Extra credentials for multi-part auth (e.g. AWS SigV4 secret_key, region) */
|
|
108
|
+
additionalCredentials;
|
|
95
109
|
constructor(data) {
|
|
96
110
|
this.tokenType = data.token_type ?? "Bearer";
|
|
97
111
|
this.expiresIn = data.expires_in ?? null;
|
|
@@ -101,6 +115,12 @@ var TokenResponse = class _TokenResponse {
|
|
|
101
115
|
this.providerId = data.provider_id ?? "";
|
|
102
116
|
this.injectionHeader = data.injection_header ?? "Authorization";
|
|
103
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
|
+
}
|
|
104
124
|
Object.freeze(this);
|
|
105
125
|
}
|
|
106
126
|
/**
|
|
@@ -240,12 +260,38 @@ var ConnectionListResult = class {
|
|
|
240
260
|
Object.freeze(this);
|
|
241
261
|
}
|
|
242
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
|
+
};
|
|
243
287
|
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
|
|
244
288
|
"authorization",
|
|
245
289
|
"cookie",
|
|
246
290
|
"set-cookie",
|
|
247
291
|
"x-api-key",
|
|
248
|
-
"x-auth-token"
|
|
292
|
+
"x-auth-token",
|
|
293
|
+
"x-amz-date",
|
|
294
|
+
"x-amz-content-sha256"
|
|
249
295
|
]);
|
|
250
296
|
var APICallAuditLog = class {
|
|
251
297
|
connectionId;
|
|
@@ -319,11 +365,171 @@ var APICallAuditLog = class {
|
|
|
319
365
|
}
|
|
320
366
|
};
|
|
321
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
|
+
|
|
322
524
|
// src/client.ts
|
|
323
525
|
var _tokenStore = /* @__PURE__ */ new WeakMap();
|
|
526
|
+
var _additionalCredsStore = /* @__PURE__ */ new WeakMap();
|
|
324
527
|
function _storeAccessToken(token, accessToken) {
|
|
325
528
|
_tokenStore.set(token, accessToken);
|
|
326
529
|
}
|
|
530
|
+
function _storeAdditionalCredentials(token, creds) {
|
|
531
|
+
_additionalCredsStore.set(token, creds);
|
|
532
|
+
}
|
|
327
533
|
function _extractAccessToken(token) {
|
|
328
534
|
const value = _tokenStore.get(token);
|
|
329
535
|
if (value === void 0) {
|
|
@@ -333,8 +539,11 @@ function _extractAccessToken(token) {
|
|
|
333
539
|
}
|
|
334
540
|
return value;
|
|
335
541
|
}
|
|
542
|
+
function _extractAdditionalCredentials(token) {
|
|
543
|
+
return _additionalCredsStore.get(token);
|
|
544
|
+
}
|
|
336
545
|
var _fetch;
|
|
337
|
-
var SDK_VERSION = "0.
|
|
546
|
+
var SDK_VERSION = "0.4.0";
|
|
338
547
|
var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
|
|
339
548
|
var HTTP_FORBIDDEN = 403;
|
|
340
549
|
var HTTP_NOT_FOUND = 404;
|
|
@@ -391,7 +600,9 @@ var HttpClient = class {
|
|
|
391
600
|
headers: mergedHeaders,
|
|
392
601
|
signal: controller.signal
|
|
393
602
|
};
|
|
394
|
-
if (options?.
|
|
603
|
+
if (options?.body !== void 0) {
|
|
604
|
+
init.body = options.body;
|
|
605
|
+
} else if (options?.json !== void 0) {
|
|
395
606
|
init.body = JSON.stringify(options.json);
|
|
396
607
|
mergedHeaders["Content-Type"] = "application/json";
|
|
397
608
|
}
|
|
@@ -454,7 +665,7 @@ var AlterVault = class _AlterVault {
|
|
|
454
665
|
for (const [name, value] of actorStrings) {
|
|
455
666
|
_AlterVault.#validateActorString(value, name);
|
|
456
667
|
}
|
|
457
|
-
this.#hmacKey =
|
|
668
|
+
this.#hmacKey = createHmac2("sha256", options.apiKey).update("alter-signing-v1").digest();
|
|
458
669
|
this.baseUrl = (process.env.ALTER_BASE_URL ?? "https://backend.alterai.dev").replace(/\/+$/, "");
|
|
459
670
|
const timeoutMs = options.timeout ?? 3e4;
|
|
460
671
|
this.#actorType = options.actorType;
|
|
@@ -558,12 +769,12 @@ var AlterVault = class _AlterVault {
|
|
|
558
769
|
*/
|
|
559
770
|
#computeHmacHeaders(method, path, body) {
|
|
560
771
|
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
561
|
-
const contentHash =
|
|
772
|
+
const contentHash = createHash2("sha256").update(body ?? "").digest("hex");
|
|
562
773
|
const stringToSign = `${method.toUpperCase()}
|
|
563
774
|
${path}
|
|
564
775
|
${timestamp}
|
|
565
776
|
${contentHash}`;
|
|
566
|
-
const signature =
|
|
777
|
+
const signature = createHmac2("sha256", this.#hmacKey).update(stringToSign).digest("hex");
|
|
567
778
|
return {
|
|
568
779
|
"X-Alter-Timestamp": timestamp,
|
|
569
780
|
"X-Alter-Content-SHA256": contentHash,
|
|
@@ -767,6 +978,9 @@ ${contentHash}`;
|
|
|
767
978
|
);
|
|
768
979
|
}
|
|
769
980
|
_storeAccessToken(tokenResponse, typedData.access_token);
|
|
981
|
+
if (typedData.additional_credentials) {
|
|
982
|
+
_storeAdditionalCredentials(tokenResponse, typedData.additional_credentials);
|
|
983
|
+
}
|
|
770
984
|
return tokenResponse;
|
|
771
985
|
}
|
|
772
986
|
/**
|
|
@@ -918,28 +1132,66 @@ ${contentHash}`;
|
|
|
918
1132
|
options?.threadId,
|
|
919
1133
|
options?.toolCallId
|
|
920
1134
|
);
|
|
921
|
-
const injectionHeaderLower = tokenResponse.injectionHeader.toLowerCase();
|
|
922
|
-
if (options?.extraHeaders && Object.keys(options.extraHeaders).some(
|
|
923
|
-
(k) => k.toLowerCase() === injectionHeaderLower
|
|
924
|
-
)) {
|
|
925
|
-
console.warn(
|
|
926
|
-
`extraHeaders contains '${tokenResponse.injectionHeader}' which will be overwritten with the auto-injected credential`
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
1135
|
const requestHeaders = options?.extraHeaders ? { ...options.extraHeaders } : {};
|
|
930
1136
|
const accessToken = _extractAccessToken(tokenResponse);
|
|
931
|
-
|
|
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
|
+
}
|
|
932
1176
|
if (!requestHeaders["User-Agent"]) {
|
|
933
1177
|
requestHeaders["User-Agent"] = SDK_USER_AGENT;
|
|
934
1178
|
}
|
|
935
1179
|
const startTime = Date.now();
|
|
936
1180
|
let response;
|
|
937
1181
|
try {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
+
}
|
|
943
1195
|
} catch (error) {
|
|
944
1196
|
if (_AlterVault.#isTimeoutOrAbortError(error)) {
|
|
945
1197
|
throw new TimeoutError(
|
|
@@ -962,9 +1214,11 @@ ${contentHash}`;
|
|
|
962
1214
|
);
|
|
963
1215
|
}
|
|
964
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]);
|
|
965
1219
|
const auditHeaders = {};
|
|
966
1220
|
for (const [key, value] of Object.entries(requestHeaders)) {
|
|
967
|
-
if (key.toLowerCase()
|
|
1221
|
+
if (!stripHeaders.has(key.toLowerCase())) {
|
|
968
1222
|
auditHeaders[key] = value;
|
|
969
1223
|
}
|
|
970
1224
|
}
|
|
@@ -1114,6 +1368,132 @@ ${contentHash}`;
|
|
|
1114
1368
|
const data = await response.json();
|
|
1115
1369
|
return new ConnectSession(data);
|
|
1116
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
|
+
}
|
|
1117
1497
|
/**
|
|
1118
1498
|
* Close HTTP clients and release resources.
|
|
1119
1499
|
* Waits for any pending audit tasks before closing.
|
|
@@ -1160,7 +1540,10 @@ export {
|
|
|
1160
1540
|
ActorType,
|
|
1161
1541
|
AlterSDKError,
|
|
1162
1542
|
AlterVault,
|
|
1543
|
+
ConnectFlowError,
|
|
1544
|
+
ConnectResult,
|
|
1163
1545
|
ConnectSession,
|
|
1546
|
+
ConnectTimeoutError,
|
|
1164
1547
|
ConnectionInfo,
|
|
1165
1548
|
ConnectionListResult,
|
|
1166
1549
|
ConnectionNotFoundError,
|