@alter-ai/alter-sdk 0.5.0 → 0.7.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 +126 -81
- package/dist/index.cjs +423 -155
- package/dist/index.d.cts +145 -80
- package/dist/index.d.ts +145 -80
- package/dist/index.js +416 -150
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
|
-
import { createHash as createHash2, createHmac as createHmac2, randomUUID } from "crypto";
|
|
2
|
+
import { createHash as createHash2, createHmac as createHmac2, randomBytes, randomUUID } from "crypto";
|
|
3
3
|
|
|
4
4
|
// src/exceptions.ts
|
|
5
5
|
var AlterSDKError = class extends Error {
|
|
@@ -29,30 +29,38 @@ var ReAuthRequiredError = class extends BackendError {
|
|
|
29
29
|
this.name = "ReAuthRequiredError";
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
|
-
var
|
|
32
|
+
var GrantExpiredError = class extends ReAuthRequiredError {
|
|
33
33
|
constructor(message, details) {
|
|
34
34
|
super(message, details);
|
|
35
|
-
this.name = "
|
|
35
|
+
this.name = "GrantExpiredError";
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
|
-
var
|
|
39
|
-
|
|
40
|
-
constructor(message,
|
|
38
|
+
var GrantRevokedError = class extends ReAuthRequiredError {
|
|
39
|
+
grantId;
|
|
40
|
+
constructor(message, grantId, details) {
|
|
41
41
|
super(message, details);
|
|
42
|
-
this.name = "
|
|
43
|
-
this.
|
|
42
|
+
this.name = "GrantRevokedError";
|
|
43
|
+
this.grantId = grantId;
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
|
-
var
|
|
46
|
+
var CredentialRevokedError = class extends ReAuthRequiredError {
|
|
47
|
+
grantId;
|
|
48
|
+
constructor(message, grantId, details) {
|
|
49
|
+
super(message, details);
|
|
50
|
+
this.name = "CredentialRevokedError";
|
|
51
|
+
this.grantId = grantId;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var GrantDeletedError = class extends ReAuthRequiredError {
|
|
47
55
|
constructor(message, details) {
|
|
48
56
|
super(message, details);
|
|
49
|
-
this.name = "
|
|
57
|
+
this.name = "GrantDeletedError";
|
|
50
58
|
}
|
|
51
59
|
};
|
|
52
|
-
var
|
|
60
|
+
var GrantNotFoundError = class extends BackendError {
|
|
53
61
|
constructor(message, details) {
|
|
54
62
|
super(message, details);
|
|
55
|
-
this.name = "
|
|
63
|
+
this.name = "GrantNotFoundError";
|
|
56
64
|
}
|
|
57
65
|
};
|
|
58
66
|
var PolicyViolationError = class extends BackendError {
|
|
@@ -98,12 +106,12 @@ var ProviderAPIError = class extends AlterSDKError {
|
|
|
98
106
|
}
|
|
99
107
|
};
|
|
100
108
|
var ScopeReauthRequiredError = class extends ProviderAPIError {
|
|
101
|
-
|
|
109
|
+
grantId;
|
|
102
110
|
providerId;
|
|
103
|
-
constructor(message,
|
|
111
|
+
constructor(message, grantId, providerId, statusCode, responseBody, details) {
|
|
104
112
|
super(message, statusCode, responseBody, details);
|
|
105
113
|
this.name = "ScopeReauthRequiredError";
|
|
106
|
-
this.
|
|
114
|
+
this.grantId = grantId;
|
|
107
115
|
this.providerId = providerId;
|
|
108
116
|
}
|
|
109
117
|
};
|
|
@@ -124,7 +132,6 @@ var TimeoutError = class extends NetworkError {
|
|
|
124
132
|
var ActorType = /* @__PURE__ */ ((ActorType2) => {
|
|
125
133
|
ActorType2["BACKEND_SERVICE"] = "backend_service";
|
|
126
134
|
ActorType2["AI_AGENT"] = "ai_agent";
|
|
127
|
-
ActorType2["MCP_SERVER"] = "mcp_server";
|
|
128
135
|
return ActorType2;
|
|
129
136
|
})(ActorType || {});
|
|
130
137
|
var TokenResponse = class _TokenResponse {
|
|
@@ -136,8 +143,8 @@ var TokenResponse = class _TokenResponse {
|
|
|
136
143
|
expiresAt;
|
|
137
144
|
/** OAuth scopes granted */
|
|
138
145
|
scopes;
|
|
139
|
-
/**
|
|
140
|
-
|
|
146
|
+
/** Grant ID that provided this token */
|
|
147
|
+
grantId;
|
|
141
148
|
/** Provider ID (google, github, etc.) */
|
|
142
149
|
providerId;
|
|
143
150
|
/** HTTP header name for credential injection (e.g., "Authorization", "X-API-Key") */
|
|
@@ -153,8 +160,8 @@ var TokenResponse = class _TokenResponse {
|
|
|
153
160
|
this.expiresIn = data.expires_in ?? null;
|
|
154
161
|
this.expiresAt = data.expires_at ? _TokenResponse.parseExpiresAt(data.expires_at) : null;
|
|
155
162
|
this.scopes = data.scopes ?? [];
|
|
156
|
-
this.
|
|
157
|
-
this.providerId = data.provider_id ??
|
|
163
|
+
this.grantId = data.grant_id;
|
|
164
|
+
this.providerId = data.provider_id ?? null;
|
|
158
165
|
this.injectionHeader = data.injection_header ?? "Authorization";
|
|
159
166
|
this.injectionFormat = data.injection_format ?? "Bearer {token}";
|
|
160
167
|
if (data.additional_credentials) {
|
|
@@ -209,14 +216,15 @@ var TokenResponse = class _TokenResponse {
|
|
|
209
216
|
expires_in: this.expiresIn,
|
|
210
217
|
expires_at: this.expiresAt?.toISOString() ?? null,
|
|
211
218
|
scopes: this.scopes,
|
|
212
|
-
|
|
219
|
+
grant_id: this.grantId,
|
|
220
|
+
provider_id: this.providerId
|
|
213
221
|
};
|
|
214
222
|
}
|
|
215
223
|
/**
|
|
216
224
|
* Custom string representation — EXCLUDES access_token for security.
|
|
217
225
|
*/
|
|
218
226
|
toString() {
|
|
219
|
-
return `TokenResponse(
|
|
227
|
+
return `TokenResponse(grant_id=${this.grantId}, token_type=${this.tokenType}, scopes=[${this.scopes.join(", ")}])`;
|
|
220
228
|
}
|
|
221
229
|
/**
|
|
222
230
|
* Custom Node.js inspect output — EXCLUDES access_token for security.
|
|
@@ -225,8 +233,8 @@ var TokenResponse = class _TokenResponse {
|
|
|
225
233
|
return this.toString();
|
|
226
234
|
}
|
|
227
235
|
};
|
|
228
|
-
var
|
|
229
|
-
|
|
236
|
+
var GrantInfo = class {
|
|
237
|
+
grantId;
|
|
230
238
|
providerId;
|
|
231
239
|
scopes;
|
|
232
240
|
accountIdentifier;
|
|
@@ -237,7 +245,7 @@ var ConnectionInfo = class {
|
|
|
237
245
|
createdAt;
|
|
238
246
|
lastUsedAt;
|
|
239
247
|
constructor(data) {
|
|
240
|
-
this.
|
|
248
|
+
this.grantId = data.grant_id;
|
|
241
249
|
this.providerId = data.provider_id;
|
|
242
250
|
this.scopes = data.scopes ?? [];
|
|
243
251
|
this.accountIdentifier = data.account_identifier ?? null;
|
|
@@ -251,7 +259,7 @@ var ConnectionInfo = class {
|
|
|
251
259
|
}
|
|
252
260
|
toJSON() {
|
|
253
261
|
return {
|
|
254
|
-
|
|
262
|
+
grant_id: this.grantId,
|
|
255
263
|
provider_id: this.providerId,
|
|
256
264
|
scopes: this.scopes,
|
|
257
265
|
account_identifier: this.accountIdentifier,
|
|
@@ -264,7 +272,7 @@ var ConnectionInfo = class {
|
|
|
264
272
|
};
|
|
265
273
|
}
|
|
266
274
|
toString() {
|
|
267
|
-
return `
|
|
275
|
+
return `GrantInfo(grant_id=${this.grantId}, provider=${this.providerId}, status=${this.status})`;
|
|
268
276
|
}
|
|
269
277
|
};
|
|
270
278
|
var ConnectSession = class {
|
|
@@ -291,46 +299,91 @@ var ConnectSession = class {
|
|
|
291
299
|
return `ConnectSession(url=${this.connectUrl}, expires_in=${this.expiresIn})`;
|
|
292
300
|
}
|
|
293
301
|
};
|
|
294
|
-
var
|
|
295
|
-
|
|
302
|
+
var GrantListResult = class {
|
|
303
|
+
grants;
|
|
296
304
|
total;
|
|
297
305
|
limit;
|
|
298
306
|
offset;
|
|
299
307
|
hasMore;
|
|
300
308
|
constructor(data) {
|
|
301
|
-
this.
|
|
309
|
+
this.grants = data.grants;
|
|
302
310
|
this.total = data.total;
|
|
303
311
|
this.limit = data.limit;
|
|
304
312
|
this.offset = data.offset;
|
|
305
313
|
this.hasMore = data.has_more;
|
|
306
314
|
Object.freeze(this);
|
|
307
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Custom JSON serialization — snake_case wire format.
|
|
318
|
+
*/
|
|
319
|
+
toJSON() {
|
|
320
|
+
return {
|
|
321
|
+
grants: this.grants.map((g) => g.toJSON()),
|
|
322
|
+
total: this.total,
|
|
323
|
+
limit: this.limit,
|
|
324
|
+
offset: this.offset,
|
|
325
|
+
has_more: this.hasMore
|
|
326
|
+
};
|
|
327
|
+
}
|
|
308
328
|
};
|
|
309
329
|
var ConnectResult = class {
|
|
310
|
-
|
|
330
|
+
grantId;
|
|
311
331
|
providerId;
|
|
312
332
|
accountIdentifier;
|
|
313
333
|
scopes;
|
|
314
|
-
|
|
334
|
+
grantPolicy;
|
|
315
335
|
constructor(data) {
|
|
316
|
-
this.
|
|
336
|
+
this.grantId = data.grant_id;
|
|
317
337
|
this.providerId = data.provider_id;
|
|
318
338
|
this.accountIdentifier = data.account_identifier ?? null;
|
|
319
339
|
this.scopes = data.scopes ?? [];
|
|
320
|
-
|
|
340
|
+
const rawPolicy = data.grant_policy;
|
|
341
|
+
if (rawPolicy) {
|
|
342
|
+
const raw = rawPolicy;
|
|
343
|
+
this.grantPolicy = Object.freeze({
|
|
344
|
+
expiresAt: raw["expires_at"] !== void 0 ? raw["expires_at"] : rawPolicy.expiresAt ?? null,
|
|
345
|
+
createdBy: raw["created_by"] !== void 0 ? raw["created_by"] : rawPolicy.createdBy ?? null,
|
|
346
|
+
createdAt: raw["created_at"] !== void 0 ? raw["created_at"] : rawPolicy.createdAt ?? null
|
|
347
|
+
});
|
|
348
|
+
} else {
|
|
349
|
+
this.grantPolicy = null;
|
|
350
|
+
}
|
|
321
351
|
Object.freeze(this);
|
|
322
352
|
}
|
|
323
353
|
toJSON() {
|
|
324
354
|
return {
|
|
325
|
-
|
|
355
|
+
grant_id: this.grantId,
|
|
326
356
|
provider_id: this.providerId,
|
|
327
357
|
account_identifier: this.accountIdentifier,
|
|
328
358
|
scopes: this.scopes,
|
|
329
|
-
|
|
359
|
+
grant_policy: this.grantPolicy ? {
|
|
360
|
+
expires_at: this.grantPolicy.expiresAt ?? null,
|
|
361
|
+
created_by: this.grantPolicy.createdBy ?? null,
|
|
362
|
+
created_at: this.grantPolicy.createdAt ?? null
|
|
363
|
+
} : null
|
|
330
364
|
};
|
|
331
365
|
}
|
|
332
366
|
toString() {
|
|
333
|
-
return `ConnectResult(
|
|
367
|
+
return `ConnectResult(grant_id=${this.grantId}, provider=${this.providerId})`;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
var AuthResult = class {
|
|
371
|
+
userToken;
|
|
372
|
+
userInfo;
|
|
373
|
+
constructor(data) {
|
|
374
|
+
this.userToken = data.user_token;
|
|
375
|
+
this.userInfo = data.user_info ?? {};
|
|
376
|
+
Object.freeze(this);
|
|
377
|
+
}
|
|
378
|
+
toJSON() {
|
|
379
|
+
return {
|
|
380
|
+
user_token: this.userToken,
|
|
381
|
+
user_info: this.userInfo
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
toString() {
|
|
385
|
+
const sub = this.userInfo?.sub ?? "unknown";
|
|
386
|
+
return `AuthResult(sub=${sub})`;
|
|
334
387
|
}
|
|
335
388
|
};
|
|
336
389
|
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
|
|
@@ -340,10 +393,11 @@ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
|
|
|
340
393
|
"x-api-key",
|
|
341
394
|
"x-auth-token",
|
|
342
395
|
"x-amz-date",
|
|
343
|
-
"x-amz-content-sha256"
|
|
396
|
+
"x-amz-content-sha256",
|
|
397
|
+
"x-amz-security-token"
|
|
344
398
|
]);
|
|
345
399
|
var APICallAuditLog = class {
|
|
346
|
-
|
|
400
|
+
grantId;
|
|
347
401
|
providerId;
|
|
348
402
|
method;
|
|
349
403
|
url;
|
|
@@ -362,22 +416,28 @@ var APICallAuditLog = class {
|
|
|
362
416
|
threadId;
|
|
363
417
|
/** Tool invocation ID for actor tracking */
|
|
364
418
|
toolCallId;
|
|
419
|
+
/** Specific tool being invoked (e.g., "read_calendar") */
|
|
420
|
+
toolId;
|
|
421
|
+
/** Tool bundle identifier (e.g., "google-calendar-mcp-server") */
|
|
422
|
+
toolBundleId;
|
|
365
423
|
constructor(data) {
|
|
366
|
-
this.
|
|
367
|
-
this.providerId = data.
|
|
424
|
+
this.grantId = data.grant_id;
|
|
425
|
+
this.providerId = data.provider_id;
|
|
368
426
|
this.method = data.method;
|
|
369
427
|
this.url = data.url;
|
|
370
|
-
this.requestHeaders = data.
|
|
371
|
-
this.requestBody = data.
|
|
372
|
-
this.responseStatus = data.
|
|
373
|
-
this.responseHeaders = data.
|
|
374
|
-
this.responseBody = data.
|
|
375
|
-
this.latencyMs = data.
|
|
428
|
+
this.requestHeaders = data.request_headers ?? null;
|
|
429
|
+
this.requestBody = data.request_body ?? null;
|
|
430
|
+
this.responseStatus = data.response_status;
|
|
431
|
+
this.responseHeaders = data.response_headers ?? null;
|
|
432
|
+
this.responseBody = data.response_body ?? null;
|
|
433
|
+
this.latencyMs = data.latency_ms;
|
|
376
434
|
this.timestamp = /* @__PURE__ */ new Date();
|
|
377
435
|
this.reason = data.reason ?? null;
|
|
378
|
-
this.runId = data.
|
|
379
|
-
this.threadId = data.
|
|
380
|
-
this.toolCallId = data.
|
|
436
|
+
this.runId = data.run_id ?? null;
|
|
437
|
+
this.threadId = data.thread_id ?? null;
|
|
438
|
+
this.toolCallId = data.tool_call_id ?? null;
|
|
439
|
+
this.toolId = data.tool_id ?? null;
|
|
440
|
+
this.toolBundleId = data.tool_bundle_id ?? null;
|
|
381
441
|
}
|
|
382
442
|
/**
|
|
383
443
|
* Sanitize sensitive data before sending.
|
|
@@ -386,7 +446,7 @@ var APICallAuditLog = class {
|
|
|
386
446
|
*/
|
|
387
447
|
sanitize() {
|
|
388
448
|
const sanitized = {
|
|
389
|
-
|
|
449
|
+
grant_id: this.grantId,
|
|
390
450
|
provider_id: this.providerId,
|
|
391
451
|
method: this.method,
|
|
392
452
|
url: this.url,
|
|
@@ -399,7 +459,9 @@ var APICallAuditLog = class {
|
|
|
399
459
|
reason: this.reason,
|
|
400
460
|
run_id: this.runId,
|
|
401
461
|
thread_id: this.threadId,
|
|
402
|
-
tool_call_id: this.toolCallId
|
|
462
|
+
tool_call_id: this.toolCallId,
|
|
463
|
+
tool_id: this.toolId,
|
|
464
|
+
tool_bundle_id: this.toolBundleId
|
|
403
465
|
};
|
|
404
466
|
return sanitized;
|
|
405
467
|
}
|
|
@@ -419,6 +481,7 @@ import { createHash, createHmac } from "crypto";
|
|
|
419
481
|
var ALGORITHM = "AWS4-HMAC-SHA256";
|
|
420
482
|
var AWS_HOST_RE = /^(?<service>[a-z0-9-]+)\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.amazonaws\.com$/;
|
|
421
483
|
var S3_VIRTUAL_HOST_RE = /^[^.]+\.s3\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.amazonaws\.com$/;
|
|
484
|
+
var VPCE_HOST_RE = /^vpce-[a-z0-9-]+\.(?<service>[a-z0-9-]+)\.(?<region>[a-z]{2}(?:-[a-z0-9]+)+-\d+)\.vpce\.amazonaws\.com$/;
|
|
422
485
|
function hmacSha256(key, message) {
|
|
423
486
|
return createHmac("sha256", key).update(message, "utf8").digest();
|
|
424
487
|
}
|
|
@@ -441,6 +504,10 @@ function detectServiceAndRegion(hostname) {
|
|
|
441
504
|
if (s3m?.groups) {
|
|
442
505
|
return { service: "s3", region: s3m.groups.region };
|
|
443
506
|
}
|
|
507
|
+
const vpcem = VPCE_HOST_RE.exec(lower);
|
|
508
|
+
if (vpcem?.groups) {
|
|
509
|
+
return { service: vpcem.groups.service, region: vpcem.groups.region };
|
|
510
|
+
}
|
|
444
511
|
return { service: null, region: null };
|
|
445
512
|
}
|
|
446
513
|
function deriveSigningKey(secretKey, dateStamp, region, service) {
|
|
@@ -475,18 +542,21 @@ function canonicalQueryString(query) {
|
|
|
475
542
|
]);
|
|
476
543
|
}
|
|
477
544
|
}
|
|
478
|
-
|
|
545
|
+
const sigv4Encode = (s) => encodeURIComponent(s).replace(
|
|
546
|
+
/[!'()*]/g,
|
|
547
|
+
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
|
548
|
+
);
|
|
549
|
+
const encoded = sorted.map(
|
|
550
|
+
([k, v]) => [sigv4Encode(k), sigv4Encode(v)]
|
|
551
|
+
);
|
|
552
|
+
encoded.sort((a, b) => {
|
|
479
553
|
if (a[0] < b[0]) return -1;
|
|
480
554
|
if (a[0] > b[0]) return 1;
|
|
481
555
|
if (a[1] < b[1]) return -1;
|
|
482
556
|
if (a[1] > b[1]) return 1;
|
|
483
557
|
return 0;
|
|
484
558
|
});
|
|
485
|
-
|
|
486
|
-
/[!'()*]/g,
|
|
487
|
-
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
|
488
|
-
);
|
|
489
|
-
return sorted.map(([k, v]) => `${sigv4Encode(k)}=${sigv4Encode(v)}`).join("&");
|
|
559
|
+
return encoded.map(([k, v]) => `${k}=${v}`).join("&");
|
|
490
560
|
}
|
|
491
561
|
function canonicalHeadersAndSigned(headers) {
|
|
492
562
|
const canonical = {};
|
|
@@ -505,11 +575,9 @@ function signAwsRequest(opts) {
|
|
|
505
575
|
const parsed = new URL(opts.url);
|
|
506
576
|
const hostname = parsed.hostname;
|
|
507
577
|
let { region, service } = opts;
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (service == null) service = detected.service;
|
|
512
|
-
}
|
|
578
|
+
const detected = detectServiceAndRegion(hostname);
|
|
579
|
+
if (detected.region != null) region = detected.region;
|
|
580
|
+
if (detected.service != null) service = detected.service;
|
|
513
581
|
if (!region) {
|
|
514
582
|
throw new Error(
|
|
515
583
|
`Cannot determine AWS region from URL '${opts.url}'. Pass region explicitly via additional_credentials.`
|
|
@@ -592,7 +660,7 @@ function _extractAdditionalCredentials(token) {
|
|
|
592
660
|
return _additionalCredsStore.get(token);
|
|
593
661
|
}
|
|
594
662
|
var _fetch;
|
|
595
|
-
var SDK_VERSION = "0.
|
|
663
|
+
var SDK_VERSION = "0.7.0";
|
|
596
664
|
var SDK_USER_AGENT = `alter-sdk-node/${SDK_VERSION}`;
|
|
597
665
|
var HTTP_FORBIDDEN = 403;
|
|
598
666
|
var HTTP_NOT_FOUND = 404;
|
|
@@ -704,6 +772,7 @@ var AlterVault = class _AlterVault {
|
|
|
704
772
|
#actorVersion;
|
|
705
773
|
#clientType;
|
|
706
774
|
#framework;
|
|
775
|
+
#toolBundleId;
|
|
707
776
|
constructor(options) {
|
|
708
777
|
_AlterVault.#validateInitParams(
|
|
709
778
|
options.apiKey,
|
|
@@ -715,13 +784,14 @@ var AlterVault = class _AlterVault {
|
|
|
715
784
|
["actorName", options.actorName],
|
|
716
785
|
["actorVersion", options.actorVersion],
|
|
717
786
|
["clientType", options.clientType],
|
|
718
|
-
["framework", options.framework]
|
|
787
|
+
["framework", options.framework],
|
|
788
|
+
["toolBundleId", options.toolBundleId]
|
|
719
789
|
];
|
|
720
790
|
for (const [name, value] of actorStrings) {
|
|
721
791
|
_AlterVault.#validateActorString(value, name);
|
|
722
792
|
}
|
|
723
793
|
this.#hmacKey = createHmac2("sha256", options.apiKey).update("alter-signing-v1").digest();
|
|
724
|
-
this.baseUrl = (process.env.ALTER_BASE_URL ?? "https://backend.
|
|
794
|
+
this.baseUrl = (process.env.ALTER_BASE_URL ?? "https://backend.alterauth.com").replace(/\/+$/, "");
|
|
725
795
|
const timeoutMs = options.timeout ?? 3e4;
|
|
726
796
|
this.#actorType = options.actorType;
|
|
727
797
|
this.#actorIdentifier = options.actorIdentifier;
|
|
@@ -730,6 +800,7 @@ var AlterVault = class _AlterVault {
|
|
|
730
800
|
this.#clientType = options.clientType;
|
|
731
801
|
this.#userTokenGetter = options.userTokenGetter ?? null;
|
|
732
802
|
this.#framework = options.framework;
|
|
803
|
+
this.#toolBundleId = options.toolBundleId;
|
|
733
804
|
if (!_fetch) {
|
|
734
805
|
_fetch = globalThis.fetch;
|
|
735
806
|
}
|
|
@@ -777,7 +848,7 @@ var AlterVault = class _AlterVault {
|
|
|
777
848
|
}
|
|
778
849
|
if (!actorType) {
|
|
779
850
|
throw new AlterSDKError(
|
|
780
|
-
"actor_type is required (use ActorType.AI_AGENT
|
|
851
|
+
"actor_type is required (use ActorType.AI_AGENT or ActorType.BACKEND_SERVICE)"
|
|
781
852
|
);
|
|
782
853
|
}
|
|
783
854
|
const validValues = Object.values(ActorType);
|
|
@@ -806,7 +877,8 @@ var AlterVault = class _AlterVault {
|
|
|
806
877
|
"X-Alter-Actor-Name": this.#actorName,
|
|
807
878
|
"X-Alter-Actor-Version": this.#actorVersion,
|
|
808
879
|
"X-Alter-Client-Type": this.#clientType,
|
|
809
|
-
"X-Alter-Framework": this.#framework
|
|
880
|
+
"X-Alter-Framework": this.#framework,
|
|
881
|
+
"X-Alter-Tool-Bundle-ID": this.#toolBundleId
|
|
810
882
|
};
|
|
811
883
|
for (const [key, value] of Object.entries(actorHeaders)) {
|
|
812
884
|
if (value) {
|
|
@@ -818,21 +890,24 @@ var AlterVault = class _AlterVault {
|
|
|
818
890
|
/**
|
|
819
891
|
* Compute HMAC-SHA256 signature headers for an Alter backend request.
|
|
820
892
|
*
|
|
821
|
-
* String-to-sign format: METHOD\nPATH_WITH_SORTED_QUERY\nTIMESTAMP\nCONTENT_HASH
|
|
893
|
+
* String-to-sign format: METHOD\nPATH_WITH_SORTED_QUERY\nTIMESTAMP\nNONCE\nCONTENT_HASH
|
|
822
894
|
*
|
|
823
895
|
* The path should include sorted query parameters if present (e.g. "/sdk/endpoint?a=1&b=2").
|
|
824
|
-
* Currently all SDK
|
|
896
|
+
* Currently all SDK->backend calls are POSTs without query params, so the path is clean.
|
|
825
897
|
*/
|
|
826
898
|
#computeHmacHeaders(method, path, body) {
|
|
827
899
|
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
900
|
+
const nonce = randomBytes(16).toString("hex");
|
|
828
901
|
const contentHash = createHash2("sha256").update(body ?? "").digest("hex");
|
|
829
902
|
const stringToSign = `${method.toUpperCase()}
|
|
830
903
|
${path}
|
|
831
904
|
${timestamp}
|
|
905
|
+
${nonce}
|
|
832
906
|
${contentHash}`;
|
|
833
907
|
const signature = createHmac2("sha256", this.#hmacKey).update(stringToSign).digest("hex");
|
|
834
908
|
return {
|
|
835
909
|
"X-Alter-Timestamp": timestamp,
|
|
910
|
+
"X-Alter-Nonce": nonce,
|
|
836
911
|
"X-Alter-Content-SHA256": contentHash,
|
|
837
912
|
"X-Alter-Signature": signature
|
|
838
913
|
};
|
|
@@ -840,7 +915,7 @@ ${contentHash}`;
|
|
|
840
915
|
/**
|
|
841
916
|
* Build per-request actor headers for instance tracking.
|
|
842
917
|
*/
|
|
843
|
-
#getActorRequestHeaders(runId, threadId, toolCallId) {
|
|
918
|
+
#getActorRequestHeaders(runId, threadId, toolCallId, toolId, toolBundleId) {
|
|
844
919
|
const headers = {};
|
|
845
920
|
if (this.#cachedActorId) {
|
|
846
921
|
headers["X-Alter-Actor-ID"] = this.#cachedActorId;
|
|
@@ -870,6 +945,24 @@ ${contentHash}`;
|
|
|
870
945
|
);
|
|
871
946
|
}
|
|
872
947
|
}
|
|
948
|
+
if (toolId) {
|
|
949
|
+
if (toolId.length <= MAX_TRACKING_ID_LENGTH && SAFE_HEADER_PATTERN.test(toolId)) {
|
|
950
|
+
headers["X-Alter-Tool-ID"] = toolId;
|
|
951
|
+
} else {
|
|
952
|
+
console.warn(
|
|
953
|
+
"Invalid tool_id (too long or invalid chars), skipping"
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (toolBundleId) {
|
|
958
|
+
if (toolBundleId.length <= MAX_TRACKING_ID_LENGTH && SAFE_HEADER_PATTERN.test(toolBundleId)) {
|
|
959
|
+
headers["X-Alter-Tool-Bundle-ID"] = toolBundleId;
|
|
960
|
+
} else {
|
|
961
|
+
console.warn(
|
|
962
|
+
"Invalid tool_bundle_id (too long or invalid chars), skipping"
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
873
966
|
return headers;
|
|
874
967
|
}
|
|
875
968
|
/**
|
|
@@ -921,9 +1014,20 @@ ${contentHash}`;
|
|
|
921
1014
|
}
|
|
922
1015
|
return null;
|
|
923
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Parse JSON from response, unwrapping FastAPI's HTTPException envelope.
|
|
1019
|
+
*
|
|
1020
|
+
* FastAPI wraps `HTTPException.detail` in `{"detail": ...}`. When the
|
|
1021
|
+
* inner detail is an object we unwrap it so callers can access error
|
|
1022
|
+
* fields (`error`, `message`, `details`) directly.
|
|
1023
|
+
*/
|
|
924
1024
|
static async #safeParseJson(response) {
|
|
925
1025
|
try {
|
|
926
|
-
|
|
1026
|
+
const data = await response.clone().json();
|
|
1027
|
+
if ("detail" in data && typeof data.detail === "object" && data.detail !== null && !Array.isArray(data.detail)) {
|
|
1028
|
+
return data.detail;
|
|
1029
|
+
}
|
|
1030
|
+
return data;
|
|
927
1031
|
} catch {
|
|
928
1032
|
try {
|
|
929
1033
|
const text = await response.clone().text();
|
|
@@ -942,38 +1046,69 @@ ${contentHash}`;
|
|
|
942
1046
|
}
|
|
943
1047
|
if (response.status === HTTP_FORBIDDEN) {
|
|
944
1048
|
const errorData = await _AlterVault.#safeParseJson(response);
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1049
|
+
const errorCode = errorData.error;
|
|
1050
|
+
if (errorCode === "grant_expired") {
|
|
1051
|
+
throw new GrantExpiredError(
|
|
1052
|
+
errorData.message ?? "Grant expired per TTL policy",
|
|
948
1053
|
errorData.details
|
|
949
1054
|
);
|
|
950
1055
|
}
|
|
1056
|
+
if (errorCode === "grant_revoked") {
|
|
1057
|
+
throw new GrantRevokedError(
|
|
1058
|
+
errorData.message ?? "Grant has been revoked. User must re-authorize.",
|
|
1059
|
+
errorData.grant_id
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
if (errorCode === "credential_revoked") {
|
|
1063
|
+
throw new CredentialRevokedError(
|
|
1064
|
+
errorData.message ?? "Credential has been revoked. User must re-authorize.",
|
|
1065
|
+
errorData.grant_id
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
if (errorData.error === "scope_mismatch") {
|
|
1069
|
+
const details = errorData.details ?? {};
|
|
1070
|
+
throw new ScopeReauthRequiredError(
|
|
1071
|
+
errorData.message ?? "Grant is missing required scopes",
|
|
1072
|
+
details.grant_id ?? void 0,
|
|
1073
|
+
details.provider_id ?? void 0,
|
|
1074
|
+
response.status,
|
|
1075
|
+
JSON.stringify(errorData),
|
|
1076
|
+
details
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
951
1079
|
throw new PolicyViolationError(
|
|
952
1080
|
errorData.message ?? "Access denied by policy",
|
|
953
|
-
|
|
1081
|
+
errorCode,
|
|
954
1082
|
errorData.details
|
|
955
1083
|
);
|
|
956
1084
|
}
|
|
957
1085
|
if (response.status === HTTP_GONE) {
|
|
958
1086
|
const errorData = await _AlterVault.#safeParseJson(response);
|
|
959
|
-
throw new
|
|
960
|
-
errorData.message ?? "
|
|
1087
|
+
throw new GrantDeletedError(
|
|
1088
|
+
errorData.message ?? "Grant has been deleted. A new grant_id will be issued on re-authorization.",
|
|
961
1089
|
errorData
|
|
962
1090
|
);
|
|
963
1091
|
}
|
|
964
1092
|
if (response.status === HTTP_NOT_FOUND) {
|
|
965
1093
|
const errorData = await _AlterVault.#safeParseJson(response);
|
|
966
|
-
throw new
|
|
967
|
-
errorData.message ?? "OAuth
|
|
1094
|
+
throw new GrantNotFoundError(
|
|
1095
|
+
errorData.message ?? "OAuth grant not found for the given grant_id",
|
|
968
1096
|
errorData
|
|
969
1097
|
);
|
|
970
1098
|
}
|
|
971
1099
|
if (response.status === HTTP_BAD_REQUEST || response.status === HTTP_BAD_GATEWAY) {
|
|
972
1100
|
const errorData = await _AlterVault.#safeParseJson(response);
|
|
973
|
-
if (errorData.error === "
|
|
974
|
-
throw new
|
|
975
|
-
errorData.message ?? "
|
|
976
|
-
errorData.
|
|
1101
|
+
if (errorData.error === "grant_revoked") {
|
|
1102
|
+
throw new GrantRevokedError(
|
|
1103
|
+
errorData.message ?? "Grant has been revoked. User must re-authorize.",
|
|
1104
|
+
errorData.grant_id,
|
|
1105
|
+
errorData
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
if (errorData.error === "credential_revoked") {
|
|
1109
|
+
throw new CredentialRevokedError(
|
|
1110
|
+
errorData.message ?? "Underlying credential has been revoked. User must re-authorize.",
|
|
1111
|
+
errorData.grant_id,
|
|
977
1112
|
errorData
|
|
978
1113
|
);
|
|
979
1114
|
}
|
|
@@ -1023,11 +1158,13 @@ ${contentHash}`;
|
|
|
1023
1158
|
}
|
|
1024
1159
|
return token;
|
|
1025
1160
|
}
|
|
1026
|
-
async #getToken(
|
|
1161
|
+
async #getToken(grantId, reason, requestMetadata, runId, threadId, toolCallId, toolId, provider, account, toolBundleId) {
|
|
1027
1162
|
const actorHeaders = this.#getActorRequestHeaders(
|
|
1028
1163
|
runId,
|
|
1029
1164
|
threadId,
|
|
1030
|
-
toolCallId
|
|
1165
|
+
toolCallId,
|
|
1166
|
+
toolId,
|
|
1167
|
+
toolBundleId
|
|
1031
1168
|
);
|
|
1032
1169
|
let response;
|
|
1033
1170
|
let tokenBody;
|
|
@@ -1044,7 +1181,7 @@ ${contentHash}`;
|
|
|
1044
1181
|
}
|
|
1045
1182
|
} else {
|
|
1046
1183
|
tokenBody = {
|
|
1047
|
-
|
|
1184
|
+
grant_id: grantId,
|
|
1048
1185
|
reason: reason ?? null,
|
|
1049
1186
|
request: requestMetadata ?? null
|
|
1050
1187
|
};
|
|
@@ -1072,7 +1209,7 @@ ${contentHash}`;
|
|
|
1072
1209
|
}
|
|
1073
1210
|
throw new BackendError(
|
|
1074
1211
|
`Failed to retrieve token: ${error instanceof Error ? error.message : String(error)}`,
|
|
1075
|
-
{
|
|
1212
|
+
{ grant_id: grantId, error: String(error) }
|
|
1076
1213
|
);
|
|
1077
1214
|
}
|
|
1078
1215
|
this.#cacheActorIdFromResponse(response);
|
|
@@ -1084,13 +1221,13 @@ ${contentHash}`;
|
|
|
1084
1221
|
if (!/^[A-Za-z][A-Za-z0-9-]*$/.test(tokenResponse.injectionHeader)) {
|
|
1085
1222
|
throw new BackendError(
|
|
1086
1223
|
`Backend returned invalid injection_header: ${tokenResponse.injectionHeader}`,
|
|
1087
|
-
{
|
|
1224
|
+
{ grantId: String(grantId) }
|
|
1088
1225
|
);
|
|
1089
1226
|
}
|
|
1090
1227
|
if (/[\r\n\x00]/.test(tokenResponse.injectionFormat)) {
|
|
1091
1228
|
throw new BackendError(
|
|
1092
1229
|
`Backend returned invalid injection_format (contains control characters)`,
|
|
1093
|
-
{
|
|
1230
|
+
{ grantId: String(grantId) }
|
|
1094
1231
|
);
|
|
1095
1232
|
}
|
|
1096
1233
|
_storeAccessToken(tokenResponse, typedData.access_token);
|
|
@@ -1108,20 +1245,22 @@ ${contentHash}`;
|
|
|
1108
1245
|
async #logApiCall(params) {
|
|
1109
1246
|
try {
|
|
1110
1247
|
const auditLog = new APICallAuditLog({
|
|
1111
|
-
|
|
1112
|
-
|
|
1248
|
+
grant_id: params.grantId,
|
|
1249
|
+
provider_id: params.providerId,
|
|
1113
1250
|
method: params.method,
|
|
1114
1251
|
url: params.url,
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1252
|
+
request_headers: params.requestHeaders,
|
|
1253
|
+
request_body: params.requestBody,
|
|
1254
|
+
response_status: params.responseStatus,
|
|
1255
|
+
response_headers: params.responseHeaders,
|
|
1256
|
+
response_body: params.responseBody,
|
|
1257
|
+
latency_ms: params.latencyMs,
|
|
1121
1258
|
reason: params.reason,
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1259
|
+
run_id: params.runId,
|
|
1260
|
+
thread_id: params.threadId,
|
|
1261
|
+
tool_call_id: params.toolCallId,
|
|
1262
|
+
tool_id: params.toolId,
|
|
1263
|
+
tool_bundle_id: params.toolBundleId
|
|
1125
1264
|
});
|
|
1126
1265
|
const sanitized = auditLog.sanitize();
|
|
1127
1266
|
const actorHeaders = this.#getActorRequestHeaders(params.runId);
|
|
@@ -1197,7 +1336,7 @@ ${contentHash}`;
|
|
|
1197
1336
|
* 4. Logs the call for audit (fire-and-forget)
|
|
1198
1337
|
* 5. Returns the raw response
|
|
1199
1338
|
*/
|
|
1200
|
-
async request(
|
|
1339
|
+
async request(grantId, method, url, options) {
|
|
1201
1340
|
if (this.#closed) {
|
|
1202
1341
|
throw new AlterSDKError(
|
|
1203
1342
|
"SDK instance has been closed. Create a new AlterVault instance to make requests."
|
|
@@ -1205,13 +1344,13 @@ ${contentHash}`;
|
|
|
1205
1344
|
}
|
|
1206
1345
|
const provider = options?.provider;
|
|
1207
1346
|
const account = options?.account;
|
|
1208
|
-
if (!
|
|
1209
|
-
throw new AlterSDKError("Provide
|
|
1347
|
+
if (!grantId && !provider) {
|
|
1348
|
+
throw new AlterSDKError("Provide grantId or options.provider");
|
|
1210
1349
|
}
|
|
1211
|
-
if (
|
|
1212
|
-
throw new AlterSDKError("Cannot provide both
|
|
1350
|
+
if (grantId && provider) {
|
|
1351
|
+
throw new AlterSDKError("Cannot provide both grantId and options.provider");
|
|
1213
1352
|
}
|
|
1214
|
-
const
|
|
1353
|
+
const effectiveGrantId = grantId ?? null;
|
|
1215
1354
|
let currentUrl = url;
|
|
1216
1355
|
const runId = options?.runId ?? randomUUID();
|
|
1217
1356
|
const methodStr = String(method).toUpperCase();
|
|
@@ -1252,14 +1391,16 @@ ${contentHash}`;
|
|
|
1252
1391
|
}
|
|
1253
1392
|
}
|
|
1254
1393
|
const { tokenResponse, scopeMismatch } = await this.#getToken(
|
|
1255
|
-
|
|
1394
|
+
effectiveGrantId,
|
|
1256
1395
|
options?.reason,
|
|
1257
1396
|
{ method: methodStr, url: currentUrl },
|
|
1258
1397
|
runId,
|
|
1259
1398
|
options?.threadId,
|
|
1260
1399
|
options?.toolCallId,
|
|
1400
|
+
options?.toolId,
|
|
1261
1401
|
provider,
|
|
1262
|
-
account
|
|
1402
|
+
account,
|
|
1403
|
+
options?.toolBundleId
|
|
1263
1404
|
);
|
|
1264
1405
|
const requestHeaders = options?.extraHeaders ? { ...options.extraHeaders } : {};
|
|
1265
1406
|
const accessToken = _extractAccessToken(tokenResponse);
|
|
@@ -1278,7 +1419,7 @@ ${contentHash}`;
|
|
|
1278
1419
|
if (!additionalCreds.secret_key) {
|
|
1279
1420
|
throw new BackendError(
|
|
1280
1421
|
"AWS SigV4 credential is missing secret_key in additional_credentials. Re-store the credential with both Access Key ID and Secret Access Key.",
|
|
1281
|
-
{
|
|
1422
|
+
{ grant_id: effectiveGrantId }
|
|
1282
1423
|
);
|
|
1283
1424
|
}
|
|
1284
1425
|
const awsHeaders = signAwsRequest({
|
|
@@ -1345,7 +1486,7 @@ ${contentHash}`;
|
|
|
1345
1486
|
throw new TimeoutError(
|
|
1346
1487
|
`Provider API request timed out: ${error instanceof Error ? error.message : String(error)}`,
|
|
1347
1488
|
{
|
|
1348
|
-
|
|
1489
|
+
grant_id: effectiveGrantId,
|
|
1349
1490
|
method: methodStr,
|
|
1350
1491
|
url: currentUrl
|
|
1351
1492
|
}
|
|
@@ -1354,7 +1495,7 @@ ${contentHash}`;
|
|
|
1354
1495
|
throw new NetworkError(
|
|
1355
1496
|
`Failed to call provider API: ${error instanceof Error ? error.message : String(error)}`,
|
|
1356
1497
|
{
|
|
1357
|
-
|
|
1498
|
+
grant_id: effectiveGrantId,
|
|
1358
1499
|
method: methodStr,
|
|
1359
1500
|
url: currentUrl,
|
|
1360
1501
|
error: String(error)
|
|
@@ -1383,8 +1524,8 @@ ${contentHash}`;
|
|
|
1383
1524
|
responseHeaders[key] = value;
|
|
1384
1525
|
});
|
|
1385
1526
|
this.#scheduleAuditLog({
|
|
1386
|
-
|
|
1387
|
-
providerId: tokenResponse.providerId ||
|
|
1527
|
+
grantId: tokenResponse.grantId,
|
|
1528
|
+
providerId: tokenResponse.providerId || effectiveGrantId || "",
|
|
1388
1529
|
method: methodStr,
|
|
1389
1530
|
url: auditUrl,
|
|
1390
1531
|
requestHeaders: auditHeaders,
|
|
@@ -1396,18 +1537,20 @@ ${contentHash}`;
|
|
|
1396
1537
|
reason: options?.reason ?? null,
|
|
1397
1538
|
runId,
|
|
1398
1539
|
threadId: options?.threadId ?? null,
|
|
1399
|
-
toolCallId: options?.toolCallId ?? null
|
|
1540
|
+
toolCallId: options?.toolCallId ?? null,
|
|
1541
|
+
toolId: options?.toolId ?? null,
|
|
1542
|
+
toolBundleId: options?.toolBundleId ?? null
|
|
1400
1543
|
});
|
|
1401
1544
|
if (response.status >= HTTP_CLIENT_ERROR_START) {
|
|
1402
1545
|
if (response.status === HTTP_FORBIDDEN && scopeMismatch) {
|
|
1403
1546
|
throw new ScopeReauthRequiredError(
|
|
1404
|
-
"Provider API returned 403 and the
|
|
1405
|
-
tokenResponse.
|
|
1406
|
-
tokenResponse.providerId,
|
|
1547
|
+
"Provider API returned 403 and the grant's scopes don't match the provider config. The user needs to re-authorize to grant updated permissions.",
|
|
1548
|
+
tokenResponse.grantId,
|
|
1549
|
+
tokenResponse.providerId ?? void 0,
|
|
1407
1550
|
response.status,
|
|
1408
1551
|
responseBody,
|
|
1409
1552
|
{
|
|
1410
|
-
|
|
1553
|
+
grant_id: tokenResponse.grantId,
|
|
1411
1554
|
provider_id: tokenResponse.providerId,
|
|
1412
1555
|
method: methodStr,
|
|
1413
1556
|
url: currentUrl
|
|
@@ -1419,7 +1562,7 @@ ${contentHash}`;
|
|
|
1419
1562
|
response.status,
|
|
1420
1563
|
responseBody,
|
|
1421
1564
|
{
|
|
1422
|
-
|
|
1565
|
+
grant_id: effectiveGrantId,
|
|
1423
1566
|
method: methodStr,
|
|
1424
1567
|
url: currentUrl
|
|
1425
1568
|
}
|
|
@@ -1428,12 +1571,12 @@ ${contentHash}`;
|
|
|
1428
1571
|
return response;
|
|
1429
1572
|
}
|
|
1430
1573
|
/**
|
|
1431
|
-
* List OAuth
|
|
1574
|
+
* List OAuth grants for this app.
|
|
1432
1575
|
*
|
|
1433
|
-
* Returns paginated
|
|
1576
|
+
* Returns paginated grant metadata (no tokens).
|
|
1434
1577
|
* Useful for discovering which services a user has connected.
|
|
1435
1578
|
*/
|
|
1436
|
-
async
|
|
1579
|
+
async listGrants(options) {
|
|
1437
1580
|
if (this.#closed) {
|
|
1438
1581
|
throw new AlterSDKError(
|
|
1439
1582
|
"SDK instance has been closed. Create a new AlterVault instance to make requests."
|
|
@@ -1451,12 +1594,12 @@ ${contentHash}`;
|
|
|
1451
1594
|
listBody.user_token = await this.#getUserToken();
|
|
1452
1595
|
} catch (err) {
|
|
1453
1596
|
console.warn(
|
|
1454
|
-
"user_token_getter failed in
|
|
1597
|
+
"user_token_getter failed in listGrants, falling back to un-scoped listing:",
|
|
1455
1598
|
err instanceof Error ? err.message : String(err)
|
|
1456
1599
|
);
|
|
1457
1600
|
}
|
|
1458
1601
|
}
|
|
1459
|
-
const listPath = "/sdk/oauth/
|
|
1602
|
+
const listPath = "/sdk/oauth/grants/list";
|
|
1460
1603
|
const listBodyStr = JSON.stringify(listBody);
|
|
1461
1604
|
const listHmac = this.#computeHmacHeaders("POST", listPath, listBodyStr);
|
|
1462
1605
|
try {
|
|
@@ -1478,19 +1621,19 @@ ${contentHash}`;
|
|
|
1478
1621
|
);
|
|
1479
1622
|
}
|
|
1480
1623
|
throw new AlterSDKError(
|
|
1481
|
-
`Failed to list
|
|
1624
|
+
`Failed to list grants: ${error instanceof Error ? error.message : String(error)}`
|
|
1482
1625
|
);
|
|
1483
1626
|
}
|
|
1484
1627
|
this.#cacheActorIdFromResponse(response);
|
|
1485
1628
|
await this.#handleErrorResponse(response);
|
|
1486
1629
|
const data = await response.json();
|
|
1487
|
-
const
|
|
1488
|
-
(
|
|
1489
|
-
|
|
1630
|
+
const grants = data.grants.map(
|
|
1631
|
+
(g) => new GrantInfo(
|
|
1632
|
+
g
|
|
1490
1633
|
)
|
|
1491
1634
|
);
|
|
1492
|
-
return new
|
|
1493
|
-
|
|
1635
|
+
return new GrantListResult({
|
|
1636
|
+
grants,
|
|
1494
1637
|
total: data.total,
|
|
1495
1638
|
limit: data.limit,
|
|
1496
1639
|
offset: data.offset,
|
|
@@ -1516,10 +1659,10 @@ ${contentHash}`;
|
|
|
1516
1659
|
allowed_origin: options.allowedOrigin ?? null,
|
|
1517
1660
|
metadata: options.metadata ?? null
|
|
1518
1661
|
};
|
|
1519
|
-
if (options.
|
|
1520
|
-
sessionBody.
|
|
1521
|
-
max_ttl_seconds: options.
|
|
1522
|
-
default_ttl_seconds: options.
|
|
1662
|
+
if (options.grantPolicy) {
|
|
1663
|
+
sessionBody.grant_policy = {
|
|
1664
|
+
max_ttl_seconds: options.grantPolicy.maxTtlSeconds ?? null,
|
|
1665
|
+
default_ttl_seconds: options.grantPolicy.defaultTtlSeconds ?? null
|
|
1523
1666
|
};
|
|
1524
1667
|
}
|
|
1525
1668
|
if (this.#userTokenGetter) {
|
|
@@ -1586,7 +1729,7 @@ ${contentHash}`;
|
|
|
1586
1729
|
const openBrowser = options.openBrowser ?? true;
|
|
1587
1730
|
const session = await this.createConnectSession({
|
|
1588
1731
|
allowedProviders: options.providers,
|
|
1589
|
-
|
|
1732
|
+
grantPolicy: options.grantPolicy
|
|
1590
1733
|
});
|
|
1591
1734
|
if (openBrowser) {
|
|
1592
1735
|
try {
|
|
@@ -1617,14 +1760,14 @@ ${contentHash}`;
|
|
|
1617
1760
|
const pollResult = await this.#pollSession(session.sessionToken);
|
|
1618
1761
|
const pollStatus = pollResult.status;
|
|
1619
1762
|
if (pollStatus === "completed") {
|
|
1620
|
-
const
|
|
1621
|
-
return
|
|
1622
|
-
(
|
|
1623
|
-
|
|
1624
|
-
provider_id:
|
|
1625
|
-
account_identifier:
|
|
1626
|
-
scopes:
|
|
1627
|
-
|
|
1763
|
+
const grantsData = pollResult.grants ?? [];
|
|
1764
|
+
return grantsData.map(
|
|
1765
|
+
(grant) => new ConnectResult({
|
|
1766
|
+
grant_id: grant.grant_id ?? "",
|
|
1767
|
+
provider_id: grant.provider_id ?? "",
|
|
1768
|
+
account_identifier: grant.account_identifier ?? null,
|
|
1769
|
+
scopes: grant.scopes ?? [],
|
|
1770
|
+
grant_policy: grant.grant_policy ?? null
|
|
1628
1771
|
})
|
|
1629
1772
|
);
|
|
1630
1773
|
}
|
|
@@ -1651,12 +1794,133 @@ ${contentHash}`;
|
|
|
1651
1794
|
"Connect session expired before OAuth was completed"
|
|
1652
1795
|
);
|
|
1653
1796
|
}
|
|
1797
|
+
if (pollStatus !== "pending") {
|
|
1798
|
+
throw new ConnectFlowError(
|
|
1799
|
+
`Unexpected poll status from server: ${pollStatus}`,
|
|
1800
|
+
{ status: pollStatus }
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1654
1803
|
}
|
|
1655
1804
|
throw new ConnectTimeoutError(
|
|
1656
1805
|
`OAuth flow did not complete within ${timeout} seconds. The user may not have finished authorizing in the browser.`,
|
|
1657
1806
|
{ timeout }
|
|
1658
1807
|
);
|
|
1659
1808
|
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Trigger IDP login for end user via browser.
|
|
1811
|
+
*
|
|
1812
|
+
* Opens the app's configured IDP login page in the user's default browser.
|
|
1813
|
+
* Polls for completion and returns the user's IDP JWT token.
|
|
1814
|
+
*
|
|
1815
|
+
* After authenticate() completes, the returned userToken is automatically
|
|
1816
|
+
* used for subsequent request() calls that use identity resolution.
|
|
1817
|
+
*
|
|
1818
|
+
* @param options - Optional configuration (timeout in milliseconds, default 300000 = 5 min)
|
|
1819
|
+
* @returns AuthResult with userToken and userInfo
|
|
1820
|
+
* @throws ConnectTimeoutError if authentication times out
|
|
1821
|
+
* @throws AlterSDKError if session creation or authentication fails
|
|
1822
|
+
*/
|
|
1823
|
+
async authenticate(options) {
|
|
1824
|
+
if (this.#closed) {
|
|
1825
|
+
throw new AlterSDKError(
|
|
1826
|
+
"SDK instance has been closed. Create a new AlterVault instance to make requests."
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
const timeoutMs = options?.timeout ?? 3e5;
|
|
1830
|
+
const actorHeaders = this.#getActorRequestHeaders();
|
|
1831
|
+
const sessionPath = "/sdk/auth/session";
|
|
1832
|
+
const sessionBodyStr = JSON.stringify({});
|
|
1833
|
+
const sessionHmac = this.#computeHmacHeaders("POST", sessionPath, sessionBodyStr);
|
|
1834
|
+
let sessionResp;
|
|
1835
|
+
try {
|
|
1836
|
+
sessionResp = await this.#alterClient.post(sessionPath, {
|
|
1837
|
+
body: sessionBodyStr,
|
|
1838
|
+
headers: { ...actorHeaders, ...sessionHmac, "Content-Type": "application/json" }
|
|
1839
|
+
});
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
if (_AlterVault.#isTimeoutOrAbortError(error)) {
|
|
1842
|
+
throw new TimeoutError(
|
|
1843
|
+
`Request to Alter Vault backend timed out: ${error instanceof Error ? error.message : String(error)}`,
|
|
1844
|
+
{ base_url: this.baseUrl }
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
if (error instanceof TypeError) {
|
|
1848
|
+
throw new NetworkError(
|
|
1849
|
+
`Failed to connect to Alter Vault backend: ${error.message}`,
|
|
1850
|
+
{ base_url: this.baseUrl }
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
throw new AlterSDKError(
|
|
1854
|
+
`Failed to create auth session: ${error instanceof Error ? error.message : String(error)}`
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1857
|
+
this.#cacheActorIdFromResponse(sessionResp);
|
|
1858
|
+
await this.#handleErrorResponse(sessionResp);
|
|
1859
|
+
const sessionData = await sessionResp.json();
|
|
1860
|
+
try {
|
|
1861
|
+
const openModule = await import("open");
|
|
1862
|
+
const openFn = openModule.default;
|
|
1863
|
+
if (openFn) {
|
|
1864
|
+
await openFn(sessionData.auth_url);
|
|
1865
|
+
} else {
|
|
1866
|
+
console.log(
|
|
1867
|
+
`Open this URL to authenticate: ${sessionData.auth_url}`
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
} catch {
|
|
1871
|
+
console.log(
|
|
1872
|
+
`Open this URL to authenticate: ${sessionData.auth_url}`
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
const startTime = Date.now();
|
|
1876
|
+
while (true) {
|
|
1877
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
1878
|
+
throw new ConnectTimeoutError(
|
|
1879
|
+
"Authentication timed out",
|
|
1880
|
+
{ timeout: timeoutMs }
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
1884
|
+
const pollPath = "/sdk/auth/poll";
|
|
1885
|
+
const pollBodyStr = JSON.stringify({ session_token: sessionData.session_token });
|
|
1886
|
+
const pollHmac = this.#computeHmacHeaders("POST", pollPath, pollBodyStr);
|
|
1887
|
+
let pollResp;
|
|
1888
|
+
try {
|
|
1889
|
+
pollResp = await this.#alterClient.post(pollPath, {
|
|
1890
|
+
body: pollBodyStr,
|
|
1891
|
+
headers: { ...actorHeaders, ...pollHmac, "Content-Type": "application/json" }
|
|
1892
|
+
});
|
|
1893
|
+
} catch {
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
this.#cacheActorIdFromResponse(pollResp);
|
|
1897
|
+
if (!pollResp.ok) {
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
const pollData = await pollResp.json();
|
|
1901
|
+
if (pollData.status === "completed") {
|
|
1902
|
+
const userToken = pollData.user_token;
|
|
1903
|
+
if (!userToken) {
|
|
1904
|
+
throw new AlterSDKError(
|
|
1905
|
+
"Authentication completed but no user token was returned by the IDP"
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
this.#userTokenGetter = () => userToken;
|
|
1909
|
+
return new AuthResult({
|
|
1910
|
+
user_token: userToken,
|
|
1911
|
+
user_info: pollData.user_info
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
if (pollData.status === "error") {
|
|
1915
|
+
throw new AlterSDKError(
|
|
1916
|
+
pollData.error_message ?? "Authentication failed"
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
if (pollData.status === "expired") {
|
|
1920
|
+
throw new AlterSDKError("Authentication session expired");
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1660
1924
|
/**
|
|
1661
1925
|
* Poll the Connect session for completion status (INTERNAL).
|
|
1662
1926
|
*
|
|
@@ -1807,6 +2071,7 @@ export {
|
|
|
1807
2071
|
ActorType,
|
|
1808
2072
|
AlterSDKError,
|
|
1809
2073
|
AlterVault,
|
|
2074
|
+
AuthResult,
|
|
1810
2075
|
BackendError,
|
|
1811
2076
|
ConnectConfigError,
|
|
1812
2077
|
ConnectDeniedError,
|
|
@@ -1814,12 +2079,13 @@ export {
|
|
|
1814
2079
|
ConnectResult,
|
|
1815
2080
|
ConnectSession,
|
|
1816
2081
|
ConnectTimeoutError,
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
2082
|
+
CredentialRevokedError,
|
|
2083
|
+
GrantDeletedError,
|
|
2084
|
+
GrantExpiredError,
|
|
2085
|
+
GrantInfo,
|
|
2086
|
+
GrantListResult,
|
|
2087
|
+
GrantNotFoundError,
|
|
2088
|
+
GrantRevokedError,
|
|
1823
2089
|
HttpMethod,
|
|
1824
2090
|
NetworkError,
|
|
1825
2091
|
PolicyViolationError,
|