@alter-ai/alter-sdk 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -33
- package/dist/index.cjs +767 -92
- package/dist/index.d.cts +298 -24
- package/dist/index.d.ts +298 -24
- package/dist/index.js +744 -88
- 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 {
|
|
@@ -17,13 +17,45 @@ var AlterSDKError = class extends Error {
|
|
|
17
17
|
return this.message;
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
|
-
var
|
|
20
|
+
var BackendError = class extends AlterSDKError {
|
|
21
21
|
constructor(message, details) {
|
|
22
22
|
super(message, details);
|
|
23
|
-
this.name = "
|
|
23
|
+
this.name = "BackendError";
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
|
-
var
|
|
26
|
+
var ReAuthRequiredError = class extends BackendError {
|
|
27
|
+
constructor(message, details) {
|
|
28
|
+
super(message, details);
|
|
29
|
+
this.name = "ReAuthRequiredError";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var ConnectionExpiredError = class extends ReAuthRequiredError {
|
|
33
|
+
constructor(message, details) {
|
|
34
|
+
super(message, details);
|
|
35
|
+
this.name = "ConnectionExpiredError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var ConnectionRevokedError = class extends ReAuthRequiredError {
|
|
39
|
+
connectionId;
|
|
40
|
+
constructor(message, connectionId, details) {
|
|
41
|
+
super(message, details);
|
|
42
|
+
this.name = "ConnectionRevokedError";
|
|
43
|
+
this.connectionId = connectionId;
|
|
44
|
+
}
|
|
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 {
|
|
27
59
|
policyError;
|
|
28
60
|
constructor(message, policyError, details) {
|
|
29
61
|
super(message, details);
|
|
@@ -31,18 +63,28 @@ var PolicyViolationError = class extends TokenRetrievalError {
|
|
|
31
63
|
this.policyError = policyError;
|
|
32
64
|
}
|
|
33
65
|
};
|
|
34
|
-
var
|
|
66
|
+
var ConnectFlowError = class extends AlterSDKError {
|
|
35
67
|
constructor(message, details) {
|
|
36
68
|
super(message, details);
|
|
37
|
-
this.name = "
|
|
69
|
+
this.name = "ConnectFlowError";
|
|
38
70
|
}
|
|
39
71
|
};
|
|
40
|
-
var
|
|
41
|
-
|
|
42
|
-
constructor(message, connectionId, details) {
|
|
72
|
+
var ConnectDeniedError = class extends ConnectFlowError {
|
|
73
|
+
constructor(message, details) {
|
|
43
74
|
super(message, details);
|
|
44
|
-
this.name = "
|
|
45
|
-
|
|
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
|
+
};
|
|
84
|
+
var ConnectTimeoutError = class extends ConnectFlowError {
|
|
85
|
+
constructor(message, details) {
|
|
86
|
+
super(message, details);
|
|
87
|
+
this.name = "ConnectTimeoutError";
|
|
46
88
|
}
|
|
47
89
|
};
|
|
48
90
|
var ProviderAPIError = class extends AlterSDKError {
|
|
@@ -55,6 +97,16 @@ var ProviderAPIError = class extends AlterSDKError {
|
|
|
55
97
|
this.responseBody = responseBody;
|
|
56
98
|
}
|
|
57
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
|
+
};
|
|
58
110
|
var NetworkError = class extends AlterSDKError {
|
|
59
111
|
constructor(message, details) {
|
|
60
112
|
super(message, details);
|
|
@@ -92,6 +144,10 @@ var TokenResponse = class _TokenResponse {
|
|
|
92
144
|
injectionHeader;
|
|
93
145
|
/** Header value format with {token} placeholder (e.g., "Bearer {token}", "{token}") */
|
|
94
146
|
injectionFormat;
|
|
147
|
+
/** Extra credentials for multi-part auth (e.g. AWS SigV4 secret_key, region) */
|
|
148
|
+
additionalCredentials;
|
|
149
|
+
/** Additional injection rules for multi-header or query param auth */
|
|
150
|
+
additionalInjections;
|
|
95
151
|
constructor(data) {
|
|
96
152
|
this.tokenType = data.token_type ?? "Bearer";
|
|
97
153
|
this.expiresIn = data.expires_in ?? null;
|
|
@@ -101,6 +157,13 @@ var TokenResponse = class _TokenResponse {
|
|
|
101
157
|
this.providerId = data.provider_id ?? "";
|
|
102
158
|
this.injectionHeader = data.injection_header ?? "Authorization";
|
|
103
159
|
this.injectionFormat = data.injection_format ?? "Bearer {token}";
|
|
160
|
+
if (data.additional_credentials) {
|
|
161
|
+
const { secret_key: _, ...safeCredentials } = data.additional_credentials;
|
|
162
|
+
this.additionalCredentials = Object.keys(safeCredentials).length > 0 ? safeCredentials : null;
|
|
163
|
+
} else {
|
|
164
|
+
this.additionalCredentials = null;
|
|
165
|
+
}
|
|
166
|
+
this.additionalInjections = data.additional_injections ?? null;
|
|
104
167
|
Object.freeze(this);
|
|
105
168
|
}
|
|
106
169
|
/**
|
|
@@ -169,6 +232,7 @@ var ConnectionInfo = class {
|
|
|
169
232
|
accountIdentifier;
|
|
170
233
|
accountDisplayName;
|
|
171
234
|
status;
|
|
235
|
+
scopeMismatch;
|
|
172
236
|
expiresAt;
|
|
173
237
|
createdAt;
|
|
174
238
|
lastUsedAt;
|
|
@@ -179,6 +243,7 @@ var ConnectionInfo = class {
|
|
|
179
243
|
this.accountIdentifier = data.account_identifier ?? null;
|
|
180
244
|
this.accountDisplayName = data.account_display_name ?? null;
|
|
181
245
|
this.status = data.status;
|
|
246
|
+
this.scopeMismatch = data.scope_mismatch ?? false;
|
|
182
247
|
this.expiresAt = data.expires_at ?? null;
|
|
183
248
|
this.createdAt = data.created_at;
|
|
184
249
|
this.lastUsedAt = data.last_used_at ?? null;
|
|
@@ -192,6 +257,7 @@ var ConnectionInfo = class {
|
|
|
192
257
|
account_identifier: this.accountIdentifier,
|
|
193
258
|
account_display_name: this.accountDisplayName,
|
|
194
259
|
status: this.status,
|
|
260
|
+
scope_mismatch: this.scopeMismatch,
|
|
195
261
|
expires_at: this.expiresAt,
|
|
196
262
|
created_at: this.createdAt,
|
|
197
263
|
last_used_at: this.lastUsedAt
|
|
@@ -240,12 +306,41 @@ var ConnectionListResult = class {
|
|
|
240
306
|
Object.freeze(this);
|
|
241
307
|
}
|
|
242
308
|
};
|
|
309
|
+
var ConnectResult = class {
|
|
310
|
+
connectionId;
|
|
311
|
+
providerId;
|
|
312
|
+
accountIdentifier;
|
|
313
|
+
scopes;
|
|
314
|
+
connectionPolicy;
|
|
315
|
+
constructor(data) {
|
|
316
|
+
this.connectionId = data.connection_id;
|
|
317
|
+
this.providerId = data.provider_id;
|
|
318
|
+
this.accountIdentifier = data.account_identifier ?? null;
|
|
319
|
+
this.scopes = data.scopes ?? [];
|
|
320
|
+
this.connectionPolicy = data.connection_policy ?? null;
|
|
321
|
+
Object.freeze(this);
|
|
322
|
+
}
|
|
323
|
+
toJSON() {
|
|
324
|
+
return {
|
|
325
|
+
connection_id: this.connectionId,
|
|
326
|
+
provider_id: this.providerId,
|
|
327
|
+
account_identifier: this.accountIdentifier,
|
|
328
|
+
scopes: this.scopes,
|
|
329
|
+
connection_policy: this.connectionPolicy
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
toString() {
|
|
333
|
+
return `ConnectResult(connection_id=${this.connectionId}, provider=${this.providerId})`;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
243
336
|
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
|
|
244
337
|
"authorization",
|
|
245
338
|
"cookie",
|
|
246
339
|
"set-cookie",
|
|
247
340
|
"x-api-key",
|
|
248
|
-
"x-auth-token"
|
|
341
|
+
"x-auth-token",
|
|
342
|
+
"x-amz-date",
|
|
343
|
+
"x-amz-content-sha256"
|
|
249
344
|
]);
|
|
250
345
|
var APICallAuditLog = class {
|
|
251
346
|
connectionId;
|
|
@@ -319,11 +414,171 @@ var APICallAuditLog = class {
|
|
|
319
414
|
}
|
|
320
415
|
};
|
|
321
416
|
|
|
417
|
+
// src/aws-sig-v4.ts
|
|
418
|
+
import { createHash, createHmac } from "crypto";
|
|
419
|
+
var ALGORITHM = "AWS4-HMAC-SHA256";
|
|
420
|
+
var AWS_HOST_RE = /^(?<service>[a-z0-9-]+)\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.amazonaws\.com$/;
|
|
421
|
+
var S3_VIRTUAL_HOST_RE = /^[^.]+\.s3\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.amazonaws\.com$/;
|
|
422
|
+
function hmacSha256(key, message) {
|
|
423
|
+
return createHmac("sha256", key).update(message, "utf8").digest();
|
|
424
|
+
}
|
|
425
|
+
function sha256Hex(data) {
|
|
426
|
+
const hash = createHash("sha256");
|
|
427
|
+
if (typeof data === "string") {
|
|
428
|
+
hash.update(data, "utf8");
|
|
429
|
+
} else {
|
|
430
|
+
hash.update(data);
|
|
431
|
+
}
|
|
432
|
+
return hash.digest("hex");
|
|
433
|
+
}
|
|
434
|
+
function detectServiceAndRegion(hostname) {
|
|
435
|
+
const lower = hostname.toLowerCase();
|
|
436
|
+
const m = AWS_HOST_RE.exec(lower);
|
|
437
|
+
if (m?.groups) {
|
|
438
|
+
return { service: m.groups.service, region: m.groups.region };
|
|
439
|
+
}
|
|
440
|
+
const s3m = S3_VIRTUAL_HOST_RE.exec(lower);
|
|
441
|
+
if (s3m?.groups) {
|
|
442
|
+
return { service: "s3", region: s3m.groups.region };
|
|
443
|
+
}
|
|
444
|
+
return { service: null, region: null };
|
|
445
|
+
}
|
|
446
|
+
function deriveSigningKey(secretKey, dateStamp, region, service) {
|
|
447
|
+
const kDate = hmacSha256(Buffer.from("AWS4" + secretKey, "utf8"), dateStamp);
|
|
448
|
+
const kRegion = hmacSha256(kDate, region);
|
|
449
|
+
const kService = hmacSha256(kRegion, service);
|
|
450
|
+
const kSigning = hmacSha256(kService, "aws4_request");
|
|
451
|
+
return kSigning;
|
|
452
|
+
}
|
|
453
|
+
function canonicalUri(path) {
|
|
454
|
+
if (!path) return "/";
|
|
455
|
+
if (!path.startsWith("/")) path = "/" + path;
|
|
456
|
+
const segments = path.split("/");
|
|
457
|
+
return segments.map(
|
|
458
|
+
(seg) => encodeURIComponent(seg).replace(
|
|
459
|
+
/[!'()*]/g,
|
|
460
|
+
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
|
461
|
+
)
|
|
462
|
+
).join("/");
|
|
463
|
+
}
|
|
464
|
+
function canonicalQueryString(query) {
|
|
465
|
+
if (!query) return "";
|
|
466
|
+
const sorted = [];
|
|
467
|
+
for (const pair of query.split("&")) {
|
|
468
|
+
const eqIdx = pair.indexOf("=");
|
|
469
|
+
if (eqIdx === -1) {
|
|
470
|
+
sorted.push([decodeURIComponent(pair), ""]);
|
|
471
|
+
} else {
|
|
472
|
+
sorted.push([
|
|
473
|
+
decodeURIComponent(pair.slice(0, eqIdx)),
|
|
474
|
+
decodeURIComponent(pair.slice(eqIdx + 1))
|
|
475
|
+
]);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
sorted.sort((a, b) => {
|
|
479
|
+
if (a[0] < b[0]) return -1;
|
|
480
|
+
if (a[0] > b[0]) return 1;
|
|
481
|
+
if (a[1] < b[1]) return -1;
|
|
482
|
+
if (a[1] > b[1]) return 1;
|
|
483
|
+
return 0;
|
|
484
|
+
});
|
|
485
|
+
const sigv4Encode = (s) => encodeURIComponent(s).replace(
|
|
486
|
+
/[!'()*]/g,
|
|
487
|
+
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
|
488
|
+
);
|
|
489
|
+
return sorted.map(([k, v]) => `${sigv4Encode(k)}=${sigv4Encode(v)}`).join("&");
|
|
490
|
+
}
|
|
491
|
+
function canonicalHeadersAndSigned(headers) {
|
|
492
|
+
const canonical = {};
|
|
493
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
494
|
+
const lowerName = name.toLowerCase().trim();
|
|
495
|
+
const trimmedValue = value.replace(/\s+/g, " ").trim();
|
|
496
|
+
canonical[lowerName] = trimmedValue;
|
|
497
|
+
}
|
|
498
|
+
const sortedNames = Object.keys(canonical).sort();
|
|
499
|
+
const canonicalStr = sortedNames.map((name) => `${name}:${canonical[name]}
|
|
500
|
+
`).join("");
|
|
501
|
+
const signedStr = sortedNames.join(";");
|
|
502
|
+
return { canonicalHeaders: canonicalStr, signedHeaders: signedStr };
|
|
503
|
+
}
|
|
504
|
+
function signAwsRequest(opts) {
|
|
505
|
+
const parsed = new URL(opts.url);
|
|
506
|
+
const hostname = parsed.hostname;
|
|
507
|
+
let { region, service } = opts;
|
|
508
|
+
if (region == null || service == null) {
|
|
509
|
+
const detected = detectServiceAndRegion(hostname);
|
|
510
|
+
if (region == null) region = detected.region;
|
|
511
|
+
if (service == null) service = detected.service;
|
|
512
|
+
}
|
|
513
|
+
if (!region) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
`Cannot determine AWS region from URL '${opts.url}'. Pass region explicitly via additional_credentials.`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
if (!service) {
|
|
519
|
+
throw new Error(
|
|
520
|
+
`Cannot determine AWS service from URL '${opts.url}'. Pass service explicitly via additional_credentials.`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
const timestamp = opts.timestamp ?? /* @__PURE__ */ new Date();
|
|
524
|
+
const amzDate = timestamp.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
525
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
526
|
+
const bodyBytes = opts.body != null ? typeof opts.body === "string" ? Buffer.from(opts.body, "utf8") : opts.body : Buffer.alloc(0);
|
|
527
|
+
const payloadHash = sha256Hex(bodyBytes);
|
|
528
|
+
const headersToSign = {};
|
|
529
|
+
const hasHost = Object.keys(opts.headers).some(
|
|
530
|
+
(k) => k.toLowerCase() === "host"
|
|
531
|
+
);
|
|
532
|
+
if (!hasHost) {
|
|
533
|
+
const port = parsed.port;
|
|
534
|
+
headersToSign["host"] = port && port !== "80" && port !== "443" ? `${hostname}:${port}` : hostname;
|
|
535
|
+
} else {
|
|
536
|
+
for (const [k, v] of Object.entries(opts.headers)) {
|
|
537
|
+
if (k.toLowerCase() === "host") {
|
|
538
|
+
headersToSign["host"] = v;
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
headersToSign["x-amz-date"] = amzDate;
|
|
544
|
+
headersToSign["x-amz-content-sha256"] = payloadHash;
|
|
545
|
+
const canonicalUriStr = canonicalUri(parsed.pathname);
|
|
546
|
+
const canonicalQs = canonicalQueryString(parsed.search.replace(/^\?/, ""));
|
|
547
|
+
const { canonicalHeaders, signedHeaders } = canonicalHeadersAndSigned(headersToSign);
|
|
548
|
+
const canonicalRequest = [
|
|
549
|
+
opts.method.toUpperCase(),
|
|
550
|
+
canonicalUriStr,
|
|
551
|
+
canonicalQs,
|
|
552
|
+
canonicalHeaders,
|
|
553
|
+
signedHeaders,
|
|
554
|
+
payloadHash
|
|
555
|
+
].join("\n");
|
|
556
|
+
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
557
|
+
const stringToSign = [
|
|
558
|
+
ALGORITHM,
|
|
559
|
+
amzDate,
|
|
560
|
+
credentialScope,
|
|
561
|
+
sha256Hex(canonicalRequest)
|
|
562
|
+
].join("\n");
|
|
563
|
+
const signingKey = deriveSigningKey(opts.secretKey, dateStamp, region, service);
|
|
564
|
+
const signature = createHmac("sha256", signingKey).update(stringToSign, "utf8").digest("hex");
|
|
565
|
+
const authorization = `${ALGORITHM} Credential=${opts.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
566
|
+
return {
|
|
567
|
+
Authorization: authorization,
|
|
568
|
+
"x-amz-date": amzDate,
|
|
569
|
+
"x-amz-content-sha256": payloadHash
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
322
573
|
// src/client.ts
|
|
323
574
|
var _tokenStore = /* @__PURE__ */ new WeakMap();
|
|
575
|
+
var _additionalCredsStore = /* @__PURE__ */ new WeakMap();
|
|
324
576
|
function _storeAccessToken(token, accessToken) {
|
|
325
577
|
_tokenStore.set(token, accessToken);
|
|
326
578
|
}
|
|
579
|
+
function _storeAdditionalCredentials(token, creds) {
|
|
580
|
+
_additionalCredsStore.set(token, creds);
|
|
581
|
+
}
|
|
327
582
|
function _extractAccessToken(token) {
|
|
328
583
|
const value = _tokenStore.get(token);
|
|
329
584
|
if (value === void 0) {
|
|
@@ -333,11 +588,15 @@ function _extractAccessToken(token) {
|
|
|
333
588
|
}
|
|
334
589
|
return value;
|
|
335
590
|
}
|
|
591
|
+
function _extractAdditionalCredentials(token) {
|
|
592
|
+
return _additionalCredsStore.get(token);
|
|
593
|
+
}
|
|
336
594
|
var _fetch;
|
|
337
|
-
var SDK_VERSION = "0.
|
|
595
|
+
var SDK_VERSION = "0.5.0";
|
|
338
596
|
var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
|
|
339
597
|
var HTTP_FORBIDDEN = 403;
|
|
340
598
|
var HTTP_NOT_FOUND = 404;
|
|
599
|
+
var HTTP_GONE = 410;
|
|
341
600
|
var HTTP_BAD_REQUEST = 400;
|
|
342
601
|
var HTTP_UNAUTHORIZED = 401;
|
|
343
602
|
var HTTP_BAD_GATEWAY = 502;
|
|
@@ -391,7 +650,12 @@ var HttpClient = class {
|
|
|
391
650
|
headers: mergedHeaders,
|
|
392
651
|
signal: controller.signal
|
|
393
652
|
};
|
|
394
|
-
if (options?.
|
|
653
|
+
if (options?.body !== void 0) {
|
|
654
|
+
init.body = options.body;
|
|
655
|
+
if (!mergedHeaders["Content-Type"]) {
|
|
656
|
+
mergedHeaders["Content-Type"] = "application/json";
|
|
657
|
+
}
|
|
658
|
+
} else if (options?.json !== void 0) {
|
|
395
659
|
init.body = JSON.stringify(options.json);
|
|
396
660
|
mergedHeaders["Content-Type"] = "application/json";
|
|
397
661
|
}
|
|
@@ -427,6 +691,8 @@ var AlterVault = class _AlterVault {
|
|
|
427
691
|
#closed = false;
|
|
428
692
|
/** Pending audit log promises (fire-and-forget) */
|
|
429
693
|
#auditPromises = /* @__PURE__ */ new Set();
|
|
694
|
+
/** JWT identity resolution: callable that returns user token */
|
|
695
|
+
#userTokenGetter = null;
|
|
430
696
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
431
697
|
// Public readonly properties (frozen by Object.freeze)
|
|
432
698
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
@@ -454,7 +720,7 @@ var AlterVault = class _AlterVault {
|
|
|
454
720
|
for (const [name, value] of actorStrings) {
|
|
455
721
|
_AlterVault.#validateActorString(value, name);
|
|
456
722
|
}
|
|
457
|
-
this.#hmacKey =
|
|
723
|
+
this.#hmacKey = createHmac2("sha256", options.apiKey).update("alter-signing-v1").digest();
|
|
458
724
|
this.baseUrl = (process.env.ALTER_BASE_URL ?? "https://backend.alterai.dev").replace(/\/+$/, "");
|
|
459
725
|
const timeoutMs = options.timeout ?? 3e4;
|
|
460
726
|
this.#actorType = options.actorType;
|
|
@@ -462,6 +728,7 @@ var AlterVault = class _AlterVault {
|
|
|
462
728
|
this.#actorName = options.actorName;
|
|
463
729
|
this.#actorVersion = options.actorVersion;
|
|
464
730
|
this.#clientType = options.clientType;
|
|
731
|
+
this.#userTokenGetter = options.userTokenGetter ?? null;
|
|
465
732
|
this.#framework = options.framework;
|
|
466
733
|
if (!_fetch) {
|
|
467
734
|
_fetch = globalThis.fetch;
|
|
@@ -558,12 +825,12 @@ var AlterVault = class _AlterVault {
|
|
|
558
825
|
*/
|
|
559
826
|
#computeHmacHeaders(method, path, body) {
|
|
560
827
|
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
561
|
-
const contentHash =
|
|
828
|
+
const contentHash = createHash2("sha256").update(body ?? "").digest("hex");
|
|
562
829
|
const stringToSign = `${method.toUpperCase()}
|
|
563
830
|
${path}
|
|
564
831
|
${timestamp}
|
|
565
832
|
${contentHash}`;
|
|
566
|
-
const signature =
|
|
833
|
+
const signature = createHmac2("sha256", this.#hmacKey).update(stringToSign).digest("hex");
|
|
567
834
|
return {
|
|
568
835
|
"X-Alter-Timestamp": timestamp,
|
|
569
836
|
"X-Alter-Content-SHA256": contentHash,
|
|
@@ -636,6 +903,24 @@ ${contentHash}`;
|
|
|
636
903
|
}
|
|
637
904
|
return false;
|
|
638
905
|
}
|
|
906
|
+
/**
|
|
907
|
+
* Resolve a value_source to the actual value for injection.
|
|
908
|
+
*
|
|
909
|
+
* @param valueSource - "token" or "additional_credentials.<field>"
|
|
910
|
+
* @param accessToken - The main credential value
|
|
911
|
+
* @param additionalCreds - Additional credentials from vault
|
|
912
|
+
* @returns The resolved value, or null if not available
|
|
913
|
+
*/
|
|
914
|
+
static #resolveInjectionValue(valueSource, accessToken, additionalCreds) {
|
|
915
|
+
if (valueSource === "token") {
|
|
916
|
+
return accessToken;
|
|
917
|
+
}
|
|
918
|
+
if (valueSource.startsWith("additional_credentials.")) {
|
|
919
|
+
const field = valueSource.slice("additional_credentials.".length);
|
|
920
|
+
return additionalCreds?.[field] ?? null;
|
|
921
|
+
}
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
639
924
|
static async #safeParseJson(response) {
|
|
640
925
|
try {
|
|
641
926
|
return await response.clone().json();
|
|
@@ -657,12 +942,25 @@ ${contentHash}`;
|
|
|
657
942
|
}
|
|
658
943
|
if (response.status === HTTP_FORBIDDEN) {
|
|
659
944
|
const errorData = await _AlterVault.#safeParseJson(response);
|
|
945
|
+
if (errorData.error === "connection_expired") {
|
|
946
|
+
throw new ConnectionExpiredError(
|
|
947
|
+
errorData.message ?? "Connection expired per TTL policy",
|
|
948
|
+
errorData.details
|
|
949
|
+
);
|
|
950
|
+
}
|
|
660
951
|
throw new PolicyViolationError(
|
|
661
952
|
errorData.message ?? "Access denied by policy",
|
|
662
953
|
errorData.error,
|
|
663
954
|
errorData.details
|
|
664
955
|
);
|
|
665
956
|
}
|
|
957
|
+
if (response.status === HTTP_GONE) {
|
|
958
|
+
const errorData = await _AlterVault.#safeParseJson(response);
|
|
959
|
+
throw new ConnectionDeletedError(
|
|
960
|
+
errorData.message ?? "Connection has been deleted. A new connection_id will be issued on re-authorization.",
|
|
961
|
+
errorData
|
|
962
|
+
);
|
|
963
|
+
}
|
|
666
964
|
if (response.status === HTTP_NOT_FOUND) {
|
|
667
965
|
const errorData = await _AlterVault.#safeParseJson(response);
|
|
668
966
|
throw new ConnectionNotFoundError(
|
|
@@ -672,35 +970,35 @@ ${contentHash}`;
|
|
|
672
970
|
}
|
|
673
971
|
if (response.status === HTTP_BAD_REQUEST || response.status === HTTP_BAD_GATEWAY) {
|
|
674
972
|
const errorData = await _AlterVault.#safeParseJson(response);
|
|
675
|
-
if (
|
|
676
|
-
throw new
|
|
677
|
-
errorData.message ?? "
|
|
973
|
+
if (errorData.error === "connection_revoked") {
|
|
974
|
+
throw new ConnectionRevokedError(
|
|
975
|
+
errorData.message ?? "Connection has been revoked. User must re-authorize.",
|
|
678
976
|
errorData.connection_id,
|
|
679
977
|
errorData
|
|
680
978
|
);
|
|
681
979
|
}
|
|
682
|
-
throw new
|
|
980
|
+
throw new BackendError(
|
|
683
981
|
errorData.message ?? `Backend error ${response.status}`,
|
|
684
982
|
errorData
|
|
685
983
|
);
|
|
686
984
|
}
|
|
687
985
|
if (response.status === HTTP_UNAUTHORIZED) {
|
|
688
986
|
const errorData = await _AlterVault.#safeParseJson(response);
|
|
689
|
-
throw new
|
|
987
|
+
throw new BackendError(
|
|
690
988
|
errorData.message ?? "Unauthorized \u2014 check your API key",
|
|
691
989
|
errorData
|
|
692
990
|
);
|
|
693
991
|
}
|
|
694
992
|
if (response.status === HTTP_INTERNAL_SERVER_ERROR || response.status === HTTP_SERVICE_UNAVAILABLE) {
|
|
695
993
|
const errorData = await _AlterVault.#safeParseJson(response);
|
|
696
|
-
throw new
|
|
994
|
+
throw new BackendError(
|
|
697
995
|
errorData.message ?? `Backend unavailable (HTTP ${response.status})`,
|
|
698
996
|
errorData
|
|
699
997
|
);
|
|
700
998
|
}
|
|
701
999
|
if (response.status >= HTTP_CLIENT_ERROR_START) {
|
|
702
1000
|
const errorData = await _AlterVault.#safeParseJson(response);
|
|
703
|
-
throw new
|
|
1001
|
+
throw new BackendError(
|
|
704
1002
|
errorData.message ?? `Unexpected backend error (HTTP ${response.status})`,
|
|
705
1003
|
errorData
|
|
706
1004
|
);
|
|
@@ -712,24 +1010,52 @@ ${contentHash}`;
|
|
|
712
1010
|
* This is a private method. Tokens are NEVER exposed to developers.
|
|
713
1011
|
* Use request() instead, which handles tokens internally.
|
|
714
1012
|
*/
|
|
715
|
-
async #
|
|
1013
|
+
async #getUserToken() {
|
|
1014
|
+
if (!this.#userTokenGetter) {
|
|
1015
|
+
throw new AlterSDKError(
|
|
1016
|
+
"userTokenGetter is required for provider-based resolution. Pass userTokenGetter to AlterVault constructor."
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
const result = this.#userTokenGetter();
|
|
1020
|
+
const token = result instanceof Promise ? await result : result;
|
|
1021
|
+
if (!token || typeof token !== "string") {
|
|
1022
|
+
throw new AlterSDKError("userTokenGetter must return a non-empty string");
|
|
1023
|
+
}
|
|
1024
|
+
return token;
|
|
1025
|
+
}
|
|
1026
|
+
async #getToken(connectionId, reason, requestMetadata, runId, threadId, toolCallId, provider, account) {
|
|
716
1027
|
const actorHeaders = this.#getActorRequestHeaders(
|
|
717
1028
|
runId,
|
|
718
1029
|
threadId,
|
|
719
1030
|
toolCallId
|
|
720
1031
|
);
|
|
721
1032
|
let response;
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1033
|
+
let tokenBody;
|
|
1034
|
+
if (provider) {
|
|
1035
|
+
const userToken = await this.#getUserToken();
|
|
1036
|
+
tokenBody = {
|
|
1037
|
+
provider_id: provider,
|
|
1038
|
+
user_token: userToken,
|
|
1039
|
+
reason: reason ?? null,
|
|
1040
|
+
request: requestMetadata ?? null
|
|
1041
|
+
};
|
|
1042
|
+
if (account) {
|
|
1043
|
+
tokenBody.account = account;
|
|
1044
|
+
}
|
|
1045
|
+
} else {
|
|
1046
|
+
tokenBody = {
|
|
1047
|
+
connection_id: connectionId,
|
|
1048
|
+
reason: reason ?? null,
|
|
1049
|
+
request: requestMetadata ?? null
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
727
1052
|
const tokenPath = "/sdk/token";
|
|
728
|
-
const
|
|
1053
|
+
const tokenBodyStr = JSON.stringify(tokenBody);
|
|
1054
|
+
const hmacHeaders = this.#computeHmacHeaders("POST", tokenPath, tokenBodyStr);
|
|
729
1055
|
try {
|
|
730
1056
|
response = await this.#alterClient.post(tokenPath, {
|
|
731
|
-
|
|
732
|
-
headers: { ...actorHeaders, ...hmacHeaders }
|
|
1057
|
+
body: tokenBodyStr,
|
|
1058
|
+
headers: { ...actorHeaders, ...hmacHeaders, "Content-Type": "application/json" }
|
|
733
1059
|
});
|
|
734
1060
|
} catch (error) {
|
|
735
1061
|
if (_AlterVault.#isTimeoutOrAbortError(error)) {
|
|
@@ -744,7 +1070,7 @@ ${contentHash}`;
|
|
|
744
1070
|
{ base_url: this.baseUrl }
|
|
745
1071
|
);
|
|
746
1072
|
}
|
|
747
|
-
throw new
|
|
1073
|
+
throw new BackendError(
|
|
748
1074
|
`Failed to retrieve token: ${error instanceof Error ? error.message : String(error)}`,
|
|
749
1075
|
{ connection_id: connectionId, error: String(error) }
|
|
750
1076
|
);
|
|
@@ -753,21 +1079,25 @@ ${contentHash}`;
|
|
|
753
1079
|
await this.#handleErrorResponse(response);
|
|
754
1080
|
const tokenData = await response.json();
|
|
755
1081
|
const typedData = tokenData;
|
|
1082
|
+
const scopeMismatch = typedData.scope_mismatch ?? false;
|
|
756
1083
|
const tokenResponse = new TokenResponse(typedData);
|
|
757
1084
|
if (!/^[A-Za-z][A-Za-z0-9-]*$/.test(tokenResponse.injectionHeader)) {
|
|
758
|
-
throw new
|
|
1085
|
+
throw new BackendError(
|
|
759
1086
|
`Backend returned invalid injection_header: ${tokenResponse.injectionHeader}`,
|
|
760
1087
|
{ connectionId: String(connectionId) }
|
|
761
1088
|
);
|
|
762
1089
|
}
|
|
763
1090
|
if (/[\r\n\x00]/.test(tokenResponse.injectionFormat)) {
|
|
764
|
-
throw new
|
|
1091
|
+
throw new BackendError(
|
|
765
1092
|
`Backend returned invalid injection_format (contains control characters)`,
|
|
766
1093
|
{ connectionId: String(connectionId) }
|
|
767
1094
|
);
|
|
768
1095
|
}
|
|
769
1096
|
_storeAccessToken(tokenResponse, typedData.access_token);
|
|
770
|
-
|
|
1097
|
+
if (typedData.additional_credentials) {
|
|
1098
|
+
_storeAdditionalCredentials(tokenResponse, typedData.additional_credentials);
|
|
1099
|
+
}
|
|
1100
|
+
return { tokenResponse, scopeMismatch };
|
|
771
1101
|
}
|
|
772
1102
|
/**
|
|
773
1103
|
* Log an API call to the backend audit endpoint (INTERNAL).
|
|
@@ -797,10 +1127,11 @@ ${contentHash}`;
|
|
|
797
1127
|
const actorHeaders = this.#getActorRequestHeaders(params.runId);
|
|
798
1128
|
const auditPath = "/sdk/oauth/audit/api-call";
|
|
799
1129
|
const auditBody = sanitized;
|
|
800
|
-
const
|
|
1130
|
+
const auditBodyStr = JSON.stringify(auditBody);
|
|
1131
|
+
const auditHmac = this.#computeHmacHeaders("POST", auditPath, auditBodyStr);
|
|
801
1132
|
const response = await this.#alterClient.post(auditPath, {
|
|
802
|
-
|
|
803
|
-
headers: { ...actorHeaders, ...auditHmac }
|
|
1133
|
+
body: auditBodyStr,
|
|
1134
|
+
headers: { ...actorHeaders, ...auditHmac, "Content-Type": "application/json" }
|
|
804
1135
|
});
|
|
805
1136
|
this.#cacheActorIdFromResponse(response);
|
|
806
1137
|
if (!response.ok) {
|
|
@@ -872,12 +1203,22 @@ ${contentHash}`;
|
|
|
872
1203
|
"SDK instance has been closed. Create a new AlterVault instance to make requests."
|
|
873
1204
|
);
|
|
874
1205
|
}
|
|
1206
|
+
const provider = options?.provider;
|
|
1207
|
+
const account = options?.account;
|
|
1208
|
+
if (!connectionId && !provider) {
|
|
1209
|
+
throw new AlterSDKError("Provide connectionId or options.provider");
|
|
1210
|
+
}
|
|
1211
|
+
if (connectionId && provider) {
|
|
1212
|
+
throw new AlterSDKError("Cannot provide both connectionId and options.provider");
|
|
1213
|
+
}
|
|
1214
|
+
const effectiveConnectionId = connectionId ?? null;
|
|
1215
|
+
let currentUrl = url;
|
|
875
1216
|
const runId = options?.runId ?? randomUUID();
|
|
876
1217
|
const methodStr = String(method).toUpperCase();
|
|
877
|
-
const urlLower =
|
|
1218
|
+
const urlLower = currentUrl.toLowerCase();
|
|
878
1219
|
if (!ALLOWED_URL_SCHEMES.some((scheme) => urlLower.startsWith(scheme))) {
|
|
879
1220
|
throw new AlterSDKError(
|
|
880
|
-
`URL must start with https:// or http://, got: ${
|
|
1221
|
+
`URL must start with https:// or http://, got: ${currentUrl.slice(0, 50)}`
|
|
881
1222
|
);
|
|
882
1223
|
}
|
|
883
1224
|
if (options?.pathParams && Object.keys(options.pathParams).length > 0) {
|
|
@@ -886,7 +1227,7 @@ ${contentHash}`;
|
|
|
886
1227
|
encodedParams[key] = encodeURIComponent(String(value));
|
|
887
1228
|
}
|
|
888
1229
|
try {
|
|
889
|
-
let resolvedUrl =
|
|
1230
|
+
let resolvedUrl = currentUrl;
|
|
890
1231
|
for (const [key, value] of Object.entries(encodedParams)) {
|
|
891
1232
|
const placeholder = `{${key}}`;
|
|
892
1233
|
if (!resolvedUrl.includes(placeholder)) {
|
|
@@ -897,74 +1238,142 @@ ${contentHash}`;
|
|
|
897
1238
|
const remaining = resolvedUrl.match(/\{(\w+)\}/);
|
|
898
1239
|
if (remaining) {
|
|
899
1240
|
throw new AlterSDKError(
|
|
900
|
-
`Invalid URL template or missing path_params: '${remaining[1]}'. URL: ${
|
|
1241
|
+
`Invalid URL template or missing path_params: '${remaining[1]}'. URL: ${currentUrl}, path_params: ${JSON.stringify(options.pathParams)}`
|
|
901
1242
|
);
|
|
902
1243
|
}
|
|
903
|
-
|
|
1244
|
+
currentUrl = resolvedUrl;
|
|
904
1245
|
} catch (error) {
|
|
905
1246
|
if (error instanceof AlterSDKError) {
|
|
906
1247
|
throw error;
|
|
907
1248
|
}
|
|
908
1249
|
throw new AlterSDKError(
|
|
909
|
-
`Invalid URL template or missing path_params: ${error instanceof Error ? error.message : String(error)}. URL: ${
|
|
1250
|
+
`Invalid URL template or missing path_params: ${error instanceof Error ? error.message : String(error)}. URL: ${currentUrl}, path_params: ${JSON.stringify(options.pathParams)}`
|
|
910
1251
|
);
|
|
911
1252
|
}
|
|
912
1253
|
}
|
|
913
|
-
const tokenResponse = await this.#getToken(
|
|
914
|
-
|
|
1254
|
+
const { tokenResponse, scopeMismatch } = await this.#getToken(
|
|
1255
|
+
effectiveConnectionId,
|
|
915
1256
|
options?.reason,
|
|
916
|
-
{ method: methodStr, url },
|
|
1257
|
+
{ method: methodStr, url: currentUrl },
|
|
917
1258
|
runId,
|
|
918
1259
|
options?.threadId,
|
|
919
|
-
options?.toolCallId
|
|
1260
|
+
options?.toolCallId,
|
|
1261
|
+
provider,
|
|
1262
|
+
account
|
|
920
1263
|
);
|
|
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
1264
|
const requestHeaders = options?.extraHeaders ? { ...options.extraHeaders } : {};
|
|
930
1265
|
const accessToken = _extractAccessToken(tokenResponse);
|
|
931
|
-
|
|
1266
|
+
const injectionHeaderLower = tokenResponse.injectionHeader.toLowerCase();
|
|
1267
|
+
const additionalCreds = _extractAdditionalCredentials(tokenResponse);
|
|
1268
|
+
const isSigV4 = tokenResponse.injectionFormat.startsWith("AWS4-HMAC-SHA256") && additionalCreds != null;
|
|
1269
|
+
let sigv4BodyStr = null;
|
|
1270
|
+
if (isSigV4) {
|
|
1271
|
+
const accessKeyId = accessToken;
|
|
1272
|
+
if (options?.json != null) {
|
|
1273
|
+
sigv4BodyStr = JSON.stringify(options.json);
|
|
1274
|
+
if (!requestHeaders["Content-Type"]) {
|
|
1275
|
+
requestHeaders["Content-Type"] = "application/json";
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (!additionalCreds.secret_key) {
|
|
1279
|
+
throw new BackendError(
|
|
1280
|
+
"AWS SigV4 credential is missing secret_key in additional_credentials. Re-store the credential with both Access Key ID and Secret Access Key.",
|
|
1281
|
+
{ connection_id: effectiveConnectionId }
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
const awsHeaders = signAwsRequest({
|
|
1285
|
+
method: methodStr,
|
|
1286
|
+
url: currentUrl,
|
|
1287
|
+
headers: requestHeaders,
|
|
1288
|
+
body: sigv4BodyStr,
|
|
1289
|
+
accessKeyId,
|
|
1290
|
+
secretKey: additionalCreds.secret_key,
|
|
1291
|
+
region: additionalCreds.region ?? null,
|
|
1292
|
+
service: additionalCreds.service ?? null
|
|
1293
|
+
});
|
|
1294
|
+
Object.assign(requestHeaders, awsHeaders);
|
|
1295
|
+
} else {
|
|
1296
|
+
if (options?.extraHeaders && Object.keys(options.extraHeaders).some(
|
|
1297
|
+
(k) => k.toLowerCase() === injectionHeaderLower
|
|
1298
|
+
)) {
|
|
1299
|
+
console.warn(
|
|
1300
|
+
`extraHeaders contains '${tokenResponse.injectionHeader}' which will be overwritten with the auto-injected credential`
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
requestHeaders[tokenResponse.injectionHeader] = tokenResponse.injectionFormat.replace("{token}", accessToken);
|
|
1304
|
+
}
|
|
1305
|
+
const auditUrl = currentUrl;
|
|
1306
|
+
if (tokenResponse.additionalInjections && !isSigV4) {
|
|
1307
|
+
for (const rule of tokenResponse.additionalInjections) {
|
|
1308
|
+
const value = _AlterVault.#resolveInjectionValue(
|
|
1309
|
+
rule.value_source,
|
|
1310
|
+
accessToken,
|
|
1311
|
+
additionalCreds
|
|
1312
|
+
);
|
|
1313
|
+
if (!value) continue;
|
|
1314
|
+
if (!/^[A-Za-z][A-Za-z0-9\-_]*$/.test(rule.key)) continue;
|
|
1315
|
+
if (rule.target === "header") {
|
|
1316
|
+
requestHeaders[rule.key] = value;
|
|
1317
|
+
} else if (rule.target === "query_param") {
|
|
1318
|
+
const urlObj = new URL(currentUrl);
|
|
1319
|
+
urlObj.searchParams.set(rule.key, value);
|
|
1320
|
+
currentUrl = urlObj.toString();
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
932
1324
|
if (!requestHeaders["User-Agent"]) {
|
|
933
1325
|
requestHeaders["User-Agent"] = SDK_USER_AGENT;
|
|
934
1326
|
}
|
|
935
1327
|
const startTime = Date.now();
|
|
936
1328
|
let response;
|
|
937
1329
|
try {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1330
|
+
if (isSigV4 && sigv4BodyStr != null) {
|
|
1331
|
+
response = await this.#providerClient.request(methodStr, currentUrl, {
|
|
1332
|
+
body: sigv4BodyStr,
|
|
1333
|
+
headers: requestHeaders,
|
|
1334
|
+
params: options?.queryParams
|
|
1335
|
+
});
|
|
1336
|
+
} else {
|
|
1337
|
+
response = await this.#providerClient.request(methodStr, currentUrl, {
|
|
1338
|
+
json: options?.json,
|
|
1339
|
+
headers: requestHeaders,
|
|
1340
|
+
params: options?.queryParams
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
943
1343
|
} catch (error) {
|
|
944
1344
|
if (_AlterVault.#isTimeoutOrAbortError(error)) {
|
|
945
1345
|
throw new TimeoutError(
|
|
946
1346
|
`Provider API request timed out: ${error instanceof Error ? error.message : String(error)}`,
|
|
947
1347
|
{
|
|
948
|
-
connection_id:
|
|
1348
|
+
connection_id: effectiveConnectionId,
|
|
949
1349
|
method: methodStr,
|
|
950
|
-
url
|
|
1350
|
+
url: currentUrl
|
|
951
1351
|
}
|
|
952
1352
|
);
|
|
953
1353
|
}
|
|
954
1354
|
throw new NetworkError(
|
|
955
1355
|
`Failed to call provider API: ${error instanceof Error ? error.message : String(error)}`,
|
|
956
1356
|
{
|
|
957
|
-
connection_id:
|
|
1357
|
+
connection_id: effectiveConnectionId,
|
|
958
1358
|
method: methodStr,
|
|
959
|
-
url,
|
|
1359
|
+
url: currentUrl,
|
|
960
1360
|
error: String(error)
|
|
961
1361
|
}
|
|
962
1362
|
);
|
|
963
1363
|
}
|
|
964
1364
|
const latencyMs = Date.now() - startTime;
|
|
1365
|
+
const sigv4Sensitive = /* @__PURE__ */ new Set(["authorization", "x-amz-date", "x-amz-content-sha256"]);
|
|
1366
|
+
const stripHeaders = isSigV4 ? sigv4Sensitive : /* @__PURE__ */ new Set([injectionHeaderLower]);
|
|
1367
|
+
if (tokenResponse.additionalInjections && !isSigV4) {
|
|
1368
|
+
for (const rule of tokenResponse.additionalInjections) {
|
|
1369
|
+
if (rule.target === "header") {
|
|
1370
|
+
stripHeaders.add(rule.key.toLowerCase());
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
965
1374
|
const auditHeaders = {};
|
|
966
1375
|
for (const [key, value] of Object.entries(requestHeaders)) {
|
|
967
|
-
if (key.toLowerCase()
|
|
1376
|
+
if (!stripHeaders.has(key.toLowerCase())) {
|
|
968
1377
|
auditHeaders[key] = value;
|
|
969
1378
|
}
|
|
970
1379
|
}
|
|
@@ -975,9 +1384,9 @@ ${contentHash}`;
|
|
|
975
1384
|
});
|
|
976
1385
|
this.#scheduleAuditLog({
|
|
977
1386
|
connectionId: tokenResponse.connectionId,
|
|
978
|
-
providerId: tokenResponse.providerId ||
|
|
1387
|
+
providerId: tokenResponse.providerId || effectiveConnectionId || "",
|
|
979
1388
|
method: methodStr,
|
|
980
|
-
url,
|
|
1389
|
+
url: auditUrl,
|
|
981
1390
|
requestHeaders: auditHeaders,
|
|
982
1391
|
requestBody: options?.json ?? null,
|
|
983
1392
|
responseStatus: response.status,
|
|
@@ -990,14 +1399,29 @@ ${contentHash}`;
|
|
|
990
1399
|
toolCallId: options?.toolCallId ?? null
|
|
991
1400
|
});
|
|
992
1401
|
if (response.status >= HTTP_CLIENT_ERROR_START) {
|
|
1402
|
+
if (response.status === HTTP_FORBIDDEN && scopeMismatch) {
|
|
1403
|
+
throw new ScopeReauthRequiredError(
|
|
1404
|
+
"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.",
|
|
1405
|
+
tokenResponse.connectionId,
|
|
1406
|
+
tokenResponse.providerId,
|
|
1407
|
+
response.status,
|
|
1408
|
+
responseBody,
|
|
1409
|
+
{
|
|
1410
|
+
connection_id: tokenResponse.connectionId,
|
|
1411
|
+
provider_id: tokenResponse.providerId,
|
|
1412
|
+
method: methodStr,
|
|
1413
|
+
url: currentUrl
|
|
1414
|
+
}
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
993
1417
|
throw new ProviderAPIError(
|
|
994
1418
|
`Provider API returned error ${response.status}`,
|
|
995
1419
|
response.status,
|
|
996
1420
|
responseBody,
|
|
997
1421
|
{
|
|
998
|
-
connection_id:
|
|
1422
|
+
connection_id: effectiveConnectionId,
|
|
999
1423
|
method: methodStr,
|
|
1000
|
-
url
|
|
1424
|
+
url: currentUrl
|
|
1001
1425
|
}
|
|
1002
1426
|
);
|
|
1003
1427
|
}
|
|
@@ -1022,12 +1446,23 @@ ${contentHash}`;
|
|
|
1022
1446
|
limit: options?.limit ?? 100,
|
|
1023
1447
|
offset: options?.offset ?? 0
|
|
1024
1448
|
};
|
|
1449
|
+
if (this.#userTokenGetter) {
|
|
1450
|
+
try {
|
|
1451
|
+
listBody.user_token = await this.#getUserToken();
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
console.warn(
|
|
1454
|
+
"user_token_getter failed in listConnections, falling back to un-scoped listing:",
|
|
1455
|
+
err instanceof Error ? err.message : String(err)
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1025
1459
|
const listPath = "/sdk/oauth/connections/list";
|
|
1026
|
-
const
|
|
1460
|
+
const listBodyStr = JSON.stringify(listBody);
|
|
1461
|
+
const listHmac = this.#computeHmacHeaders("POST", listPath, listBodyStr);
|
|
1027
1462
|
try {
|
|
1028
1463
|
response = await this.#alterClient.post(listPath, {
|
|
1029
|
-
|
|
1030
|
-
headers: { ...actorHeaders, ...listHmac }
|
|
1464
|
+
body: listBodyStr,
|
|
1465
|
+
headers: { ...actorHeaders, ...listHmac, "Content-Type": "application/json" }
|
|
1031
1466
|
});
|
|
1032
1467
|
} catch (error) {
|
|
1033
1468
|
if (_AlterVault.#isTimeoutOrAbortError(error)) {
|
|
@@ -1073,24 +1508,37 @@ ${contentHash}`;
|
|
|
1073
1508
|
"SDK instance has been closed. Create a new AlterVault instance to make requests."
|
|
1074
1509
|
);
|
|
1075
1510
|
}
|
|
1076
|
-
if (!options.endUser?.id) {
|
|
1077
|
-
throw new AlterSDKError("endUser.id is required");
|
|
1078
|
-
}
|
|
1079
1511
|
const actorHeaders = this.#getActorRequestHeaders();
|
|
1080
1512
|
let response;
|
|
1081
1513
|
const sessionBody = {
|
|
1082
|
-
end_user: options.endUser,
|
|
1083
1514
|
allowed_providers: options.allowedProviders ?? null,
|
|
1084
1515
|
return_url: options.returnUrl ?? null,
|
|
1085
1516
|
allowed_origin: options.allowedOrigin ?? null,
|
|
1086
1517
|
metadata: options.metadata ?? null
|
|
1087
1518
|
};
|
|
1519
|
+
if (options.connectionPolicy) {
|
|
1520
|
+
sessionBody.connection_policy = {
|
|
1521
|
+
max_ttl_seconds: options.connectionPolicy.maxTtlSeconds ?? null,
|
|
1522
|
+
default_ttl_seconds: options.connectionPolicy.defaultTtlSeconds ?? null
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
if (this.#userTokenGetter) {
|
|
1526
|
+
try {
|
|
1527
|
+
sessionBody.user_token = await this.#getUserToken();
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
console.warn(
|
|
1530
|
+
"userTokenGetter failed in createConnectSession, session will not have identity tagging:",
|
|
1531
|
+
err instanceof Error ? err.message : String(err)
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1088
1535
|
const sessionPath = "/sdk/oauth/connect/session";
|
|
1089
|
-
const
|
|
1536
|
+
const sessionBodyStr = JSON.stringify(sessionBody);
|
|
1537
|
+
const sessionHmac = this.#computeHmacHeaders("POST", sessionPath, sessionBodyStr);
|
|
1090
1538
|
try {
|
|
1091
1539
|
response = await this.#alterClient.post(sessionPath, {
|
|
1092
|
-
|
|
1093
|
-
headers: { ...actorHeaders, ...sessionHmac }
|
|
1540
|
+
body: sessionBodyStr,
|
|
1541
|
+
headers: { ...actorHeaders, ...sessionHmac, "Content-Type": "application/json" }
|
|
1094
1542
|
});
|
|
1095
1543
|
} catch (error) {
|
|
1096
1544
|
if (_AlterVault.#isTimeoutOrAbortError(error)) {
|
|
@@ -1114,6 +1562,143 @@ ${contentHash}`;
|
|
|
1114
1562
|
const data = await response.json();
|
|
1115
1563
|
return new ConnectSession(data);
|
|
1116
1564
|
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Open OAuth in the user's browser and wait for completion.
|
|
1567
|
+
*
|
|
1568
|
+
* This is the headless connect flow for CLI tools, scripts, and
|
|
1569
|
+
* server-side applications. It creates a Connect session, opens the
|
|
1570
|
+
* browser, and polls until the user completes OAuth.
|
|
1571
|
+
*
|
|
1572
|
+
* @param options - Connect options
|
|
1573
|
+
* @returns Array of ConnectResult objects (one per connected provider)
|
|
1574
|
+
* @throws ConnectTimeoutError if the user doesn't complete within timeout
|
|
1575
|
+
* @throws ConnectFlowError if the user denies or provider returns error
|
|
1576
|
+
* @throws AlterSDKError if SDK is closed or session creation fails
|
|
1577
|
+
*/
|
|
1578
|
+
async connect(options) {
|
|
1579
|
+
if (this.#closed) {
|
|
1580
|
+
throw new AlterSDKError(
|
|
1581
|
+
"SDK instance has been closed. Create a new AlterVault instance to make requests."
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
const timeout = options.timeout ?? 300;
|
|
1585
|
+
const pollInterval = options.pollInterval ?? 2;
|
|
1586
|
+
const openBrowser = options.openBrowser ?? true;
|
|
1587
|
+
const session = await this.createConnectSession({
|
|
1588
|
+
allowedProviders: options.providers,
|
|
1589
|
+
connectionPolicy: options.connectionPolicy
|
|
1590
|
+
});
|
|
1591
|
+
if (openBrowser) {
|
|
1592
|
+
try {
|
|
1593
|
+
const openModule = await import("open");
|
|
1594
|
+
const openFn = openModule.default;
|
|
1595
|
+
if (openFn) {
|
|
1596
|
+
await openFn(session.connectUrl);
|
|
1597
|
+
} else {
|
|
1598
|
+
console.log(
|
|
1599
|
+
`Open this URL to authorize: ${session.connectUrl}`
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
} catch {
|
|
1603
|
+
console.log(
|
|
1604
|
+
`Open this URL to authorize: ${session.connectUrl}`
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
} else {
|
|
1608
|
+
console.log(
|
|
1609
|
+
`Open this URL to authorize: ${session.connectUrl}`
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
const deadline = Date.now() + timeout * 1e3;
|
|
1613
|
+
while (Date.now() < deadline) {
|
|
1614
|
+
await new Promise(
|
|
1615
|
+
(resolve) => setTimeout(resolve, pollInterval * 1e3)
|
|
1616
|
+
);
|
|
1617
|
+
const pollResult = await this.#pollSession(session.sessionToken);
|
|
1618
|
+
const pollStatus = pollResult.status;
|
|
1619
|
+
if (pollStatus === "completed") {
|
|
1620
|
+
const connectionsData = pollResult.connections ?? [];
|
|
1621
|
+
return connectionsData.map(
|
|
1622
|
+
(conn) => new ConnectResult({
|
|
1623
|
+
connection_id: conn.connection_id ?? "",
|
|
1624
|
+
provider_id: conn.provider_id ?? "",
|
|
1625
|
+
account_identifier: conn.account_identifier ?? null,
|
|
1626
|
+
scopes: conn.scopes ?? [],
|
|
1627
|
+
connection_policy: conn.connection_policy ?? null
|
|
1628
|
+
})
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
if (pollStatus === "error") {
|
|
1632
|
+
const err = pollResult.error;
|
|
1633
|
+
const errorCode = err?.error_code ?? "unknown_error";
|
|
1634
|
+
const errorMessage = err?.error_message ?? "OAuth flow failed";
|
|
1635
|
+
const errorDetails = { error_code: errorCode };
|
|
1636
|
+
if (errorCode === "connect_denied") {
|
|
1637
|
+
throw new ConnectDeniedError(errorMessage, errorDetails);
|
|
1638
|
+
}
|
|
1639
|
+
if ([
|
|
1640
|
+
"connect_config_error",
|
|
1641
|
+
"invalid_redirect_uri",
|
|
1642
|
+
"invalid_client",
|
|
1643
|
+
"unauthorized_client"
|
|
1644
|
+
].includes(errorCode)) {
|
|
1645
|
+
throw new ConnectConfigError(errorMessage, errorDetails);
|
|
1646
|
+
}
|
|
1647
|
+
throw new ConnectFlowError(errorMessage, errorDetails);
|
|
1648
|
+
}
|
|
1649
|
+
if (pollStatus === "expired") {
|
|
1650
|
+
throw new ConnectFlowError(
|
|
1651
|
+
"Connect session expired before OAuth was completed"
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
throw new ConnectTimeoutError(
|
|
1656
|
+
`OAuth flow did not complete within ${timeout} seconds. The user may not have finished authorizing in the browser.`,
|
|
1657
|
+
{ timeout }
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Poll the Connect session for completion status (INTERNAL).
|
|
1662
|
+
*
|
|
1663
|
+
* Makes an HMAC-signed POST to the poll endpoint.
|
|
1664
|
+
*/
|
|
1665
|
+
async #pollSession(sessionToken) {
|
|
1666
|
+
const actorHeaders = this.#getActorRequestHeaders();
|
|
1667
|
+
const pollPath = "/sdk/oauth/connect/session/poll";
|
|
1668
|
+
const pollBody = { session_token: sessionToken };
|
|
1669
|
+
const pollBodyStr = JSON.stringify(pollBody);
|
|
1670
|
+
const pollHmac = this.#computeHmacHeaders(
|
|
1671
|
+
"POST",
|
|
1672
|
+
pollPath,
|
|
1673
|
+
pollBodyStr
|
|
1674
|
+
);
|
|
1675
|
+
let response;
|
|
1676
|
+
try {
|
|
1677
|
+
response = await this.#alterClient.post(pollPath, {
|
|
1678
|
+
body: pollBodyStr,
|
|
1679
|
+
headers: { ...actorHeaders, ...pollHmac, "Content-Type": "application/json" }
|
|
1680
|
+
});
|
|
1681
|
+
} catch (error) {
|
|
1682
|
+
if (_AlterVault.#isTimeoutOrAbortError(error)) {
|
|
1683
|
+
throw new TimeoutError(
|
|
1684
|
+
`Request to Alter Vault backend timed out: ${error instanceof Error ? error.message : String(error)}`,
|
|
1685
|
+
{ base_url: this.baseUrl }
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
if (error instanceof TypeError) {
|
|
1689
|
+
throw new NetworkError(
|
|
1690
|
+
`Failed to connect to Alter Vault backend: ${error.message}`,
|
|
1691
|
+
{ base_url: this.baseUrl }
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
throw new AlterSDKError(
|
|
1695
|
+
`Failed to poll connect session: ${error instanceof Error ? error.message : String(error)}`
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
this.#cacheActorIdFromResponse(response);
|
|
1699
|
+
await this.#handleErrorResponse(response);
|
|
1700
|
+
return await response.json();
|
|
1701
|
+
}
|
|
1117
1702
|
/**
|
|
1118
1703
|
* Close HTTP clients and release resources.
|
|
1119
1704
|
* Waits for any pending audit tasks before closing.
|
|
@@ -1139,10 +1724,72 @@ Object.freeze(AlterVault.prototype);
|
|
|
1139
1724
|
|
|
1140
1725
|
// src/providers/enums.ts
|
|
1141
1726
|
var Provider = /* @__PURE__ */ ((Provider2) => {
|
|
1142
|
-
Provider2["
|
|
1727
|
+
Provider2["ACUITY_SCHEDULING"] = "acuity-scheduling";
|
|
1728
|
+
Provider2["ADOBE"] = "adobe";
|
|
1729
|
+
Provider2["AIRCALL"] = "aircall";
|
|
1730
|
+
Provider2["AIRTABLE"] = "airtable";
|
|
1731
|
+
Provider2["APOLLO"] = "apollo";
|
|
1732
|
+
Provider2["ASANA"] = "asana";
|
|
1733
|
+
Provider2["ATLASSIAN"] = "atlassian";
|
|
1734
|
+
Provider2["ATTIO"] = "attio";
|
|
1735
|
+
Provider2["AUTODESK"] = "autodesk";
|
|
1736
|
+
Provider2["BASECAMP"] = "basecamp";
|
|
1737
|
+
Provider2["BITBUCKET"] = "bitbucket";
|
|
1738
|
+
Provider2["BITLY"] = "bitly";
|
|
1739
|
+
Provider2["BOX"] = "box";
|
|
1740
|
+
Provider2["BREX"] = "brex";
|
|
1741
|
+
Provider2["CALENDLY"] = "calendly";
|
|
1742
|
+
Provider2["CAL_COM"] = "cal-com";
|
|
1743
|
+
Provider2["CANVA"] = "canva";
|
|
1744
|
+
Provider2["CLICKUP"] = "clickup";
|
|
1745
|
+
Provider2["CLOSE"] = "close";
|
|
1746
|
+
Provider2["CONSTANT_CONTACT"] = "constant-contact";
|
|
1747
|
+
Provider2["CONTENTFUL"] = "contentful";
|
|
1748
|
+
Provider2["DEEL"] = "deel";
|
|
1749
|
+
Provider2["DIALPAD"] = "dialpad";
|
|
1750
|
+
Provider2["DIGITALOCEAN"] = "digitalocean";
|
|
1751
|
+
Provider2["DISCORD"] = "discord";
|
|
1752
|
+
Provider2["DOCUSIGN"] = "docusign";
|
|
1753
|
+
Provider2["DROPBOX"] = "dropbox";
|
|
1754
|
+
Provider2["EBAY"] = "ebay";
|
|
1755
|
+
Provider2["EVENTBRITE"] = "eventbrite";
|
|
1756
|
+
Provider2["FACEBOOK"] = "facebook";
|
|
1757
|
+
Provider2["FIGMA"] = "figma";
|
|
1143
1758
|
Provider2["GITHUB"] = "github";
|
|
1144
|
-
Provider2["
|
|
1759
|
+
Provider2["GOOGLE"] = "google";
|
|
1760
|
+
Provider2["HUBSPOT"] = "hubspot";
|
|
1761
|
+
Provider2["INSTAGRAM"] = "instagram";
|
|
1762
|
+
Provider2["LINEAR"] = "linear";
|
|
1763
|
+
Provider2["LINKEDIN"] = "linkedin";
|
|
1764
|
+
Provider2["MAILCHIMP"] = "mailchimp";
|
|
1765
|
+
Provider2["MERCURY"] = "mercury";
|
|
1766
|
+
Provider2["MICROSOFT"] = "microsoft";
|
|
1767
|
+
Provider2["MIRO"] = "miro";
|
|
1768
|
+
Provider2["MONDAY"] = "monday";
|
|
1769
|
+
Provider2["NOTION"] = "notion";
|
|
1770
|
+
Provider2["OUTREACH"] = "outreach";
|
|
1771
|
+
Provider2["PAGERDUTY"] = "pagerduty";
|
|
1772
|
+
Provider2["PAYPAL"] = "paypal";
|
|
1773
|
+
Provider2["PINTEREST"] = "pinterest";
|
|
1774
|
+
Provider2["PIPEDRIVE"] = "pipedrive";
|
|
1775
|
+
Provider2["QUICKBOOKS"] = "quickbooks";
|
|
1776
|
+
Provider2["RAMP"] = "ramp";
|
|
1777
|
+
Provider2["REDDIT"] = "reddit";
|
|
1778
|
+
Provider2["RINGCENTRAL"] = "ringcentral";
|
|
1779
|
+
Provider2["SALESFORCE"] = "salesforce";
|
|
1145
1780
|
Provider2["SENTRY"] = "sentry";
|
|
1781
|
+
Provider2["SLACK"] = "slack";
|
|
1782
|
+
Provider2["SNAPCHAT"] = "snapchat";
|
|
1783
|
+
Provider2["SPOTIFY"] = "spotify";
|
|
1784
|
+
Provider2["SQUARE"] = "square";
|
|
1785
|
+
Provider2["SQUARESPACE"] = "squarespace";
|
|
1786
|
+
Provider2["STRIPE"] = "stripe";
|
|
1787
|
+
Provider2["TIKTOK"] = "tiktok";
|
|
1788
|
+
Provider2["TODOIST"] = "todoist";
|
|
1789
|
+
Provider2["TWITTER"] = "twitter";
|
|
1790
|
+
Provider2["TYPEFORM"] = "typeform";
|
|
1791
|
+
Provider2["WEBEX"] = "webex";
|
|
1792
|
+
Provider2["WEBFLOW"] = "webflow";
|
|
1146
1793
|
return Provider2;
|
|
1147
1794
|
})(Provider || {});
|
|
1148
1795
|
var HttpMethod = /* @__PURE__ */ ((HttpMethod2) => {
|
|
@@ -1160,17 +1807,26 @@ export {
|
|
|
1160
1807
|
ActorType,
|
|
1161
1808
|
AlterSDKError,
|
|
1162
1809
|
AlterVault,
|
|
1810
|
+
BackendError,
|
|
1811
|
+
ConnectConfigError,
|
|
1812
|
+
ConnectDeniedError,
|
|
1813
|
+
ConnectFlowError,
|
|
1814
|
+
ConnectResult,
|
|
1163
1815
|
ConnectSession,
|
|
1816
|
+
ConnectTimeoutError,
|
|
1817
|
+
ConnectionDeletedError,
|
|
1818
|
+
ConnectionExpiredError,
|
|
1164
1819
|
ConnectionInfo,
|
|
1165
1820
|
ConnectionListResult,
|
|
1166
1821
|
ConnectionNotFoundError,
|
|
1822
|
+
ConnectionRevokedError,
|
|
1167
1823
|
HttpMethod,
|
|
1168
1824
|
NetworkError,
|
|
1169
1825
|
PolicyViolationError,
|
|
1170
1826
|
Provider,
|
|
1171
1827
|
ProviderAPIError,
|
|
1828
|
+
ReAuthRequiredError,
|
|
1829
|
+
ScopeReauthRequiredError,
|
|
1172
1830
|
TimeoutError,
|
|
1173
|
-
|
|
1174
|
-
TokenResponse,
|
|
1175
|
-
TokenRetrievalError
|
|
1831
|
+
TokenResponse
|
|
1176
1832
|
};
|