@tangle-network/agent-runtime 0.279.0 → 0.281.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/dist/{activation-B5NExoOz.d.ts → activation-BcTWDNOD.d.ts} +151 -25
- package/dist/{activation-BkUdrvqh.js → activation-C1ZAyQrQ.js} +2 -2
- package/dist/{activation-BkUdrvqh.js.map → activation-C1ZAyQrQ.js.map} +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +2 -2
- package/dist/{coordination-driver-BBL_OgRw.js → coordination-driver-B-HOpGN4.js} +2 -2
- package/dist/{coordination-driver-BBL_OgRw.js.map → coordination-driver-B-HOpGN4.js.map} +1 -1
- package/dist/{delegate-CZc7B-Tl.js → delegate-C1eIBN70.js} +2 -2
- package/dist/{delegate-CZc7B-Tl.js.map → delegate-C1eIBN70.js.map} +1 -1
- package/dist/durable.d.ts +2 -2
- package/dist/durable.js +768 -499
- package/dist/durable.js.map +1 -1
- package/dist/{graph-Ci8Ufs_f.js → graph-KjJaTmbO.js} +4 -4
- package/dist/{graph-Ci8Ufs_f.js.map → graph-KjJaTmbO.js.map} +1 -1
- package/dist/{improvement-cycle-Bi43xCVa.js → improvement-cycle-DCTCTmNl.js} +763 -43
- package/dist/improvement-cycle-DCTCTmNl.js.map +1 -0
- package/dist/{index-Dm8SHDGW.d.ts → index-BSSGMvUd.d.ts} +97 -144
- package/dist/index.d.ts +4 -4
- package/dist/index.js +8 -8
- package/dist/intelligence.d.ts +1 -1
- package/dist/intelligence.js +3 -3
- package/dist/kernel.d.ts +1 -1
- package/dist/kernel.js +8 -8
- package/dist/{loop-runner-bin-C9FJshTP.js → loop-runner-bin-Co2eBFbb.js} +3 -3
- package/dist/{loop-runner-bin-C9FJshTP.js.map → loop-runner-bin-Co2eBFbb.js.map} +1 -1
- package/dist/{loop-runner-bin-D6khQtoC.d.ts → loop-runner-bin-DgeZHpWJ.d.ts} +2 -2
- package/dist/loop-runner-bin.d.ts +1 -1
- package/dist/loop-runner-bin.js +1 -1
- package/dist/mcp/bin.js +3 -3
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +4 -4
- package/dist/platform.d.ts +34 -36
- package/dist/platform.js +122 -29
- package/dist/platform.js.map +1 -1
- package/dist/{provision-supervisor-BvnihiyB.js → provision-supervisor-CkN8RRjv.js} +3 -3
- package/dist/{provision-supervisor-BvnihiyB.js.map → provision-supervisor-CkN8RRjv.js.map} +1 -1
- package/dist/{runtime-DsAHXig2.js → runtime-B_yRPBjj.js} +16 -966
- package/dist/runtime-B_yRPBjj.js.map +1 -0
- package/dist/{server-Dyc1zvug.js → server-dOyG5Tw6.js} +3 -3
- package/dist/{server-Dyc1zvug.js.map → server-dOyG5Tw6.js.map} +1 -1
- package/dist/{structural-rollout-D-0X6lsU.js → shared-box-CZFdugL1.js} +959 -9
- package/dist/shared-box-CZFdugL1.js.map +1 -0
- package/dist/{supervise-DBQdrp7H.js → supervise-zt91I62x.js} +3 -3
- package/dist/{supervise-DBQdrp7H.js.map → supervise-zt91I62x.js.map} +1 -1
- package/dist/{supervisor-DtlPj9me.js → supervisor-UXgaPisu.js} +49 -58
- package/dist/supervisor-UXgaPisu.js.map +1 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +12 -12
- package/dist/tui/index.d.ts +1 -1
- package/dist/tui/index.js +1 -1
- package/package.json +4 -4
- package/skills/profile-authoring/SKILL.md +210 -0
- package/skills/supervise/SKILL.md +2 -2
- package/dist/improvement-cycle-Bi43xCVa.js.map +0 -1
- package/dist/runtime-DsAHXig2.js.map +0 -1
- package/dist/structural-rollout-D-0X6lsU.js.map +0 -1
- package/dist/supervisor-DtlPj9me.js.map +0 -1
- package/skills/supervise/references/profile-authoring.md +0 -19
package/dist/platform.d.ts
CHANGED
|
@@ -261,47 +261,24 @@ declare class PlatformHubClient {
|
|
|
261
261
|
}
|
|
262
262
|
//#endregion
|
|
263
263
|
//#region src/platform/oidc.d.ts
|
|
264
|
-
/**
|
|
265
|
-
* Server-side OpenID Connect client for the Tangle authorization server
|
|
266
|
-
* (Better Auth `oauthProvider`, mounted at `/api/auth/oauth2/*`).
|
|
267
|
-
*
|
|
268
|
-
* This is the standard authorization-code + S256 PKCE path. It returns OIDC
|
|
269
|
-
* tokens and a verified identity, not the `sk-tan` API key that
|
|
270
|
-
* {@link PlatformAuthClient} returns. Identity scopes do not authorize paid
|
|
271
|
-
* platform calls, so a consumer that uses the legacy API key as its Hub
|
|
272
|
-
* bearer must keep {@link PlatformAuthClient} until it no longer needs one.
|
|
273
|
-
*
|
|
274
|
-
* The client id and redirect URI must be registered in the platform's
|
|
275
|
-
* `oauthClient` registry. The legacy `TRUSTED_APPS` registration does not
|
|
276
|
-
* carry over.
|
|
277
|
-
*/
|
|
278
264
|
interface PlatformOidcClientOptions {
|
|
279
|
-
/** Platform base URL, e.g. `https://id.tangle.tools`. */
|
|
280
265
|
baseUrl: string;
|
|
281
|
-
/** Client id from the platform `oauthClient` registry. */
|
|
282
266
|
clientId: string;
|
|
283
|
-
/**
|
|
284
|
-
* Client secret for a confidential client. Sent with HTTP Basic, the
|
|
285
|
-
* provider's default `client_secret_basic` method. Omit for a public client
|
|
286
|
-
* registered with `token_endpoint_auth_method: none`.
|
|
287
|
-
*/
|
|
267
|
+
/** Omit only for clients registered with token_endpoint_auth_method=none. */
|
|
288
268
|
clientSecret?: string;
|
|
289
|
-
/**
|
|
290
|
-
redirectUri
|
|
291
|
-
/** Requested scopes. Defaults to `openid profile email offline_access`. */
|
|
269
|
+
/** Required for authorization code; not required for the device grant. */
|
|
270
|
+
redirectUri?: string;
|
|
292
271
|
scope?: string;
|
|
293
|
-
/**
|
|
272
|
+
/** RFC 8707 resource identifiers registered at the authorization server. */
|
|
273
|
+
resources?: readonly string[];
|
|
294
274
|
fetchImpl?: typeof fetch;
|
|
295
275
|
}
|
|
296
276
|
interface OidcAuthorizeUrlOptions {
|
|
297
|
-
/** Required CSRF token; the consumer verifies it on the callback. */
|
|
298
277
|
state: string;
|
|
299
|
-
/** RFC 7636 S256 code challenge. See {@link createPkcePair}. */
|
|
300
278
|
codeChallenge: string;
|
|
301
|
-
/**
|
|
279
|
+
/** Only supply a nonce when the consumer also validates the ID token. */
|
|
302
280
|
nonce?: string;
|
|
303
281
|
prompt?: 'login' | 'consent' | 'none';
|
|
304
|
-
/** Pre-fill the email field on the login screen. */
|
|
305
282
|
loginHint?: string;
|
|
306
283
|
}
|
|
307
284
|
interface OidcTokens {
|
|
@@ -322,33 +299,54 @@ interface OidcExchangeResult {
|
|
|
322
299
|
tokens: OidcTokens;
|
|
323
300
|
user: OidcUser;
|
|
324
301
|
}
|
|
302
|
+
/** Keep deviceCode secret. Only userCode and the verification URLs are displayed. */
|
|
303
|
+
interface OidcDeviceAuthorization {
|
|
304
|
+
deviceCode: string;
|
|
305
|
+
userCode: string;
|
|
306
|
+
verificationUri: string;
|
|
307
|
+
verificationUriComplete?: string;
|
|
308
|
+
expiresIn: number;
|
|
309
|
+
/** Absolute deadline preserves expiry when a polling process resumes. */
|
|
310
|
+
expiresAt: number;
|
|
311
|
+
interval: number;
|
|
312
|
+
}
|
|
325
313
|
/** Create an RFC 7636 verifier and its S256 challenge with Web Crypto. */
|
|
326
314
|
declare function createPkcePair(): Promise<{
|
|
327
315
|
verifier: string;
|
|
328
316
|
challenge: string;
|
|
329
317
|
}>;
|
|
330
|
-
/**
|
|
318
|
+
/** OIDC code + PKCE, refresh/revoke and discovery-bound RFC 8628 device grants. */
|
|
331
319
|
declare class PlatformOidcClient {
|
|
332
320
|
private readonly baseUrl;
|
|
333
321
|
private readonly clientId;
|
|
334
322
|
private readonly clientSecret;
|
|
335
323
|
private readonly redirectUri;
|
|
336
324
|
private readonly scope;
|
|
325
|
+
private readonly resources;
|
|
337
326
|
private readonly fetchImpl;
|
|
338
327
|
constructor(options: PlatformOidcClientOptions);
|
|
339
|
-
/** Build the `/api/auth/oauth2/authorize` URL for a browser redirect. */
|
|
340
328
|
authorizeUrl(options: OidcAuthorizeUrlOptions): string;
|
|
341
|
-
/** Exchange the callback code, then resolve the verified identity from userinfo. */
|
|
342
329
|
exchange(code: string, codeVerifier: string): Promise<OidcExchangeResult>;
|
|
343
|
-
/**
|
|
330
|
+
/** Save the replacement refresh token. Never retry an old token as a new grant. */
|
|
344
331
|
refresh(refreshToken: string): Promise<OidcTokens>;
|
|
345
|
-
/** Read the verified identity behind an access token. */
|
|
346
332
|
userinfo(accessToken: string): Promise<OidcUser>;
|
|
347
|
-
/**
|
|
333
|
+
/** RFC 7009. A local logout alone is not a provider disconnect. */
|
|
348
334
|
revoke(token: string, tokenTypeHint?: 'access_token' | 'refresh_token'): Promise<void>;
|
|
335
|
+
/**
|
|
336
|
+
* Start a device grant only when this issuer advertises the OAuth-integrated
|
|
337
|
+
* device endpoint. No fallback to /cross-site/device or a standalone session
|
|
338
|
+
* device endpoint: those issue different credentials.
|
|
339
|
+
*/
|
|
340
|
+
deviceAuthorize(options?: {
|
|
341
|
+
signal?: AbortSignal;
|
|
342
|
+
}): Promise<OidcDeviceAuthorization>;
|
|
343
|
+
/** RFC 8628 pending/slow_down handling, expiry, cancellation and timeout backoff. */
|
|
344
|
+
pollDeviceAuthorization(grant: OidcDeviceAuthorization, options?: {
|
|
345
|
+
signal?: AbortSignal;
|
|
346
|
+
}): Promise<OidcTokens>;
|
|
349
347
|
private token;
|
|
350
348
|
private post;
|
|
351
349
|
}
|
|
352
350
|
//#endregion
|
|
353
|
-
export { type AuthorizeUrlOptions, type CatalogResult, type ConnectionHealth, type ConnectionHealthResult, type ExchangeCodeResult, type ExecInput, type HealthCheck, type MintTokenInput, type MintTokenResult, type OidcAuthorizeUrlOptions, type OidcExchangeResult, type OidcTokens, type OidcUser, PlatformAuthClient, type PlatformAuthClientOptions, PlatformAuthError, type PlatformCatalogProvider, type PlatformConnection, PlatformHubClient, type PlatformHubClientOptions, PlatformHubError, type PlatformHubStatus, PlatformOidcClient, type PlatformOidcClientOptions, type StartAuthInput, type StartAuthResult, createPkcePair };
|
|
351
|
+
export { type AuthorizeUrlOptions, type CatalogResult, type ConnectionHealth, type ConnectionHealthResult, type ExchangeCodeResult, type ExecInput, type HealthCheck, type MintTokenInput, type MintTokenResult, type OidcAuthorizeUrlOptions, type OidcDeviceAuthorization, type OidcExchangeResult, type OidcTokens, type OidcUser, PlatformAuthClient, type PlatformAuthClientOptions, PlatformAuthError, type PlatformCatalogProvider, type PlatformConnection, PlatformHubClient, type PlatformHubClientOptions, PlatformHubError, type PlatformHubStatus, PlatformOidcClient, type PlatformOidcClientOptions, type StartAuthInput, type StartAuthResult, createPkcePair };
|
|
354
352
|
//# sourceMappingURL=platform.d.ts.map
|
package/dist/platform.js
CHANGED
|
@@ -222,20 +222,12 @@ var PlatformHubClient = class {
|
|
|
222
222
|
//#endregion
|
|
223
223
|
//#region src/platform/oidc.ts
|
|
224
224
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* This is the standard authorization-code + S256 PKCE path. It returns OIDC
|
|
229
|
-
* tokens and a verified identity, not the `sk-tan` API key that
|
|
230
|
-
* {@link PlatformAuthClient} returns. Identity scopes do not authorize paid
|
|
231
|
-
* platform calls, so a consumer that uses the legacy API key as its Hub
|
|
232
|
-
* bearer must keep {@link PlatformAuthClient} until it no longer needs one.
|
|
233
|
-
*
|
|
234
|
-
* The client id and redirect URI must be registered in the platform's
|
|
235
|
-
* `oauthClient` registry. The legacy `TRUSTED_APPS` registration does not
|
|
236
|
-
* carry over.
|
|
225
|
+
* OpenID Connect client for the Tangle authorization server. Identity tokens
|
|
226
|
+
* are not platform API keys. Resource scopes and audiences must be registered
|
|
227
|
+
* by the provider before a consumer can use these tokens at a paid API.
|
|
237
228
|
*/
|
|
238
229
|
const DEFAULT_SCOPE = "openid profile email offline_access";
|
|
230
|
+
const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
239
231
|
function isRecord(value) {
|
|
240
232
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
241
233
|
}
|
|
@@ -273,27 +265,51 @@ function parseTokens(body, status) {
|
|
|
273
265
|
...isNonemptyString(body.scope) ? { scope: body.scope } : {}
|
|
274
266
|
};
|
|
275
267
|
}
|
|
276
|
-
|
|
268
|
+
function formComponent(value) {
|
|
269
|
+
return new URLSearchParams({ v: value }).toString().slice(2);
|
|
270
|
+
}
|
|
271
|
+
function pause(ms, signal) {
|
|
272
|
+
signal?.throwIfAborted();
|
|
273
|
+
return new Promise((resolve, reject) => {
|
|
274
|
+
const finish = () => {
|
|
275
|
+
signal?.removeEventListener("abort", abort);
|
|
276
|
+
resolve();
|
|
277
|
+
};
|
|
278
|
+
const timer = setTimeout(finish, ms);
|
|
279
|
+
const abort = () => {
|
|
280
|
+
clearTimeout(timer);
|
|
281
|
+
signal?.removeEventListener("abort", abort);
|
|
282
|
+
reject(signal?.reason ?? /* @__PURE__ */ new Error("Device authorization cancelled"));
|
|
283
|
+
};
|
|
284
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/** OIDC code + PKCE, refresh/revoke and discovery-bound RFC 8628 device grants. */
|
|
277
288
|
var PlatformOidcClient = class {
|
|
278
289
|
baseUrl;
|
|
279
290
|
clientId;
|
|
280
291
|
clientSecret;
|
|
281
292
|
redirectUri;
|
|
282
293
|
scope;
|
|
294
|
+
resources;
|
|
283
295
|
fetchImpl;
|
|
284
296
|
constructor(options) {
|
|
285
297
|
if (!options.baseUrl) throw new Error("PlatformOidcClient: baseUrl is required");
|
|
286
298
|
if (!options.clientId) throw new Error("PlatformOidcClient: clientId is required");
|
|
287
|
-
if (!options.redirectUri) throw new Error("PlatformOidcClient: redirectUri is required");
|
|
288
299
|
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
289
300
|
this.clientId = options.clientId;
|
|
290
301
|
this.clientSecret = options.clientSecret || void 0;
|
|
291
302
|
this.redirectUri = options.redirectUri;
|
|
292
303
|
this.scope = options.scope ?? DEFAULT_SCOPE;
|
|
293
|
-
this.
|
|
304
|
+
this.resources = [...new Set(options.resources ?? [])];
|
|
305
|
+
for (const resource of this.resources) {
|
|
306
|
+
const url = new URL(resource);
|
|
307
|
+
if (url.hash || url.username || url.password) throw new Error("PlatformOidcClient: resource must not contain a fragment or credentials");
|
|
308
|
+
}
|
|
309
|
+
this.fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
294
310
|
}
|
|
295
|
-
/** Build the `/api/auth/oauth2/authorize` URL for a browser redirect. */
|
|
296
311
|
authorizeUrl(options) {
|
|
312
|
+
if (!this.redirectUri) throw new Error("PlatformOidcClient: redirectUri is required for authorization code");
|
|
297
313
|
if (!options.state) throw new Error("PlatformOidcClient.authorizeUrl: state is required for CSRF");
|
|
298
314
|
if (!options.codeChallenge) throw new Error("PlatformOidcClient.authorizeUrl: codeChallenge is required for PKCE");
|
|
299
315
|
const url = new URL("/api/auth/oauth2/authorize", this.baseUrl);
|
|
@@ -304,13 +320,14 @@ var PlatformOidcClient = class {
|
|
|
304
320
|
url.searchParams.set("state", options.state);
|
|
305
321
|
url.searchParams.set("code_challenge", options.codeChallenge);
|
|
306
322
|
url.searchParams.set("code_challenge_method", "S256");
|
|
323
|
+
for (const resource of this.resources) url.searchParams.append("resource", resource);
|
|
307
324
|
if (options.nonce) url.searchParams.set("nonce", options.nonce);
|
|
308
325
|
if (options.prompt) url.searchParams.set("prompt", options.prompt);
|
|
309
326
|
if (options.loginHint) url.searchParams.set("login_hint", options.loginHint);
|
|
310
327
|
return url.toString();
|
|
311
328
|
}
|
|
312
|
-
/** Exchange the callback code, then resolve the verified identity from userinfo. */
|
|
313
329
|
async exchange(code, codeVerifier) {
|
|
330
|
+
if (!this.redirectUri) throw new Error("PlatformOidcClient: redirectUri is required for authorization code");
|
|
314
331
|
if (!code) throw new Error("PlatformOidcClient.exchange: code is required");
|
|
315
332
|
if (!codeVerifier) throw new Error("PlatformOidcClient.exchange: codeVerifier is required for PKCE");
|
|
316
333
|
const tokens = await this.token({
|
|
@@ -324,7 +341,7 @@ var PlatformOidcClient = class {
|
|
|
324
341
|
user: await this.userinfo(tokens.accessToken)
|
|
325
342
|
};
|
|
326
343
|
}
|
|
327
|
-
/**
|
|
344
|
+
/** Save the replacement refresh token. Never retry an old token as a new grant. */
|
|
328
345
|
async refresh(refreshToken) {
|
|
329
346
|
if (!refreshToken) throw new Error("PlatformOidcClient.refresh: refreshToken is required");
|
|
330
347
|
return this.token({
|
|
@@ -332,9 +349,12 @@ var PlatformOidcClient = class {
|
|
|
332
349
|
refresh_token: refreshToken
|
|
333
350
|
});
|
|
334
351
|
}
|
|
335
|
-
/** Read the verified identity behind an access token. */
|
|
336
352
|
async userinfo(accessToken) {
|
|
337
|
-
|
|
353
|
+
if (!accessToken) throw new Error("PlatformOidcClient.userinfo: accessToken is required");
|
|
354
|
+
const res = await this.fetchImpl(`${this.baseUrl}/api/auth/oauth2/userinfo`, {
|
|
355
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
356
|
+
redirect: "error"
|
|
357
|
+
});
|
|
338
358
|
const body = await res.json().catch(() => null);
|
|
339
359
|
if (!res.ok) throw new PlatformAuthError(oauthError(body, `Platform userinfo failed (${res.status})`), res.status, body);
|
|
340
360
|
if (!isRecord(body) || !isNonemptyString(body.sub) || !isNonemptyString(body.email) || body.email_verified !== true) throw new PlatformAuthError("Platform userinfo has no verified identity", res.status, { code: "INVALID_USERINFO_RESPONSE" });
|
|
@@ -345,33 +365,106 @@ var PlatformOidcClient = class {
|
|
|
345
365
|
...isNonemptyString(body.name) ? { name: body.name } : {}
|
|
346
366
|
};
|
|
347
367
|
}
|
|
348
|
-
/**
|
|
368
|
+
/** RFC 7009. A local logout alone is not a provider disconnect. */
|
|
349
369
|
async revoke(token, tokenTypeHint) {
|
|
350
370
|
if (!token) throw new Error("PlatformOidcClient.revoke: token is required");
|
|
351
|
-
const res = await this.post(
|
|
371
|
+
const res = await this.post(`${this.baseUrl}/api/auth/oauth2/revoke`, {
|
|
352
372
|
token,
|
|
353
373
|
...tokenTypeHint ? { token_type_hint: tokenTypeHint } : {}
|
|
354
|
-
});
|
|
374
|
+
}, void 0, false);
|
|
355
375
|
if (!res.ok) {
|
|
356
376
|
const body = await res.json().catch(() => null);
|
|
357
377
|
throw new PlatformAuthError(oauthError(body, `Platform revoke failed (${res.status})`), res.status, body);
|
|
358
378
|
}
|
|
359
379
|
}
|
|
360
|
-
|
|
361
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Start a device grant only when this issuer advertises the OAuth-integrated
|
|
382
|
+
* device endpoint. No fallback to /cross-site/device or a standalone session
|
|
383
|
+
* device endpoint: those issue different credentials.
|
|
384
|
+
*/
|
|
385
|
+
async deviceAuthorize(options = {}) {
|
|
386
|
+
const discovery = await this.fetchImpl(`${this.baseUrl}/api/auth/.well-known/openid-configuration`, {
|
|
387
|
+
redirect: "error",
|
|
388
|
+
signal: options.signal
|
|
389
|
+
});
|
|
390
|
+
const metadata = await discovery.json().catch(() => null);
|
|
391
|
+
const issuer = `${this.baseUrl}/api/auth`;
|
|
392
|
+
if (!discovery.ok || !isRecord(metadata) || metadata.issuer !== issuer || metadata.token_endpoint !== `${issuer}/oauth2/token` || !Array.isArray(metadata.grant_types_supported) || !metadata.grant_types_supported.includes(DEVICE_GRANT) || !isNonemptyString(metadata.device_authorization_endpoint)) throw new PlatformAuthError("Issuer does not advertise an OAuth device grant", discovery.status, { code: "OAUTH_DEVICE_GRANT_UNAVAILABLE" });
|
|
393
|
+
const endpoint = new URL(metadata.device_authorization_endpoint);
|
|
394
|
+
const origin = new URL(this.baseUrl).origin;
|
|
395
|
+
if (endpoint.origin !== origin || endpoint.username || endpoint.password || endpoint.hash) throw new PlatformAuthError("Device endpoint is outside the configured issuer", 400, { code: "INVALID_DEVICE_ENDPOINT" });
|
|
396
|
+
const startedAt = Date.now();
|
|
397
|
+
const res = await this.post(endpoint.href, { scope: this.scope }, options.signal);
|
|
398
|
+
const body = await res.json().catch(() => null);
|
|
399
|
+
if (!res.ok) throw new PlatformAuthError(oauthError(body, `Device authorization failed (${res.status})`), res.status, body);
|
|
400
|
+
if (!isRecord(body) || !isNonemptyString(body.device_code) || !isNonemptyString(body.user_code) || !isNonemptyString(body.verification_uri) || typeof body.expires_in !== "number" || !Number.isSafeInteger(body.expires_in) || body.expires_in <= 0 || body.interval !== void 0 && (typeof body.interval !== "number" || !Number.isSafeInteger(body.interval) || body.interval <= 0)) throw new PlatformAuthError("Malformed device authorization response", res.status, { code: "INVALID_DEVICE_RESPONSE" });
|
|
401
|
+
for (const value of [body.verification_uri, body.verification_uri_complete]) {
|
|
402
|
+
if (value === void 0) continue;
|
|
403
|
+
if (!isNonemptyString(value)) throw new PlatformAuthError("Invalid device verification URI", 400, { code: "INVALID_DEVICE_RESPONSE" });
|
|
404
|
+
const uri = new URL(value);
|
|
405
|
+
if (uri.origin !== origin || uri.username || uri.password || uri.hash) throw new PlatformAuthError("Device verification URI is outside the issuer", 400, { code: "INVALID_DEVICE_RESPONSE" });
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
deviceCode: body.device_code,
|
|
409
|
+
userCode: body.user_code,
|
|
410
|
+
verificationUri: body.verification_uri,
|
|
411
|
+
...isNonemptyString(body.verification_uri_complete) ? { verificationUriComplete: body.verification_uri_complete } : {},
|
|
412
|
+
expiresIn: body.expires_in,
|
|
413
|
+
expiresAt: startedAt + body.expires_in * 1e3,
|
|
414
|
+
interval: typeof body.interval === "number" ? body.interval : 5
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/** RFC 8628 pending/slow_down handling, expiry, cancellation and timeout backoff. */
|
|
418
|
+
async pollDeviceAuthorization(grant, options = {}) {
|
|
419
|
+
if (!grant.deviceCode || !Number.isFinite(grant.expiresAt) || !Number.isFinite(grant.interval) || grant.interval <= 0) throw new Error("PlatformOidcClient.pollDeviceAuthorization: invalid device grant");
|
|
420
|
+
let intervalMs = grant.interval * 1e3;
|
|
421
|
+
while (Date.now() < grant.expiresAt) {
|
|
422
|
+
options.signal?.throwIfAborted();
|
|
423
|
+
await pause(Math.min(intervalMs, Math.max(0, grant.expiresAt - Date.now())), options.signal);
|
|
424
|
+
const remaining = grant.expiresAt - Date.now();
|
|
425
|
+
if (remaining <= 0) break;
|
|
426
|
+
const timeout = AbortSignal.timeout(Math.max(1, Math.min(3e4, Math.floor(remaining))));
|
|
427
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
428
|
+
try {
|
|
429
|
+
return await this.token({
|
|
430
|
+
grant_type: DEVICE_GRANT,
|
|
431
|
+
device_code: grant.deviceCode
|
|
432
|
+
}, signal);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
options.signal?.throwIfAborted();
|
|
435
|
+
if (timeout.aborted) {
|
|
436
|
+
intervalMs *= 2;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (!(error instanceof PlatformAuthError) || !isRecord(error.body)) throw error;
|
|
440
|
+
if (error.body.error === "authorization_pending") continue;
|
|
441
|
+
if (error.body.error === "slow_down") {
|
|
442
|
+
intervalMs += 5e3;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
throw new PlatformAuthError("Device authorization expired", 400, { error: "expired_token" });
|
|
449
|
+
}
|
|
450
|
+
async token(params, signal) {
|
|
451
|
+
const res = await this.post(`${this.baseUrl}/api/auth/oauth2/token`, params, signal);
|
|
362
452
|
const body = await res.json().catch(() => null);
|
|
363
453
|
if (!res.ok) throw new PlatformAuthError(oauthError(body, `Platform token request failed (${res.status})`), res.status, body);
|
|
364
454
|
return parseTokens(body, res.status);
|
|
365
455
|
}
|
|
366
|
-
post(
|
|
456
|
+
post(url, params, signal, includeResources = true) {
|
|
367
457
|
const body = new URLSearchParams(params);
|
|
368
458
|
const headers = { "content-type": "application/x-www-form-urlencoded" };
|
|
369
|
-
if (this.clientSecret) headers.authorization = `Basic ${btoa(`${this.clientId}:${this.clientSecret}`)}`;
|
|
459
|
+
if (this.clientSecret) headers.authorization = `Basic ${btoa(`${formComponent(this.clientId)}:${formComponent(this.clientSecret)}`)}`;
|
|
370
460
|
else body.set("client_id", this.clientId);
|
|
371
|
-
|
|
461
|
+
if (includeResources) for (const resource of this.resources) body.append("resource", resource);
|
|
462
|
+
return this.fetchImpl(url, {
|
|
372
463
|
method: "POST",
|
|
373
464
|
headers,
|
|
374
|
-
body
|
|
465
|
+
body,
|
|
466
|
+
redirect: "error",
|
|
467
|
+
signal
|
|
375
468
|
});
|
|
376
469
|
}
|
|
377
470
|
};
|
package/dist/platform.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.js","names":["isRecord","isNonemptyString"],"sources":["../src/platform/auth.ts","../src/platform/integrations.ts","../src/platform/oidc.ts"],"sourcesContent":["/**\n * Server-side client for the Tangle platform's cross-site SSO bridge.\n *\n * Consumer apps (gtm-agent, tax-agent, legal-agent, creative-agent, …)\n * use this to:\n * 1. Build an /authorize URL that lands the user on id.tangle.tools\n * and brings them back with a single-use code.\n * 2. Exchange that code for an API key + the user's identity.\n *\n * The platform endpoint contract is documented in\n * `products/platform/api/src/routes/cross-site.ts`. This client only\n * speaks HTTP — no SDK weight, no transitive deps.\n */\n\nexport interface PlatformAuthClientOptions {\n /** Platform base URL, e.g. `https://id.tangle.tools`. */\n baseUrl: string\n /** App id as registered in the platform's TRUSTED_APPS registry. */\n appId: string\n /** Override the global fetch (useful for tests + edge runtimes). */\n fetchImpl?: typeof fetch\n}\n\nexport interface AuthorizeUrlOptions {\n /** Required CSRF token; the consumer verifies it on the callback. */\n state: string\n /**\n * Final redirect URI. Must be one of the URIs registered for `appId`\n * on the platform. Omit to use the first registered URI.\n */\n redirectUri?: string\n /** Force the login screen even if a session is already active. */\n prompt?: 'login'\n /** Pre-fill the email field on the login screen. */\n email?: string\n}\n\nexport interface ExchangeCodeResult {\n apiKey: string\n emailVerified: true\n user: {\n id: string\n email: string\n name?: string | null\n }\n /** Null when the platform could not provide a subscription. This is not a paid-access grant. */\n plan: {\n tier: string\n } | null\n}\n\n/** Thrown when a `PlatformAuthClient` request returns a non-success status. */\nexport class PlatformAuthError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly body: unknown,\n ) {\n super(message)\n this.name = 'PlatformAuthError'\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction isNonemptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0\n}\n\n/** Validate the platform's verified identity before consumers create a local session. */\nfunction parseExchangeResult(body: unknown, status: number): ExchangeCodeResult {\n const invalid = (): never => {\n // A successful but malformed response can contain the one-time API secret.\n throw new PlatformAuthError(\n 'Platform exchange response has no valid verified identity',\n status,\n { code: 'INVALID_EXCHANGE_RESPONSE' },\n )\n }\n if (\n !isRecord(body) ||\n !isNonemptyString(body.apiKey) ||\n body.emailVerified !== true ||\n !isRecord(body.user)\n )\n return invalid()\n const user = body.user\n if (!isNonemptyString(user.id) || !isNonemptyString(user.email)) return invalid()\n const email = user.email.trim()\n if (\n email.length > 320 ||\n !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email) ||\n /(?:@users\\.noreply\\.tangle\\.tools$|^0x[a-f0-9]{40}@tangle\\.tools$)/i.test(email)\n )\n return invalid()\n if (user.name !== undefined && user.name !== null && typeof user.name !== 'string')\n return invalid()\n let plan: ExchangeCodeResult['plan'] = null\n if (body.subscription !== undefined) {\n if (!isRecord(body.subscription) || !isNonemptyString(body.subscription.plan)) return invalid()\n plan = { tier: body.subscription.plan }\n }\n return {\n apiKey: body.apiKey,\n emailVerified: true,\n user: { id: user.id, email, ...(user.name !== undefined ? { name: user.name } : {}) },\n plan,\n }\n}\n\n/** HTTP client for the Tangle Platform SSO: builds authorize URLs and exchanges auth codes for API keys. */\nexport class PlatformAuthClient {\n private readonly baseUrl: string\n private readonly appId: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: PlatformAuthClientOptions) {\n if (!options.baseUrl) throw new Error('PlatformAuthClient: baseUrl is required')\n if (!options.appId) throw new Error('PlatformAuthClient: appId is required')\n this.baseUrl = options.baseUrl.replace(/\\/+$/, '')\n this.appId = options.appId\n this.fetchImpl =\n options.fetchImpl ??\n ((url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => fetch(url, init))\n }\n\n /**\n * Build the URL the user is redirected to in order to start SSO.\n * The platform redirects back to one of `appId`'s registered\n * `redirectUris` with `?code=...&app=...&state=...`.\n */\n authorizeUrl(options: AuthorizeUrlOptions): string {\n if (!options.state) {\n throw new Error('PlatformAuthClient.authorizeUrl: state is required for CSRF')\n }\n const url = new URL('/cross-site/authorize', this.baseUrl)\n url.searchParams.set('app', this.appId)\n url.searchParams.set('state', options.state)\n if (options.redirectUri) url.searchParams.set('redirect', options.redirectUri)\n if (options.prompt) url.searchParams.set('prompt', options.prompt)\n if (options.email) url.searchParams.set('email', options.email)\n return url.toString()\n }\n\n /**\n * Exchange a single-use auth code (delivered to the consumer's\n * callback by the platform) for an API key + the user's identity.\n * Codes are single-use and expire ~5 minutes after issue.\n */\n async exchange(code: string): Promise<ExchangeCodeResult> {\n if (!code) throw new Error('PlatformAuthClient.exchange: code is required')\n const res = await this.fetchImpl(`${this.baseUrl}/cross-site/exchange`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ code, app: this.appId }),\n })\n const body = await res.json().catch(() => null)\n if (!res.ok) {\n const message =\n body && typeof body === 'object' && 'error' in body && typeof body.error === 'string'\n ? body.error\n : `Platform exchange failed (${res.status})`\n throw new PlatformAuthError(message, res.status, body)\n }\n return parseExchangeResult(body, res.status)\n }\n}\n","/**\n * Server-side client for the Tangle platform's integration hub\n * (`/v1/hub/*`). Consumer apps use this instead of rolling their own\n * OAuth + connection tables.\n *\n * Auth: the caller supplies a bearer (either the user's API key from\n * cross-site exchange, or a platform service token) on construction.\n *\n * Endpoint contract (authoritative): the platform's `src/lib/hub-contract.ts`\n * + `src/routes/hub.ts`. The platform wraps every response in\n * `{ success, data }`; non-2xx or `success:false` surfaces as `PlatformHubError`\n * carrying the real upstream status.\n */\n\nexport interface PlatformHubClientOptions {\n /** Platform base URL, e.g. `https://id.tangle.tools`. */\n baseUrl: string\n /** Bearer credential — user API key or service token. */\n bearer: string\n /** Override fetch (tests + edge runtimes). */\n fetchImpl?: typeof fetch\n}\n\n/** A live integration connection, as returned by `/v1/hub/connections`. */\nexport interface PlatformConnection {\n id: string\n providerId: string\n displayName: string\n accountDisplay: string | null\n scopes: string[]\n status: 'active' | 'revoked' | 'unhealthy' | 'reconnect_required' | (string & {})\n health: 'unknown' | 'healthy' | 'unhealthy' | 'rate_limited' | (string & {})\n createdAt: string\n updatedAt: string\n lastUsedAt: string | null\n}\n\n/** A connectable provider in the catalog (`/v1/hub/providers`). */\nexport interface PlatformCatalogProvider {\n providerId: string\n title?: string\n authKind?: string\n category?: string\n scopes?: string[]\n capabilityCount?: number\n native?: boolean\n /** Whether the OAuth app's credentials are wired — the UI offers Connect\n * only when true. */\n configured?: boolean\n [k: string]: unknown\n}\n\nexport interface CatalogResult {\n providers: PlatformCatalogProvider[]\n /** Count of substrate-bundled connectors behind the catalog. */\n substrateBundled?: number\n [k: string]: unknown\n}\n\nexport interface StartAuthInput {\n /** The provider to connect (goes in the URL path). */\n providerId: string\n /** Accepted for interface compatibility; the platform's start endpoint is\n * provider-level and does not consume a connector id. */\n connectorId?: string\n /** Where the platform redirects the user back to after OAuth. */\n returnUrl: string\n /** Accepted for interface compatibility; not consumed by the start endpoint. */\n requestedScopes?: string[]\n /** CLI flow flag — affects the platform's post-auth redirect handling. */\n cli?: boolean\n}\n\nexport interface StartAuthResult {\n /** The URL to send the user to. Normalized across the platform's two start\n * branches: github returns `authorizationUrl`, substrate returns\n * `redirectUrl`. */\n authorizationUrl: string\n state: string\n expiresAt?: string\n scopes?: string[]\n}\n\nexport interface ConnectionHealth {\n status: 'unknown' | 'healthy' | 'unhealthy' | 'rate_limited' | (string & {})\n checkedAt: string\n error?: { code: string; message: string }\n}\n\nexport interface ConnectionHealthResult {\n connection: PlatformConnection\n health: ConnectionHealth\n}\n\n/** Last-known health for a connection, derived from the connection row. */\nexport interface HealthCheck {\n connectionId: string\n providerId: string\n /** Mirrors `PlatformConnection.health`. */\n status: ConnectionHealth['status']\n checkedAt?: string\n}\n\nexport interface MintTokenInput {\n /** The hub action the token authorizes (e.g. `slack.chat.postMessage`). */\n actionPath: string\n /** Bind to a specific connection, or … */\n connectionId?: string\n /** … resolve the connection by provider for the calling user. */\n provider?: string\n}\n\nexport interface MintTokenResult {\n tokenId: string\n token: string\n expiresAt: string\n}\n\nexport interface ExecInput {\n /** The hub action path to execute. */\n path: string\n input?: unknown\n connectionId?: string\n}\n\nexport interface PlatformHubStatus {\n contract?: unknown\n principal: { kind: string; userId: string; [k: string]: unknown }\n connections: { connectedProviderCount: number; unhealthyProviderCount: number }\n}\n\n/** Thrown when a `PlatformHubClient` request returns a non-success status. */\nexport class PlatformHubError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly code: string | undefined,\n public readonly body: unknown,\n ) {\n super(message)\n this.name = 'PlatformHubError'\n }\n}\n\ninterface PlatformEnvelope<T> {\n success: boolean\n data?: T\n error?: { code?: string; message?: string } | string\n}\n\n/** HTTP client for the Tangle Platform Hub API: provider catalog, connection flow, and status. */\nexport class PlatformHubClient {\n private readonly baseUrl: string\n private readonly bearer: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: PlatformHubClientOptions) {\n if (!options.baseUrl) throw new Error('PlatformHubClient: baseUrl is required')\n if (!options.bearer) throw new Error('PlatformHubClient: bearer is required')\n this.baseUrl = options.baseUrl.replace(/\\/+$/, '')\n this.bearer = options.bearer\n this.fetchImpl =\n options.fetchImpl ??\n ((url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => fetch(url, init))\n }\n\n /** GET /v1/hub/providers — the connectable provider catalog. */\n catalog(): Promise<CatalogResult> {\n return this.request('GET', '/v1/hub/providers')\n }\n\n /** GET /v1/hub/connections — the calling user's live connections. */\n async listConnections(): Promise<PlatformConnection[]> {\n const data = await this.request<{ connections: PlatformConnection[] }>(\n 'GET',\n '/v1/hub/connections',\n )\n return data.connections\n }\n\n /** DELETE /v1/hub/connections/:connectionId — revoke + disable a connection. */\n revokeConnection(connectionId: string): Promise<{ connection: PlatformConnection }> {\n return this.request('DELETE', `/v1/hub/connections/${encodeURIComponent(connectionId)}`)\n }\n\n /**\n * POST /v1/hub/connections/:provider/start — begin OAuth/grant. The provider\n * is taken from the URL; the body carries `returnUrl` (+ `cli`). The platform's\n * two start branches name the URL field differently (github → `authorizationUrl`,\n * substrate → `redirectUrl`); this normalizes to `authorizationUrl`.\n */\n async startAuth(input: StartAuthInput): Promise<StartAuthResult> {\n const body: { returnUrl: string; cli?: boolean } = { returnUrl: input.returnUrl }\n if (input.cli !== undefined) body.cli = input.cli\n const data = await this.request<{\n authorizationUrl?: string\n redirectUrl?: string\n state: string\n expiresAt?: string\n scopes?: string[]\n }>('POST', `/v1/hub/connections/${encodeURIComponent(input.providerId)}/start`, body)\n const authorizationUrl = data.authorizationUrl ?? data.redirectUrl\n if (!authorizationUrl) {\n throw new PlatformHubError(\n 'Platform hub start response missing an authorization URL',\n 502,\n 'HUB_INVALID_START_RESPONSE',\n data,\n )\n }\n return { authorizationUrl, state: data.state, expiresAt: data.expiresAt, scopes: data.scopes }\n }\n\n /**\n * Last-known health for every connection. The platform has no global\n * healthcheck listing — health rides on each connection row — so this derives\n * the list from `listConnections()` (one request, no extra round-trips).\n */\n async listHealthchecks(): Promise<HealthCheck[]> {\n const connections = await this.listConnections()\n return connections.map((c) => ({\n connectionId: c.id,\n providerId: c.providerId,\n status: c.health,\n checkedAt: c.updatedAt,\n }))\n }\n\n /**\n * POST /v1/hub/connections/:connectionId/health — trigger a fresh health\n * probe for one connection and return its updated state.\n */\n checkConnectionHealth(connectionId: string): Promise<ConnectionHealthResult> {\n return this.request('POST', `/v1/hub/connections/${encodeURIComponent(connectionId)}/health`)\n }\n\n /**\n * Trigger a fresh health probe across all of the user's connections. The\n * platform exposes health per-connection only, so this fans out over\n * `listConnections()`. `scheduled` is the number of probes dispatched.\n */\n async runHealthchecks(): Promise<{ scheduled: number }> {\n const connections = await this.listConnections()\n await Promise.allSettled(connections.map((c) => this.checkConnectionHealth(c.id)))\n return { scheduled: connections.length }\n }\n\n /** GET /v1/hub/status — principal + aggregate connection counts. */\n status(): Promise<PlatformHubStatus> {\n return this.request('GET', '/v1/hub/status')\n }\n\n /**\n * POST /v1/hub/tokens — mint a short-lived, action-scoped capability token a\n * sandbox can use to invoke one hub action on the user's behalf without\n * seeing the underlying provider credential.\n */\n mintToken(input: MintTokenInput): Promise<MintTokenResult> {\n return this.request('POST', '/v1/hub/tokens', input)\n }\n\n /** POST /v1/hub/exec — execute a hub action and return its result. */\n async exec(input: ExecInput): Promise<unknown> {\n const data = await this.request<{ result: unknown }>('POST', '/v1/hub/exec', input)\n return data.result\n }\n\n private async request<T>(\n method: 'GET' | 'POST' | 'DELETE' | 'PUT',\n path: string,\n body?: unknown,\n ): Promise<T> {\n const headers: Record<string, string> = {\n authorization: `Bearer ${this.bearer}`,\n accept: 'application/json',\n }\n if (body !== undefined) headers['content-type'] = 'application/json'\n\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n })\n const text = await res.text()\n let parsed: PlatformEnvelope<T> | null = null\n if (text) {\n try {\n parsed = JSON.parse(text)\n } catch {\n // fall through to error handling below\n }\n }\n if (!res.ok || (parsed && parsed.success === false)) {\n const code = parsed?.error && typeof parsed.error === 'object' ? parsed.error.code : undefined\n const message =\n (parsed?.error && typeof parsed.error === 'object' && parsed.error.message) ||\n (typeof parsed?.error === 'string' ? parsed.error : `Platform hub error (${res.status})`)\n throw new PlatformHubError(message, res.status, code, parsed ?? text)\n }\n if (!parsed) {\n throw new PlatformHubError(\n `Platform hub returned non-JSON success (${res.status})`,\n res.status,\n undefined,\n text,\n )\n }\n if (parsed.data === undefined) {\n throw new PlatformHubError(\n 'Platform hub envelope missing `data`',\n res.status,\n undefined,\n parsed,\n )\n }\n return parsed.data\n }\n}\n","/**\n * Server-side OpenID Connect client for the Tangle authorization server\n * (Better Auth `oauthProvider`, mounted at `/api/auth/oauth2/*`).\n *\n * This is the standard authorization-code + S256 PKCE path. It returns OIDC\n * tokens and a verified identity, not the `sk-tan` API key that\n * {@link PlatformAuthClient} returns. Identity scopes do not authorize paid\n * platform calls, so a consumer that uses the legacy API key as its Hub\n * bearer must keep {@link PlatformAuthClient} until it no longer needs one.\n *\n * The client id and redirect URI must be registered in the platform's\n * `oauthClient` registry. The legacy `TRUSTED_APPS` registration does not\n * carry over.\n */\n\nimport { PlatformAuthError } from './auth.js'\n\nexport interface PlatformOidcClientOptions {\n /** Platform base URL, e.g. `https://id.tangle.tools`. */\n baseUrl: string\n /** Client id from the platform `oauthClient` registry. */\n clientId: string\n /**\n * Client secret for a confidential client. Sent with HTTP Basic, the\n * provider's default `client_secret_basic` method. Omit for a public client\n * registered with `token_endpoint_auth_method: none`.\n */\n clientSecret?: string\n /** Registered callback URI. */\n redirectUri: string\n /** Requested scopes. Defaults to `openid profile email offline_access`. */\n scope?: string\n /** Override the global fetch (useful for tests + edge runtimes). */\n fetchImpl?: typeof fetch\n}\n\nexport interface OidcAuthorizeUrlOptions {\n /** Required CSRF token; the consumer verifies it on the callback. */\n state: string\n /** RFC 7636 S256 code challenge. See {@link createPkcePair}. */\n codeChallenge: string\n /** OIDC nonce; the consumer checks it against the ID token. */\n nonce?: string\n prompt?: 'login' | 'consent' | 'none'\n /** Pre-fill the email field on the login screen. */\n loginHint?: string\n}\n\nexport interface OidcTokens {\n accessToken: string\n tokenType: string\n expiresIn?: number\n refreshToken?: string\n idToken?: string\n scope?: string\n}\n\nexport interface OidcUser {\n id: string\n email: string\n emailVerified: true\n name?: string\n}\n\nexport interface OidcExchangeResult {\n tokens: OidcTokens\n user: OidcUser\n}\n\nconst DEFAULT_SCOPE = 'openid profile email offline_access'\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction isNonemptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let binary = ''\n for (const byte of bytes) binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\n/** Create an RFC 7636 verifier and its S256 challenge with Web Crypto. */\nexport async function createPkcePair(): Promise<{ verifier: string; challenge: string }> {\n const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)))\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))\n return { verifier, challenge: base64url(new Uint8Array(digest)) }\n}\n\nfunction oauthError(body: unknown, fallback: string): string {\n if (!isRecord(body)) return fallback\n if (isNonemptyString(body.error_description)) return body.error_description\n if (isNonemptyString(body.error)) return body.error\n return fallback\n}\n\nfunction parseTokens(body: unknown, status: number): OidcTokens {\n if (!isRecord(body) || !isNonemptyString(body.access_token)) {\n // A malformed success response may still hold a secret; never echo it.\n throw new PlatformAuthError('Platform token response is malformed', status, {\n code: 'INVALID_TOKEN_RESPONSE',\n })\n }\n return {\n accessToken: body.access_token,\n tokenType: isNonemptyString(body.token_type) ? body.token_type : 'Bearer',\n ...(typeof body.expires_in === 'number' ? { expiresIn: body.expires_in } : {}),\n ...(isNonemptyString(body.refresh_token) ? { refreshToken: body.refresh_token } : {}),\n ...(isNonemptyString(body.id_token) ? { idToken: body.id_token } : {}),\n ...(isNonemptyString(body.scope) ? { scope: body.scope } : {}),\n }\n}\n\n/** Standard OIDC authorization-code + PKCE client for \"Sign in with Tangle\". */\nexport class PlatformOidcClient {\n private readonly baseUrl: string\n private readonly clientId: string\n private readonly clientSecret: string | undefined\n private readonly redirectUri: string\n private readonly scope: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: PlatformOidcClientOptions) {\n if (!options.baseUrl) throw new Error('PlatformOidcClient: baseUrl is required')\n if (!options.clientId) throw new Error('PlatformOidcClient: clientId is required')\n if (!options.redirectUri) throw new Error('PlatformOidcClient: redirectUri is required')\n this.baseUrl = options.baseUrl.replace(/\\/+$/, '')\n this.clientId = options.clientId\n this.clientSecret = options.clientSecret || undefined\n this.redirectUri = options.redirectUri\n this.scope = options.scope ?? DEFAULT_SCOPE\n // Arrow wrapper: an unbound global fetch throws \"Illegal invocation\" on Workers.\n this.fetchImpl =\n options.fetchImpl ??\n ((url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => fetch(url, init))\n }\n\n /** Build the `/api/auth/oauth2/authorize` URL for a browser redirect. */\n authorizeUrl(options: OidcAuthorizeUrlOptions): string {\n if (!options.state)\n throw new Error('PlatformOidcClient.authorizeUrl: state is required for CSRF')\n if (!options.codeChallenge) {\n throw new Error('PlatformOidcClient.authorizeUrl: codeChallenge is required for PKCE')\n }\n const url = new URL('/api/auth/oauth2/authorize', this.baseUrl)\n url.searchParams.set('client_id', this.clientId)\n url.searchParams.set('redirect_uri', this.redirectUri)\n url.searchParams.set('response_type', 'code')\n url.searchParams.set('scope', this.scope)\n url.searchParams.set('state', options.state)\n url.searchParams.set('code_challenge', options.codeChallenge)\n url.searchParams.set('code_challenge_method', 'S256')\n if (options.nonce) url.searchParams.set('nonce', options.nonce)\n if (options.prompt) url.searchParams.set('prompt', options.prompt)\n if (options.loginHint) url.searchParams.set('login_hint', options.loginHint)\n return url.toString()\n }\n\n /** Exchange the callback code, then resolve the verified identity from userinfo. */\n async exchange(code: string, codeVerifier: string): Promise<OidcExchangeResult> {\n if (!code) throw new Error('PlatformOidcClient.exchange: code is required')\n if (!codeVerifier)\n throw new Error('PlatformOidcClient.exchange: codeVerifier is required for PKCE')\n const tokens = await this.token({\n grant_type: 'authorization_code',\n code,\n redirect_uri: this.redirectUri,\n code_verifier: codeVerifier,\n })\n return { tokens, user: await this.userinfo(tokens.accessToken) }\n }\n\n /** Redeem a refresh token. Store the returned refresh token; the provider rotates it. */\n async refresh(refreshToken: string): Promise<OidcTokens> {\n if (!refreshToken) throw new Error('PlatformOidcClient.refresh: refreshToken is required')\n return this.token({ grant_type: 'refresh_token', refresh_token: refreshToken })\n }\n\n /** Read the verified identity behind an access token. */\n async userinfo(accessToken: string): Promise<OidcUser> {\n const res = await this.fetchImpl(`${this.baseUrl}/api/auth/oauth2/userinfo`, {\n headers: { authorization: `Bearer ${accessToken}` },\n })\n const body: unknown = await res.json().catch(() => null)\n if (!res.ok) {\n throw new PlatformAuthError(\n oauthError(body, `Platform userinfo failed (${res.status})`),\n res.status,\n body,\n )\n }\n if (\n !isRecord(body) ||\n !isNonemptyString(body.sub) ||\n !isNonemptyString(body.email) ||\n body.email_verified !== true\n ) {\n throw new PlatformAuthError('Platform userinfo has no verified identity', res.status, {\n code: 'INVALID_USERINFO_RESPONSE',\n })\n }\n return {\n id: body.sub,\n email: body.email,\n emailVerified: true,\n ...(isNonemptyString(body.name) ? { name: body.name } : {}),\n }\n }\n\n /** Revoke an access or refresh token (RFC 7009). Disconnect revokes the refresh token. */\n async revoke(token: string, tokenTypeHint?: 'access_token' | 'refresh_token'): Promise<void> {\n if (!token) throw new Error('PlatformOidcClient.revoke: token is required')\n const res = await this.post('/api/auth/oauth2/revoke', {\n token,\n ...(tokenTypeHint ? { token_type_hint: tokenTypeHint } : {}),\n })\n if (!res.ok) {\n const body: unknown = await res.json().catch(() => null)\n throw new PlatformAuthError(\n oauthError(body, `Platform revoke failed (${res.status})`),\n res.status,\n body,\n )\n }\n }\n\n private async token(params: Record<string, string>): Promise<OidcTokens> {\n const res = await this.post('/api/auth/oauth2/token', params)\n const body: unknown = await res.json().catch(() => null)\n if (!res.ok) {\n throw new PlatformAuthError(\n oauthError(body, `Platform token request failed (${res.status})`),\n res.status,\n body,\n )\n }\n return parseTokens(body, res.status)\n }\n\n private post(path: string, params: Record<string, string>): Promise<Response> {\n const body = new URLSearchParams(params)\n const headers: Record<string, string> = { 'content-type': 'application/x-www-form-urlencoded' }\n if (this.clientSecret) {\n headers.authorization = `Basic ${btoa(`${this.clientId}:${this.clientSecret}`)}`\n } else {\n body.set('client_id', this.clientId)\n }\n return this.fetchImpl(`${this.baseUrl}${path}`, { method: 'POST', headers, body })\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"platform.js","names":["isRecord","isNonemptyString"],"sources":["../src/platform/auth.ts","../src/platform/integrations.ts","../src/platform/oidc.ts"],"sourcesContent":["/**\n * Server-side client for the Tangle platform's cross-site SSO bridge.\n *\n * Consumer apps (gtm-agent, tax-agent, legal-agent, creative-agent, …)\n * use this to:\n * 1. Build an /authorize URL that lands the user on id.tangle.tools\n * and brings them back with a single-use code.\n * 2. Exchange that code for an API key + the user's identity.\n *\n * The platform endpoint contract is documented in\n * `products/platform/api/src/routes/cross-site.ts`. This client only\n * speaks HTTP — no SDK weight, no transitive deps.\n */\n\nexport interface PlatformAuthClientOptions {\n /** Platform base URL, e.g. `https://id.tangle.tools`. */\n baseUrl: string\n /** App id as registered in the platform's TRUSTED_APPS registry. */\n appId: string\n /** Override the global fetch (useful for tests + edge runtimes). */\n fetchImpl?: typeof fetch\n}\n\nexport interface AuthorizeUrlOptions {\n /** Required CSRF token; the consumer verifies it on the callback. */\n state: string\n /**\n * Final redirect URI. Must be one of the URIs registered for `appId`\n * on the platform. Omit to use the first registered URI.\n */\n redirectUri?: string\n /** Force the login screen even if a session is already active. */\n prompt?: 'login'\n /** Pre-fill the email field on the login screen. */\n email?: string\n}\n\nexport interface ExchangeCodeResult {\n apiKey: string\n emailVerified: true\n user: {\n id: string\n email: string\n name?: string | null\n }\n /** Null when the platform could not provide a subscription. This is not a paid-access grant. */\n plan: {\n tier: string\n } | null\n}\n\n/** Thrown when a `PlatformAuthClient` request returns a non-success status. */\nexport class PlatformAuthError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly body: unknown,\n ) {\n super(message)\n this.name = 'PlatformAuthError'\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction isNonemptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0\n}\n\n/** Validate the platform's verified identity before consumers create a local session. */\nfunction parseExchangeResult(body: unknown, status: number): ExchangeCodeResult {\n const invalid = (): never => {\n // A successful but malformed response can contain the one-time API secret.\n throw new PlatformAuthError(\n 'Platform exchange response has no valid verified identity',\n status,\n { code: 'INVALID_EXCHANGE_RESPONSE' },\n )\n }\n if (\n !isRecord(body) ||\n !isNonemptyString(body.apiKey) ||\n body.emailVerified !== true ||\n !isRecord(body.user)\n )\n return invalid()\n const user = body.user\n if (!isNonemptyString(user.id) || !isNonemptyString(user.email)) return invalid()\n const email = user.email.trim()\n if (\n email.length > 320 ||\n !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email) ||\n /(?:@users\\.noreply\\.tangle\\.tools$|^0x[a-f0-9]{40}@tangle\\.tools$)/i.test(email)\n )\n return invalid()\n if (user.name !== undefined && user.name !== null && typeof user.name !== 'string')\n return invalid()\n let plan: ExchangeCodeResult['plan'] = null\n if (body.subscription !== undefined) {\n if (!isRecord(body.subscription) || !isNonemptyString(body.subscription.plan)) return invalid()\n plan = { tier: body.subscription.plan }\n }\n return {\n apiKey: body.apiKey,\n emailVerified: true,\n user: { id: user.id, email, ...(user.name !== undefined ? { name: user.name } : {}) },\n plan,\n }\n}\n\n/** HTTP client for the Tangle Platform SSO: builds authorize URLs and exchanges auth codes for API keys. */\nexport class PlatformAuthClient {\n private readonly baseUrl: string\n private readonly appId: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: PlatformAuthClientOptions) {\n if (!options.baseUrl) throw new Error('PlatformAuthClient: baseUrl is required')\n if (!options.appId) throw new Error('PlatformAuthClient: appId is required')\n this.baseUrl = options.baseUrl.replace(/\\/+$/, '')\n this.appId = options.appId\n this.fetchImpl =\n options.fetchImpl ??\n ((url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => fetch(url, init))\n }\n\n /**\n * Build the URL the user is redirected to in order to start SSO.\n * The platform redirects back to one of `appId`'s registered\n * `redirectUris` with `?code=...&app=...&state=...`.\n */\n authorizeUrl(options: AuthorizeUrlOptions): string {\n if (!options.state) {\n throw new Error('PlatformAuthClient.authorizeUrl: state is required for CSRF')\n }\n const url = new URL('/cross-site/authorize', this.baseUrl)\n url.searchParams.set('app', this.appId)\n url.searchParams.set('state', options.state)\n if (options.redirectUri) url.searchParams.set('redirect', options.redirectUri)\n if (options.prompt) url.searchParams.set('prompt', options.prompt)\n if (options.email) url.searchParams.set('email', options.email)\n return url.toString()\n }\n\n /**\n * Exchange a single-use auth code (delivered to the consumer's\n * callback by the platform) for an API key + the user's identity.\n * Codes are single-use and expire ~5 minutes after issue.\n */\n async exchange(code: string): Promise<ExchangeCodeResult> {\n if (!code) throw new Error('PlatformAuthClient.exchange: code is required')\n const res = await this.fetchImpl(`${this.baseUrl}/cross-site/exchange`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ code, app: this.appId }),\n })\n const body = await res.json().catch(() => null)\n if (!res.ok) {\n const message =\n body && typeof body === 'object' && 'error' in body && typeof body.error === 'string'\n ? body.error\n : `Platform exchange failed (${res.status})`\n throw new PlatformAuthError(message, res.status, body)\n }\n return parseExchangeResult(body, res.status)\n }\n}\n","/**\n * Server-side client for the Tangle platform's integration hub\n * (`/v1/hub/*`). Consumer apps use this instead of rolling their own\n * OAuth + connection tables.\n *\n * Auth: the caller supplies a bearer (either the user's API key from\n * cross-site exchange, or a platform service token) on construction.\n *\n * Endpoint contract (authoritative): the platform's `src/lib/hub-contract.ts`\n * + `src/routes/hub.ts`. The platform wraps every response in\n * `{ success, data }`; non-2xx or `success:false` surfaces as `PlatformHubError`\n * carrying the real upstream status.\n */\n\nexport interface PlatformHubClientOptions {\n /** Platform base URL, e.g. `https://id.tangle.tools`. */\n baseUrl: string\n /** Bearer credential — user API key or service token. */\n bearer: string\n /** Override fetch (tests + edge runtimes). */\n fetchImpl?: typeof fetch\n}\n\n/** A live integration connection, as returned by `/v1/hub/connections`. */\nexport interface PlatformConnection {\n id: string\n providerId: string\n displayName: string\n accountDisplay: string | null\n scopes: string[]\n status: 'active' | 'revoked' | 'unhealthy' | 'reconnect_required' | (string & {})\n health: 'unknown' | 'healthy' | 'unhealthy' | 'rate_limited' | (string & {})\n createdAt: string\n updatedAt: string\n lastUsedAt: string | null\n}\n\n/** A connectable provider in the catalog (`/v1/hub/providers`). */\nexport interface PlatformCatalogProvider {\n providerId: string\n title?: string\n authKind?: string\n category?: string\n scopes?: string[]\n capabilityCount?: number\n native?: boolean\n /** Whether the OAuth app's credentials are wired — the UI offers Connect\n * only when true. */\n configured?: boolean\n [k: string]: unknown\n}\n\nexport interface CatalogResult {\n providers: PlatformCatalogProvider[]\n /** Count of substrate-bundled connectors behind the catalog. */\n substrateBundled?: number\n [k: string]: unknown\n}\n\nexport interface StartAuthInput {\n /** The provider to connect (goes in the URL path). */\n providerId: string\n /** Accepted for interface compatibility; the platform's start endpoint is\n * provider-level and does not consume a connector id. */\n connectorId?: string\n /** Where the platform redirects the user back to after OAuth. */\n returnUrl: string\n /** Accepted for interface compatibility; not consumed by the start endpoint. */\n requestedScopes?: string[]\n /** CLI flow flag — affects the platform's post-auth redirect handling. */\n cli?: boolean\n}\n\nexport interface StartAuthResult {\n /** The URL to send the user to. Normalized across the platform's two start\n * branches: github returns `authorizationUrl`, substrate returns\n * `redirectUrl`. */\n authorizationUrl: string\n state: string\n expiresAt?: string\n scopes?: string[]\n}\n\nexport interface ConnectionHealth {\n status: 'unknown' | 'healthy' | 'unhealthy' | 'rate_limited' | (string & {})\n checkedAt: string\n error?: { code: string; message: string }\n}\n\nexport interface ConnectionHealthResult {\n connection: PlatformConnection\n health: ConnectionHealth\n}\n\n/** Last-known health for a connection, derived from the connection row. */\nexport interface HealthCheck {\n connectionId: string\n providerId: string\n /** Mirrors `PlatformConnection.health`. */\n status: ConnectionHealth['status']\n checkedAt?: string\n}\n\nexport interface MintTokenInput {\n /** The hub action the token authorizes (e.g. `slack.chat.postMessage`). */\n actionPath: string\n /** Bind to a specific connection, or … */\n connectionId?: string\n /** … resolve the connection by provider for the calling user. */\n provider?: string\n}\n\nexport interface MintTokenResult {\n tokenId: string\n token: string\n expiresAt: string\n}\n\nexport interface ExecInput {\n /** The hub action path to execute. */\n path: string\n input?: unknown\n connectionId?: string\n}\n\nexport interface PlatformHubStatus {\n contract?: unknown\n principal: { kind: string; userId: string; [k: string]: unknown }\n connections: { connectedProviderCount: number; unhealthyProviderCount: number }\n}\n\n/** Thrown when a `PlatformHubClient` request returns a non-success status. */\nexport class PlatformHubError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly code: string | undefined,\n public readonly body: unknown,\n ) {\n super(message)\n this.name = 'PlatformHubError'\n }\n}\n\ninterface PlatformEnvelope<T> {\n success: boolean\n data?: T\n error?: { code?: string; message?: string } | string\n}\n\n/** HTTP client for the Tangle Platform Hub API: provider catalog, connection flow, and status. */\nexport class PlatformHubClient {\n private readonly baseUrl: string\n private readonly bearer: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: PlatformHubClientOptions) {\n if (!options.baseUrl) throw new Error('PlatformHubClient: baseUrl is required')\n if (!options.bearer) throw new Error('PlatformHubClient: bearer is required')\n this.baseUrl = options.baseUrl.replace(/\\/+$/, '')\n this.bearer = options.bearer\n this.fetchImpl =\n options.fetchImpl ??\n ((url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => fetch(url, init))\n }\n\n /** GET /v1/hub/providers — the connectable provider catalog. */\n catalog(): Promise<CatalogResult> {\n return this.request('GET', '/v1/hub/providers')\n }\n\n /** GET /v1/hub/connections — the calling user's live connections. */\n async listConnections(): Promise<PlatformConnection[]> {\n const data = await this.request<{ connections: PlatformConnection[] }>(\n 'GET',\n '/v1/hub/connections',\n )\n return data.connections\n }\n\n /** DELETE /v1/hub/connections/:connectionId — revoke + disable a connection. */\n revokeConnection(connectionId: string): Promise<{ connection: PlatformConnection }> {\n return this.request('DELETE', `/v1/hub/connections/${encodeURIComponent(connectionId)}`)\n }\n\n /**\n * POST /v1/hub/connections/:provider/start — begin OAuth/grant. The provider\n * is taken from the URL; the body carries `returnUrl` (+ `cli`). The platform's\n * two start branches name the URL field differently (github → `authorizationUrl`,\n * substrate → `redirectUrl`); this normalizes to `authorizationUrl`.\n */\n async startAuth(input: StartAuthInput): Promise<StartAuthResult> {\n const body: { returnUrl: string; cli?: boolean } = { returnUrl: input.returnUrl }\n if (input.cli !== undefined) body.cli = input.cli\n const data = await this.request<{\n authorizationUrl?: string\n redirectUrl?: string\n state: string\n expiresAt?: string\n scopes?: string[]\n }>('POST', `/v1/hub/connections/${encodeURIComponent(input.providerId)}/start`, body)\n const authorizationUrl = data.authorizationUrl ?? data.redirectUrl\n if (!authorizationUrl) {\n throw new PlatformHubError(\n 'Platform hub start response missing an authorization URL',\n 502,\n 'HUB_INVALID_START_RESPONSE',\n data,\n )\n }\n return { authorizationUrl, state: data.state, expiresAt: data.expiresAt, scopes: data.scopes }\n }\n\n /**\n * Last-known health for every connection. The platform has no global\n * healthcheck listing — health rides on each connection row — so this derives\n * the list from `listConnections()` (one request, no extra round-trips).\n */\n async listHealthchecks(): Promise<HealthCheck[]> {\n const connections = await this.listConnections()\n return connections.map((c) => ({\n connectionId: c.id,\n providerId: c.providerId,\n status: c.health,\n checkedAt: c.updatedAt,\n }))\n }\n\n /**\n * POST /v1/hub/connections/:connectionId/health — trigger a fresh health\n * probe for one connection and return its updated state.\n */\n checkConnectionHealth(connectionId: string): Promise<ConnectionHealthResult> {\n return this.request('POST', `/v1/hub/connections/${encodeURIComponent(connectionId)}/health`)\n }\n\n /**\n * Trigger a fresh health probe across all of the user's connections. The\n * platform exposes health per-connection only, so this fans out over\n * `listConnections()`. `scheduled` is the number of probes dispatched.\n */\n async runHealthchecks(): Promise<{ scheduled: number }> {\n const connections = await this.listConnections()\n await Promise.allSettled(connections.map((c) => this.checkConnectionHealth(c.id)))\n return { scheduled: connections.length }\n }\n\n /** GET /v1/hub/status — principal + aggregate connection counts. */\n status(): Promise<PlatformHubStatus> {\n return this.request('GET', '/v1/hub/status')\n }\n\n /**\n * POST /v1/hub/tokens — mint a short-lived, action-scoped capability token a\n * sandbox can use to invoke one hub action on the user's behalf without\n * seeing the underlying provider credential.\n */\n mintToken(input: MintTokenInput): Promise<MintTokenResult> {\n return this.request('POST', '/v1/hub/tokens', input)\n }\n\n /** POST /v1/hub/exec — execute a hub action and return its result. */\n async exec(input: ExecInput): Promise<unknown> {\n const data = await this.request<{ result: unknown }>('POST', '/v1/hub/exec', input)\n return data.result\n }\n\n private async request<T>(\n method: 'GET' | 'POST' | 'DELETE' | 'PUT',\n path: string,\n body?: unknown,\n ): Promise<T> {\n const headers: Record<string, string> = {\n authorization: `Bearer ${this.bearer}`,\n accept: 'application/json',\n }\n if (body !== undefined) headers['content-type'] = 'application/json'\n\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n })\n const text = await res.text()\n let parsed: PlatformEnvelope<T> | null = null\n if (text) {\n try {\n parsed = JSON.parse(text)\n } catch {\n // fall through to error handling below\n }\n }\n if (!res.ok || (parsed && parsed.success === false)) {\n const code = parsed?.error && typeof parsed.error === 'object' ? parsed.error.code : undefined\n const message =\n (parsed?.error && typeof parsed.error === 'object' && parsed.error.message) ||\n (typeof parsed?.error === 'string' ? parsed.error : `Platform hub error (${res.status})`)\n throw new PlatformHubError(message, res.status, code, parsed ?? text)\n }\n if (!parsed) {\n throw new PlatformHubError(\n `Platform hub returned non-JSON success (${res.status})`,\n res.status,\n undefined,\n text,\n )\n }\n if (parsed.data === undefined) {\n throw new PlatformHubError(\n 'Platform hub envelope missing `data`',\n res.status,\n undefined,\n parsed,\n )\n }\n return parsed.data\n }\n}\n","/**\n * OpenID Connect client for the Tangle authorization server. Identity tokens\n * are not platform API keys. Resource scopes and audiences must be registered\n * by the provider before a consumer can use these tokens at a paid API.\n */\nimport { PlatformAuthError } from './auth.js'\n\nexport interface PlatformOidcClientOptions {\n baseUrl: string\n clientId: string\n /** Omit only for clients registered with token_endpoint_auth_method=none. */\n clientSecret?: string\n /** Required for authorization code; not required for the device grant. */\n redirectUri?: string\n scope?: string\n /** RFC 8707 resource identifiers registered at the authorization server. */\n resources?: readonly string[]\n fetchImpl?: typeof fetch\n}\n\nexport interface OidcAuthorizeUrlOptions {\n state: string\n codeChallenge: string\n /** Only supply a nonce when the consumer also validates the ID token. */\n nonce?: string\n prompt?: 'login' | 'consent' | 'none'\n loginHint?: string\n}\n\nexport interface OidcTokens {\n accessToken: string\n tokenType: string\n expiresIn?: number\n refreshToken?: string\n idToken?: string\n scope?: string\n}\n\nexport interface OidcUser {\n id: string\n email: string\n emailVerified: true\n name?: string\n}\n\nexport interface OidcExchangeResult {\n tokens: OidcTokens\n user: OidcUser\n}\n\n/** Keep deviceCode secret. Only userCode and the verification URLs are displayed. */\nexport interface OidcDeviceAuthorization {\n deviceCode: string\n userCode: string\n verificationUri: string\n verificationUriComplete?: string\n expiresIn: number\n /** Absolute deadline preserves expiry when a polling process resumes. */\n expiresAt: number\n interval: number\n}\n\nconst DEFAULT_SCOPE = 'openid profile email offline_access'\nconst DEVICE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code'\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction isNonemptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let binary = ''\n for (const byte of bytes) binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\n/** Create an RFC 7636 verifier and its S256 challenge with Web Crypto. */\nexport async function createPkcePair(): Promise<{ verifier: string; challenge: string }> {\n const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)))\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))\n return { verifier, challenge: base64url(new Uint8Array(digest)) }\n}\n\nfunction oauthError(body: unknown, fallback: string): string {\n if (!isRecord(body)) return fallback\n if (isNonemptyString(body.error_description)) return body.error_description\n if (isNonemptyString(body.error)) return body.error\n return fallback\n}\n\nfunction parseTokens(body: unknown, status: number): OidcTokens {\n if (!isRecord(body) || !isNonemptyString(body.access_token)) {\n throw new PlatformAuthError('Platform token response is malformed', status, {\n code: 'INVALID_TOKEN_RESPONSE',\n })\n }\n return {\n accessToken: body.access_token,\n tokenType: isNonemptyString(body.token_type) ? body.token_type : 'Bearer',\n ...(typeof body.expires_in === 'number' ? { expiresIn: body.expires_in } : {}),\n ...(isNonemptyString(body.refresh_token) ? { refreshToken: body.refresh_token } : {}),\n ...(isNonemptyString(body.id_token) ? { idToken: body.id_token } : {}),\n ...(isNonemptyString(body.scope) ? { scope: body.scope } : {}),\n }\n}\n\nfunction formComponent(value: string): string {\n return new URLSearchParams({ v: value }).toString().slice(2)\n}\n\nfunction pause(ms: number, signal?: AbortSignal): Promise<void> {\n signal?.throwIfAborted()\n return new Promise((resolve, reject) => {\n const finish = () => {\n signal?.removeEventListener('abort', abort)\n resolve()\n }\n const timer = setTimeout(finish, ms)\n const abort = () => {\n clearTimeout(timer)\n signal?.removeEventListener('abort', abort)\n reject(signal?.reason ?? new Error('Device authorization cancelled'))\n }\n signal?.addEventListener('abort', abort, { once: true })\n })\n}\n\n/** OIDC code + PKCE, refresh/revoke and discovery-bound RFC 8628 device grants. */\nexport class PlatformOidcClient {\n private readonly baseUrl: string\n private readonly clientId: string\n private readonly clientSecret: string | undefined\n private readonly redirectUri: string | undefined\n private readonly scope: string\n private readonly resources: readonly string[]\n private readonly fetchImpl: typeof fetch\n\n constructor(options: PlatformOidcClientOptions) {\n if (!options.baseUrl) throw new Error('PlatformOidcClient: baseUrl is required')\n if (!options.clientId) throw new Error('PlatformOidcClient: clientId is required')\n this.baseUrl = options.baseUrl.replace(/\\/+$/, '')\n this.clientId = options.clientId\n this.clientSecret = options.clientSecret || undefined\n this.redirectUri = options.redirectUri\n this.scope = options.scope ?? DEFAULT_SCOPE\n this.resources = [...new Set(options.resources ?? [])]\n for (const resource of this.resources) {\n const url = new URL(resource)\n if (url.hash || url.username || url.password) {\n throw new Error('PlatformOidcClient: resource must not contain a fragment or credentials')\n }\n }\n this.fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init))\n }\n\n authorizeUrl(options: OidcAuthorizeUrlOptions): string {\n if (!this.redirectUri)\n throw new Error('PlatformOidcClient: redirectUri is required for authorization code')\n if (!options.state)\n throw new Error('PlatformOidcClient.authorizeUrl: state is required for CSRF')\n if (!options.codeChallenge)\n throw new Error('PlatformOidcClient.authorizeUrl: codeChallenge is required for PKCE')\n const url = new URL('/api/auth/oauth2/authorize', this.baseUrl)\n url.searchParams.set('client_id', this.clientId)\n url.searchParams.set('redirect_uri', this.redirectUri)\n url.searchParams.set('response_type', 'code')\n url.searchParams.set('scope', this.scope)\n url.searchParams.set('state', options.state)\n url.searchParams.set('code_challenge', options.codeChallenge)\n url.searchParams.set('code_challenge_method', 'S256')\n for (const resource of this.resources) url.searchParams.append('resource', resource)\n if (options.nonce) url.searchParams.set('nonce', options.nonce)\n if (options.prompt) url.searchParams.set('prompt', options.prompt)\n if (options.loginHint) url.searchParams.set('login_hint', options.loginHint)\n return url.toString()\n }\n\n async exchange(code: string, codeVerifier: string): Promise<OidcExchangeResult> {\n if (!this.redirectUri)\n throw new Error('PlatformOidcClient: redirectUri is required for authorization code')\n if (!code) throw new Error('PlatformOidcClient.exchange: code is required')\n if (!codeVerifier)\n throw new Error('PlatformOidcClient.exchange: codeVerifier is required for PKCE')\n const tokens = await this.token({\n grant_type: 'authorization_code',\n code,\n redirect_uri: this.redirectUri,\n code_verifier: codeVerifier,\n })\n return { tokens, user: await this.userinfo(tokens.accessToken) }\n }\n\n /** Save the replacement refresh token. Never retry an old token as a new grant. */\n async refresh(refreshToken: string): Promise<OidcTokens> {\n if (!refreshToken) throw new Error('PlatformOidcClient.refresh: refreshToken is required')\n return this.token({ grant_type: 'refresh_token', refresh_token: refreshToken })\n }\n\n async userinfo(accessToken: string): Promise<OidcUser> {\n if (!accessToken) throw new Error('PlatformOidcClient.userinfo: accessToken is required')\n const res = await this.fetchImpl(`${this.baseUrl}/api/auth/oauth2/userinfo`, {\n headers: { authorization: `Bearer ${accessToken}` },\n redirect: 'error',\n })\n const body: unknown = await res.json().catch(() => null)\n if (!res.ok)\n throw new PlatformAuthError(\n oauthError(body, `Platform userinfo failed (${res.status})`),\n res.status,\n body,\n )\n if (\n !isRecord(body) ||\n !isNonemptyString(body.sub) ||\n !isNonemptyString(body.email) ||\n body.email_verified !== true\n ) {\n throw new PlatformAuthError('Platform userinfo has no verified identity', res.status, {\n code: 'INVALID_USERINFO_RESPONSE',\n })\n }\n return {\n id: body.sub,\n email: body.email,\n emailVerified: true,\n ...(isNonemptyString(body.name) ? { name: body.name } : {}),\n }\n }\n\n /** RFC 7009. A local logout alone is not a provider disconnect. */\n async revoke(token: string, tokenTypeHint?: 'access_token' | 'refresh_token'): Promise<void> {\n if (!token) throw new Error('PlatformOidcClient.revoke: token is required')\n const res = await this.post(\n `${this.baseUrl}/api/auth/oauth2/revoke`,\n {\n token,\n ...(tokenTypeHint ? { token_type_hint: tokenTypeHint } : {}),\n },\n undefined,\n false,\n )\n if (!res.ok) {\n const body: unknown = await res.json().catch(() => null)\n throw new PlatformAuthError(\n oauthError(body, `Platform revoke failed (${res.status})`),\n res.status,\n body,\n )\n }\n }\n\n /**\n * Start a device grant only when this issuer advertises the OAuth-integrated\n * device endpoint. No fallback to /cross-site/device or a standalone session\n * device endpoint: those issue different credentials.\n */\n async deviceAuthorize(options: { signal?: AbortSignal } = {}): Promise<OidcDeviceAuthorization> {\n const discovery = await this.fetchImpl(\n `${this.baseUrl}/api/auth/.well-known/openid-configuration`,\n {\n redirect: 'error',\n signal: options.signal,\n },\n )\n const metadata: unknown = await discovery.json().catch(() => null)\n const issuer = `${this.baseUrl}/api/auth`\n if (\n !discovery.ok ||\n !isRecord(metadata) ||\n metadata.issuer !== issuer ||\n metadata.token_endpoint !== `${issuer}/oauth2/token` ||\n !Array.isArray(metadata.grant_types_supported) ||\n !metadata.grant_types_supported.includes(DEVICE_GRANT) ||\n !isNonemptyString(metadata.device_authorization_endpoint)\n ) {\n throw new PlatformAuthError(\n 'Issuer does not advertise an OAuth device grant',\n discovery.status,\n { code: 'OAUTH_DEVICE_GRANT_UNAVAILABLE' },\n )\n }\n const endpoint = new URL(metadata.device_authorization_endpoint)\n const origin = new URL(this.baseUrl).origin\n if (endpoint.origin !== origin || endpoint.username || endpoint.password || endpoint.hash) {\n throw new PlatformAuthError('Device endpoint is outside the configured issuer', 400, {\n code: 'INVALID_DEVICE_ENDPOINT',\n })\n }\n const startedAt = Date.now()\n const res = await this.post(endpoint.href, { scope: this.scope }, options.signal)\n const body: unknown = await res.json().catch(() => null)\n if (!res.ok)\n throw new PlatformAuthError(\n oauthError(body, `Device authorization failed (${res.status})`),\n res.status,\n body,\n )\n if (\n !isRecord(body) ||\n !isNonemptyString(body.device_code) ||\n !isNonemptyString(body.user_code) ||\n !isNonemptyString(body.verification_uri) ||\n typeof body.expires_in !== 'number' ||\n !Number.isSafeInteger(body.expires_in) ||\n body.expires_in <= 0 ||\n (body.interval !== undefined &&\n (typeof body.interval !== 'number' ||\n !Number.isSafeInteger(body.interval) ||\n body.interval <= 0))\n ) {\n throw new PlatformAuthError('Malformed device authorization response', res.status, {\n code: 'INVALID_DEVICE_RESPONSE',\n })\n }\n for (const value of [body.verification_uri, body.verification_uri_complete]) {\n if (value === undefined) continue\n if (!isNonemptyString(value))\n throw new PlatformAuthError('Invalid device verification URI', 400, {\n code: 'INVALID_DEVICE_RESPONSE',\n })\n const uri = new URL(value)\n if (uri.origin !== origin || uri.username || uri.password || uri.hash) {\n throw new PlatformAuthError('Device verification URI is outside the issuer', 400, {\n code: 'INVALID_DEVICE_RESPONSE',\n })\n }\n }\n return {\n deviceCode: body.device_code,\n userCode: body.user_code,\n verificationUri: body.verification_uri,\n ...(isNonemptyString(body.verification_uri_complete)\n ? { verificationUriComplete: body.verification_uri_complete }\n : {}),\n expiresIn: body.expires_in,\n expiresAt: startedAt + body.expires_in * 1000,\n interval: typeof body.interval === 'number' ? body.interval : 5,\n }\n }\n\n /** RFC 8628 pending/slow_down handling, expiry, cancellation and timeout backoff. */\n async pollDeviceAuthorization(\n grant: OidcDeviceAuthorization,\n options: { signal?: AbortSignal } = {},\n ): Promise<OidcTokens> {\n if (\n !grant.deviceCode ||\n !Number.isFinite(grant.expiresAt) ||\n !Number.isFinite(grant.interval) ||\n grant.interval <= 0\n ) {\n throw new Error('PlatformOidcClient.pollDeviceAuthorization: invalid device grant')\n }\n let intervalMs = grant.interval * 1000\n while (Date.now() < grant.expiresAt) {\n options.signal?.throwIfAborted()\n await pause(Math.min(intervalMs, Math.max(0, grant.expiresAt - Date.now())), options.signal)\n const remaining = grant.expiresAt - Date.now()\n if (remaining <= 0) break\n const timeout = AbortSignal.timeout(Math.max(1, Math.min(30_000, Math.floor(remaining))))\n const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout\n try {\n return await this.token({ grant_type: DEVICE_GRANT, device_code: grant.deviceCode }, signal)\n } catch (error) {\n options.signal?.throwIfAborted()\n if (timeout.aborted) {\n intervalMs *= 2\n continue\n }\n if (!(error instanceof PlatformAuthError) || !isRecord(error.body)) throw error\n if (error.body.error === 'authorization_pending') continue\n if (error.body.error === 'slow_down') {\n intervalMs += 5_000\n continue\n }\n throw error\n }\n }\n throw new PlatformAuthError('Device authorization expired', 400, { error: 'expired_token' })\n }\n\n private async token(params: Record<string, string>, signal?: AbortSignal): Promise<OidcTokens> {\n const res = await this.post(`${this.baseUrl}/api/auth/oauth2/token`, params, signal)\n const body: unknown = await res.json().catch(() => null)\n if (!res.ok)\n throw new PlatformAuthError(\n oauthError(body, `Platform token request failed (${res.status})`),\n res.status,\n body,\n )\n return parseTokens(body, res.status)\n }\n\n private post(\n url: string,\n params: Record<string, string>,\n signal?: AbortSignal,\n includeResources = true,\n ): Promise<Response> {\n const body = new URLSearchParams(params)\n const headers: Record<string, string> = { 'content-type': 'application/x-www-form-urlencoded' }\n if (this.clientSecret) {\n // RFC 6749 section 2.3.1: form-encode each component before HTTP Basic.\n headers.authorization = `Basic ${btoa(`${formComponent(this.clientId)}:${formComponent(this.clientSecret)}`)}`\n } else body.set('client_id', this.clientId)\n if (includeResources) for (const resource of this.resources) body.append('resource', resource)\n return this.fetchImpl(url, { method: 'POST', headers, body, redirect: 'error', signal })\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { $i as detachedSnapshot, Fi as finalizeRuntimeOwnedPendingExecutor, Ia as armDeadlineTimer, Mi as attestRuntimeOwnedPendingExecutor, Na as abortError, Sa as sleep, Sr as
|
|
1
|
+
import { $i as detachedSnapshot, Cr as linkAbort, Ct as startRetainedInteractiveRun, Fi as finalizeRuntimeOwnedPendingExecutor, Ia as armDeadlineTimer, Mi as attestRuntimeOwnedPendingExecutor, Na as abortError, Sa as sleep, Sr as abortError$1, Xi as executableAgentProfileSnapshot, ar as sandboxClientAsProvider, br as environmentGone, hn as taskToPrompt, kr as contentAddress, mt as interactiveAdmissionSeamKey, n as createRootHandle, r as createSupervisor, vr as retainedCreateMaterial, vt as readWorkerInteractiveBinding, yn as createInbox, yr as destroyInteractiveEnvironment, zi as newExecutionAttemptId } from "./supervisor-UXgaPisu.js";
|
|
2
2
|
import { a as concreteProfileModel } from "./model-policy-BbSCSak0.js";
|
|
3
3
|
import { a as writeAtomicDurableFile } from "./durable-file-DWQA4ooo.js";
|
|
4
|
-
import { A as createCoordinationTools, n as createSteerAcknowledger, t as createCancelAcknowledger, v as createFileRunContext } from "./coordination-driver-
|
|
4
|
+
import { A as createCoordinationTools, n as createSteerAcknowledger, t as createCancelAcknowledger, v as createFileRunContext } from "./coordination-driver-B-HOpGN4.js";
|
|
5
5
|
import { h as supervisorRunDir } from "./run-layout-vwd4SwX_.js";
|
|
6
6
|
import { agentInteractiveSessionControlClaimRequestDigest, agentInteractiveSessionPromptRequestDigest, agentInteractiveSessionStopRequestDigest, agentProfileSchema, canonicalAgentProfileDigest, canonicalCandidateDigest } from "@tangle-network/agent-interface";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
@@ -1026,4 +1026,4 @@ function writeState(path, state) {
|
|
|
1026
1026
|
//#endregion
|
|
1027
1027
|
export { workerFromInteractiveProvider as n, claimRetainedInteractiveControl as r, provisionSupervisor as t };
|
|
1028
1028
|
|
|
1029
|
-
//# sourceMappingURL=provision-supervisor-
|
|
1029
|
+
//# sourceMappingURL=provision-supervisor-CkN8RRjv.js.map
|