@absolutejs/auth 0.54.8 → 0.55.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.
Files changed (44) hide show
  1. package/README.md +19 -5
  2. package/dist/agents/config.d.ts +16 -0
  3. package/dist/agents/idJag.d.ts +49 -0
  4. package/dist/agents/inMemoryStores.d.ts +2 -1
  5. package/dist/agents/index.d.ts +5 -2
  6. package/dist/agents/index.js +1391 -66
  7. package/dist/agents/index.js.map +12 -8
  8. package/dist/agents/postgresStores.d.ts +280 -1
  9. package/dist/agents/registration.d.ts +162 -0
  10. package/dist/agents/registrationClient.d.ts +50 -0
  11. package/dist/agents/routes.d.ts +112 -1
  12. package/dist/agents/types.d.ts +59 -0
  13. package/dist/apikeys/routes.d.ts +2 -2
  14. package/dist/cli/migrate.js +57 -8
  15. package/dist/cli/migrate.js.map +4 -4
  16. package/dist/credentials/login.d.ts +4 -4
  17. package/dist/credentials/routes.d.ts +4 -4
  18. package/dist/index.d.ts +185 -2
  19. package/dist/index.js +1388 -102
  20. package/dist/index.js.map +15 -12
  21. package/dist/manifest.js +12 -6
  22. package/dist/manifest.js.map +4 -4
  23. package/dist/manifest.json +2 -2
  24. package/dist/mfa/routes.d.ts +2 -2
  25. package/dist/mfa/sms.d.ts +2 -2
  26. package/dist/oidc/clientAuth.d.ts +6 -2
  27. package/dist/oidc/config.d.ts +19 -5
  28. package/dist/oidc/keys.d.ts +7 -3
  29. package/dist/oidc/logout.d.ts +2 -2
  30. package/dist/oidc/routes.d.ts +13 -11
  31. package/dist/organizations/routes.d.ts +1 -1
  32. package/dist/portal/routes.d.ts +3 -3
  33. package/dist/roles/routes.d.ts +1 -1
  34. package/dist/routes/refresh.d.ts +1 -1
  35. package/dist/routes/revoke.d.ts +1 -1
  36. package/dist/routes/sessions.d.ts +3 -3
  37. package/dist/sso/discoveryRoute.d.ts +1 -1
  38. package/dist/sso/oidcRoutes.d.ts +2 -2
  39. package/dist/sso/samlRoutes.d.ts +4 -4
  40. package/docs/AGENT-AUTH.md +54 -0
  41. package/docs/MIGRATE-FROM-LUCIA.md +235 -0
  42. package/docs/OAUTH-PROVIDER-QUIRKS.md +150 -0
  43. package/docs/UI-COMPONENTS.md +226 -0
  44. package/package.json +7 -3
package/dist/index.js CHANGED
@@ -3112,6 +3112,673 @@ var resolveAgentPrincipal = async (request, config) => {
3112
3112
  };
3113
3113
  };
3114
3114
 
