@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/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
|
|
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.
|
|
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?.
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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()
|
|
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,
|