3115
+ // src/agents/registration.ts
3116
+ init_constants();
3117
+ init_crypto();
3118
+
3119
+ // src/oidc/keys.ts
3120
+ var ENCODER = new TextEncoder;
3121
+ var ES256 = { hash: "SHA-256", name: "ECDSA" };
3122
+ var KEY_PARAMS = { name: "ECDSA", namedCurve: "P-256" };
3123
+ var toBase64Url = (bytes) => Buffer.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)).toString("base64url");
3124
+ var fromBase64Url = (value) => new Uint8Array(Buffer.from(value, "base64url"));
3125
+ var encodeSegment = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
3126
+ var decodeSegment = (segment) => {
3127
+ try {
3128
+ const value = JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
3129
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3130
+ return;
3131
+ }
3132
+ return Object.fromEntries(Object.entries(value));
3133
+ } catch {
3134
+ return;
3135
+ }
3136
+ };
3137
+ var generateSigningKey = async () => {
3138
+ const pair = await crypto.subtle.generateKey(KEY_PARAMS, true, [
3139
+ "sign",
3140
+ "verify"
3141
+ ]);
3142
+ const privateJwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
3143
+ const publicJwk = await crypto.subtle.exportKey("jwk", pair.publicKey);
3144
+ return { kid: await jwkThumbprint(publicJwk), privateJwk, publicJwk };
3145
+ };
3146
+ var jwkThumbprint = async (jwk) => {
3147
+ const canonical = JSON.stringify({
3148
+ crv: jwk.crv,
3149
+ kty: jwk.kty,
3150
+ x: jwk.x,
3151
+ y: jwk.y
3152
+ });
3153
+ return toBase64Url(await crypto.subtle.digest("SHA-256", ENCODER.encode(canonical)));
3154
+ };
3155
+ var signJwt = async (payload, signing, typ = "JWT") => {
3156
+ const key = await crypto.subtle.importKey("jwk", signing.privateJwk, KEY_PARAMS, false, ["sign"]);
3157
+ const input = `${encodeSegment({ alg: "ES256", kid: signing.kid, typ })}.${encodeSegment(payload)}`;
3158
+ const signature = await crypto.subtle.sign(ES256, key, ENCODER.encode(input));
3159
+ return `${input}.${toBase64Url(signature)}`;
3160
+ };
3161
+ var toPublicJwk = (key) => ({
3162
+ alg: "ES256",
3163
+ crv: key.publicJwk.crv,
3164
+ kid: key.kid,
3165
+ kty: key.publicJwk.kty,
3166
+ use: "sig",
3167
+ x: key.publicJwk.x,
3168
+ y: key.publicJwk.y
3169
+ });
3170
+ var verifyJwt = async (token, publicJwk) => {
3171
+ const [headerSegment, payloadSegment, signatureSegment] = token.split(".");
3172
+ if (headerSegment === undefined || payloadSegment === undefined || signatureSegment === undefined) {
3173
+ return;
3174
+ }
3175
+ const key = await crypto.subtle.importKey("jwk", publicJwk, KEY_PARAMS, false, ["verify"]);
3176
+ const valid = await crypto.subtle.verify(ES256, key, fromBase64Url(signatureSegment), ENCODER.encode(`${headerSegment}.${payloadSegment}`));
3177
+ if (!valid)
3178
+ return;
3179
+ const header = decodeSegment(headerSegment);
3180
+ const payload = decodeSegment(payloadSegment);
3181
+ if (header === undefined || payload === undefined)
3182
+ return;
3183
+ return {
3184
+ header,
3185
+ payload
3186
+ };
3187
+ };
3188
+
3189
+ // src/agents/registration.ts
3190
+ var AGENT_CLAIM_GRANT_TYPE = "urn:workos:agent-auth:grant-type:claim";
3191
+ var AGENT_IDENTITY_ASSERTION_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
3192
+ var AGENT_IDENTITY_ASSERTION_TYPE = "urn:ietf:params:oauth:token-type:id-jag";
3193
+ var DEFAULT_IDENTITY_ROUTE = "/agent/identity";
3194
+ var DEFAULT_CLAIM_ROUTE = "/agent/identity/claim";
3195
+ var DEFAULT_COMPLETE_ROUTE = "/agent/identity/claim/complete";
3196
+ var DEFAULT_GUIDE_ROUTE = "/auth.md";
3197
+ var DEFAULT_CLAIM_TTL_MS = 24 * 60 * MILLISECONDS_IN_A_MINUTE;
3198
+ var DEFAULT_ATTEMPT_TTL_MS = 10 * MILLISECONDS_IN_A_MINUTE;
3199
+ var DEFAULT_ASSERTION_TTL_MS = 60 * MILLISECONDS_IN_A_MINUTE;
3200
+ var DEFAULT_ACCESS_TOKEN_TTL_MS2 = 15 * MILLISECONDS_IN_A_MINUTE;
3201
+ var DEFAULT_MAX_AUTH_AGE_MS = 60 * MILLISECONDS_IN_A_MINUTE;
3202
+ var DEFAULT_POLL_INTERVAL_SECONDS = 5;
3203
+ var DEFAULT_MAX_CODE_ATTEMPTS = 5;
3204
+ var MAX_CONCURRENT_UPDATE_RETRIES = 5;
3205
+ var TOKEN_BYTES2 = 32;
3206
+ var agentRegistrationDiscoveryMetadata = (config) => {
3207
+ const registration = requiredRegistration(config);
3208
+ const endpoints = agentRegistrationEndpoints(config);
3209
+ const identityTypes = ["identity_assertion"];
3210
+ if (registration.allowAnonymous === true)
3211
+ identityTypes.unshift("anonymous");
3212
+ if (registration.allowServiceAuth === true)
3213
+ identityTypes.push("service_auth");
3214
+ return {
3215
+ claim_endpoint: endpoints.claimEndpoint,
3216
+ identity_assertion: {
3217
+ assertion_types_supported: [AGENT_IDENTITY_ASSERTION_TYPE]
3218
+ },
3219
+ identity_endpoint: endpoints.identityEndpoint,
3220
+ identity_types_supported: identityTypes,
3221
+ skill: endpoints.guide
3222
+ };
3223
+ };
3224
+ var agentRegistrationEndpoints = (config) => {
3225
+ const registration = requiredRegistration(config);
3226
+ const base = config.authorizationServer;
3227
+ const oidcRoute = config.oidcRoute ?? "/oauth2";
3228
+ return {
3229
+ claimEndpoint: new URL(registration.claimRoute ?? DEFAULT_CLAIM_ROUTE, base).toString(),
3230
+ completeEndpoint: new URL(registration.completeRoute ?? DEFAULT_COMPLETE_ROUTE, base).toString(),
3231
+ guide: new URL(registration.guideRoute ?? DEFAULT_GUIDE_ROUTE, base).toString(),
3232
+ identityEndpoint: new URL(registration.identityRoute ?? DEFAULT_IDENTITY_ROUTE, base).toString(),
3233
+ tokenEndpoint: new URL(`${oidcRoute}/token`, base).toString()
3234
+ };
3235
+ };
3236
+ var markdownJson = (value) => `\`\`\`json
3237
+ ${JSON.stringify(value, null, 2)}
3238
+ \`\`\``;
3239
+ var generateAgentRegistrationGuide = (config) => {
3240
+ const registration = requiredRegistration(config);
3241
+ const endpoints = agentRegistrationEndpoints(config);
3242
+ const metadataUrl = new URL(config.metadataRoute ?? "/.well-known/oauth-protected-resource", config.resource).toString();
3243
+ const methods = [
3244
+ "- `identity_assertion`: present an audience-bound ID-JAG from a trusted provider.",
3245
+ ...registration.allowServiceAuth === true ? [
3246
+ "- `service_auth`: provide a user login hint and complete the service-owned claim ceremony."
3247
+ ] : [],
3248
+ ...registration.allowAnonymous === true ? [
3249
+ "- `anonymous`: receive pre-claim scopes, then optionally let a signed-in user claim the registration."
3250
+ ] : []
3251
+ ].join(`
3252
+ `);
3253
+ return `# Agent registration for ${config.resourceName ?? config.resource}
3254
+
3255
+ This service supports the open auth.md agent-registration profile. Structured
3256
+ OAuth metadata is authoritative; this document is its agent-readable companion.
3257
+
3258
+ ## 1. Discover
3259
+
3260
+ Fetch ${metadataUrl}, follow \`authorization_servers\`, then fetch
3261
+ \`/.well-known/oauth-authorization-server\`. Read its \`agent_auth\` object and
3262
+ top-level \`token_endpoint\` and \`grant_types_supported\` fields.
3263
+
3264
+ ## 2. Choose a method
3265
+
3266
+ ${methods}
3267
+
3268
+ Before asserting a user identity, show the service name and requested scopes to
3269
+ the user and obtain consent. Never ask the user to send a password or OTP to the
3270
+ agent.
3271
+
3272
+ ## 3. Register
3273
+
3274
+ POST one of these bodies to ${endpoints.identityEndpoint}:
3275
+
3276
+ ${markdownJson({
3277
+ assertion: "<ID-JAG>",
3278
+ assertion_type: AGENT_IDENTITY_ASSERTION_TYPE,
3279
+ type: "identity_assertion"
3280
+ })}
3281
+
3282
+ ${registration.allowServiceAuth === true ? markdownJson({ login_hint: "user@example.com", type: "service_auth" }) : ""}
3283
+
3284
+ ${registration.allowAnonymous === true ? markdownJson({ type: "anonymous" }) : ""}
3285
+
3286
+ ## 4. Claim
3287
+
3288
+ Surface \`verification_uri\` and \`user_code\` together. The user opens the
3289
+ service-owned page, signs in using the service's normal MFA/SSO policy, and types
3290
+ the code there. Poll ${endpoints.tokenEndpoint} using
3291
+ \`grant_type=${AGENT_CLAIM_GRANT_TYPE}\` and the one-time \`claim_token\`.
3292
+ Treat \`authorization_pending\` as retryable, honor \`interval\`, and restart
3293
+ when the server returns \`expired_token\`.
3294
+
3295
+ ## 5. Exchange and use credentials
3296
+
3297
+ Exchange \`identity_assertion\` at ${endpoints.tokenEndpoint} with
3298
+ \`grant_type=${AGENT_IDENTITY_ASSERTION_GRANT_TYPE}\`. Present the resulting
3299
+ access token as \`Authorization: Bearer <access_token>\`. Credentials are scoped,
3300
+ short-lived, revocable, and bound to the registered agent identity.
3301
+
3302
+ ## Errors and safety
3303
+
3304
+ - Stop on \`invalid_issuer\`, \`invalid_signature\`, \`invalid_audience\`, or \`replay_detected\`.
3305
+ - Reauthenticate the user on \`login_required\`.
3306
+ - Open the service-owned confirmation URL on \`interaction_required\`.
3307
+ - Never persist claim tokens after completion and never expose credentials in model context.
3308
+ `;
3309
+ };
3310
+ var requiredRegistration = (config) => {
3311
+ if (config.agentRegistration === undefined) {
3312
+ throw new Error("agentAuth.agentRegistration is not configured");
3313
+ }
3314
+ return config.agentRegistration;
3315
+ };
3316
+ var randomCode = () => {
3317
+ const bytes = crypto.getRandomValues(new Uint8Array(4));
3318
+ const value = new DataView(bytes.buffer).getUint32(0) % 1e6;
3319
+ return value.toString().padStart(6, "0");
3320
+ };
3321
+ var makeSecret = (prefix) => `${prefix}_${generateSecureToken(TOKEN_BYTES2)}`;
3322
+ var makeAttempt = async ({
3323
+ email,
3324
+ now,
3325
+ registration
3326
+ }) => {
3327
+ const attemptToken = makeSecret("cat");
3328
+ const userCode = randomCode();
3329
+ const expiresAt = now + (registration.attemptTtlMs ?? DEFAULT_ATTEMPT_TTL_MS);
3330
+ return {
3331
+ attempt: {
3332
+ attempts: 0,
3333
+ email: email.toLowerCase(),
3334
+ expiresAt,
3335
+ tokenHash: await hashToken(attemptToken),
3336
+ userCodeHash: await hashToken(userCode)
3337
+ },
3338
+ attemptToken,
3339
+ expiresAt,
3340
+ userCode
3341
+ };
3342
+ };
3343
+ var createFlow = async ({
3344
+ agentId,
3345
+ kind,
3346
+ loginHint,
3347
+ now,
3348
+ registration,
3349
+ status,
3350
+ upstream,
3351
+ userId,
3352
+ withAttempt
3353
+ }) => {
3354
+ const claimToken = makeSecret("clm");
3355
+ const registrationId = `air_${crypto.randomUUID()}`;
3356
+ const claimExpiresAt = now + (registration.claimTtlMs ?? DEFAULT_CLAIM_TTL_MS);
3357
+ const attempt = withAttempt === undefined ? undefined : await makeAttempt({ email: withAttempt, now, registration });
3358
+ const flow = {
3359
+ agentId,
3360
+ claimAttempt: attempt?.attempt,
3361
+ claimExpiresAt,
3362
+ claimTokenHash: await hashToken(claimToken),
3363
+ createdAt: now,
3364
+ expiresAt: claimExpiresAt,
3365
+ kind,
3366
+ loginHint,
3367
+ registrationId,
3368
+ status,
3369
+ updatedAt: now,
3370
+ upstream,
3371
+ userId,
3372
+ version: 1
3373
+ };
3374
+ if (!await registration.identityStore.create(flow)) {
3375
+ throw new Error("Agent identity registration id collision");
3376
+ }
3377
+ return { attempt, claimToken, flow };
3378
+ };
3379
+ var assertionClaims = (config, flow, now) => ({
3380
+ aud: config.authorizationServer,
3381
+ exp: Math.floor((now + (config.agentRegistration?.assertionTtlMs ?? DEFAULT_ASSERTION_TTL_MS)) / MILLISECONDS_IN_A_SECOND),
3382
+ iat: Math.floor(now / MILLISECONDS_IN_A_SECOND),
3383
+ iss: config.authorizationServer,
3384
+ jti: crypto.randomUUID(),
3385
+ registration_version: flow.version,
3386
+ sub: flow.registrationId
3387
+ });
3388
+ var issueAgentServiceAssertion = async (config, flow, now = Date.now()) => ({
3389
+ assertion: await signJwt(assertionClaims(config, flow, now), requiredRegistration(config).signingKey, "oauth-id-jag+jwt"),
3390
+ expiresAt: now + (requiredRegistration(config).assertionTtlMs ?? DEFAULT_ASSERTION_TTL_MS)
3391
+ });
3392
+ var activateAgent = async (config, flow, scopes) => {
3393
+ const now = Date.now();
3394
+ await config.registrationStore.saveRegistration({
3395
+ agentId: flow.agentId,
3396
+ allowedScopes: scopes.filter((scope) => config.scopes.includes(scope)),
3397
+ createdAt: now,
3398
+ metadata: {
3399
+ identityRegistrationId: flow.registrationId,
3400
+ registrationKind: flow.kind
3401
+ },
3402
+ name: `Agent registration ${flow.registrationId}`,
3403
+ status: "active",
3404
+ updatedAt: now
3405
+ });
3406
+ if (flow.userId !== undefined) {
3407
+ await config.delegationStore.saveDelegation({
3408
+ agentId: flow.agentId,
3409
+ createdAt: now,
3410
+ delegationId: `agd_${crypto.randomUUID()}`,
3411
+ scopes: scopes.filter((scope) => config.scopes.includes(scope)),
3412
+ status: "active",
3413
+ updatedAt: now,
3414
+ userId: flow.userId
3415
+ });
3416
+ }
3417
+ };
3418
+ var ceremony = (config, flow, attempt) => ({
3419
+ expires_in: Math.max(0, Math.floor((attempt.expiresAt - Date.now()) / MILLISECONDS_IN_A_SECOND)),
3420
+ interval: requiredRegistration(config).pollIntervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS,
3421
+ user_code: attempt.userCode,
3422
+ verification_uri: `${agentRegistrationEndpoints(config).completeEndpoint}?claim_attempt_token=${encodeURIComponent(attempt.attemptToken)}`
3423
+ });
3424
+ var startAgentRegistration = async (config, input, now = Date.now()) => {
3425
+ const registration = requiredRegistration(config);
3426
+ const agentId = `agent_${crypto.randomUUID()}`;
3427
+ if (input.type === "anonymous") {
3428
+ if (registration.allowAnonymous !== true) {
3429
+ return { error: "anonymous_not_enabled", status: 403 };
3430
+ }
3431
+ if (registration.revokeAccessTokens === undefined) {
3432
+ throw new Error("Anonymous agent registration requires revokeAccessTokens");
3433
+ }
3434
+ const created2 = await createFlow({
3435
+ agentId,
3436
+ kind: "anonymous",
3437
+ now,
3438
+ registration,
3439
+ status: "pending"
3440
+ });
3441
+ await activateAgent(config, created2.flow, registration.preClaimScopes ?? []);
3442
+ const assertion2 = await issueAgentServiceAssertion(config, created2.flow, now);
3443
+ return {
3444
+ assertionExpires: assertion2.expiresAt,
3445
+ claimToken: created2.claimToken,
3446
+ claimTokenExpires: created2.flow.claimExpiresAt,
3447
+ identityAssertion: assertion2.assertion,
3448
+ postClaimScopes: registration.postClaimScopes,
3449
+ preClaimScopes: registration.preClaimScopes ?? [],
3450
+ registrationId: created2.flow.registrationId,
3451
+ registrationType: "anonymous",
3452
+ status: 200
3453
+ };
3454
+ }
3455
+ if (input.type === "service_auth") {
3456
+ if (registration.allowServiceAuth !== true) {
3457
+ return { error: "service_auth_not_enabled", status: 403 };
3458
+ }
3459
+ const email = input.loginHint.trim().toLowerCase();
3460
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/u.test(email)) {
3461
+ return { error: "invalid_login_hint", status: 400 };
3462
+ }
3463
+ const created2 = await createFlow({
3464
+ agentId,
3465
+ kind: "service_auth",
3466
+ loginHint: email,
3467
+ now,
3468
+ registration,
3469
+ status: "pending",
3470
+ withAttempt: email
3471
+ });
3472
+ if (created2.attempt === undefined)
3473
+ throw new Error("Claim attempt missing");
3474
+ return {
3475
+ assertionExpires: 0,
3476
+ claim: ceremony(config, created2.flow, created2.attempt),
3477
+ claimToken: created2.claimToken,
3478
+ claimTokenExpires: created2.flow.claimExpiresAt,
3479
+ postClaimScopes: registration.postClaimScopes,
3480
+ registrationId: created2.flow.registrationId,
3481
+ registrationType: "service_auth",
3482
+ status: 200
3483
+ };
3484
+ }
3485
+ if (input.assertionType !== AGENT_IDENTITY_ASSERTION_TYPE || registration.verifyIdentityAssertion === undefined) {
3486
+ return { error: "invalid_request", status: 400 };
3487
+ }
3488
+ const identity = await registration.verifyIdentityAssertion(input.assertion);
3489
+ if (identity === undefined) {
3490
+ return { error: "invalid_identity_assertion", status: 401 };
3491
+ }
3492
+ const authAge = now - identity.authenticatedAt;
3493
+ if (authAge < -MILLISECONDS_IN_A_MINUTE || authAge > (registration.maxAuthenticationAgeMs ?? DEFAULT_MAX_AUTH_AGE_MS)) {
3494
+ return { error: "login_required", status: 401 };
3495
+ }
3496
+ if (identity.emailVerified !== true && identity.phoneNumberVerified !== true) {
3497
+ return { error: "missing_verified_identity", status: 403 };
3498
+ }
3499
+ const existing = await registration.identityStore.findByUpstreamIdentity({
3500
+ clientId: identity.clientId,
3501
+ issuer: identity.issuer,
3502
+ subject: identity.subject
3503
+ });
3504
+ if (existing?.status === "claimed") {
3505
+ const assertion2 = await issueAgentServiceAssertion(config, existing, now);
3506
+ return {
3507
+ assertionExpires: assertion2.expiresAt,
3508
+ identityAssertion: assertion2.assertion,
3509
+ postClaimScopes: registration.postClaimScopes,
3510
+ registrationId: existing.registrationId,
3511
+ registrationType: "identity_assertion",
3512
+ status: 200
3513
+ };
3514
+ }
3515
+ const match = await registration.resolveVerifiedIdentity?.(identity);
3516
+ const verifiedEmail = identity.emailVerified === true ? identity.email : undefined;
3517
+ if (match?.userId === undefined && verifiedEmail === undefined) {
3518
+ return { error: "interaction_required", status: 401 };
3519
+ }
3520
+ const created = await createFlow({
3521
+ agentId,
3522
+ kind: "identity_assertion",
3523
+ now,
3524
+ registration,
3525
+ status: match?.userId === undefined ? "pending" : "claimed",
3526
+ upstream: {
3527
+ clientId: identity.clientId,
3528
+ issuer: identity.issuer,
3529
+ subject: identity.subject
3530
+ },
3531
+ userId: match?.userId,
3532
+ withAttempt: match?.userId === undefined ? verifiedEmail : undefined
3533
+ });
3534
+ if (match?.userId === undefined) {
3535
+ if (created.attempt === undefined)
3536
+ throw new Error("Claim attempt missing");
3537
+ return {
3538
+ assertionExpires: 0,
3539
+ claim: ceremony(config, created.flow, created.attempt),
3540
+ claimToken: created.claimToken,
3541
+ claimTokenExpires: created.flow.claimExpiresAt,
3542
+ postClaimScopes: registration.postClaimScopes,
3543
+ registrationId: created.flow.registrationId,
3544
+ registrationType: "identity_assertion",
3545
+ status: 200
3546
+ };
3547
+ }
3548
+ await activateAgent(config, created.flow, registration.postClaimScopes);
3549
+ const assertion = await issueAgentServiceAssertion(config, created.flow, now);
3550
+ return {
3551
+ assertionExpires: assertion.expiresAt,
3552
+ identityAssertion: assertion.assertion,
3553
+ postClaimScopes: registration.postClaimScopes,
3554
+ registrationId: created.flow.registrationId,
3555
+ registrationType: "identity_assertion",
3556
+ status: 200
3557
+ };
3558
+ };
3559
+ var beginAgentClaim = async (config, input, now = Date.now()) => {
3560
+ const registration = requiredRegistration(config);
3561
+ const email = input.email.trim().toLowerCase();
3562
+ const claimTokenHash = await hashToken(input.claimToken);
3563
+ for (let retry = 0;retry < MAX_CONCURRENT_UPDATE_RETRIES; retry += 1) {
3564
+ const flow = await registration.identityStore.findByClaimTokenHash(claimTokenHash);
3565
+ if (flow === undefined) {
3566
+ return { error: "invalid_claim_token", status: 400 };
3567
+ }
3568
+ if (flow.claimExpiresAt <= now || flow.status === "revoked") {
3569
+ return { error: "claim_expired", status: 400 };
3570
+ }
3571
+ if (flow.loginHint !== undefined && flow.loginHint !== email) {
3572
+ return { error: "invalid_claim_token", status: 400 };
3573
+ }
3574
+ const attempt = await makeAttempt({ email, now, registration });
3575
+ const replacement = {
3576
+ ...flow,
3577
+ claimAttempt: attempt.attempt,
3578
+ loginHint: email,
3579
+ updatedAt: now
3580
+ };
3581
+ if (await registration.identityStore.replace(replacement, flow.version)) {
3582
+ return {
3583
+ claimAttempt: ceremony(config, replacement, attempt),
3584
+ status: 200
3585
+ };
3586
+ }
3587
+ }
3588
+ return { error: "concurrent_update", status: 409 };
3589
+ };
3590
+ var completeAgentClaim = async (config, input, now = Date.now()) => {
3591
+ const registration = requiredRegistration(config);
3592
+ const user = await registration.resolveAuthenticatedUser(input.request);
3593
+ if (user === undefined)
3594
+ return { error: "wrong_user", status: 403 };
3595
+ const attemptTokenHash = await hashToken(input.attemptToken);
3596
+ const userCodeHash = await hashToken(input.userCode);
3597
+ for (let retry = 0;retry < MAX_CONCURRENT_UPDATE_RETRIES; retry += 1) {
3598
+ const flow = await registration.identityStore.findByAttemptTokenHash(attemptTokenHash);
3599
+ const attempt = flow?.claimAttempt;
3600
+ if (flow === undefined || attempt === undefined) {
3601
+ return { error: "invalid_claim_attempt", status: 400 };
3602
+ }
3603
+ if (flow.claimExpiresAt <= now || attempt.expiresAt <= now) {
3604
+ return { error: "claim_expired", status: 400 };
3605
+ }
3606
+ if (user.email?.toLowerCase() !== attempt.email) {
3607
+ return { error: "wrong_user", status: 403 };
3608
+ }
3609
+ const matches = await constantTimeEqual(userCodeHash, attempt.userCodeHash);
3610
+ if (!matches) {
3611
+ const attempts = attempt.attempts + 1;
3612
+ const locked = attempts >= (registration.maxCodeAttempts ?? DEFAULT_MAX_CODE_ATTEMPTS);
3613
+ const replaced = await registration.identityStore.replace({
3614
+ ...flow,
3615
+ claimAttempt: locked ? undefined : { ...attempt, attempts },
3616
+ updatedAt: now
3617
+ }, flow.version);
3618
+ if (replaced) {
3619
+ return {
3620
+ error: "user_code_invalid",
3621
+ status: locked ? 429 : 400
3622
+ };
3623
+ }
3624
+ continue;
3625
+ }
3626
+ if (flow.kind === "anonymous") {
3627
+ await registration.revokeAccessTokens?.(flow.agentId);
3628
+ }
3629
+ const replacement = {
3630
+ ...flow,
3631
+ claimAttempt: undefined,
3632
+ status: "claimed",
3633
+ updatedAt: now,
3634
+ userId: user.userId
3635
+ };
3636
+ if (!await registration.identityStore.replace(replacement, flow.version)) {
3637
+ continue;
3638
+ }
3639
+ const persisted = await registration.identityStore.findByRegistrationId(flow.registrationId);
3640
+ if (persisted === undefined) {
3641
+ throw new Error("Completed agent registration disappeared");
3642
+ }
3643
+ await activateAgent(config, persisted, registration.postClaimScopes);
3644
+ return { status: 204 };
3645
+ }
3646
+ return { error: "concurrent_update", status: 409 };
3647
+ };
3648
+ var issueAgentAccessToken = async (config, flow, now) => {
3649
+ const registration = requiredRegistration(config);
3650
+ const accessToken = `at_${generateSecureToken(TOKEN_BYTES2)}`;
3651
+ const scopes = (flow.status === "claimed" ? registration.postClaimScopes : registration.preClaimScopes ?? []).filter((scope) => config.scopes.includes(scope));
3652
+ const expiresAt = now + (registration.tokenTtlMs ?? DEFAULT_ACCESS_TOKEN_TTL_MS2);
3653
+ await registration.accessTokenStore.saveToken({
3654
+ clientId: flow.agentId,
3655
+ createdAt: now,
3656
+ expiresAt,
3657
+ hashedToken: await hashToken(accessToken),
3658
+ ownerId: flow.userId,
3659
+ scopes,
3660
+ tokenId: crypto.randomUUID()
3661
+ });
3662
+ return {
3663
+ accessToken,
3664
+ expiresIn: Math.floor((expiresAt - now) / MILLISECONDS_IN_A_SECOND),
3665
+ scopes
3666
+ };
3667
+ };
3668
+ var createAgentRegistrationCredentialVerifier = (accessTokenStore, identityStore) => async (request) => {
3669
+ const authorization = request.headers.get("authorization");
3670
+ if (authorization?.startsWith("Bearer at_") !== true)
3671
+ return;
3672
+ const token = authorization.slice("Bearer ".length).trim();
3673
+ const record = await accessTokenStore.findByHashedToken(await hashToken(token));
3674
+ if (record === undefined || record.expiresAt <= Date.now())
3675
+ return;
3676
+ if (identityStore !== undefined) {
3677
+ const identity = await identityStore.findByAgentId(record.clientId);
3678
+ if (identity === undefined || identity.status === "revoked") {
3679
+ return;
3680
+ }
3681
+ }
3682
+ return {
3683
+ agentId: record.clientId,
3684
+ expiresAt: record.expiresAt,
3685
+ scopes: record.scopes,
3686
+ userId: record.ownerId
3687
+ };
3688
+ };
3689
+ var handleAgentTokenGrant = async (config, body, now = Date.now()) => {
3690
+ const registration = requiredRegistration(config);
3691
+ if (body.grant_type === AGENT_CLAIM_GRANT_TYPE) {
3692
+ if (body.claim_token === undefined) {
3693
+ return { body: { error: "invalid_request" }, status: 400 };
3694
+ }
3695
+ const claimTokenHash = await hashToken(body.claim_token);
3696
+ let flow2;
3697
+ for (let retry = 0;retry < MAX_CONCURRENT_UPDATE_RETRIES; retry += 1) {
3698
+ flow2 = await registration.identityStore.findByClaimTokenHash(claimTokenHash);
3699
+ if (flow2 === undefined || flow2.claimExpiresAt <= now) {
3700
+ return { body: { error: "expired_token" }, status: 400 };
3701
+ }
3702
+ if (flow2.status === "claimed")
3703
+ break;
3704
+ if (flow2.claimAttempt !== undefined && flow2.claimAttempt.expiresAt <= now) {
3705
+ return { body: { error: "expired_token" }, status: 400 };
3706
+ }
3707
+ const intervalMs = (registration.pollIntervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS) * MILLISECONDS_IN_A_SECOND;
3708
+ if (flow2.lastPolledAt !== undefined && now - flow2.lastPolledAt < intervalMs) {
3709
+ return { body: { error: "slow_down" }, status: 400 };
3710
+ }
3711
+ if (await registration.identityStore.replace({ ...flow2, lastPolledAt: now, updatedAt: now }, flow2.version)) {
3712
+ return {
3713
+ body: { error: "authorization_pending" },
3714
+ status: 400
3715
+ };
3716
+ }
3717
+ }
3718
+ if (flow2?.status !== "claimed") {
3719
+ return { body: { error: "temporarily_unavailable" }, status: 400 };
3720
+ }
3721
+ const assertion = await issueAgentServiceAssertion(config, flow2, now);
3722
+ const token2 = await issueAgentAccessToken(config, flow2, now);
3723
+ return {
3724
+ body: {
3725
+ access_token: token2.accessToken,
3726
+ assertion_expires: new Date(assertion.expiresAt).toISOString(),
3727
+ expires_in: token2.expiresIn,
3728
+ identity_assertion: assertion.assertion,
3729
+ scope: token2.scopes.join(" "),
3730
+ token_type: "Bearer"
3731
+ },
3732
+ status: 200
3733
+ };
3734
+ }
3735
+ if (body.grant_type !== AGENT_IDENTITY_ASSERTION_GRANT_TYPE) {
3736
+ return;
3737
+ }
3738
+ if (body.assertion === undefined) {
3739
+ return { body: { error: "invalid_request" }, status: 400 };
3740
+ }
3741
+ const verified = await verifyJwt(body.assertion, registration.signingKey.publicJwk);
3742
+ const payload = verified?.payload;
3743
+ if (verified?.header?.typ !== "oauth-id-jag+jwt" || payload?.iss !== config.authorizationServer || payload.aud !== config.authorizationServer || typeof payload.sub !== "string" || typeof payload.exp !== "number" || payload.exp <= Math.floor(now / MILLISECONDS_IN_A_SECOND)) {
3744
+ return { body: { error: "invalid_grant" }, status: 400 };
3745
+ }
3746
+ const flow = await registration.identityStore.findByRegistrationId(payload.sub);
3747
+ if (flow === undefined || flow.status === "revoked" || payload.registration_version !== flow.version) {
3748
+ return { body: { error: "invalid_grant" }, status: 400 };
3749
+ }
3750
+ const token = await issueAgentAccessToken(config, flow, now);
3751
+ return {
3752
+ body: {
3753
+ access_token: token.accessToken,
3754
+ expires_in: token.expiresIn,
3755
+ scope: token.scopes.join(" "),
3756
+ token_type: "Bearer"
3757
+ },
3758
+ status: 200
3759
+ };
3760
+ };
3761
+ var revokeAgentIdentityRegistration = async (config, registrationId, now = Date.now()) => {
3762
+ const registration = requiredRegistration(config);
3763
+ for (let retry = 0;retry < MAX_CONCURRENT_UPDATE_RETRIES; retry += 1) {
3764
+ const flow = await registration.identityStore.findByRegistrationId(registrationId);
3765
+ if (flow === undefined)
3766
+ return false;
3767
+ if (flow.status === "revoked")
3768
+ return true;
3769
+ await registration.revokeAccessTokens?.(flow.agentId);
3770
+ if (await registration.identityStore.replace({
3771
+ ...flow,
3772
+ claimAttempt: undefined,
3773
+ status: "revoked",
3774
+ updatedAt: now
3775
+ }, flow.version)) {
3776
+ return true;
3777
+ }
3778
+ }
3779
+ throw new Error("Could not revoke agent identity after concurrent updates");
3780
+ };
3781
+
3115
3782
  // src/agents/routes.ts
3116
3783
  var quoteHeaderValue = (value) => {
3117
3784
  const printable = [...value].filter((character) => {
@@ -3151,6 +3818,67 @@ var failureResponse = (config, failure, requiredScopes) => new Response(JSON.str
3151
3818
  },
3152
3819
  status: failure.code === "Forbidden" ? 403 : 401
3153
3820
  });
3821
+ var json = (value, status = 200) => new Response(JSON.stringify(value), {
3822
+ headers: {
3823
+ "cache-control": "no-store",
3824
+ "content-type": "application/json"
3825
+ },
3826
+ status
3827
+ });
3828
+ var recordBody = (body) => {
3829
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
3830
+ return;
3831
+ }
3832
+ return Object.fromEntries(Object.entries(body));
3833
+ };
3834
+ var registrationResponse = (result) => {
3835
+ const body = {
3836
+ ...result.assertionExpires > 0 ? {
3837
+ assertion_expires: new Date(result.assertionExpires).toISOString()
3838
+ } : {},
3839
+ ...result.claim === undefined ? {} : { claim: result.claim },
3840
+ ...result.claimToken === undefined ? {} : { claim_token: result.claimToken },
3841
+ ...result.claimTokenExpires === undefined ? {} : {
3842
+ claim_token_expires: new Date(result.claimTokenExpires).toISOString()
3843
+ },
3844
+ ...result.identityAssertion === undefined ? {} : { identity_assertion: result.identityAssertion },
3845
+ ...result.preClaimScopes === undefined ? {} : { pre_claim_scopes: result.preClaimScopes },
3846
+ post_claim_scopes: result.postClaimScopes,
3847
+ registration_id: result.registrationId,
3848
+ registration_type: result.registrationType
3849
+ };
3850
+ if (result.registrationType === "identity_assertion" && result.identityAssertion === undefined) {
3851
+ return json({
3852
+ ...body,
3853
+ error: "interaction_required",
3854
+ error_description: "Authenticate at the service and confirm the account link."
3855
+ }, 401);
3856
+ }
3857
+ return json(body);
3858
+ };
3859
+ var parseRegistrationInput = (value) => {
3860
+ if (value.type === "anonymous") {
3861
+ const input2 = { type: "anonymous" };
3862
+ return input2;
3863
+ }
3864
+ if (value.type === "service_auth" && typeof value.login_hint === "string") {
3865
+ const input2 = {
3866
+ loginHint: value.login_hint,
3867
+ type: "service_auth"
3868
+ };
3869
+ return input2;
3870
+ }
3871
+ if (value.type !== "identity_assertion" || value.assertion_type !== AGENT_IDENTITY_ASSERTION_TYPE || typeof value.assertion !== "string") {
3872
+ return;
3873
+ }
3874
+ const input = {
3875
+ assertion: value.assertion,
3876
+ assertionType: AGENT_IDENTITY_ASSERTION_TYPE,
3877
+ type: "identity_assertion"
3878
+ };
3879
+ return input;
3880
+ };
3881
+ var escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
3154
3882
  var agentAuthPlugin = (config) => {
3155
3883
  const plugin = new Elysia2().derive(({ request }) => ({
3156
3884
  protectAgent: async (requiredScopes, handleAuth, handleAuthFail) => {
@@ -3181,7 +3909,76 @@ var agentAuthPlugin = (config) => {
3181
3909
  }));
3182
3910
  if (config === undefined)
3183
3911
  return plugin.as("global");
3184
- return plugin.get(config.metadataRoute ?? DEFAULT_AGENT_RESOURCE_METADATA_ROUTE, () => agentProtectedResourceMetadata(config)).as("global");
3912
+ if (config.agentRegistration === undefined) {
3913
+ return plugin.get(config.metadataRoute ?? DEFAULT_AGENT_RESOURCE_METADATA_ROUTE, () => agentProtectedResourceMetadata(config)).as("global");
3914
+ }
3915
+ const registration = config.agentRegistration;
3916
+ const identityRoute = registration.identityRoute ?? "/agent/identity";
3917
+ const claimRoute = registration.claimRoute ?? "/agent/identity/claim";
3918
+ const completeRoute = registration.completeRoute ?? "/agent/identity/claim/complete";
3919
+ const guideRoute = registration.guideRoute ?? "/auth.md";
3920
+ return plugin.get(config.metadataRoute ?? DEFAULT_AGENT_RESOURCE_METADATA_ROUTE, () => agentProtectedResourceMetadata(config)).get(guideRoute, () => new Response(generateAgentRegistrationGuide(config), {
3921
+ headers: {
3922
+ "cache-control": "public, max-age=300",
3923
+ "content-type": "text/markdown; charset=utf-8"
3924
+ }
3925
+ })).get(completeRoute, ({ query }) => {
3926
+ const token = typeof query.claim_attempt_token === "string" ? query.claim_attempt_token : "";
3927
+ if (token.length === 0)
3928
+ return new Response("Invalid claim link", { status: 400 });
3929
+ return new Response(`<!doctype html><html><head><meta charset="utf-8"><meta name="robots" content="noindex"><title>Confirm agent registration</title></head><body><main><h1>Confirm agent registration</h1><p>Sign in to this service, verify the agent and scopes shown by your agent, then enter the six-digit code.</p><form method="post"><input type="hidden" name="claim_attempt_token" value="${escapeHtml(token)}"><label>Code <input name="user_code" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{6}" required></label><button type="submit">Confirm</button></form></main></body></html>`, {
3930
+ headers: {
3931
+ "cache-control": "no-store",
3932
+ "content-security-policy": "default-src 'none'; form-action 'self'; style-src 'none'; base-uri 'none'; frame-ancestors 'none'",
3933
+ "content-type": "text/html; charset=utf-8",
3934
+ "x-content-type-options": "nosniff"
3935
+ }
3936
+ });
3937
+ }).post(identityRoute, async ({ body }) => {
3938
+ const value = recordBody(body);
3939
+ if (value === undefined || typeof value.type !== "string") {
3940
+ return json({ error: "invalid_request" }, 400);
3941
+ }
3942
+ const input = parseRegistrationInput(value);
3943
+ if (input === undefined)
3944
+ return json({ error: "invalid_request" }, 400);
3945
+ const result = await startAgentRegistration(config, input);
3946
+ if ("error" in result) {
3947
+ return json({
3948
+ error: result.error,
3949
+ ...result.message === undefined ? {} : { error_description: result.message }
3950
+ }, result.status);
3951
+ }
3952
+ return registrationResponse(result);
3953
+ }).post(claimRoute, async ({ body }) => {
3954
+ const value = recordBody(body);
3955
+ if (value === undefined || typeof value.claim_token !== "string" || typeof value.email !== "string") {
3956
+ return json({ error: "invalid_request" }, 400);
3957
+ }
3958
+ const result = await beginAgentClaim(config, {
3959
+ claimToken: value.claim_token,
3960
+ email: value.email
3961
+ });
3962
+ if ("error" in result)
3963
+ return json({ error: result.error }, result.status);
3964
+ return json({ claim_attempt: result.claimAttempt });
3965
+ }).post(completeRoute, async ({ body, request }) => {
3966
+ let value = recordBody(body);
3967
+ if (value === undefined && typeof body === "string") {
3968
+ value = Object.fromEntries(new URLSearchParams(body));
3969
+ }
3970
+ if (value === undefined || typeof value.claim_attempt_token !== "string" || typeof value.user_code !== "string") {
3971
+ return json({ error: "invalid_request" }, 400);
3972
+ }
3973
+ const result = await completeAgentClaim(config, {
3974
+ attemptToken: value.claim_attempt_token,
3975
+ request,
3976
+ userCode: value.user_code
3977
+ });
3978
+ if ("error" in result)
3979
+ return json({ error: result.error }, result.status);
3980
+ return new Response(null, { status: 204 });
3981
+ }).as("global");
3185
3982
  };
3186
3983
 
3187
3984
  // src/audit/config.ts
@@ -4381,7 +5178,7 @@ var protectRoutePlugin = ({
4381
5178
  })).as("global");
4382
5179
 
4383
5180
  // src/htmx/renderers.ts
4384
- var escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
5181
+ var escapeHtml2 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
4385
5182
  var defaultAuthorizationHref = (provider, client) => client ? `/oauth2/${provider}/authorization?client=${client}` : `/oauth2/${provider}/authorization`;
4386
5183
  var resolveAuthHtmxRenderers = (config) => {
4387
5184
  const { connectorTargets, featuredLoginProviders, providerData } = config;
@@ -4394,10 +5191,10 @@ var resolveAuthHtmxRenderers = (config) => {
4394
5191
  return `<div class="grid-2">
4395
5192
  <div class="card"><h2 class="card__title">Canonical account</h2><p class="muted">Absolute Auth keeps one canonical user and links every OAuth identity to it. Conflicting identities raise a merge request.</p></div>
4396
5193
  <div class="card text-left"><h2 class="card__title">Profile fields</h2>
4397
- <div class="spread"><span class="muted">Subject</span><span>${escapeHtml(user.sub)}</span></div>
4398
- <div class="spread"><span class="muted">Name</span><span>${escapeHtml(fullName || "\u2014")}</span></div>
4399
- <div class="spread"><span class="muted">Email</span><span>${escapeHtml(user.email ?? "\u2014")}</span></div>
4400
- <div class="spread"><span class="muted">Primary identity</span><span>${escapeHtml(user.primary_auth_identity_id ?? "\u2014")}</span></div>
5194
+ <div class="spread"><span class="muted">Subject</span><span>${escapeHtml2(user.sub)}</span></div>
5195
+ <div class="spread"><span class="muted">Name</span><span>${escapeHtml2(fullName || "\u2014")}</span></div>
5196
+ <div class="spread"><span class="muted">Email</span><span>${escapeHtml2(user.email ?? "\u2014")}</span></div>
5197
+ <div class="spread"><span class="muted">Primary identity</span><span>${escapeHtml2(user.primary_auth_identity_id ?? "\u2014")}</span></div>
4401
5198
  </div>
4402
5199
  </div>`;
4403
5200
  };
@@ -4406,18 +5203,18 @@ var resolveAuthHtmxRenderers = (config) => {
4406
5203
  return `<a class="btn btn--primary btn--sm" href="/htmx">Sign in</a>`;
4407
5204
  }
4408
5205
  const label = user.email ?? user.first_name ?? "Account";
4409
- return `<span class="muted">${escapeHtml(label)}</span><a class="btn btn--ghost btn--sm" href="/htmx/signout">Sign out</a>`;
5206
+ return `<span class="muted">${escapeHtml2(label)}</span><a class="btn btn--ghost btn--sm" href="/htmx/signout">Sign out</a>`;
4410
5207
  };
4411
- const connectorLinks = () => connectorTargets.map((target) => `<div class="card text-left"><h2 class="card__title row"><img class="entity__logo" alt="" src="${providerLogo(target.provider)}" />${escapeHtml(target.label)}</h2><p class="muted">${escapeHtml(target.description)}</p><a class="btn btn--primary" href="${authorizationHref(target.provider, "connector")}">Link ${escapeHtml(target.label)}</a></div>`).join("");
5208
+ const connectorLinks = () => connectorTargets.map((target) => `<div class="card text-left"><h2 class="card__title row"><img class="entity__logo" alt="" src="${providerLogo(target.provider)}" />${escapeHtml2(target.label)}</h2><p class="muted">${escapeHtml2(target.description)}</p><a class="btn btn--primary" href="${authorizationHref(target.provider, "connector")}">Link ${escapeHtml2(target.label)}</a></div>`).join("");
4412
5209
  const connectors = (payload) => {
4413
- const scopeList = (scopes) => scopes.map((scope) => `<span class="scope">${escapeHtml(scope)}</span>`).join("");
4414
- const bindings = payload.bindings.length === 0 ? `<div class="empty-state">No external accounts linked.</div>` : `<div class="entity-list">${payload.bindings.map((binding) => `<div class="entity"><div class="entity__meta"><span class="entity__title">${escapeHtml(binding.label ?? binding.externalAccountId)}<span class="pill">${escapeHtml(binding.connectorProvider)}</span></span><span class="entity__sub">${escapeHtml(binding.externalAccountType)} \xB7 ${escapeHtml(binding.status)}</span><div class="scope-list">${scopeList(binding.availableScopes)}</div></div><div class="entity__actions"><form hx-delete="/htmx/connectors/bindings/${binding.id}" hx-target="#connector-list" hx-swap="innerHTML"><button class="btn btn--danger btn--sm" type="submit">Remove</button></form></div></div>`).join("")}</div>`;
4415
- const grants = payload.grants.length === 0 ? `<div class="empty-state">No connector grants yet.</div>` : `<div class="entity-list">${payload.grants.map((grant) => `<div class="entity"><div class="entity__meta"><span class="entity__title">${escapeHtml(grant.authProviderKey)}<span class="pill pill--indigo">${escapeHtml(grant.status)}</span></span><span class="entity__sub">Subject ${escapeHtml(grant.providerSubject)}</span><div class="scope-list">${scopeList(grant.grantedScopes)}</div></div><div class="entity__actions"><form hx-delete="/htmx/connectors/grants/${grant.id}" hx-target="#connector-list" hx-swap="innerHTML"><button class="btn btn--danger btn--sm" type="submit">Remove</button></form></div></div>`).join("")}</div>`;
5210
+ const scopeList = (scopes) => scopes.map((scope) => `<span class="scope">${escapeHtml2(scope)}</span>`).join("");
5211
+ const bindings = payload.bindings.length === 0 ? `<div class="empty-state">No external accounts linked.</div>` : `<div class="entity-list">${payload.bindings.map((binding) => `<div class="entity"><div class="entity__meta"><span class="entity__title">${escapeHtml2(binding.label ?? binding.externalAccountId)}<span class="pill">${escapeHtml2(binding.connectorProvider)}</span></span><span class="entity__sub">${escapeHtml2(binding.externalAccountType)} \xB7 ${escapeHtml2(binding.status)}</span><div class="scope-list">${scopeList(binding.availableScopes)}</div></div><div class="entity__actions"><form hx-delete="/htmx/connectors/bindings/${binding.id}" hx-target="#connector-list" hx-swap="innerHTML"><button class="btn btn--danger btn--sm" type="submit">Remove</button></form></div></div>`).join("")}</div>`;
5212
+ const grants = payload.grants.length === 0 ? `<div class="empty-state">No connector grants yet.</div>` : `<div class="entity-list">${payload.grants.map((grant) => `<div class="entity"><div class="entity__meta"><span class="entity__title">${escapeHtml2(grant.authProviderKey)}<span class="pill pill--indigo">${escapeHtml2(grant.status)}</span></span><span class="entity__sub">Subject ${escapeHtml2(grant.providerSubject)}</span><div class="scope-list">${scopeList(grant.grantedScopes)}</div></div><div class="entity__actions"><form hx-delete="/htmx/connectors/grants/${grant.id}" hx-target="#connector-list" hx-swap="innerHTML"><button class="btn btn--danger btn--sm" type="submit">Remove</button></form></div></div>`).join("")}</div>`;
4416
5213
  return `<h3 class="provider-heading">External accounts</h3>${bindings}<h3 class="provider-heading">Grants</h3>${grants}`;
4417
5214
  };
4418
5215
  const identities = (payload, query) => {
4419
5216
  const pending = payload.mergeRequests.filter((req) => req.status === "pending");
4420
- const mergesHtml = pending.length === 0 ? "" : `<div class="stack"><h3 class="provider-heading">Merge requests</h3><div class="entity-list">${pending.map((req) => `<div class="entity card--danger"><div class="entity__meta"><span class="entity__title">${escapeHtml(providerLabel(req.conflicting_auth_provider))} conflict</span><span class="entity__sub">Subject ${escapeHtml(req.conflicting_provider_subject)}</span></div><div class="entity__actions">
5217
+ const mergesHtml = pending.length === 0 ? "" : `<div class="stack"><h3 class="provider-heading">Merge requests</h3><div class="entity-list">${pending.map((req) => `<div class="entity card--danger"><div class="entity__meta"><span class="entity__title">${escapeHtml2(providerLabel(req.conflicting_auth_provider))} conflict</span><span class="entity__sub">Subject ${escapeHtml2(req.conflicting_provider_subject)}</span></div><div class="entity__actions">
4421
5218
  <form hx-post="/htmx/merge/${req.id}" hx-target="#identities-list" hx-swap="innerHTML" hx-include="#identity-query"><button class="btn btn--primary btn--sm" type="submit">Merge</button></form>
4422
5219
  <form hx-delete="/htmx/merge/${req.id}" hx-target="#identities-list" hx-swap="innerHTML" hx-include="#identity-query"><button class="btn btn--ghost btn--sm" type="submit">Dismiss</button></form>
4423
5220
  </div></div>`).join("")}</div></div>`;
@@ -4426,19 +5223,19 @@ var resolveAuthHtmxRenderers = (config) => {
4426
5223
  identities: list.filter((identity) => term === "" || providerLabel(provider).toLowerCase().includes(term) || identity.id.toLowerCase().includes(term) || identity.provider_subject.toLowerCase().includes(term)),
4427
5224
  provider
4428
5225
  })).filter((group) => group.identities.length > 0);
4429
- const groupsHtml = groups.length === 0 ? `<div class="empty-state">No identities match your search.</div>` : groups.map((group) => `<div class="provider-group"><h3 class="provider-heading">${providerLogo(group.provider) ? `<img class="entity__logo" alt="" src="${providerLogo(group.provider)}" />` : ""}${escapeHtml(providerLabel(group.provider))}</h3><div class="entity-list">${group.identities.map((identity) => `<div class="entity"><div class="entity__main"><div class="entity__meta"><span class="entity__title">${escapeHtml(identity.provider_subject)}${identity.isPrimary ? `<span class="pill pill--primary">Primary</span>` : ""}</span><span class="entity__sub">${escapeHtml(identity.id)}</span></div></div><div class="entity__actions">
5226
+ const groupsHtml = groups.length === 0 ? `<div class="empty-state">No identities match your search.</div>` : groups.map((group) => `<div class="provider-group"><h3 class="provider-heading">${providerLogo(group.provider) ? `<img class="entity__logo" alt="" src="${providerLogo(group.provider)}" />` : ""}${escapeHtml2(providerLabel(group.provider))}</h3><div class="entity-list">${group.identities.map((identity) => `<div class="entity"><div class="entity__main"><div class="entity__meta"><span class="entity__title">${escapeHtml2(identity.provider_subject)}${identity.isPrimary ? `<span class="pill pill--primary">Primary</span>` : ""}</span><span class="entity__sub">${escapeHtml2(identity.id)}</span></div></div><div class="entity__actions">
4430
5227
  ${identity.isPrimary ? "" : `<form hx-post="/htmx/identities/${identity.id}/primary" hx-target="#identities-list" hx-swap="innerHTML" hx-include="#identity-query"><button class="btn btn--neutral btn--sm" type="submit">Set primary</button></form>`}
4431
5228
  <form hx-delete="/htmx/identities/${identity.id}" hx-target="#identities-list" hx-swap="innerHTML" hx-include="#identity-query"><button class="btn btn--danger btn--sm" type="submit">Remove</button></form>
4432
5229
  </div></div>`).join("")}</div></div>`).join("");
4433
5230
  return `${mergesHtml}${groupsHtml}`;
4434
5231
  };
4435
- const protectedView = (user) => `<section class="auth-section stack"><div><h1 class="page-heading">Protected page</h1><p class="muted">Your authenticated session resolves to this user record.</p></div><pre class="json">${escapeHtml(JSON.stringify(user, null, 2))}</pre></section>`;
5232
+ const protectedView = (user) => `<section class="auth-section stack"><div><h1 class="page-heading">Protected page</h1><p class="muted">Your authenticated session resolves to this user record.</p></div><pre class="json">${escapeHtml2(JSON.stringify(user, null, 2))}</pre></section>`;
4436
5233
  const providerLogin = (verb, includeDropdown) => {
4437
- const featured = featuredLoginProviders.map((provider) => `<a class="oauth-button" href="${authorizationHref(provider)}"><img class="oauth-button__icon" alt="" src="${providerLogo(provider)}" /><span class="oauth-button__text">${escapeHtml(verb)} ${escapeHtml(providerLabel(provider))}</span></a>`).join("");
5234
+ const featured = featuredLoginProviders.map((provider) => `<a class="oauth-button" href="${authorizationHref(provider)}"><img class="oauth-button__icon" alt="" src="${providerLogo(provider)}" /><span class="oauth-button__text">${escapeHtml2(verb)} ${escapeHtml2(providerLabel(provider))}</span></a>`).join("");
4438
5235
  if (!includeDropdown) {
4439
5236
  return `<div class="oauth-grid">${featured}</div>`;
4440
5237
  }
4441
- const options = providerOptions.map((provider) => `<option value="${provider}">${escapeHtml(providerLabel(provider))}</option>`).join("");
5238
+ const options = providerOptions.map((provider) => `<option value="${provider}">${escapeHtml2(providerLabel(provider))}</option>`).join("");
4442
5239
  return `<div class="oauth-grid">${featured}
4443
5240
  <div class="separator"><span class="separator__line"></span><span class="separator__text">or any provider</span><span class="separator__line"></span></div>
4444
5241
  <form class="oauth-grid" action="/htmx/login-redirect" method="get">
@@ -4454,7 +5251,7 @@ var resolveAuthHtmxRenderers = (config) => {
4454
5251
  authMenu: overrides.authMenu ?? authMenu,
4455
5252
  connectorLinks: overrides.connectorLinks ?? connectorLinks,
4456
5253
  connectors: overrides.connectors ?? connectors,
4457
- escapeHtml,
5254
+ escapeHtml: escapeHtml2,
4458
5255
  identities: overrides.identities ?? identities,
4459
5256
  protected: overrides.protected ?? protectedView,
4460
5257
  providerLogin: overrides.providerLogin ?? providerLogin
@@ -5071,75 +5868,17 @@ import { Elysia as Elysia18, t as t14 } from "elysia";
5071
5868
  // src/oidc/config.ts
5072
5869
  init_constants();
5073
5870
  init_crypto();
5074
-
5075
- // src/oidc/keys.ts
5076
- var ENCODER = new TextEncoder;
5077
- var ES256 = { hash: "SHA-256", name: "ECDSA" };
5078
- var KEY_PARAMS = { name: "ECDSA", namedCurve: "P-256" };
5079
- var toBase64Url = (bytes) => Buffer.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)).toString("base64url");
5080
- var fromBase64Url = (value) => new Uint8Array(Buffer.from(value, "base64url"));
5081
- var encodeSegment = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
5082
- var decodeSegment = (segment) => JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
5083
- var generateSigningKey = async () => {
5084
- const pair = await crypto.subtle.generateKey(KEY_PARAMS, true, [
5085
- "sign",
5086
- "verify"
5087
- ]);
5088
- const privateJwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
5089
- const publicJwk = await crypto.subtle.exportKey("jwk", pair.publicKey);
5090
- return { kid: await jwkThumbprint(publicJwk), privateJwk, publicJwk };
5091
- };
5092
- var jwkThumbprint = async (jwk) => {
5093
- const canonical = JSON.stringify({
5094
- crv: jwk.crv,
5095
- kty: jwk.kty,
5096
- x: jwk.x,
5097
- y: jwk.y
5098
- });
5099
- return toBase64Url(await crypto.subtle.digest("SHA-256", ENCODER.encode(canonical)));
5100
- };
5101
- var signJwt = async (payload, signing) => {
5102
- const key = await crypto.subtle.importKey("jwk", signing.privateJwk, KEY_PARAMS, false, ["sign"]);
5103
- const input = `${encodeSegment({ alg: "ES256", kid: signing.kid, typ: "JWT" })}.${encodeSegment(payload)}`;
5104
- const signature = await crypto.subtle.sign(ES256, key, ENCODER.encode(input));
5105
- return `${input}.${toBase64Url(signature)}`;
5106
- };
5107
- var toPublicJwk = (key) => ({
5108
- alg: "ES256",
5109
- crv: key.publicJwk.crv,
5110
- kid: key.kid,
5111
- kty: key.publicJwk.kty,
5112
- use: "sig",
5113
- x: key.publicJwk.x,
5114
- y: key.publicJwk.y
5115
- });
5116
- var verifyJwt = async (token, publicJwk) => {
5117
- const [headerSegment, payloadSegment, signatureSegment] = token.split(".");
5118
- if (headerSegment === undefined || payloadSegment === undefined || signatureSegment === undefined) {
5119
- return;
5120
- }
5121
- const key = await crypto.subtle.importKey("jwk", publicJwk, KEY_PARAMS, false, ["verify"]);
5122
- const valid = await crypto.subtle.verify(ES256, key, fromBase64Url(signatureSegment), ENCODER.encode(`${headerSegment}.${payloadSegment}`));
5123
- if (!valid)
5124
- return;
5125
- return {
5126
- header: decodeSegment(headerSegment),
5127
- payload: decodeSegment(payloadSegment)
5128
- };
5129
- };
5130
-
5131
- // src/oidc/config.ts
5132
5871
  var DEFAULT_OIDC_ROUTE = "/oauth2";
5133
5872
  var MS_PER_SECOND = 1000;
5134
- var TOKEN_BYTES2 = 32;
5873
+ var TOKEN_BYTES3 = 32;
5135
5874
  var REFRESH_TTL_DAYS = 30;
5136
- var DEFAULT_ACCESS_TOKEN_TTL_MS2 = MILLISECONDS_IN_AN_HOUR;
5875
+ var DEFAULT_ACCESS_TOKEN_TTL_MS3 = MILLISECONDS_IN_AN_HOUR;
5137
5876
  var DEFAULT_ID_TOKEN_TTL_MS = MILLISECONDS_IN_AN_HOUR;
5138
5877
  var DEFAULT_REFRESH_TOKEN_TTL_MS = MILLISECONDS_IN_A_DAY * REFRESH_TTL_DAYS;
5139
5878
  var resolveAccessTtl = (ttl, scopes) => {
5140
5879
  if (typeof ttl === "function")
5141
5880
  return ttl({ scopes });
5142
- return ttl ?? DEFAULT_ACCESS_TOKEN_TTL_MS2;
5881
+ return ttl ?? DEFAULT_ACCESS_TOKEN_TTL_MS3;
5143
5882
  };
5144
5883
  var nowSeconds = (milliseconds) => Math.floor(milliseconds / MS_PER_SECOND);
5145
5884
  var narrowScopes = (available, requested) => requested === undefined || requested.length === 0 ? available : requested.filter((scope) => available.includes(scope));
@@ -5280,7 +6019,7 @@ var issueTokenSet = async ({
5280
6019
  idPayload.nonce = nonce;
5281
6020
  if (acr !== undefined)
5282
6021
  idPayload.acr = acr;
5283
- const refreshToken = generateSecureToken(TOKEN_BYTES2);
6022
+ const refreshToken = generateSecureToken(TOKEN_BYTES3);
5284
6023
  await config.refreshTokenStore.saveToken({
5285
6024
  acr,
5286
6025
  claims,
@@ -6714,7 +7453,7 @@ var HTTP_UNAUTHORIZED2 = 401;
6714
7453
  var HTTP_NOT_IMPLEMENTED = 501;
6715
7454
  var CODE_TTL_MINUTES = 10;
6716
7455
  var CODE_TTL_MS = MILLISECONDS_IN_A_MINUTE * CODE_TTL_MINUTES;
6717
- var TOKEN_BYTES3 = 32;
7456
+ var TOKEN_BYTES4 = 32;
6718
7457
  var BASIC_PREFIX2 = "Basic ";
6719
7458
  var jsonResponse = (value, status) => new Response(JSON.stringify(value), {
6720
7459
  headers: {
@@ -6797,7 +7536,7 @@ var oidcProviderRoutes = (config) => {
6797
7536
  const userinfoRoute = `${oidcRoute}/userinfo`;
6798
7537
  const registrationBaseUrl = `${issuer}${registrationRoute}`;
6799
7538
  const tokenUrl = `${issuer}${oidcRoute}/token`;
6800
- const resolveClient = async (clientId) => await clientStore.findClient(clientId) ?? await config.resolveClientIdMetadata?.(clientId);
7539
+ const resolveClient = async (clientId) => await clientStore.findClient(clientId) ?? config.resolveClientIdMetadata?.(clientId);
6801
7540
  const authenticateClient = async (clientId, clientSecret) => {
6802
7541
  const client = await resolveClient(clientId);
6803
7542
  if (client === undefined)
@@ -7059,13 +7798,17 @@ var oidcProviderRoutes = (config) => {
7059
7798
  if (config.vciConfig !== undefined) {
7060
7799
  grantTypes.push(PRE_AUTHORIZED_CODE_GRANT);
7061
7800
  }
7801
+ for (const grantType of config.publicTokenGrantTypes ?? []) {
7802
+ if (!grantTypes.includes(grantType))
7803
+ grantTypes.push(grantType);
7804
+ }
7062
7805
  const discovery = {
7063
7806
  authorization_endpoint: `${issuer}${authorizeRoute}`,
7064
7807
  authorization_response_iss_parameter_supported: true,
7065
7808
  backchannel_logout_session_supported: false,
7066
7809
  backchannel_logout_supported: true,
7067
- code_challenge_methods_supported: ["S256"],
7068
7810
  client_id_metadata_document_supported: config.resolveClientIdMetadata !== undefined,
7811
+ code_challenge_methods_supported: ["S256"],
7069
7812
  dpop_signing_alg_values_supported: ["ES256"],
7070
7813
  end_session_endpoint: `${issuer}${endSessionRoute}`,
7071
7814
  grant_types_supported: grantTypes,
@@ -7113,6 +7856,11 @@ var oidcProviderRoutes = (config) => {
7113
7856
  if (config.acrValuesSupported !== undefined && config.acrValuesSupported.length > 0) {
7114
7857
  discovery.acr_values_supported = config.acrValuesSupported;
7115
7858
  }
7859
+ const reservedDiscoveryKeys = new Set(Object.keys(discovery));
7860
+ for (const [key, value] of Object.entries(config.additionalDiscoveryMetadata ?? {})) {
7861
+ if (!reservedDiscoveryKeys.has(key))
7862
+ discovery[key] = value;
7863
+ }
7116
7864
  const handleEndSession = async ({
7117
7865
  cookie,
7118
7866
  inMemorySession,
@@ -7287,7 +8035,7 @@ var oidcProviderRoutes = (config) => {
7287
8035
  if (requestedAcr !== undefined && (userAcr === undefined || !requestedAcr.includes(userAcr))) {
7288
8036
  return errorRedirect("insufficient_user_authentication");
7289
8037
  }
7290
- const code = generateSecureToken(TOKEN_BYTES3);
8038
+ const code = generateSecureToken(TOKEN_BYTES4);
7291
8039
  await authorizationCodeStore.saveCode({
7292
8040
  acr: userAcr,
7293
8041
  claims: getClaims?.(userSession.user),
@@ -7352,6 +8100,15 @@ var oidcProviderRoutes = (config) => {
7352
8100
  token_type: result.token_type
7353
8101
  }, HTTP_OK2);
7354
8102
  }
8103
+ if (typeof body.grant_type === "string" && config.publicTokenGrantTypes?.includes(body.grant_type) === true && config.handlePublicTokenGrant !== undefined) {
8104
+ const result = await config.handlePublicTokenGrant({
8105
+ body,
8106
+ request
8107
+ });
8108
+ if (result !== undefined) {
8109
+ return jsonResponse(result.body, result.status);
8110
+ }
8111
+ }
7355
8112
  const basic = readBasicAuth2(headers.authorization);
7356
8113
  const auth = await authenticateTokenClient({
7357
8114
  basicClientId: basic.clientId,
@@ -7387,8 +8144,10 @@ var oidcProviderRoutes = (config) => {
7387
8144
  return oauthError2(HTTP_BAD_REQUEST2, "unsupported_grant_type");
7388
8145
  }, {
7389
8146
  body: t14.Object({
8147
+ assertion: t14.Optional(t14.String()),
7390
8148
  audience: t14.Optional(t14.String()),
7391
8149
  auth_req_id: t14.Optional(t14.String()),
8150
+ claim_token: t14.Optional(t14.String()),
7392
8151
  client_assertion: t14.Optional(t14.String()),
7393
8152
  client_assertion_type: t14.Optional(t14.String()),
7394
8153
  client_id: t14.Optional(t14.String()),
@@ -13742,7 +14501,7 @@ var PgJson = class extends PgColumn {
13742
14501
  return "json";
13743
14502
  }
13744
14503
  };
13745
- function json(name2) {
14504
+ function json2(name2) {
13746
14505
  return new PgJsonBuilder(name2 ?? "");
13747
14506
  }
13748
14507
 
@@ -14279,7 +15038,7 @@ function getPgColumnBuilders() {
14279
15038
  inet,
14280
15039
  integer,
14281
15040
  interval,
14282
- json,
15041
+ json: json2,
14283
15042
  jsonb,
14284
15043
  line,
14285
15044
  macaddr,
@@ -14496,6 +15255,9 @@ var Index = class {
14496
15255
  function index(name2) {
14497
15256
  return new IndexBuilderOn(false, name2);
14498
15257
  }
15258
+ function uniqueIndex(name2) {
15259
+ return new IndexBuilderOn(true, name2);
15260
+ }
14499
15261
  // node_modules/drizzle-orm/pg-core/checks.js
14500
15262
  var CheckBuilder = class {
14501
15263
  static [entityKind] = "PgCheckBuilder";
@@ -24966,6 +25728,284 @@ var createPostgresScimTokenStore = (db) => ({
24966
25728
  });
24967
25729
  }
24968
25730
  });
25731
+ // src/agents/registrationClient.ts
25732
+ var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
25733
+ var secureUrl = (value, allowLocalhost) => {
25734
+ if (typeof value !== "string")
25735
+ return;
25736
+ try {
25737
+ const url = new URL(value);
25738
+ if (url.protocol === "https:")
25739
+ return url.toString();
25740
+ if (allowLocalhost && url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1")) {
25741
+ return url.toString();
25742
+ }
25743
+ } catch {
25744
+ return;
25745
+ }
25746
+ return;
25747
+ };
25748
+ var readBoundedJson = async (response, maxBytes) => {
25749
+ const length = Number(response.headers.get("content-length"));
25750
+ if (Number.isFinite(length) && length > maxBytes) {
25751
+ throw new Error("Agent registration metadata exceeds the response limit");
25752
+ }
25753
+ const bytes = new Uint8Array(await response.arrayBuffer());
25754
+ if (bytes.byteLength > maxBytes) {
25755
+ throw new Error("Agent registration metadata exceeds the response limit");
25756
+ }
25757
+ const parsed = JSON.parse(new TextDecoder().decode(bytes));
25758
+ return parsed;
25759
+ };
25760
+ var stringArray = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
25761
+ var requestJson = async (request, url, init, maxBytes) => {
25762
+ const response = await request(url, {
25763
+ ...init,
25764
+ headers: {
25765
+ accept: "application/json",
25766
+ ...init.headers
25767
+ },
25768
+ redirect: "error"
25769
+ });
25770
+ const body = await readBoundedJson(response, maxBytes);
25771
+ if (!isObject2(body))
25772
+ throw new Error("Expected a JSON object");
25773
+ return { body, response };
25774
+ };
25775
+ var createAgentRegistrationClient = (discovery, options = {}) => {
25776
+ const request = options.request ?? fetch;
25777
+ const maxBytes = options.maxResponseBytes ?? 256 * 1024;
25778
+ const post = (url, body, form = false) => requestJson(request, url, {
25779
+ body: form ? new URLSearchParams(Object.fromEntries(Object.entries(body).map(([key, value]) => [
25780
+ key,
25781
+ String(value)
25782
+ ]))) : JSON.stringify(body),
25783
+ headers: {
25784
+ "content-type": form ? "application/x-www-form-urlencoded" : "application/json"
25785
+ },
25786
+ method: "POST"
25787
+ }, maxBytes);
25788
+ return {
25789
+ beginAnonymous: () => post(discovery.agentAuth.identityEndpoint, { type: "anonymous" }),
25790
+ beginServiceAuth: (loginHint) => post(discovery.agentAuth.identityEndpoint, {
25791
+ login_hint: loginHint,
25792
+ type: "service_auth"
25793
+ }),
25794
+ beginVerified: (assertion) => {
25795
+ if (!discovery.agentAuth.identityAssertionTypes.includes(AGENT_IDENTITY_ASSERTION_TYPE)) {
25796
+ throw new Error("Service does not accept ID-JAG assertions");
25797
+ }
25798
+ return post(discovery.agentAuth.identityEndpoint, {
25799
+ assertion,
25800
+ assertion_type: AGENT_IDENTITY_ASSERTION_TYPE,
25801
+ type: "identity_assertion"
25802
+ });
25803
+ },
25804
+ claim: (claimToken, email) => post(discovery.agentAuth.claimEndpoint, {
25805
+ claim_token: claimToken,
25806
+ email
25807
+ }),
25808
+ exchangeAssertion: (assertion) => post(discovery.tokenEndpoint, {
25809
+ assertion,
25810
+ grant_type: AGENT_IDENTITY_ASSERTION_GRANT_TYPE,
25811
+ resource: discovery.resource
25812
+ }, true),
25813
+ pollClaim: (claimToken) => post(discovery.tokenEndpoint, {
25814
+ claim_token: claimToken,
25815
+ grant_type: AGENT_CLAIM_GRANT_TYPE
25816
+ }, true)
25817
+ };
25818
+ };
25819
+ var discoverAgentRegistration = async (resource, options = {}) => {
25820
+ const request = options.request ?? fetch;
25821
+ const maxBytes = options.maxResponseBytes ?? 256 * 1024;
25822
+ const allowLocalhost = options.allowInsecureLocalhost === true;
25823
+ const resourceUrl = secureUrl(resource, allowLocalhost);
25824
+ if (resourceUrl === undefined)
25825
+ throw new Error("Resource URL must use HTTPS");
25826
+ const resourceMetadataUrl = new URL("/.well-known/oauth-protected-resource", resourceUrl).toString();
25827
+ const prm = await requestJson(request, resourceMetadataUrl, {}, maxBytes);
25828
+ const advertisedResource = secureUrl(prm.body.resource, allowLocalhost);
25829
+ if (advertisedResource === undefined || advertisedResource !== resourceUrl) {
25830
+ throw new Error("Protected resource metadata identity mismatch");
25831
+ }
25832
+ const authorizationServer = secureUrl(stringArray(prm.body.authorization_servers)[0], allowLocalhost);
25833
+ if (authorizationServer === undefined) {
25834
+ throw new Error("No secure authorization server is advertised");
25835
+ }
25836
+ const asUrl = new URL("/.well-known/oauth-authorization-server", authorizationServer).toString();
25837
+ const metadata = await requestJson(request, asUrl, {}, maxBytes);
25838
+ if (secureUrl(metadata.body.issuer, allowLocalhost) !== authorizationServer) {
25839
+ throw new Error("Authorization server issuer mismatch");
25840
+ }
25841
+ const tokenEndpoint = secureUrl(metadata.body.token_endpoint, allowLocalhost);
25842
+ const agentAuth = metadata.body.agent_auth;
25843
+ if (tokenEndpoint === undefined || !isObject2(agentAuth)) {
25844
+ throw new Error("Authorization server does not advertise agent registration");
25845
+ }
25846
+ const identityEndpoint = secureUrl(agentAuth.identity_endpoint, allowLocalhost);
25847
+ const claimEndpoint = secureUrl(agentAuth.claim_endpoint, allowLocalhost);
25848
+ const skill = secureUrl(agentAuth.skill, allowLocalhost);
25849
+ const assertionMetadata = agentAuth.identity_assertion;
25850
+ if (identityEndpoint === undefined || claimEndpoint === undefined || skill === undefined || !isObject2(assertionMetadata)) {
25851
+ throw new Error("Agent registration metadata is incomplete");
25852
+ }
25853
+ return {
25854
+ agentAuth: {
25855
+ claimEndpoint,
25856
+ identityAssertionTypes: stringArray(assertionMetadata.assertion_types_supported),
25857
+ identityEndpoint,
25858
+ identityTypes: stringArray(agentAuth.identity_types_supported),
25859
+ skill
25860
+ },
25861
+ authorizationServer,
25862
+ resource: resourceUrl,
25863
+ resourceMetadataUrl,
25864
+ scopes: stringArray(prm.body.scopes_supported),
25865
+ tokenEndpoint
25866
+ };
25867
+ };
25868
+ // src/agents/idJag.ts
25869
+ init_constants();
25870
+ var createInMemoryAgentIdentityAssertionJtiStore = () => {
25871
+ const entries = new Map;
25872
+ return {
25873
+ recordIfFresh: async (issuer, jti, expiresAt) => {
25874
+ const now = Date.now();
25875
+ for (const [key2, expiry] of entries) {
25876
+ if (expiry <= now)
25877
+ entries.delete(key2);
25878
+ }
25879
+ const key = `${issuer}\x00${jti}`;
25880
+ if (entries.has(key))
25881
+ return false;
25882
+ entries.set(key, expiresAt);
25883
+ return true;
25884
+ }
25885
+ };
25886
+ };
25887
+ var issueAgentIdentityAssertion = async ({
25888
+ agentContextId,
25889
+ agentPlatform,
25890
+ audience,
25891
+ clientId,
25892
+ issuer,
25893
+ now = Date.now(),
25894
+ resource,
25895
+ signingKey,
25896
+ ttlMs = 5 * 60 * MILLISECONDS_IN_A_SECOND,
25897
+ user
25898
+ }) => {
25899
+ if (user.emailVerified !== true && user.phoneNumberVerified !== true) {
25900
+ throw new Error("ID-JAG issuance requires a verified email or phone number");
25901
+ }
25902
+ const expiresAt = now + ttlMs;
25903
+ const payload = {
25904
+ aud: audience,
25905
+ auth_time: Math.floor(user.authenticatedAt / MILLISECONDS_IN_A_SECOND),
25906
+ client_id: clientId,
25907
+ exp: Math.floor(expiresAt / MILLISECONDS_IN_A_SECOND),
25908
+ iat: Math.floor(now / MILLISECONDS_IN_A_SECOND),
25909
+ iss: issuer,
25910
+ jti: crypto.randomUUID(),
25911
+ sub: user.subject
25912
+ };
25913
+ if (user.email !== undefined)
25914
+ payload.email = user.email;
25915
+ if (user.emailVerified !== undefined)
25916
+ payload.email_verified = user.emailVerified;
25917
+ if (user.name !== undefined)
25918
+ payload.name = user.name;
25919
+ if (user.phoneNumber !== undefined)
25920
+ payload.phone_number = user.phoneNumber;
25921
+ if (user.phoneNumberVerified !== undefined)
25922
+ payload.phone_number_verified = user.phoneNumberVerified;
25923
+ if (user.methods !== undefined)
25924
+ payload.amr = user.methods;
25925
+ if (resource !== undefined)
25926
+ payload.resource = resource;
25927
+ if (agentPlatform !== undefined)
25928
+ payload.agent_platform = agentPlatform;
25929
+ if (agentContextId !== undefined)
25930
+ payload.agent_context_id = agentContextId;
25931
+ return {
25932
+ assertion: await signJwt(payload, signingKey, "oauth-id-jag+jwt"),
25933
+ assertionType: AGENT_IDENTITY_ASSERTION_TYPE,
25934
+ expiresAt
25935
+ };
25936
+ };
25937
+ var numberClaim2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
25938
+ var stringClaim2 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
25939
+ var createAgentIdentityAssertionVerifier = ({
25940
+ audience,
25941
+ clockSkewMs = MILLISECONDS_IN_A_SECOND * 60,
25942
+ jtiStore,
25943
+ maxAssertionLifetimeMs = MILLISECONDS_IN_A_SECOND * 60 * 60,
25944
+ maxAuthenticationAgeMs = MILLISECONDS_IN_A_SECOND * 60 * 60,
25945
+ resolveIssuer
25946
+ }) => async (assertion, now = Date.now()) => {
25947
+ const segments = assertion.split(".");
25948
+ if (segments.length !== 3 || segments[1] === undefined)
25949
+ return;
25950
+ let decoded;
25951
+ try {
25952
+ decoded = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf8"));
25953
+ } catch {
25954
+ return;
25955
+ }
25956
+ if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
25957
+ return;
25958
+ }
25959
+ const unverified = Object.fromEntries(Object.entries(decoded));
25960
+ const issuer = stringClaim2(unverified.iss);
25961
+ if (issuer === undefined)
25962
+ return;
25963
+ const trusted = await resolveIssuer(issuer);
25964
+ if (trusted === undefined)
25965
+ return;
25966
+ const verified = await verifyJwt(assertion, trusted.publicJwk);
25967
+ if (verified?.header?.typ !== "oauth-id-jag+jwt")
25968
+ return;
25969
+ const { payload } = verified;
25970
+ const subject = stringClaim2(payload.sub);
25971
+ const jti = stringClaim2(payload.jti);
25972
+ const clientId = stringClaim2(payload.client_id);
25973
+ const expiresAtSeconds = numberClaim2(payload.exp);
25974
+ const issuedAtSeconds = numberClaim2(payload.iat);
25975
+ const authenticatedAtSeconds = numberClaim2(payload.auth_time);
25976
+ if (payload.iss !== issuer || payload.aud !== audience || subject === undefined || jti === undefined || clientId === undefined || expiresAtSeconds === undefined || issuedAtSeconds === undefined || authenticatedAtSeconds === undefined) {
25977
+ return;
25978
+ }
25979
+ const expiresAt = expiresAtSeconds * MILLISECONDS_IN_A_SECOND;
25980
+ const issuedAt = issuedAtSeconds * MILLISECONDS_IN_A_SECOND;
25981
+ const authenticatedAt = authenticatedAtSeconds * MILLISECONDS_IN_A_SECOND;
25982
+ if (expiresAt <= now - clockSkewMs || issuedAt > now + clockSkewMs || expiresAt <= issuedAt || expiresAt - issuedAt > maxAssertionLifetimeMs + clockSkewMs || authenticatedAt > now + clockSkewMs || authenticatedAt > issuedAt + clockSkewMs || now - authenticatedAt > maxAuthenticationAgeMs + clockSkewMs) {
25983
+ return;
25984
+ }
25985
+ if (trusted.allowedClientIds !== undefined && !trusted.allowedClientIds.includes(clientId)) {
25986
+ return;
25987
+ }
25988
+ const email = stringClaim2(payload.email);
25989
+ const phoneNumber = stringClaim2(payload.phone_number);
25990
+ const emailVerified = payload.email_verified === true;
25991
+ const phoneNumberVerified = payload.phone_number_verified === true;
25992
+ if (!emailVerified && !phoneNumberVerified)
25993
+ return;
25994
+ if (!await jtiStore.recordIfFresh(issuer, jti, expiresAt)) {
25995
+ return;
25996
+ }
25997
+ return {
25998
+ authenticatedAt,
25999
+ clientId,
26000
+ email,
26001
+ emailVerified,
26002
+ issuer,
26003
+ name: stringClaim2(payload.name),
26004
+ phoneNumber,
26005
+ phoneNumberVerified,
26006
+ subject
26007
+ };
26008
+ };
24969
26009
  // src/agents/oidcAdapter.ts
24970
26010
  var BEARER_PREFIX6 = "Bearer ";
24971
26011
  var MS_PER_SECOND7 = 1000;
@@ -25027,6 +26067,11 @@ var createOidcAgentCredentialVerifier = ({
25027
26067
  return verifier;
25028
26068
  };
25029
26069
  // src/agents/inMemoryStores.ts
26070
+ var cloneIdentityRegistration = (value) => ({
26071
+ ...value,
26072
+ claimAttempt: value.claimAttempt === undefined ? undefined : { ...value.claimAttempt },
26073
+ upstream: value.upstream === undefined ? undefined : { ...value.upstream }
26074
+ });
25030
26075
  var cloneRegistration = (value) => ({
25031
26076
  ...value,
25032
26077
  allowedScopes: [...value.allowedScopes],
@@ -25061,6 +26106,48 @@ var createInMemoryAgentDelegationStore = () => {
25061
26106
  }
25062
26107
  };
25063
26108
  };
26109
+ var createInMemoryAgentIdentityRegistrationStore = () => {
26110
+ const registrations = new Map;
26111
+ return {
26112
+ create: async (registration) => {
26113
+ const conflicts = [...registrations.values()].some((existing) => existing.registrationId === registration.registrationId || existing.agentId === registration.agentId || existing.claimTokenHash === registration.claimTokenHash || existing.claimAttempt !== undefined && existing.claimAttempt.tokenHash === registration.claimAttempt?.tokenHash || existing.upstream !== undefined && registration.upstream !== undefined && existing.upstream.clientId === registration.upstream.clientId && existing.upstream.issuer === registration.upstream.issuer && existing.upstream.subject === registration.upstream.subject);
26114
+ if (conflicts)
26115
+ return false;
26116
+ registrations.set(registration.registrationId, cloneIdentityRegistration(registration));
26117
+ return true;
26118
+ },
26119
+ findByAgentId: async (agentId) => {
26120
+ const value = [...registrations.values()].find((registration) => registration.agentId === agentId);
26121
+ return value === undefined ? undefined : cloneIdentityRegistration(value);
26122
+ },
26123
+ findByAttemptTokenHash: async (attemptTokenHash) => {
26124
+ const value = [...registrations.values()].find((registration) => registration.claimAttempt?.tokenHash === attemptTokenHash);
26125
+ return value === undefined ? undefined : cloneIdentityRegistration(value);
26126
+ },
26127
+ findByClaimTokenHash: async (claimTokenHash) => {
26128
+ const value = [...registrations.values()].find((registration) => registration.claimTokenHash === claimTokenHash);
26129
+ return value === undefined ? undefined : cloneIdentityRegistration(value);
26130
+ },
26131
+ findByRegistrationId: async (registrationId) => {
26132
+ const value = registrations.get(registrationId);
26133
+ return value === undefined ? undefined : cloneIdentityRegistration(value);
26134
+ },
26135
+ findByUpstreamIdentity: async ({ clientId, issuer, subject }) => {
26136
+ const value = [...registrations.values()].find((registration) => registration.upstream?.clientId === clientId && registration.upstream?.issuer === issuer && registration.upstream.subject === subject);
26137
+ return value === undefined ? undefined : cloneIdentityRegistration(value);
26138
+ },
26139
+ replace: async (registration, expectedVersion) => {
26140
+ const current = registrations.get(registration.registrationId);
26141
+ if (current?.version !== expectedVersion)
26142
+ return false;
26143
+ registrations.set(registration.registrationId, cloneIdentityRegistration({
26144
+ ...registration,
26145
+ version: expectedVersion + 1
26146
+ }));
26147
+ return true;
26148
+ }
26149
+ };
26150
+ };
25064
26151
  var createInMemoryAgentRegistrationStore = () => {
25065
26152
  const registrations = new Map;
25066
26153
  return {
@@ -25096,6 +26183,38 @@ var agentDelegationsTable = pgTable("auth_agent_delegations", {
25096
26183
  updated_at_ms: bigint("updated_at_ms", { mode: "number" }).notNull(),
25097
26184
  user_id: varchar("user_id", { length: ID_LENGTH7 }).notNull()
25098
26185
  });
26186
+ var agentIdentityRegistrationsTable = pgTable("auth_agent_identity_registrations", {
26187
+ agent_id: varchar("agent_id", { length: ID_LENGTH7 }).notNull().unique(),
26188
+ claim_attempt: jsonb("claim_attempt").$type(),
26189
+ claim_attempt_token_hash: varchar("claim_attempt_token_hash", {
26190
+ length: ID_LENGTH7
26191
+ }).unique(),
26192
+ claim_expires_at_ms: bigint("claim_expires_at_ms", {
26193
+ mode: "number"
26194
+ }).notNull(),
26195
+ claim_token_hash: varchar("claim_token_hash", {
26196
+ length: ID_LENGTH7
26197
+ }).notNull().unique(),
26198
+ created_at_ms: bigint("created_at_ms", { mode: "number" }).notNull(),
26199
+ expires_at_ms: bigint("expires_at_ms", { mode: "number" }).notNull(),
26200
+ kind: varchar("kind", { length: 32 }).$type().notNull(),
26201
+ last_polled_at_ms: bigint("last_polled_at_ms", { mode: "number" }),
26202
+ login_hint: varchar("login_hint", { length: ID_LENGTH7 }),
26203
+ registration_id: varchar("registration_id", {
26204
+ length: ID_LENGTH7
26205
+ }).primaryKey(),
26206
+ status: varchar("status", { length: STATUS_LENGTH2 }).$type().notNull(),
26207
+ updated_at_ms: bigint("updated_at_ms", { mode: "number" }).notNull(),
26208
+ upstream_client_id: varchar("upstream_client_id", {
26209
+ length: ID_LENGTH7
26210
+ }),
26211
+ upstream_issuer: varchar("upstream_issuer", { length: ID_LENGTH7 }),
26212
+ upstream_subject: varchar("upstream_subject", { length: ID_LENGTH7 }),
26213
+ user_id: varchar("user_id", { length: ID_LENGTH7 }),
26214
+ version: integer("version").notNull()
26215
+ }, (table) => [
26216
+ uniqueIndex("auth_agent_identity_upstream_unique").on(table.upstream_issuer, table.upstream_subject, table.upstream_client_id)
26217
+ ]);
25099
26218
  var agentRegistrationsTable = pgTable("auth_agent_registrations", {
25100
26219
  agent_id: varchar("agent_id", { length: ID_LENGTH7 }).primaryKey(),
25101
26220
  allowed_scopes: jsonb("allowed_scopes").$type().notNull().default([]),
@@ -25128,7 +26247,49 @@ var toDelegation = (row) => ({
25128
26247
  updatedAt: row.updated_at_ms,
25129
26248
  userId: row.user_id
25130
26249
  });
26250
+ var toIdentityRegistration = (row) => ({
26251
+ agentId: row.agent_id,
26252
+ claimAttempt: row.claim_attempt ?? undefined,
26253
+ claimExpiresAt: row.claim_expires_at_ms,
26254
+ claimTokenHash: row.claim_token_hash,
26255
+ createdAt: row.created_at_ms,
26256
+ expiresAt: row.expires_at_ms,
26257
+ kind: row.kind,
26258
+ lastPolledAt: row.last_polled_at_ms ?? undefined,
26259
+ loginHint: row.login_hint ?? undefined,
26260
+ registrationId: row.registration_id,
26261
+ status: row.status,
26262
+ updatedAt: row.updated_at_ms,
26263
+ upstream: row.upstream_client_id === null || row.upstream_issuer === null || row.upstream_subject === null ? undefined : {
26264
+ clientId: row.upstream_client_id,
26265
+ issuer: row.upstream_issuer,
26266
+ subject: row.upstream_subject
26267
+ },
26268
+ userId: row.user_id ?? undefined,
26269
+ version: row.version
26270
+ });
26271
+ var identityRegistrationValues = (registration) => ({
26272
+ agent_id: registration.agentId,
26273
+ claim_attempt: registration.claimAttempt ?? null,
26274
+ claim_attempt_token_hash: registration.claimAttempt?.tokenHash ?? null,
26275
+ claim_expires_at_ms: registration.claimExpiresAt,
26276
+ claim_token_hash: registration.claimTokenHash,
26277
+ created_at_ms: registration.createdAt,
26278
+ expires_at_ms: registration.expiresAt,
26279
+ kind: registration.kind,
26280
+ last_polled_at_ms: registration.lastPolledAt ?? null,
26281
+ login_hint: registration.loginHint ?? null,
26282
+ registration_id: registration.registrationId,
26283
+ status: registration.status,
26284
+ updated_at_ms: registration.updatedAt,
26285
+ upstream_client_id: registration.upstream?.clientId ?? null,
26286
+ upstream_issuer: registration.upstream?.issuer ?? null,
26287
+ upstream_subject: registration.upstream?.subject ?? null,
26288
+ user_id: registration.userId ?? null,
26289
+ version: registration.version
26290
+ });
25131
26291
  var createNeonAgentDelegationStore = (databaseUrl) => createPostgresAgentDelegationStore(createNeonDatabase(databaseUrl));
26292
+ var createNeonAgentIdentityRegistrationStore = (databaseUrl) => createPostgresAgentIdentityRegistrationStore(createNeonDatabase(databaseUrl));
25132
26293
  var createNeonAgentRegistrationStore = (databaseUrl) => createPostgresAgentRegistrationStore(createNeonDatabase(databaseUrl));
25133
26294
  var createPostgresAgentDelegationStore = (db) => ({
25134
26295
  findActiveDelegation: async ({
@@ -25169,6 +26330,41 @@ var createPostgresAgentDelegationStore = (db) => ({
25169
26330
  });
25170
26331
  }
25171
26332
  });
26333
+ var createPostgresAgentIdentityRegistrationStore = (db) => ({
26334
+ create: async (registration) => {
26335
+ const rows = await db.insert(agentIdentityRegistrationsTable).values(identityRegistrationValues(registration)).onConflictDoNothing().returning({ id: agentIdentityRegistrationsTable.registration_id });
26336
+ return rows.length === 1;
26337
+ },
26338
+ findByAgentId: async (agentId) => {
26339
+ const [row] = await db.select().from(agentIdentityRegistrationsTable).where(eq(agentIdentityRegistrationsTable.agent_id, agentId)).limit(1);
26340
+ return row === undefined ? undefined : toIdentityRegistration(row);
26341
+ },
26342
+ findByAttemptTokenHash: async (attemptTokenHash) => {
26343
+ const [row] = await db.select().from(agentIdentityRegistrationsTable).where(eq(agentIdentityRegistrationsTable.claim_attempt_token_hash, attemptTokenHash)).limit(1);
26344
+ return row === undefined ? undefined : toIdentityRegistration(row);
26345
+ },
26346
+ findByClaimTokenHash: async (claimTokenHash) => {
26347
+ const [row] = await db.select().from(agentIdentityRegistrationsTable).where(eq(agentIdentityRegistrationsTable.claim_token_hash, claimTokenHash)).limit(1);
26348
+ return row === undefined ? undefined : toIdentityRegistration(row);
26349
+ },
26350
+ findByRegistrationId: async (registrationId) => {
26351
+ const [row] = await db.select().from(agentIdentityRegistrationsTable).where(eq(agentIdentityRegistrationsTable.registration_id, registrationId)).limit(1);
26352
+ return row === undefined ? undefined : toIdentityRegistration(row);
26353
+ },
26354
+ findByUpstreamIdentity: async ({ clientId, issuer, subject }) => {
26355
+ const [row] = await db.select().from(agentIdentityRegistrationsTable).where(and(eq(agentIdentityRegistrationsTable.upstream_client_id, clientId), eq(agentIdentityRegistrationsTable.upstream_issuer, issuer), eq(agentIdentityRegistrationsTable.upstream_subject, subject))).limit(1);
26356
+ return row === undefined ? undefined : toIdentityRegistration(row);
26357
+ },
26358
+ replace: async (registration, expectedVersion) => {
26359
+ const next = {
26360
+ ...registration,
26361
+ version: expectedVersion + 1
26362
+ };
26363
+ const values = identityRegistrationValues(next);
26364
+ const rows = await db.update(agentIdentityRegistrationsTable).set(values).where(and(eq(agentIdentityRegistrationsTable.registration_id, registration.registrationId), eq(agentIdentityRegistrationsTable.version, expectedVersion))).returning({ id: agentIdentityRegistrationsTable.registration_id });
26365
+ return rows.length === 1;
26366
+ }
26367
+ });
25172
26368
  var createPostgresAgentRegistrationStore = (db) => ({
25173
26369
  findByAgentId: async (agentId) => {
25174
26370
  const [row] = await db.select().from(agentRegistrationsTable).where(eq(agentRegistrationsTable.agent_id, agentId)).limit(1);
@@ -25402,7 +26598,7 @@ var createPostgresApiKeyStore = (db) => ({
25402
26598
  }
25403
26599
  });
25404
26600
  // src/oidc/clientIdMetadata.ts
25405
- var secureUrl = (value) => {
26601
+ var secureUrl2 = (value) => {
25406
26602
  try {
25407
26603
  return new URL(value).protocol === "https:";
25408
26604
  } catch {
@@ -25413,7 +26609,7 @@ var validateClientIdMetadataDocument = (document, expectedClientId) => {
25413
26609
  const errors = [];
25414
26610
  if (document.client_id !== expectedClientId)
25415
26611
  errors.push("client_id does not match the metadata document URL");
25416
- if (!secureUrl(document.client_id))
26612
+ if (!secureUrl2(document.client_id))
25417
26613
  errors.push("client_id must use HTTPS");
25418
26614
  if (!Array.isArray(document.redirect_uris) || document.redirect_uris.length === 0)
25419
26615
  errors.push("redirect_uris is required");
@@ -25433,7 +26629,7 @@ var validateClientIdMetadataDocument = (document, expectedClientId) => {
25433
26629
  ["tos_uri", document.tos_uri],
25434
26630
  ["jwks_uri", document.jwks_uri]
25435
26631
  ]) {
25436
- if (value !== undefined && !secureUrl(value))
26632
+ if (value !== undefined && !secureUrl2(value))
25437
26633
  errors.push(`${name2} must use HTTPS`);
25438
26634
  }
25439
26635
  return errors;
@@ -25456,7 +26652,7 @@ var createClientIdMetadataResolver = ({
25456
26652
  }) => {
25457
26653
  const cache = new Map;
25458
26654
  return async (clientId) => {
25459
- if (!secureUrl(clientId) || !await allow(clientId))
26655
+ if (!secureUrl2(clientId) || !await allow(clientId))
25460
26656
  return;
25461
26657
  const cached = cache.get(clientId);
25462
26658
  if (cached !== undefined && cached.expiresAt > now())
@@ -27368,10 +28564,20 @@ var mfaTotpLockoutMigration = {
27368
28564
  };
27369
28565
  var blockMigrations = {
27370
28566
  adaptive: initMigration("adaptive", [knownDevicesTable, loginHistoryTable]),
27371
- agents: initMigration("agents", [
27372
- agentRegistrationsTable,
27373
- agentDelegationsTable
27374
- ]),
28567
+ agents: {
28568
+ block: "agents",
28569
+ migrations: [
28570
+ ...initMigration("agents", [
28571
+ agentRegistrationsTable,
28572
+ agentDelegationsTable,
28573
+ agentIdentityRegistrationsTable
28574
+ ]).migrations,
28575
+ {
28576
+ id: "0002_identity_registration",
28577
+ sql: tablesToInitSql([agentIdentityRegistrationsTable])
28578
+ }
28579
+ ]
28580
+ },
27375
28581
  apikeys: initMigration("apikeys", [
27376
28582
  accessTokensTable,
27377
28583
  apiClientsTable,
@@ -27893,6 +29099,38 @@ var auth = async ({
27893
29099
  onRevocationError,
27894
29100
  onSessionCleanup
27895
29101
  }) => {
29102
+ if (agentAuth?.agentRegistration !== undefined) {
29103
+ if (oidc === undefined) {
29104
+ throw new Error("agentAuth.agentRegistration requires the OIDC provider");
29105
+ }
29106
+ if (agentAuth.authorizationServer !== oidc.issuer) {
29107
+ throw new Error("agentAuth.authorizationServer must equal oidc.issuer");
29108
+ }
29109
+ const registration2 = agentAuth.agentRegistration;
29110
+ const unknownScopes = [
29111
+ ...registration2.preClaimScopes ?? [],
29112
+ ...registration2.postClaimScopes
29113
+ ].filter((scope) => !agentAuth.scopes.includes(scope));
29114
+ if (unknownScopes.length > 0) {
29115
+ throw new Error(`Agent registration uses undeclared scopes: ${[...new Set(unknownScopes)].join(", ")}`);
29116
+ }
29117
+ if (registration2.allowAnonymous === true && registration2.revokeAccessTokens === undefined) {
29118
+ throw new Error("Anonymous agent registration requires revokeAccessTokens");
29119
+ }
29120
+ for (const [name2, value] of Object.entries({
29121
+ assertionTtlMs: registration2.assertionTtlMs,
29122
+ attemptTtlMs: registration2.attemptTtlMs,
29123
+ claimTtlMs: registration2.claimTtlMs,
29124
+ maxAuthenticationAgeMs: registration2.maxAuthenticationAgeMs,
29125
+ maxCodeAttempts: registration2.maxCodeAttempts,
29126
+ pollIntervalSeconds: registration2.pollIntervalSeconds,
29127
+ tokenTtlMs: registration2.tokenTtlMs
29128
+ })) {
29129
+ if (value !== undefined && (!Number.isFinite(value) || value <= 0)) {
29130
+ throw new Error(`agentAuth.agentRegistration.${name2} must be positive`);
29131
+ }
29132
+ }
29133
+ }
27896
29134
  if (tracing !== undefined)
27897
29135
  await initTracing(tracing);
27898
29136
  const clientProviders = await buildClientProviders(providersConfiguration, createOAuth2Client, customProviders);
@@ -27906,8 +29144,34 @@ var auth = async ({
27906
29144
  }
27907
29145
  }) : undefined;
27908
29146
  const lockoutGuard = lockout ? createLockoutGuard(lockout) : undefined;
29147
+ const resolvedAgentAuth = agentAuth === undefined ? undefined : {
29148
+ ...agentAuth,
29149
+ oidcRoute: agentAuth.oidcRoute ?? oidc?.oidcRoute
29150
+ };
27909
29151
  const oidcConfig = oidc ? {
27910
29152
  ...oidc,
29153
+ additionalDiscoveryMetadata: {
29154
+ ...oidc.additionalDiscoveryMetadata,
29155
+ ...resolvedAgentAuth?.agentRegistration === undefined ? {} : {
29156
+ agent_auth: agentRegistrationDiscoveryMetadata(resolvedAgentAuth)
29157
+ }
29158
+ },
29159
+ publicTokenGrantTypes: [
29160
+ ...oidc.publicTokenGrantTypes ?? [],
29161
+ ...resolvedAgentAuth?.agentRegistration === undefined ? [] : [
29162
+ AGENT_CLAIM_GRANT_TYPE,
29163
+ AGENT_IDENTITY_ASSERTION_GRANT_TYPE
29164
+ ]
29165
+ ],
29166
+ handlePublicTokenGrant: async (context) => {
29167
+ const consumerResult = await oidc.handlePublicTokenGrant?.(context);
29168
+ if (consumerResult !== undefined)
29169
+ return consumerResult;
29170
+ if (resolvedAgentAuth?.agentRegistration === undefined) {
29171
+ return;
29172
+ }
29173
+ return handleAgentTokenGrant(resolvedAgentAuth, context.body);
29174
+ },
27911
29175
  onClientRegistered: async (context) => {
27912
29176
  await oidc.onClientRegistered?.(context);
27913
29177
  if (agentAuth?.registerDynamicClients !== true)
@@ -27932,28 +29196,28 @@ var auth = async ({
27932
29196
  await oidc.onDeviceAuthorizationApproved?.(context);
27933
29197
  if (agentAuth === undefined)
27934
29198
  return;
27935
- const registration = await agentAuth.registrationStore.findByClientId(context.clientId);
27936
- if (registration === undefined || registration.status !== "active") {
29199
+ const registration2 = await agentAuth.registrationStore.findByClientId(context.clientId);
29200
+ if (registration2 === undefined || registration2.status !== "active") {
27937
29201
  return;
27938
29202
  }
27939
29203
  const now = Date.now();
27940
29204
  const existing = await agentAuth.delegationStore.findActiveDelegation({
27941
- agentId: registration.agentId,
29205
+ agentId: registration2.agentId,
27942
29206
  now,
27943
29207
  userId: context.userSub
27944
29208
  });
27945
29209
  await agentAuth.delegationStore.saveDelegation({
27946
- agentId: registration.agentId,
29210
+ agentId: registration2.agentId,
27947
29211
  createdAt: existing?.createdAt ?? now,
27948
29212
  delegationId: existing?.delegationId ?? `agd_${crypto.randomUUID()}`,
27949
- scopes: context.scopes.filter((scope) => registration.allowedScopes.includes(scope) && agentAuth.scopes.includes(scope)),
29213
+ scopes: context.scopes.filter((scope) => registration2.allowedScopes.includes(scope) && agentAuth.scopes.includes(scope)),
27950
29214
  status: "active",
27951
29215
  updatedAt: now,
27952
29216
  userId: context.userSub
27953
29217
  });
27954
29218
  await auditEmit?.({
27955
29219
  at: now,
27956
- metadata: { agentId: registration.agentId },
29220
+ metadata: { agentId: registration2.agentId },
27957
29221
  type: "agent_delegated",
27958
29222
  userId: context.userSub
27959
29223
  });
@@ -28066,7 +29330,7 @@ var auth = async ({
28066
29330
  ...htmx,
28067
29331
  authSessionStore
28068
29332
  }) : new Elysia42);
28069
- const authWithAgent = composedAuth.use(agentAuthPlugin(agentAuth));
29333
+ const authWithAgent = composedAuth.use(agentAuthPlugin(resolvedAgentAuth));
28070
29334
  return authWithAgent;
28071
29335
  };
28072
29336
  export {
@@ -28118,6 +29382,7 @@ export {
28118
29382
  stepUpPlugin,
28119
29383
  statusListRoutes,
28120
29384
  startImpersonation,
29385
+ startAgentRegistration,
28121
29386
  ssoDiscoveryRoute,
28122
29387
  ssoConnectionsTable,
28123
29388
  signWebhook,
@@ -28144,6 +29409,7 @@ export {
28144
29409
  roleRoutes,
28145
29410
  revokeUserSessions,
28146
29411
  revokeRefreshToken,
29412
+ revokeAgentIdentityRegistration,
28147
29413
  revocableProviderOptions,
28148
29414
  resolveSetupSession,
28149
29415
  resolveScimOrganization,
@@ -28223,6 +29489,8 @@ export {
28223
29489
  issueDeviceAuthorization,
28224
29490
  issueCredential,
28225
29491
  issueBackchannelAuth,
29492
+ issueAgentServiceAssertion,
29493
+ issueAgentIdentityAssertion,
28226
29494
  isValidUser,
28227
29495
  isValidProviderOption,
28228
29496
  isUserSessionId,
@@ -28251,6 +29519,7 @@ export {
28251
29519
  hashAuditEvent,
28252
29520
  hasScopes,
28253
29521
  hasOrganizationScope,
29522
+ handleAgentTokenGrant,
28254
29523
  getUserSessionId,
28255
29524
  getStatus,
28256
29525
  getRegisteredClient,
@@ -28262,6 +29531,7 @@ export {
28262
29531
  generateSecureToken,
28263
29532
  generateEncryptionKey,
28264
29533
  generateBackupCodes,
29534
+ generateAgentRegistrationGuide,
28265
29535
  fromBase64Url2 as fromBase64Url,
28266
29536
  fingerprintDevice,
28267
29537
  fetchUserInfo,
@@ -28279,6 +29549,7 @@ export {
28279
29549
  endImpersonation,
28280
29550
  encryptTotpSecret,
28281
29551
  encryptSecret,
29552
+ discoverAgentRegistration,
28282
29553
  diffScimGroupMembers,
28283
29554
  denyDeviceAuthorization,
28284
29555
  denyBackchannelAuth,
@@ -28349,6 +29620,7 @@ export {
28349
29620
  createPostgresApiKeyStore,
28350
29621
  createPostgresApiClientStore,
28351
29622
  createPostgresAgentRegistrationStore,
29623
+ createPostgresAgentIdentityRegistrationStore,
28352
29624
  createPostgresAgentDelegationStore,
28353
29625
  createPostgresAccessTokenStore,
28354
29626
  createOrganization,
@@ -28391,6 +29663,7 @@ export {
28391
29663
  createNeonApiKeyStore,
28392
29664
  createNeonApiClientStore,
28393
29665
  createNeonAgentRegistrationStore,
29666
+ createNeonAgentIdentityRegistrationStore,
28394
29667
  createNeonAgentDelegationStore,
28395
29668
  createNeonAccessTokenStore,
28396
29669
  createMfaGate,
@@ -28433,6 +29706,8 @@ export {
28433
29706
  createInMemoryApiKeyStore,
28434
29707
  createInMemoryApiClientStore,
28435
29708
  createInMemoryAgentRegistrationStore,
29709
+ createInMemoryAgentIdentityRegistrationStore,
29710
+ createInMemoryAgentIdentityAssertionJtiStore,
28436
29711
  createInMemoryAgentDelegationStore,
28437
29712
  createInMemoryAccessTokenStore,
28438
29713
  createFgaEngine,
@@ -28446,6 +29721,9 @@ export {
28446
29721
  createApiKey,
28447
29722
  createApiClient,
28448
29723
  createAnonymousSession,
29724
+ createAgentRegistrationCredentialVerifier,
29725
+ createAgentRegistrationClient,
29726
+ createAgentIdentityAssertionVerifier,
28449
29727
  createActionPipeline,
28450
29728
  createAbuseGuard,
28451
29729
  consumePushedRequest,
@@ -28453,6 +29731,7 @@ export {
28453
29731
  constantTimeEqual,
28454
29732
  computeCertThumbprint,
28455
29733
  complianceRoutes,
29734
+ completeAgentClaim,
28456
29735
  clientIdMetadataToOAuthClient,
28457
29736
  check2 as check,
28458
29737
  buildStatusClaim,
@@ -28460,6 +29739,7 @@ export {
28460
29739
  buildHolderKeyBindingJwt,
28461
29740
  buildClientProviders,
28462
29741
  blockMigrations,
29742
+ beginAgentClaim,
28463
29743
  base32Encode,
28464
29744
  base32Decode,
28465
29745
  authProviderOption,
@@ -28475,7 +29755,10 @@ export {
28475
29755
  apiKeysRoutes,
28476
29756
  apiClientsTable,
28477
29757
  agentRegistrationsTable,
29758
+ agentRegistrationEndpoints,
29759
+ agentRegistrationDiscoveryMetadata,
28478
29760
  agentProtectedResourceMetadata,
29761
+ agentIdentityRegistrationsTable,
28479
29762
  agentHasScopes,
28480
29763
  agentDelegationsTable,
28481
29764
  agentAuthPlugin,
@@ -28526,8 +29809,11 @@ export {
28526
29809
  DEFAULT_AGENT_RESOURCE_METADATA_ROUTE,
28527
29810
  CLIENT_ASSERTION_TYPE,
28528
29811
  CIBA_GRANT_TYPE,
28529
- AuthIdentityConflictError
29812
+ AuthIdentityConflictError,
29813
+ AGENT_IDENTITY_ASSERTION_TYPE,
29814
+ AGENT_IDENTITY_ASSERTION_GRANT_TYPE,
29815
+ AGENT_CLAIM_GRANT_TYPE
28530
29816
  };
28531
29817
 
28532
- //# debugId=D8A79A996B677FF164756E2164756E21
29818
+ //# debugId=8E4D1AD9D7CAB11764756E2164756E21
28533
29819
  //# sourceMappingURL=index.js.map