@absolutejs/auth 0.54.9 → 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.
- package/README.md +19 -5
- package/dist/agents/config.d.ts +16 -0
- package/dist/agents/idJag.d.ts +49 -0
- package/dist/agents/inMemoryStores.d.ts +2 -1
- package/dist/agents/index.d.ts +5 -2
- package/dist/agents/index.js +1391 -66
- package/dist/agents/index.js.map +12 -8
- package/dist/agents/postgresStores.d.ts +280 -1
- package/dist/agents/registration.d.ts +162 -0
- package/dist/agents/registrationClient.d.ts +50 -0
- package/dist/agents/routes.d.ts +112 -1
- package/dist/agents/types.d.ts +59 -0
- package/dist/apikeys/routes.d.ts +2 -2
- package/dist/cli/migrate.js +57 -8
- package/dist/cli/migrate.js.map +4 -4
- package/dist/credentials/login.d.ts +4 -4
- package/dist/credentials/routes.d.ts +4 -4
- package/dist/index.d.ts +185 -2
- package/dist/index.js +1388 -102
- package/dist/index.js.map +15 -12
- package/dist/manifest.js +12 -6
- package/dist/manifest.js.map +4 -4
- package/dist/manifest.json +2 -2
- package/dist/mfa/routes.d.ts +2 -2
- package/dist/mfa/sms.d.ts +2 -2
- package/dist/oidc/clientAuth.d.ts +6 -2
- package/dist/oidc/config.d.ts +19 -5
- package/dist/oidc/keys.d.ts +7 -3
- package/dist/oidc/logout.d.ts +2 -2
- package/dist/oidc/routes.d.ts +13 -11
- package/dist/organizations/routes.d.ts +1 -1
- package/dist/portal/routes.d.ts +3 -3
- package/dist/roles/routes.d.ts +1 -1
- package/dist/routes/refresh.d.ts +1 -1
- package/dist/routes/revoke.d.ts +1 -1
- package/dist/routes/sessions.d.ts +3 -3
- package/dist/sso/discoveryRoute.d.ts +1 -1
- package/dist/sso/oidcRoutes.d.ts +2 -2
- package/dist/sso/samlRoutes.d.ts +4 -4
- package/docs/AGENT-AUTH.md +54 -0
- package/docs/MIGRATE-FROM-LUCIA.md +235 -0
- package/docs/OAUTH-PROVIDER-QUIRKS.md +150 -0
- package/docs/UI-COMPONENTS.md +226 -0
- package/package.json +6 -3
package/dist/agents/index.js
CHANGED
|
@@ -55,6 +55,132 @@ var init_constants = __esm(() => {
|
|
|
55
55
|
COOKIE_DURATION = SECONDS_IN_A_MINUTE * COOKIE_MINUTES;
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
// src/crypto.ts
|
|
59
|
+
var exports_crypto = {};
|
|
60
|
+
__export(exports_crypto, {
|
|
61
|
+
verifyTotp: () => verifyTotp,
|
|
62
|
+
verifyPassword: () => verifyPassword,
|
|
63
|
+
hashToken: () => hashToken,
|
|
64
|
+
hashPassword: () => hashPassword,
|
|
65
|
+
generateTotpSecret: () => generateTotpSecret,
|
|
66
|
+
generateTotp: () => generateTotp,
|
|
67
|
+
generateSecureToken: () => generateSecureToken,
|
|
68
|
+
generateEncryptionKey: () => generateEncryptionKey,
|
|
69
|
+
encryptSecret: () => encryptSecret,
|
|
70
|
+
decryptSecret: () => decryptSecret,
|
|
71
|
+
createTotpKeyUri: () => createTotpKeyUri,
|
|
72
|
+
constantTimeEqual: () => constantTimeEqual,
|
|
73
|
+
base32Encode: () => base32Encode,
|
|
74
|
+
base32Decode: () => base32Decode
|
|
75
|
+
});
|
|
76
|
+
var DEFAULT_TOKEN_BYTES = 32, AES_KEY_BYTES = 32, AES_IV_BYTES = 12, HOTP_COUNTER_BYTES = 8, TOTP_SECRET_BYTES = 20, TOTP_DIGITS = 6, TOTP_PERIOD_SECONDS = 30, DEFAULT_TOTP_WINDOW = 1, DECIMAL_RADIX = 10, LAST_NIBBLE_MASK = 15, SIGN_BIT_MASK = 2147483647, BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", BASE32_GROUP_BITS = 5, BASE32_MASK = 31, BYTE_BITS = 8, textEncoder, textDecoder, base64UrlEncode = (bytes) => Buffer.from(bytes).toString("base64url"), base64UrlDecode = (encoded) => new Uint8Array(Buffer.from(encoded, "base64url")), sha256 = async (input) => {
|
|
77
|
+
const digest = await crypto.subtle.digest("SHA-256", textEncoder.encode(input));
|
|
78
|
+
return new Uint8Array(digest);
|
|
79
|
+
}, hmacSha1 = async (key, message) => {
|
|
80
|
+
const cryptoKey = await crypto.subtle.importKey("raw", key, { hash: "SHA-1", name: "HMAC" }, false, ["sign"]);
|
|
81
|
+
const signature = await crypto.subtle.sign("HMAC", cryptoKey, message);
|
|
82
|
+
return new Uint8Array(signature);
|
|
83
|
+
}, counterToBytes = (counter) => {
|
|
84
|
+
const bytes = new Uint8Array(HOTP_COUNTER_BYTES);
|
|
85
|
+
new DataView(bytes.buffer).setBigUint64(0, BigInt(counter), false);
|
|
86
|
+
return bytes;
|
|
87
|
+
}, generateHotp = async (secret, counter, digits = TOTP_DIGITS) => {
|
|
88
|
+
const hmac = await hmacSha1(secret, counterToBytes(counter));
|
|
89
|
+
const view = new DataView(hmac.buffer, hmac.byteOffset, hmac.byteLength);
|
|
90
|
+
const offset = view.getUint8(hmac.byteLength - 1) & LAST_NIBBLE_MASK;
|
|
91
|
+
const truncated = view.getUint32(offset, false) & SIGN_BIT_MASK;
|
|
92
|
+
const otp = truncated % DECIMAL_RADIX ** digits;
|
|
93
|
+
return otp.toString().padStart(digits, "0");
|
|
94
|
+
}, importAesKey = (keyMaterial) => crypto.subtle.importKey("raw", base64UrlDecode(keyMaterial), { name: "AES-GCM" }, false, ["decrypt", "encrypt"]), base32Decode = (encoded) => {
|
|
95
|
+
const normalized = encoded.toUpperCase().replace(/[^A-Z2-7]/gu, "");
|
|
96
|
+
const bits = [...normalized].map((char) => BASE32_ALPHABET.indexOf(char).toString(2).padStart(BASE32_GROUP_BITS, "0")).join("");
|
|
97
|
+
const byteChunks = bits.match(/.{8}/gu) ?? [];
|
|
98
|
+
return new Uint8Array(byteChunks.map((chunk) => parseInt(chunk, 2)));
|
|
99
|
+
}, base32Encode = (bytes) => {
|
|
100
|
+
const bits = Array.from(bytes, (byte) => byte.toString(2).padStart(BYTE_BITS, "0")).join("");
|
|
101
|
+
const groups = bits.match(/.{1,5}/gu) ?? [];
|
|
102
|
+
return groups.map((group) => BASE32_ALPHABET[parseInt(group.padEnd(BASE32_GROUP_BITS, "0"), 2) & BASE32_MASK] ?? "").join("");
|
|
103
|
+
}, constantTimeEqual = async (left, right) => {
|
|
104
|
+
const [leftDigest, rightDigest] = await Promise.all([
|
|
105
|
+
sha256(left),
|
|
106
|
+
sha256(right)
|
|
107
|
+
]);
|
|
108
|
+
const leftView = new DataView(leftDigest.buffer, leftDigest.byteOffset, leftDigest.byteLength);
|
|
109
|
+
const rightView = new DataView(rightDigest.buffer, rightDigest.byteOffset, rightDigest.byteLength);
|
|
110
|
+
let mismatch = 0;
|
|
111
|
+
for (let index = 0;index < leftDigest.byteLength; index += 1) {
|
|
112
|
+
mismatch |= leftView.getUint8(index) ^ rightView.getUint8(index);
|
|
113
|
+
}
|
|
114
|
+
return mismatch === 0;
|
|
115
|
+
}, createTotpKeyUri = ({
|
|
116
|
+
accountName,
|
|
117
|
+
digits = TOTP_DIGITS,
|
|
118
|
+
issuer,
|
|
119
|
+
period = TOTP_PERIOD_SECONDS,
|
|
120
|
+
secret
|
|
121
|
+
}) => {
|
|
122
|
+
const params = new URLSearchParams({
|
|
123
|
+
algorithm: "SHA1",
|
|
124
|
+
digits: `${digits}`,
|
|
125
|
+
issuer,
|
|
126
|
+
period: `${period}`,
|
|
127
|
+
secret
|
|
128
|
+
});
|
|
129
|
+
const label = encodeURIComponent(`${issuer}:${accountName}`);
|
|
130
|
+
return `otpauth://totp/${label}?${params.toString()}`;
|
|
131
|
+
}, decryptSecret = async (ciphertext, keyMaterial) => {
|
|
132
|
+
const key = await importAesKey(keyMaterial);
|
|
133
|
+
const combined = base64UrlDecode(ciphertext);
|
|
134
|
+
const nonce = combined.subarray(0, AES_IV_BYTES);
|
|
135
|
+
const data = combined.subarray(AES_IV_BYTES);
|
|
136
|
+
const plaintext = await crypto.subtle.decrypt({ iv: nonce, name: "AES-GCM" }, key, data);
|
|
137
|
+
return textDecoder.decode(plaintext);
|
|
138
|
+
}, encryptSecret = async (plaintext, keyMaterial) => {
|
|
139
|
+
const key = await importAesKey(keyMaterial);
|
|
140
|
+
const nonce = new Uint8Array(AES_IV_BYTES);
|
|
141
|
+
crypto.getRandomValues(nonce);
|
|
142
|
+
const ciphertext = await crypto.subtle.encrypt({ iv: nonce, name: "AES-GCM" }, key, textEncoder.encode(plaintext));
|
|
143
|
+
const combined = new Uint8Array(nonce.byteLength + ciphertext.byteLength);
|
|
144
|
+
combined.set(nonce, 0);
|
|
145
|
+
combined.set(new Uint8Array(ciphertext), nonce.byteLength);
|
|
146
|
+
return base64UrlEncode(combined);
|
|
147
|
+
}, generateEncryptionKey = () => generateSecureToken(AES_KEY_BYTES), generateSecureToken = (byteLength = DEFAULT_TOKEN_BYTES) => {
|
|
148
|
+
const bytes = new Uint8Array(byteLength);
|
|
149
|
+
crypto.getRandomValues(bytes);
|
|
150
|
+
return base64UrlEncode(bytes);
|
|
151
|
+
}, generateTotp = async ({
|
|
152
|
+
digits = TOTP_DIGITS,
|
|
153
|
+
now = Date.now(),
|
|
154
|
+
period = TOTP_PERIOD_SECONDS,
|
|
155
|
+
secret
|
|
156
|
+
}) => {
|
|
157
|
+
const counter = Math.floor(now / MILLISECONDS_IN_A_SECOND / period);
|
|
158
|
+
return generateHotp(base32Decode(secret), counter, digits);
|
|
159
|
+
}, generateTotpSecret = (byteLength = TOTP_SECRET_BYTES) => {
|
|
160
|
+
const bytes = new Uint8Array(byteLength);
|
|
161
|
+
crypto.getRandomValues(bytes);
|
|
162
|
+
return base32Encode(bytes);
|
|
163
|
+
}, hashPassword = (password) => Bun.password.hash(password, { algorithm: "argon2id" }), hashToken = async (token) => base64UrlEncode(await sha256(token)), verifyPassword = (password, hash) => Bun.password.verify(password, hash), verifyTotp = async ({
|
|
164
|
+
digits = TOTP_DIGITS,
|
|
165
|
+
now = Date.now(),
|
|
166
|
+
period = TOTP_PERIOD_SECONDS,
|
|
167
|
+
secret,
|
|
168
|
+
token,
|
|
169
|
+
window = DEFAULT_TOTP_WINDOW
|
|
170
|
+
}) => {
|
|
171
|
+
const secretBytes = base32Decode(secret);
|
|
172
|
+
const counter = Math.floor(now / MILLISECONDS_IN_A_SECOND / period);
|
|
173
|
+
const drifts = Array.from({ length: window * 2 + 1 }, (_, offset) => counter - window + offset);
|
|
174
|
+
const candidates = await Promise.all(drifts.map((value) => generateHotp(secretBytes, value, digits)));
|
|
175
|
+
const matches = await Promise.all(candidates.map((candidate) => constantTimeEqual(candidate, token)));
|
|
176
|
+
return matches.includes(true);
|
|
177
|
+
};
|
|
178
|
+
var init_crypto = __esm(() => {
|
|
179
|
+
init_constants();
|
|
180
|
+
textEncoder = new TextEncoder;
|
|
181
|
+
textDecoder = new TextDecoder;
|
|
182
|
+
});
|
|
183
|
+
|
|
58
184
|
// src/agents/config.ts
|
|
59
185
|
var DEFAULT_AGENT_RESOURCE_METADATA_ROUTE = "/.well-known/oauth-protected-resource";
|
|
60
186
|
var agentProtectedResourceMetadata = (config) => ({
|
|
@@ -65,8 +191,952 @@ var agentProtectedResourceMetadata = (config) => ({
|
|
|
65
191
|
resource: config.resource,
|
|
66
192
|
scopes_supported: config.scopes
|
|
67
193
|
});
|
|
194
|
+
// src/agents/registration.ts
|
|
195
|
+
init_constants();
|
|
196
|
+
init_crypto();
|
|
197
|
+
|
|
198
|
+
// src/oidc/keys.ts
|
|
199
|
+
var ENCODER = new TextEncoder;
|
|
200
|
+
var ES256 = { hash: "SHA-256", name: "ECDSA" };
|
|
201
|
+
var KEY_PARAMS = { name: "ECDSA", namedCurve: "P-256" };
|
|
202
|
+
var toBase64Url = (bytes) => Buffer.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)).toString("base64url");
|
|
203
|
+
var fromBase64Url = (value) => new Uint8Array(Buffer.from(value, "base64url"));
|
|
204
|
+
var encodeSegment = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
205
|
+
var decodeSegment = (segment) => {
|
|
206
|
+
try {
|
|
207
|
+
const value = JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
|
|
208
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
return Object.fromEntries(Object.entries(value));
|
|
212
|
+
} catch {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
var generateSigningKey = async () => {
|
|
217
|
+
const pair = await crypto.subtle.generateKey(KEY_PARAMS, true, [
|
|
218
|
+
"sign",
|
|
219
|
+
"verify"
|
|
220
|
+
]);
|
|
221
|
+
const privateJwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
222
|
+
const publicJwk = await crypto.subtle.exportKey("jwk", pair.publicKey);
|
|
223
|
+
return { kid: await jwkThumbprint(publicJwk), privateJwk, publicJwk };
|
|
224
|
+
};
|
|
225
|
+
var jwkThumbprint = async (jwk) => {
|
|
226
|
+
const canonical = JSON.stringify({
|
|
227
|
+
crv: jwk.crv,
|
|
228
|
+
kty: jwk.kty,
|
|
229
|
+
x: jwk.x,
|
|
230
|
+
y: jwk.y
|
|
231
|
+
});
|
|
232
|
+
return toBase64Url(await crypto.subtle.digest("SHA-256", ENCODER.encode(canonical)));
|
|
233
|
+
};
|
|
234
|
+
var signJwt = async (payload, signing, typ = "JWT") => {
|
|
235
|
+
const key = await crypto.subtle.importKey("jwk", signing.privateJwk, KEY_PARAMS, false, ["sign"]);
|
|
236
|
+
const input = `${encodeSegment({ alg: "ES256", kid: signing.kid, typ })}.${encodeSegment(payload)}`;
|
|
237
|
+
const signature = await crypto.subtle.sign(ES256, key, ENCODER.encode(input));
|
|
238
|
+
return `${input}.${toBase64Url(signature)}`;
|
|
239
|
+
};
|
|
240
|
+
var toPublicJwk = (key) => ({
|
|
241
|
+
alg: "ES256",
|
|
242
|
+
crv: key.publicJwk.crv,
|
|
243
|
+
kid: key.kid,
|
|
244
|
+
kty: key.publicJwk.kty,
|
|
245
|
+
use: "sig",
|
|
246
|
+
x: key.publicJwk.x,
|
|
247
|
+
y: key.publicJwk.y
|
|
248
|
+
});
|
|
249
|
+
var verifyJwt = async (token, publicJwk) => {
|
|
250
|
+
const [headerSegment, payloadSegment, signatureSegment] = token.split(".");
|
|
251
|
+
if (headerSegment === undefined || payloadSegment === undefined || signatureSegment === undefined) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const key = await crypto.subtle.importKey("jwk", publicJwk, KEY_PARAMS, false, ["verify"]);
|
|
255
|
+
const valid = await crypto.subtle.verify(ES256, key, fromBase64Url(signatureSegment), ENCODER.encode(`${headerSegment}.${payloadSegment}`));
|
|
256
|
+
if (!valid)
|
|
257
|
+
return;
|
|
258
|
+
const header = decodeSegment(headerSegment);
|
|
259
|
+
const payload = decodeSegment(payloadSegment);
|
|
260
|
+
if (header === undefined || payload === undefined)
|
|
261
|
+
return;
|
|
262
|
+
return {
|
|
263
|
+
header,
|
|
264
|
+
payload
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// src/agents/registration.ts
|
|
269
|
+
var AGENT_CLAIM_GRANT_TYPE = "urn:workos:agent-auth:grant-type:claim";
|
|
270
|
+
var AGENT_IDENTITY_ASSERTION_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
|
271
|
+
var AGENT_IDENTITY_ASSERTION_TYPE = "urn:ietf:params:oauth:token-type:id-jag";
|
|
272
|
+
var DEFAULT_IDENTITY_ROUTE = "/agent/identity";
|
|
273
|
+
var DEFAULT_CLAIM_ROUTE = "/agent/identity/claim";
|
|
274
|
+
var DEFAULT_COMPLETE_ROUTE = "/agent/identity/claim/complete";
|
|
275
|
+
var DEFAULT_GUIDE_ROUTE = "/auth.md";
|
|
276
|
+
var DEFAULT_CLAIM_TTL_MS = 24 * 60 * MILLISECONDS_IN_A_MINUTE;
|
|
277
|
+
var DEFAULT_ATTEMPT_TTL_MS = 10 * MILLISECONDS_IN_A_MINUTE;
|
|
278
|
+
var DEFAULT_ASSERTION_TTL_MS = 60 * MILLISECONDS_IN_A_MINUTE;
|
|
279
|
+
var DEFAULT_ACCESS_TOKEN_TTL_MS = 15 * MILLISECONDS_IN_A_MINUTE;
|
|
280
|
+
var DEFAULT_MAX_AUTH_AGE_MS = 60 * MILLISECONDS_IN_A_MINUTE;
|
|
281
|
+
var DEFAULT_POLL_INTERVAL_SECONDS = 5;
|
|
282
|
+
var DEFAULT_MAX_CODE_ATTEMPTS = 5;
|
|
283
|
+
var MAX_CONCURRENT_UPDATE_RETRIES = 5;
|
|
284
|
+
var TOKEN_BYTES = 32;
|
|
285
|
+
var agentRegistrationDiscoveryMetadata = (config) => {
|
|
286
|
+
const registration = requiredRegistration(config);
|
|
287
|
+
const endpoints = agentRegistrationEndpoints(config);
|
|
288
|
+
const identityTypes = ["identity_assertion"];
|
|
289
|
+
if (registration.allowAnonymous === true)
|
|
290
|
+
identityTypes.unshift("anonymous");
|
|
291
|
+
if (registration.allowServiceAuth === true)
|
|
292
|
+
identityTypes.push("service_auth");
|
|
293
|
+
return {
|
|
294
|
+
claim_endpoint: endpoints.claimEndpoint,
|
|
295
|
+
identity_assertion: {
|
|
296
|
+
assertion_types_supported: [AGENT_IDENTITY_ASSERTION_TYPE]
|
|
297
|
+
},
|
|
298
|
+
identity_endpoint: endpoints.identityEndpoint,
|
|
299
|
+
identity_types_supported: identityTypes,
|
|
300
|
+
skill: endpoints.guide
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
var agentRegistrationEndpoints = (config) => {
|
|
304
|
+
const registration = requiredRegistration(config);
|
|
305
|
+
const base = config.authorizationServer;
|
|
306
|
+
const oidcRoute = config.oidcRoute ?? "/oauth2";
|
|
307
|
+
return {
|
|
308
|
+
claimEndpoint: new URL(registration.claimRoute ?? DEFAULT_CLAIM_ROUTE, base).toString(),
|
|
309
|
+
completeEndpoint: new URL(registration.completeRoute ?? DEFAULT_COMPLETE_ROUTE, base).toString(),
|
|
310
|
+
guide: new URL(registration.guideRoute ?? DEFAULT_GUIDE_ROUTE, base).toString(),
|
|
311
|
+
identityEndpoint: new URL(registration.identityRoute ?? DEFAULT_IDENTITY_ROUTE, base).toString(),
|
|
312
|
+
tokenEndpoint: new URL(`${oidcRoute}/token`, base).toString()
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
var markdownJson = (value) => `\`\`\`json
|
|
316
|
+
${JSON.stringify(value, null, 2)}
|
|
317
|
+
\`\`\``;
|
|
318
|
+
var generateAgentRegistrationGuide = (config) => {
|
|
319
|
+
const registration = requiredRegistration(config);
|
|
320
|
+
const endpoints = agentRegistrationEndpoints(config);
|
|
321
|
+
const metadataUrl = new URL(config.metadataRoute ?? "/.well-known/oauth-protected-resource", config.resource).toString();
|
|
322
|
+
const methods = [
|
|
323
|
+
"- `identity_assertion`: present an audience-bound ID-JAG from a trusted provider.",
|
|
324
|
+
...registration.allowServiceAuth === true ? [
|
|
325
|
+
"- `service_auth`: provide a user login hint and complete the service-owned claim ceremony."
|
|
326
|
+
] : [],
|
|
327
|
+
...registration.allowAnonymous === true ? [
|
|
328
|
+
"- `anonymous`: receive pre-claim scopes, then optionally let a signed-in user claim the registration."
|
|
329
|
+
] : []
|
|
330
|
+
].join(`
|
|
331
|
+
`);
|
|
332
|
+
return `# Agent registration for ${config.resourceName ?? config.resource}
|
|
333
|
+
|
|
334
|
+
This service supports the open auth.md agent-registration profile. Structured
|
|
335
|
+
OAuth metadata is authoritative; this document is its agent-readable companion.
|
|
336
|
+
|
|
337
|
+
## 1. Discover
|
|
338
|
+
|
|
339
|
+
Fetch ${metadataUrl}, follow \`authorization_servers\`, then fetch
|
|
340
|
+
\`/.well-known/oauth-authorization-server\`. Read its \`agent_auth\` object and
|
|
341
|
+
top-level \`token_endpoint\` and \`grant_types_supported\` fields.
|
|
342
|
+
|
|
343
|
+
## 2. Choose a method
|
|
344
|
+
|
|
345
|
+
${methods}
|
|
346
|
+
|
|
347
|
+
Before asserting a user identity, show the service name and requested scopes to
|
|
348
|
+
the user and obtain consent. Never ask the user to send a password or OTP to the
|
|
349
|
+
agent.
|
|
350
|
+
|
|
351
|
+
## 3. Register
|
|
352
|
+
|
|
353
|
+
POST one of these bodies to ${endpoints.identityEndpoint}:
|
|
354
|
+
|
|
355
|
+
${markdownJson({
|
|
356
|
+
assertion: "<ID-JAG>",
|
|
357
|
+
assertion_type: AGENT_IDENTITY_ASSERTION_TYPE,
|
|
358
|
+
type: "identity_assertion"
|
|
359
|
+
})}
|
|
360
|
+
|
|
361
|
+
${registration.allowServiceAuth === true ? markdownJson({ login_hint: "user@example.com", type: "service_auth" }) : ""}
|
|
362
|
+
|
|
363
|
+
${registration.allowAnonymous === true ? markdownJson({ type: "anonymous" }) : ""}
|
|
364
|
+
|
|
365
|
+
## 4. Claim
|
|
366
|
+
|
|
367
|
+
Surface \`verification_uri\` and \`user_code\` together. The user opens the
|
|
368
|
+
service-owned page, signs in using the service's normal MFA/SSO policy, and types
|
|
369
|
+
the code there. Poll ${endpoints.tokenEndpoint} using
|
|
370
|
+
\`grant_type=${AGENT_CLAIM_GRANT_TYPE}\` and the one-time \`claim_token\`.
|
|
371
|
+
Treat \`authorization_pending\` as retryable, honor \`interval\`, and restart
|
|
372
|
+
when the server returns \`expired_token\`.
|
|
373
|
+
|
|
374
|
+
## 5. Exchange and use credentials
|
|
375
|
+
|
|
376
|
+
Exchange \`identity_assertion\` at ${endpoints.tokenEndpoint} with
|
|
377
|
+
\`grant_type=${AGENT_IDENTITY_ASSERTION_GRANT_TYPE}\`. Present the resulting
|
|
378
|
+
access token as \`Authorization: Bearer <access_token>\`. Credentials are scoped,
|
|
379
|
+
short-lived, revocable, and bound to the registered agent identity.
|
|
380
|
+
|
|
381
|
+
## Errors and safety
|
|
382
|
+
|
|
383
|
+
- Stop on \`invalid_issuer\`, \`invalid_signature\`, \`invalid_audience\`, or \`replay_detected\`.
|
|
384
|
+
- Reauthenticate the user on \`login_required\`.
|
|
385
|
+
- Open the service-owned confirmation URL on \`interaction_required\`.
|
|
386
|
+
- Never persist claim tokens after completion and never expose credentials in model context.
|
|
387
|
+
`;
|
|
388
|
+
};
|
|
389
|
+
var requiredRegistration = (config) => {
|
|
390
|
+
if (config.agentRegistration === undefined) {
|
|
391
|
+
throw new Error("agentAuth.agentRegistration is not configured");
|
|
392
|
+
}
|
|
393
|
+
return config.agentRegistration;
|
|
394
|
+
};
|
|
395
|
+
var randomCode = () => {
|
|
396
|
+
const bytes = crypto.getRandomValues(new Uint8Array(4));
|
|
397
|
+
const value = new DataView(bytes.buffer).getUint32(0) % 1e6;
|
|
398
|
+
return value.toString().padStart(6, "0");
|
|
399
|
+
};
|
|
400
|
+
var makeSecret = (prefix) => `${prefix}_${generateSecureToken(TOKEN_BYTES)}`;
|
|
401
|
+
var makeAttempt = async ({
|
|
402
|
+
email,
|
|
403
|
+
now,
|
|
404
|
+
registration
|
|
405
|
+
}) => {
|
|
406
|
+
const attemptToken = makeSecret("cat");
|
|
407
|
+
const userCode = randomCode();
|
|
408
|
+
const expiresAt = now + (registration.attemptTtlMs ?? DEFAULT_ATTEMPT_TTL_MS);
|
|
409
|
+
return {
|
|
410
|
+
attempt: {
|
|
411
|
+
attempts: 0,
|
|
412
|
+
email: email.toLowerCase(),
|
|
413
|
+
expiresAt,
|
|
414
|
+
tokenHash: await hashToken(attemptToken),
|
|
415
|
+
userCodeHash: await hashToken(userCode)
|
|
416
|
+
},
|
|
417
|
+
attemptToken,
|
|
418
|
+
expiresAt,
|
|
419
|
+
userCode
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
var createFlow = async ({
|
|
423
|
+
agentId,
|
|
424
|
+
kind,
|
|
425
|
+
loginHint,
|
|
426
|
+
now,
|
|
427
|
+
registration,
|
|
428
|
+
status,
|
|
429
|
+
upstream,
|
|
430
|
+
userId,
|
|
431
|
+
withAttempt
|
|
432
|
+
}) => {
|
|
433
|
+
const claimToken = makeSecret("clm");
|
|
434
|
+
const registrationId = `air_${crypto.randomUUID()}`;
|
|
435
|
+
const claimExpiresAt = now + (registration.claimTtlMs ?? DEFAULT_CLAIM_TTL_MS);
|
|
436
|
+
const attempt = withAttempt === undefined ? undefined : await makeAttempt({ email: withAttempt, now, registration });
|
|
437
|
+
const flow = {
|
|
438
|
+
agentId,
|
|
439
|
+
claimAttempt: attempt?.attempt,
|
|
440
|
+
claimExpiresAt,
|
|
441
|
+
claimTokenHash: await hashToken(claimToken),
|
|
442
|
+
createdAt: now,
|
|
443
|
+
expiresAt: claimExpiresAt,
|
|
444
|
+
kind,
|
|
445
|
+
loginHint,
|
|
446
|
+
registrationId,
|
|
447
|
+
status,
|
|
448
|
+
updatedAt: now,
|
|
449
|
+
upstream,
|
|
450
|
+
userId,
|
|
451
|
+
version: 1
|
|
452
|
+
};
|
|
453
|
+
if (!await registration.identityStore.create(flow)) {
|
|
454
|
+
throw new Error("Agent identity registration id collision");
|
|
455
|
+
}
|
|
456
|
+
return { attempt, claimToken, flow };
|
|
457
|
+
};
|
|
458
|
+
var assertionClaims = (config, flow, now) => ({
|
|
459
|
+
aud: config.authorizationServer,
|
|
460
|
+
exp: Math.floor((now + (config.agentRegistration?.assertionTtlMs ?? DEFAULT_ASSERTION_TTL_MS)) / MILLISECONDS_IN_A_SECOND),
|
|
461
|
+
iat: Math.floor(now / MILLISECONDS_IN_A_SECOND),
|
|
462
|
+
iss: config.authorizationServer,
|
|
463
|
+
jti: crypto.randomUUID(),
|
|
464
|
+
registration_version: flow.version,
|
|
465
|
+
sub: flow.registrationId
|
|
466
|
+
});
|
|
467
|
+
var issueAgentServiceAssertion = async (config, flow, now = Date.now()) => ({
|
|
468
|
+
assertion: await signJwt(assertionClaims(config, flow, now), requiredRegistration(config).signingKey, "oauth-id-jag+jwt"),
|
|
469
|
+
expiresAt: now + (requiredRegistration(config).assertionTtlMs ?? DEFAULT_ASSERTION_TTL_MS)
|
|
470
|
+
});
|
|
471
|
+
var activateAgent = async (config, flow, scopes) => {
|
|
472
|
+
const now = Date.now();
|
|
473
|
+
await config.registrationStore.saveRegistration({
|
|
474
|
+
agentId: flow.agentId,
|
|
475
|
+
allowedScopes: scopes.filter((scope) => config.scopes.includes(scope)),
|
|
476
|
+
createdAt: now,
|
|
477
|
+
metadata: {
|
|
478
|
+
identityRegistrationId: flow.registrationId,
|
|
479
|
+
registrationKind: flow.kind
|
|
480
|
+
},
|
|
481
|
+
name: `Agent registration ${flow.registrationId}`,
|
|
482
|
+
status: "active",
|
|
483
|
+
updatedAt: now
|
|
484
|
+
});
|
|
485
|
+
if (flow.userId !== undefined) {
|
|
486
|
+
await config.delegationStore.saveDelegation({
|
|
487
|
+
agentId: flow.agentId,
|
|
488
|
+
createdAt: now,
|
|
489
|
+
delegationId: `agd_${crypto.randomUUID()}`,
|
|
490
|
+
scopes: scopes.filter((scope) => config.scopes.includes(scope)),
|
|
491
|
+
status: "active",
|
|
492
|
+
updatedAt: now,
|
|
493
|
+
userId: flow.userId
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
var ceremony = (config, flow, attempt) => ({
|
|
498
|
+
expires_in: Math.max(0, Math.floor((attempt.expiresAt - Date.now()) / MILLISECONDS_IN_A_SECOND)),
|
|
499
|
+
interval: requiredRegistration(config).pollIntervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS,
|
|
500
|
+
user_code: attempt.userCode,
|
|
501
|
+
verification_uri: `${agentRegistrationEndpoints(config).completeEndpoint}?claim_attempt_token=${encodeURIComponent(attempt.attemptToken)}`
|
|
502
|
+
});
|
|
503
|
+
var startAgentRegistration = async (config, input, now = Date.now()) => {
|
|
504
|
+
const registration = requiredRegistration(config);
|
|
505
|
+
const agentId = `agent_${crypto.randomUUID()}`;
|
|
506
|
+
if (input.type === "anonymous") {
|
|
507
|
+
if (registration.allowAnonymous !== true) {
|
|
508
|
+
return { error: "anonymous_not_enabled", status: 403 };
|
|
509
|
+
}
|
|
510
|
+
if (registration.revokeAccessTokens === undefined) {
|
|
511
|
+
throw new Error("Anonymous agent registration requires revokeAccessTokens");
|
|
512
|
+
}
|
|
513
|
+
const created2 = await createFlow({
|
|
514
|
+
agentId,
|
|
515
|
+
kind: "anonymous",
|
|
516
|
+
now,
|
|
517
|
+
registration,
|
|
518
|
+
status: "pending"
|
|
519
|
+
});
|
|
520
|
+
await activateAgent(config, created2.flow, registration.preClaimScopes ?? []);
|
|
521
|
+
const assertion2 = await issueAgentServiceAssertion(config, created2.flow, now);
|
|
522
|
+
return {
|
|
523
|
+
assertionExpires: assertion2.expiresAt,
|
|
524
|
+
claimToken: created2.claimToken,
|
|
525
|
+
claimTokenExpires: created2.flow.claimExpiresAt,
|
|
526
|
+
identityAssertion: assertion2.assertion,
|
|
527
|
+
postClaimScopes: registration.postClaimScopes,
|
|
528
|
+
preClaimScopes: registration.preClaimScopes ?? [],
|
|
529
|
+
registrationId: created2.flow.registrationId,
|
|
530
|
+
registrationType: "anonymous",
|
|
531
|
+
status: 200
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
if (input.type === "service_auth") {
|
|
535
|
+
if (registration.allowServiceAuth !== true) {
|
|
536
|
+
return { error: "service_auth_not_enabled", status: 403 };
|
|
537
|
+
}
|
|
538
|
+
const email = input.loginHint.trim().toLowerCase();
|
|
539
|
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/u.test(email)) {
|
|
540
|
+
return { error: "invalid_login_hint", status: 400 };
|
|
541
|
+
}
|
|
542
|
+
const created2 = await createFlow({
|
|
543
|
+
agentId,
|
|
544
|
+
kind: "service_auth",
|
|
545
|
+
loginHint: email,
|
|
546
|
+
now,
|
|
547
|
+
registration,
|
|
548
|
+
status: "pending",
|
|
549
|
+
withAttempt: email
|
|
550
|
+
});
|
|
551
|
+
if (created2.attempt === undefined)
|
|
552
|
+
throw new Error("Claim attempt missing");
|
|
553
|
+
return {
|
|
554
|
+
assertionExpires: 0,
|
|
555
|
+
claim: ceremony(config, created2.flow, created2.attempt),
|
|
556
|
+
claimToken: created2.claimToken,
|
|
557
|
+
claimTokenExpires: created2.flow.claimExpiresAt,
|
|
558
|
+
postClaimScopes: registration.postClaimScopes,
|
|
559
|
+
registrationId: created2.flow.registrationId,
|
|
560
|
+
registrationType: "service_auth",
|
|
561
|
+
status: 200
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
if (input.assertionType !== AGENT_IDENTITY_ASSERTION_TYPE || registration.verifyIdentityAssertion === undefined) {
|
|
565
|
+
return { error: "invalid_request", status: 400 };
|
|
566
|
+
}
|
|
567
|
+
const identity = await registration.verifyIdentityAssertion(input.assertion);
|
|
568
|
+
if (identity === undefined) {
|
|
569
|
+
return { error: "invalid_identity_assertion", status: 401 };
|
|
570
|
+
}
|
|
571
|
+
const authAge = now - identity.authenticatedAt;
|
|
572
|
+
if (authAge < -MILLISECONDS_IN_A_MINUTE || authAge > (registration.maxAuthenticationAgeMs ?? DEFAULT_MAX_AUTH_AGE_MS)) {
|
|
573
|
+
return { error: "login_required", status: 401 };
|
|
574
|
+
}
|
|
575
|
+
if (identity.emailVerified !== true && identity.phoneNumberVerified !== true) {
|
|
576
|
+
return { error: "missing_verified_identity", status: 403 };
|
|
577
|
+
}
|
|
578
|
+
const existing = await registration.identityStore.findByUpstreamIdentity({
|
|
579
|
+
clientId: identity.clientId,
|
|
580
|
+
issuer: identity.issuer,
|
|
581
|
+
subject: identity.subject
|
|
582
|
+
});
|
|
583
|
+
if (existing?.status === "claimed") {
|
|
584
|
+
const assertion2 = await issueAgentServiceAssertion(config, existing, now);
|
|
585
|
+
return {
|
|
586
|
+
assertionExpires: assertion2.expiresAt,
|
|
587
|
+
identityAssertion: assertion2.assertion,
|
|
588
|
+
postClaimScopes: registration.postClaimScopes,
|
|
589
|
+
registrationId: existing.registrationId,
|
|
590
|
+
registrationType: "identity_assertion",
|
|
591
|
+
status: 200
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
const match = await registration.resolveVerifiedIdentity?.(identity);
|
|
595
|
+
const verifiedEmail = identity.emailVerified === true ? identity.email : undefined;
|
|
596
|
+
if (match?.userId === undefined && verifiedEmail === undefined) {
|
|
597
|
+
return { error: "interaction_required", status: 401 };
|
|
598
|
+
}
|
|
599
|
+
const created = await createFlow({
|
|
600
|
+
agentId,
|
|
601
|
+
kind: "identity_assertion",
|
|
602
|
+
now,
|
|
603
|
+
registration,
|
|
604
|
+
status: match?.userId === undefined ? "pending" : "claimed",
|
|
605
|
+
upstream: {
|
|
606
|
+
clientId: identity.clientId,
|
|
607
|
+
issuer: identity.issuer,
|
|
608
|
+
subject: identity.subject
|
|
609
|
+
},
|
|
610
|
+
userId: match?.userId,
|
|
611
|
+
withAttempt: match?.userId === undefined ? verifiedEmail : undefined
|
|
612
|
+
});
|
|
613
|
+
if (match?.userId === undefined) {
|
|
614
|
+
if (created.attempt === undefined)
|
|
615
|
+
throw new Error("Claim attempt missing");
|
|
616
|
+
return {
|
|
617
|
+
assertionExpires: 0,
|
|
618
|
+
claim: ceremony(config, created.flow, created.attempt),
|
|
619
|
+
claimToken: created.claimToken,
|
|
620
|
+
claimTokenExpires: created.flow.claimExpiresAt,
|
|
621
|
+
postClaimScopes: registration.postClaimScopes,
|
|
622
|
+
registrationId: created.flow.registrationId,
|
|
623
|
+
registrationType: "identity_assertion",
|
|
624
|
+
status: 200
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
await activateAgent(config, created.flow, registration.postClaimScopes);
|
|
628
|
+
const assertion = await issueAgentServiceAssertion(config, created.flow, now);
|
|
629
|
+
return {
|
|
630
|
+
assertionExpires: assertion.expiresAt,
|
|
631
|
+
identityAssertion: assertion.assertion,
|
|
632
|
+
postClaimScopes: registration.postClaimScopes,
|
|
633
|
+
registrationId: created.flow.registrationId,
|
|
634
|
+
registrationType: "identity_assertion",
|
|
635
|
+
status: 200
|
|
636
|
+
};
|
|
637
|
+
};
|
|
638
|
+
var beginAgentClaim = async (config, input, now = Date.now()) => {
|
|
639
|
+
const registration = requiredRegistration(config);
|
|
640
|
+
const email = input.email.trim().toLowerCase();
|
|
641
|
+
const claimTokenHash = await hashToken(input.claimToken);
|
|
642
|
+
for (let retry = 0;retry < MAX_CONCURRENT_UPDATE_RETRIES; retry += 1) {
|
|
643
|
+
const flow = await registration.identityStore.findByClaimTokenHash(claimTokenHash);
|
|
644
|
+
if (flow === undefined) {
|
|
645
|
+
return { error: "invalid_claim_token", status: 400 };
|
|
646
|
+
}
|
|
647
|
+
if (flow.claimExpiresAt <= now || flow.status === "revoked") {
|
|
648
|
+
return { error: "claim_expired", status: 400 };
|
|
649
|
+
}
|
|
650
|
+
if (flow.loginHint !== undefined && flow.loginHint !== email) {
|
|
651
|
+
return { error: "invalid_claim_token", status: 400 };
|
|
652
|
+
}
|
|
653
|
+
const attempt = await makeAttempt({ email, now, registration });
|
|
654
|
+
const replacement = {
|
|
655
|
+
...flow,
|
|
656
|
+
claimAttempt: attempt.attempt,
|
|
657
|
+
loginHint: email,
|
|
658
|
+
updatedAt: now
|
|
659
|
+
};
|
|
660
|
+
if (await registration.identityStore.replace(replacement, flow.version)) {
|
|
661
|
+
return {
|
|
662
|
+
claimAttempt: ceremony(config, replacement, attempt),
|
|
663
|
+
status: 200
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return { error: "concurrent_update", status: 409 };
|
|
668
|
+
};
|
|
669
|
+
var completeAgentClaim = async (config, input, now = Date.now()) => {
|
|
670
|
+
const registration = requiredRegistration(config);
|
|
671
|
+
const user = await registration.resolveAuthenticatedUser(input.request);
|
|
672
|
+
if (user === undefined)
|
|
673
|
+
return { error: "wrong_user", status: 403 };
|
|
674
|
+
const attemptTokenHash = await hashToken(input.attemptToken);
|
|
675
|
+
const userCodeHash = await hashToken(input.userCode);
|
|
676
|
+
for (let retry = 0;retry < MAX_CONCURRENT_UPDATE_RETRIES; retry += 1) {
|
|
677
|
+
const flow = await registration.identityStore.findByAttemptTokenHash(attemptTokenHash);
|
|
678
|
+
const attempt = flow?.claimAttempt;
|
|
679
|
+
if (flow === undefined || attempt === undefined) {
|
|
680
|
+
return { error: "invalid_claim_attempt", status: 400 };
|
|
681
|
+
}
|
|
682
|
+
if (flow.claimExpiresAt <= now || attempt.expiresAt <= now) {
|
|
683
|
+
return { error: "claim_expired", status: 400 };
|
|
684
|
+
}
|
|
685
|
+
if (user.email?.toLowerCase() !== attempt.email) {
|
|
686
|
+
return { error: "wrong_user", status: 403 };
|
|
687
|
+
}
|
|
688
|
+
const matches = await constantTimeEqual(userCodeHash, attempt.userCodeHash);
|
|
689
|
+
if (!matches) {
|
|
690
|
+
const attempts = attempt.attempts + 1;
|
|
691
|
+
const locked = attempts >= (registration.maxCodeAttempts ?? DEFAULT_MAX_CODE_ATTEMPTS);
|
|
692
|
+
const replaced = await registration.identityStore.replace({
|
|
693
|
+
...flow,
|
|
694
|
+
claimAttempt: locked ? undefined : { ...attempt, attempts },
|
|
695
|
+
updatedAt: now
|
|
696
|
+
}, flow.version);
|
|
697
|
+
if (replaced) {
|
|
698
|
+
return {
|
|
699
|
+
error: "user_code_invalid",
|
|
700
|
+
status: locked ? 429 : 400
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (flow.kind === "anonymous") {
|
|
706
|
+
await registration.revokeAccessTokens?.(flow.agentId);
|
|
707
|
+
}
|
|
708
|
+
const replacement = {
|
|
709
|
+
...flow,
|
|
710
|
+
claimAttempt: undefined,
|
|
711
|
+
status: "claimed",
|
|
712
|
+
updatedAt: now,
|
|
713
|
+
userId: user.userId
|
|
714
|
+
};
|
|
715
|
+
if (!await registration.identityStore.replace(replacement, flow.version)) {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
const persisted = await registration.identityStore.findByRegistrationId(flow.registrationId);
|
|
719
|
+
if (persisted === undefined) {
|
|
720
|
+
throw new Error("Completed agent registration disappeared");
|
|
721
|
+
}
|
|
722
|
+
await activateAgent(config, persisted, registration.postClaimScopes);
|
|
723
|
+
return { status: 204 };
|
|
724
|
+
}
|
|
725
|
+
return { error: "concurrent_update", status: 409 };
|
|
726
|
+
};
|
|
727
|
+
var issueAgentAccessToken = async (config, flow, now) => {
|
|
728
|
+
const registration = requiredRegistration(config);
|
|
729
|
+
const accessToken = `at_${generateSecureToken(TOKEN_BYTES)}`;
|
|
730
|
+
const scopes = (flow.status === "claimed" ? registration.postClaimScopes : registration.preClaimScopes ?? []).filter((scope) => config.scopes.includes(scope));
|
|
731
|
+
const expiresAt = now + (registration.tokenTtlMs ?? DEFAULT_ACCESS_TOKEN_TTL_MS);
|
|
732
|
+
await registration.accessTokenStore.saveToken({
|
|
733
|
+
clientId: flow.agentId,
|
|
734
|
+
createdAt: now,
|
|
735
|
+
expiresAt,
|
|
736
|
+
hashedToken: await hashToken(accessToken),
|
|
737
|
+
ownerId: flow.userId,
|
|
738
|
+
scopes,
|
|
739
|
+
tokenId: crypto.randomUUID()
|
|
740
|
+
});
|
|
741
|
+
return {
|
|
742
|
+
accessToken,
|
|
743
|
+
expiresIn: Math.floor((expiresAt - now) / MILLISECONDS_IN_A_SECOND),
|
|
744
|
+
scopes
|
|
745
|
+
};
|
|
746
|
+
};
|
|
747
|
+
var createAgentRegistrationCredentialVerifier = (accessTokenStore, identityStore) => async (request) => {
|
|
748
|
+
const authorization = request.headers.get("authorization");
|
|
749
|
+
if (authorization?.startsWith("Bearer at_") !== true)
|
|
750
|
+
return;
|
|
751
|
+
const token = authorization.slice("Bearer ".length).trim();
|
|
752
|
+
const record = await accessTokenStore.findByHashedToken(await hashToken(token));
|
|
753
|
+
if (record === undefined || record.expiresAt <= Date.now())
|
|
754
|
+
return;
|
|
755
|
+
if (identityStore !== undefined) {
|
|
756
|
+
const identity = await identityStore.findByAgentId(record.clientId);
|
|
757
|
+
if (identity === undefined || identity.status === "revoked") {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
agentId: record.clientId,
|
|
763
|
+
expiresAt: record.expiresAt,
|
|
764
|
+
scopes: record.scopes,
|
|
765
|
+
userId: record.ownerId
|
|
766
|
+
};
|
|
767
|
+
};
|
|
768
|
+
var handleAgentTokenGrant = async (config, body, now = Date.now()) => {
|
|
769
|
+
const registration = requiredRegistration(config);
|
|
770
|
+
if (body.grant_type === AGENT_CLAIM_GRANT_TYPE) {
|
|
771
|
+
if (body.claim_token === undefined) {
|
|
772
|
+
return { body: { error: "invalid_request" }, status: 400 };
|
|
773
|
+
}
|
|
774
|
+
const claimTokenHash = await hashToken(body.claim_token);
|
|
775
|
+
let flow2;
|
|
776
|
+
for (let retry = 0;retry < MAX_CONCURRENT_UPDATE_RETRIES; retry += 1) {
|
|
777
|
+
flow2 = await registration.identityStore.findByClaimTokenHash(claimTokenHash);
|
|
778
|
+
if (flow2 === undefined || flow2.claimExpiresAt <= now) {
|
|
779
|
+
return { body: { error: "expired_token" }, status: 400 };
|
|
780
|
+
}
|
|
781
|
+
if (flow2.status === "claimed")
|
|
782
|
+
break;
|
|
783
|
+
if (flow2.claimAttempt !== undefined && flow2.claimAttempt.expiresAt <= now) {
|
|
784
|
+
return { body: { error: "expired_token" }, status: 400 };
|
|
785
|
+
}
|
|
786
|
+
const intervalMs = (registration.pollIntervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS) * MILLISECONDS_IN_A_SECOND;
|
|
787
|
+
if (flow2.lastPolledAt !== undefined && now - flow2.lastPolledAt < intervalMs) {
|
|
788
|
+
return { body: { error: "slow_down" }, status: 400 };
|
|
789
|
+
}
|
|
790
|
+
if (await registration.identityStore.replace({ ...flow2, lastPolledAt: now, updatedAt: now }, flow2.version)) {
|
|
791
|
+
return {
|
|
792
|
+
body: { error: "authorization_pending" },
|
|
793
|
+
status: 400
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (flow2?.status !== "claimed") {
|
|
798
|
+
return { body: { error: "temporarily_unavailable" }, status: 400 };
|
|
799
|
+
}
|
|
800
|
+
const assertion = await issueAgentServiceAssertion(config, flow2, now);
|
|
801
|
+
const token2 = await issueAgentAccessToken(config, flow2, now);
|
|
802
|
+
return {
|
|
803
|
+
body: {
|
|
804
|
+
access_token: token2.accessToken,
|
|
805
|
+
assertion_expires: new Date(assertion.expiresAt).toISOString(),
|
|
806
|
+
expires_in: token2.expiresIn,
|
|
807
|
+
identity_assertion: assertion.assertion,
|
|
808
|
+
scope: token2.scopes.join(" "),
|
|
809
|
+
token_type: "Bearer"
|
|
810
|
+
},
|
|
811
|
+
status: 200
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
if (body.grant_type !== AGENT_IDENTITY_ASSERTION_GRANT_TYPE) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (body.assertion === undefined) {
|
|
818
|
+
return { body: { error: "invalid_request" }, status: 400 };
|
|
819
|
+
}
|
|
820
|
+
const verified = await verifyJwt(body.assertion, registration.signingKey.publicJwk);
|
|
821
|
+
const payload = verified?.payload;
|
|
822
|
+
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)) {
|
|
823
|
+
return { body: { error: "invalid_grant" }, status: 400 };
|
|
824
|
+
}
|
|
825
|
+
const flow = await registration.identityStore.findByRegistrationId(payload.sub);
|
|
826
|
+
if (flow === undefined || flow.status === "revoked" || payload.registration_version !== flow.version) {
|
|
827
|
+
return { body: { error: "invalid_grant" }, status: 400 };
|
|
828
|
+
}
|
|
829
|
+
const token = await issueAgentAccessToken(config, flow, now);
|
|
830
|
+
return {
|
|
831
|
+
body: {
|
|
832
|
+
access_token: token.accessToken,
|
|
833
|
+
expires_in: token.expiresIn,
|
|
834
|
+
scope: token.scopes.join(" "),
|
|
835
|
+
token_type: "Bearer"
|
|
836
|
+
},
|
|
837
|
+
status: 200
|
|
838
|
+
};
|
|
839
|
+
};
|
|
840
|
+
var revokeAgentIdentityRegistration = async (config, registrationId, now = Date.now()) => {
|
|
841
|
+
const registration = requiredRegistration(config);
|
|
842
|
+
for (let retry = 0;retry < MAX_CONCURRENT_UPDATE_RETRIES; retry += 1) {
|
|
843
|
+
const flow = await registration.identityStore.findByRegistrationId(registrationId);
|
|
844
|
+
if (flow === undefined)
|
|
845
|
+
return false;
|
|
846
|
+
if (flow.status === "revoked")
|
|
847
|
+
return true;
|
|
848
|
+
await registration.revokeAccessTokens?.(flow.agentId);
|
|
849
|
+
if (await registration.identityStore.replace({
|
|
850
|
+
...flow,
|
|
851
|
+
claimAttempt: undefined,
|
|
852
|
+
status: "revoked",
|
|
853
|
+
updatedAt: now
|
|
854
|
+
}, flow.version)) {
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
throw new Error("Could not revoke agent identity after concurrent updates");
|
|
859
|
+
};
|
|
860
|
+
// src/agents/registrationClient.ts
|
|
861
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
862
|
+
var secureUrl = (value, allowLocalhost) => {
|
|
863
|
+
if (typeof value !== "string")
|
|
864
|
+
return;
|
|
865
|
+
try {
|
|
866
|
+
const url = new URL(value);
|
|
867
|
+
if (url.protocol === "https:")
|
|
868
|
+
return url.toString();
|
|
869
|
+
if (allowLocalhost && url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1")) {
|
|
870
|
+
return url.toString();
|
|
871
|
+
}
|
|
872
|
+
} catch {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
return;
|
|
876
|
+
};
|
|
877
|
+
var readBoundedJson = async (response, maxBytes) => {
|
|
878
|
+
const length = Number(response.headers.get("content-length"));
|
|
879
|
+
if (Number.isFinite(length) && length > maxBytes) {
|
|
880
|
+
throw new Error("Agent registration metadata exceeds the response limit");
|
|
881
|
+
}
|
|
882
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
883
|
+
if (bytes.byteLength > maxBytes) {
|
|
884
|
+
throw new Error("Agent registration metadata exceeds the response limit");
|
|
885
|
+
}
|
|
886
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
887
|
+
return parsed;
|
|
888
|
+
};
|
|
889
|
+
var stringArray = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
890
|
+
var requestJson = async (request, url, init, maxBytes) => {
|
|
891
|
+
const response = await request(url, {
|
|
892
|
+
...init,
|
|
893
|
+
headers: {
|
|
894
|
+
accept: "application/json",
|
|
895
|
+
...init.headers
|
|
896
|
+
},
|
|
897
|
+
redirect: "error"
|
|
898
|
+
});
|
|
899
|
+
const body = await readBoundedJson(response, maxBytes);
|
|
900
|
+
if (!isObject(body))
|
|
901
|
+
throw new Error("Expected a JSON object");
|
|
902
|
+
return { body, response };
|
|
903
|
+
};
|
|
904
|
+
var createAgentRegistrationClient = (discovery, options = {}) => {
|
|
905
|
+
const request = options.request ?? fetch;
|
|
906
|
+
const maxBytes = options.maxResponseBytes ?? 256 * 1024;
|
|
907
|
+
const post = (url, body, form = false) => requestJson(request, url, {
|
|
908
|
+
body: form ? new URLSearchParams(Object.fromEntries(Object.entries(body).map(([key, value]) => [
|
|
909
|
+
key,
|
|
910
|
+
String(value)
|
|
911
|
+
]))) : JSON.stringify(body),
|
|
912
|
+
headers: {
|
|
913
|
+
"content-type": form ? "application/x-www-form-urlencoded" : "application/json"
|
|
914
|
+
},
|
|
915
|
+
method: "POST"
|
|
916
|
+
}, maxBytes);
|
|
917
|
+
return {
|
|
918
|
+
beginAnonymous: () => post(discovery.agentAuth.identityEndpoint, { type: "anonymous" }),
|
|
919
|
+
beginServiceAuth: (loginHint) => post(discovery.agentAuth.identityEndpoint, {
|
|
920
|
+
login_hint: loginHint,
|
|
921
|
+
type: "service_auth"
|
|
922
|
+
}),
|
|
923
|
+
beginVerified: (assertion) => {
|
|
924
|
+
if (!discovery.agentAuth.identityAssertionTypes.includes(AGENT_IDENTITY_ASSERTION_TYPE)) {
|
|
925
|
+
throw new Error("Service does not accept ID-JAG assertions");
|
|
926
|
+
}
|
|
927
|
+
return post(discovery.agentAuth.identityEndpoint, {
|
|
928
|
+
assertion,
|
|
929
|
+
assertion_type: AGENT_IDENTITY_ASSERTION_TYPE,
|
|
930
|
+
type: "identity_assertion"
|
|
931
|
+
});
|
|
932
|
+
},
|
|
933
|
+
claim: (claimToken, email) => post(discovery.agentAuth.claimEndpoint, {
|
|
934
|
+
claim_token: claimToken,
|
|
935
|
+
email
|
|
936
|
+
}),
|
|
937
|
+
exchangeAssertion: (assertion) => post(discovery.tokenEndpoint, {
|
|
938
|
+
assertion,
|
|
939
|
+
grant_type: AGENT_IDENTITY_ASSERTION_GRANT_TYPE,
|
|
940
|
+
resource: discovery.resource
|
|
941
|
+
}, true),
|
|
942
|
+
pollClaim: (claimToken) => post(discovery.tokenEndpoint, {
|
|
943
|
+
claim_token: claimToken,
|
|
944
|
+
grant_type: AGENT_CLAIM_GRANT_TYPE
|
|
945
|
+
}, true)
|
|
946
|
+
};
|
|
947
|
+
};
|
|
948
|
+
var discoverAgentRegistration = async (resource, options = {}) => {
|
|
949
|
+
const request = options.request ?? fetch;
|
|
950
|
+
const maxBytes = options.maxResponseBytes ?? 256 * 1024;
|
|
951
|
+
const allowLocalhost = options.allowInsecureLocalhost === true;
|
|
952
|
+
const resourceUrl = secureUrl(resource, allowLocalhost);
|
|
953
|
+
if (resourceUrl === undefined)
|
|
954
|
+
throw new Error("Resource URL must use HTTPS");
|
|
955
|
+
const resourceMetadataUrl = new URL("/.well-known/oauth-protected-resource", resourceUrl).toString();
|
|
956
|
+
const prm = await requestJson(request, resourceMetadataUrl, {}, maxBytes);
|
|
957
|
+
const advertisedResource = secureUrl(prm.body.resource, allowLocalhost);
|
|
958
|
+
if (advertisedResource === undefined || advertisedResource !== resourceUrl) {
|
|
959
|
+
throw new Error("Protected resource metadata identity mismatch");
|
|
960
|
+
}
|
|
961
|
+
const authorizationServer = secureUrl(stringArray(prm.body.authorization_servers)[0], allowLocalhost);
|
|
962
|
+
if (authorizationServer === undefined) {
|
|
963
|
+
throw new Error("No secure authorization server is advertised");
|
|
964
|
+
}
|
|
965
|
+
const asUrl = new URL("/.well-known/oauth-authorization-server", authorizationServer).toString();
|
|
966
|
+
const metadata = await requestJson(request, asUrl, {}, maxBytes);
|
|
967
|
+
if (secureUrl(metadata.body.issuer, allowLocalhost) !== authorizationServer) {
|
|
968
|
+
throw new Error("Authorization server issuer mismatch");
|
|
969
|
+
}
|
|
970
|
+
const tokenEndpoint = secureUrl(metadata.body.token_endpoint, allowLocalhost);
|
|
971
|
+
const agentAuth = metadata.body.agent_auth;
|
|
972
|
+
if (tokenEndpoint === undefined || !isObject(agentAuth)) {
|
|
973
|
+
throw new Error("Authorization server does not advertise agent registration");
|
|
974
|
+
}
|
|
975
|
+
const identityEndpoint = secureUrl(agentAuth.identity_endpoint, allowLocalhost);
|
|
976
|
+
const claimEndpoint = secureUrl(agentAuth.claim_endpoint, allowLocalhost);
|
|
977
|
+
const skill = secureUrl(agentAuth.skill, allowLocalhost);
|
|
978
|
+
const assertionMetadata = agentAuth.identity_assertion;
|
|
979
|
+
if (identityEndpoint === undefined || claimEndpoint === undefined || skill === undefined || !isObject(assertionMetadata)) {
|
|
980
|
+
throw new Error("Agent registration metadata is incomplete");
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
agentAuth: {
|
|
984
|
+
claimEndpoint,
|
|
985
|
+
identityAssertionTypes: stringArray(assertionMetadata.assertion_types_supported),
|
|
986
|
+
identityEndpoint,
|
|
987
|
+
identityTypes: stringArray(agentAuth.identity_types_supported),
|
|
988
|
+
skill
|
|
989
|
+
},
|
|
990
|
+
authorizationServer,
|
|
991
|
+
resource: resourceUrl,
|
|
992
|
+
resourceMetadataUrl,
|
|
993
|
+
scopes: stringArray(prm.body.scopes_supported),
|
|
994
|
+
tokenEndpoint
|
|
995
|
+
};
|
|
996
|
+
};
|
|
997
|
+
// src/agents/idJag.ts
|
|
998
|
+
init_constants();
|
|
999
|
+
var createInMemoryAgentIdentityAssertionJtiStore = () => {
|
|
1000
|
+
const entries = new Map;
|
|
1001
|
+
return {
|
|
1002
|
+
recordIfFresh: async (issuer, jti, expiresAt) => {
|
|
1003
|
+
const now = Date.now();
|
|
1004
|
+
for (const [key2, expiry] of entries) {
|
|
1005
|
+
if (expiry <= now)
|
|
1006
|
+
entries.delete(key2);
|
|
1007
|
+
}
|
|
1008
|
+
const key = `${issuer}\x00${jti}`;
|
|
1009
|
+
if (entries.has(key))
|
|
1010
|
+
return false;
|
|
1011
|
+
entries.set(key, expiresAt);
|
|
1012
|
+
return true;
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
};
|
|
1016
|
+
var issueAgentIdentityAssertion = async ({
|
|
1017
|
+
agentContextId,
|
|
1018
|
+
agentPlatform,
|
|
1019
|
+
audience,
|
|
1020
|
+
clientId,
|
|
1021
|
+
issuer,
|
|
1022
|
+
now = Date.now(),
|
|
1023
|
+
resource,
|
|
1024
|
+
signingKey,
|
|
1025
|
+
ttlMs = 5 * 60 * MILLISECONDS_IN_A_SECOND,
|
|
1026
|
+
user
|
|
1027
|
+
}) => {
|
|
1028
|
+
if (user.emailVerified !== true && user.phoneNumberVerified !== true) {
|
|
1029
|
+
throw new Error("ID-JAG issuance requires a verified email or phone number");
|
|
1030
|
+
}
|
|
1031
|
+
const expiresAt = now + ttlMs;
|
|
1032
|
+
const payload = {
|
|
1033
|
+
aud: audience,
|
|
1034
|
+
auth_time: Math.floor(user.authenticatedAt / MILLISECONDS_IN_A_SECOND),
|
|
1035
|
+
client_id: clientId,
|
|
1036
|
+
exp: Math.floor(expiresAt / MILLISECONDS_IN_A_SECOND),
|
|
1037
|
+
iat: Math.floor(now / MILLISECONDS_IN_A_SECOND),
|
|
1038
|
+
iss: issuer,
|
|
1039
|
+
jti: crypto.randomUUID(),
|
|
1040
|
+
sub: user.subject
|
|
1041
|
+
};
|
|
1042
|
+
if (user.email !== undefined)
|
|
1043
|
+
payload.email = user.email;
|
|
1044
|
+
if (user.emailVerified !== undefined)
|
|
1045
|
+
payload.email_verified = user.emailVerified;
|
|
1046
|
+
if (user.name !== undefined)
|
|
1047
|
+
payload.name = user.name;
|
|
1048
|
+
if (user.phoneNumber !== undefined)
|
|
1049
|
+
payload.phone_number = user.phoneNumber;
|
|
1050
|
+
if (user.phoneNumberVerified !== undefined)
|
|
1051
|
+
payload.phone_number_verified = user.phoneNumberVerified;
|
|
1052
|
+
if (user.methods !== undefined)
|
|
1053
|
+
payload.amr = user.methods;
|
|
1054
|
+
if (resource !== undefined)
|
|
1055
|
+
payload.resource = resource;
|
|
1056
|
+
if (agentPlatform !== undefined)
|
|
1057
|
+
payload.agent_platform = agentPlatform;
|
|
1058
|
+
if (agentContextId !== undefined)
|
|
1059
|
+
payload.agent_context_id = agentContextId;
|
|
1060
|
+
return {
|
|
1061
|
+
assertion: await signJwt(payload, signingKey, "oauth-id-jag+jwt"),
|
|
1062
|
+
assertionType: AGENT_IDENTITY_ASSERTION_TYPE,
|
|
1063
|
+
expiresAt
|
|
1064
|
+
};
|
|
1065
|
+
};
|
|
1066
|
+
var numberClaim = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1067
|
+
var stringClaim = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
|
|
1068
|
+
var createAgentIdentityAssertionVerifier = ({
|
|
1069
|
+
audience,
|
|
1070
|
+
clockSkewMs = MILLISECONDS_IN_A_SECOND * 60,
|
|
1071
|
+
jtiStore,
|
|
1072
|
+
maxAssertionLifetimeMs = MILLISECONDS_IN_A_SECOND * 60 * 60,
|
|
1073
|
+
maxAuthenticationAgeMs = MILLISECONDS_IN_A_SECOND * 60 * 60,
|
|
1074
|
+
resolveIssuer
|
|
1075
|
+
}) => async (assertion, now = Date.now()) => {
|
|
1076
|
+
const segments = assertion.split(".");
|
|
1077
|
+
if (segments.length !== 3 || segments[1] === undefined)
|
|
1078
|
+
return;
|
|
1079
|
+
let decoded;
|
|
1080
|
+
try {
|
|
1081
|
+
decoded = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf8"));
|
|
1082
|
+
} catch {
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const unverified = Object.fromEntries(Object.entries(decoded));
|
|
1089
|
+
const issuer = stringClaim(unverified.iss);
|
|
1090
|
+
if (issuer === undefined)
|
|
1091
|
+
return;
|
|
1092
|
+
const trusted = await resolveIssuer(issuer);
|
|
1093
|
+
if (trusted === undefined)
|
|
1094
|
+
return;
|
|
1095
|
+
const verified = await verifyJwt(assertion, trusted.publicJwk);
|
|
1096
|
+
if (verified?.header?.typ !== "oauth-id-jag+jwt")
|
|
1097
|
+
return;
|
|
1098
|
+
const { payload } = verified;
|
|
1099
|
+
const subject = stringClaim(payload.sub);
|
|
1100
|
+
const jti = stringClaim(payload.jti);
|
|
1101
|
+
const clientId = stringClaim(payload.client_id);
|
|
1102
|
+
const expiresAtSeconds = numberClaim(payload.exp);
|
|
1103
|
+
const issuedAtSeconds = numberClaim(payload.iat);
|
|
1104
|
+
const authenticatedAtSeconds = numberClaim(payload.auth_time);
|
|
1105
|
+
if (payload.iss !== issuer || payload.aud !== audience || subject === undefined || jti === undefined || clientId === undefined || expiresAtSeconds === undefined || issuedAtSeconds === undefined || authenticatedAtSeconds === undefined) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const expiresAt = expiresAtSeconds * MILLISECONDS_IN_A_SECOND;
|
|
1109
|
+
const issuedAt = issuedAtSeconds * MILLISECONDS_IN_A_SECOND;
|
|
1110
|
+
const authenticatedAt = authenticatedAtSeconds * MILLISECONDS_IN_A_SECOND;
|
|
1111
|
+
if (expiresAt <= now - clockSkewMs || issuedAt > now + clockSkewMs || expiresAt <= issuedAt || expiresAt - issuedAt > maxAssertionLifetimeMs + clockSkewMs || authenticatedAt > now + clockSkewMs || authenticatedAt > issuedAt + clockSkewMs || now - authenticatedAt > maxAuthenticationAgeMs + clockSkewMs) {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (trusted.allowedClientIds !== undefined && !trusted.allowedClientIds.includes(clientId)) {
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const email = stringClaim(payload.email);
|
|
1118
|
+
const phoneNumber = stringClaim(payload.phone_number);
|
|
1119
|
+
const emailVerified = payload.email_verified === true;
|
|
1120
|
+
const phoneNumberVerified = payload.phone_number_verified === true;
|
|
1121
|
+
if (!emailVerified && !phoneNumberVerified)
|
|
1122
|
+
return;
|
|
1123
|
+
if (!await jtiStore.recordIfFresh(issuer, jti, expiresAt)) {
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
authenticatedAt,
|
|
1128
|
+
clientId,
|
|
1129
|
+
email,
|
|
1130
|
+
emailVerified,
|
|
1131
|
+
issuer,
|
|
1132
|
+
name: stringClaim(payload.name),
|
|
1133
|
+
phoneNumber,
|
|
1134
|
+
phoneNumberVerified,
|
|
1135
|
+
subject
|
|
1136
|
+
};
|
|
1137
|
+
};
|
|
68
1138
|
// src/oidc/clientIdMetadata.ts
|
|
69
|
-
var
|
|
1139
|
+
var secureUrl2 = (value) => {
|
|
70
1140
|
try {
|
|
71
1141
|
return new URL(value).protocol === "https:";
|
|
72
1142
|
} catch {
|
|
@@ -77,7 +1147,7 @@ var validateClientIdMetadataDocument = (document, expectedClientId) => {
|
|
|
77
1147
|
const errors = [];
|
|
78
1148
|
if (document.client_id !== expectedClientId)
|
|
79
1149
|
errors.push("client_id does not match the metadata document URL");
|
|
80
|
-
if (!
|
|
1150
|
+
if (!secureUrl2(document.client_id))
|
|
81
1151
|
errors.push("client_id must use HTTPS");
|
|
82
1152
|
if (!Array.isArray(document.redirect_uris) || document.redirect_uris.length === 0)
|
|
83
1153
|
errors.push("redirect_uris is required");
|
|
@@ -97,7 +1167,7 @@ var validateClientIdMetadataDocument = (document, expectedClientId) => {
|
|
|
97
1167
|
["tos_uri", document.tos_uri],
|
|
98
1168
|
["jwks_uri", document.jwks_uri]
|
|
99
1169
|
]) {
|
|
100
|
-
if (value !== undefined && !
|
|
1170
|
+
if (value !== undefined && !secureUrl2(value))
|
|
101
1171
|
errors.push(`${name} must use HTTPS`);
|
|
102
1172
|
}
|
|
103
1173
|
return errors;
|
|
@@ -120,7 +1190,7 @@ var createClientIdMetadataResolver = ({
|
|
|
120
1190
|
}) => {
|
|
121
1191
|
const cache = new Map;
|
|
122
1192
|
return async (clientId) => {
|
|
123
|
-
if (!
|
|
1193
|
+
if (!secureUrl2(clientId) || !await allow(clientId))
|
|
124
1194
|
return;
|
|
125
1195
|
const cached = cache.get(clientId);
|
|
126
1196
|
if (cached !== undefined && cached.expiresAt > now())
|
|
@@ -150,62 +1220,6 @@ var createClientIdMetadataResolver = ({
|
|
|
150
1220
|
return client;
|
|
151
1221
|
};
|
|
152
1222
|
};
|
|
153
|
-
// src/oidc/keys.ts
|
|
154
|
-
var ENCODER = new TextEncoder;
|
|
155
|
-
var ES256 = { hash: "SHA-256", name: "ECDSA" };
|
|
156
|
-
var KEY_PARAMS = { name: "ECDSA", namedCurve: "P-256" };
|
|
157
|
-
var toBase64Url = (bytes) => Buffer.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)).toString("base64url");
|
|
158
|
-
var fromBase64Url = (value) => new Uint8Array(Buffer.from(value, "base64url"));
|
|
159
|
-
var encodeSegment = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
160
|
-
var decodeSegment = (segment) => JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
|
|
161
|
-
var generateSigningKey = async () => {
|
|
162
|
-
const pair = await crypto.subtle.generateKey(KEY_PARAMS, true, [
|
|
163
|
-
"sign",
|
|
164
|
-
"verify"
|
|
165
|
-
]);
|
|
166
|
-
const privateJwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
167
|
-
const publicJwk = await crypto.subtle.exportKey("jwk", pair.publicKey);
|
|
168
|
-
return { kid: await jwkThumbprint(publicJwk), privateJwk, publicJwk };
|
|
169
|
-
};
|
|
170
|
-
var jwkThumbprint = async (jwk) => {
|
|
171
|
-
const canonical = JSON.stringify({
|
|
172
|
-
crv: jwk.crv,
|
|
173
|
-
kty: jwk.kty,
|
|
174
|
-
x: jwk.x,
|
|
175
|
-
y: jwk.y
|
|
176
|
-
});
|
|
177
|
-
return toBase64Url(await crypto.subtle.digest("SHA-256", ENCODER.encode(canonical)));
|
|
178
|
-
};
|
|
179
|
-
var signJwt = async (payload, signing) => {
|
|
180
|
-
const key = await crypto.subtle.importKey("jwk", signing.privateJwk, KEY_PARAMS, false, ["sign"]);
|
|
181
|
-
const input = `${encodeSegment({ alg: "ES256", kid: signing.kid, typ: "JWT" })}.${encodeSegment(payload)}`;
|
|
182
|
-
const signature = await crypto.subtle.sign(ES256, key, ENCODER.encode(input));
|
|
183
|
-
return `${input}.${toBase64Url(signature)}`;
|
|
184
|
-
};
|
|
185
|
-
var toPublicJwk = (key) => ({
|
|
186
|
-
alg: "ES256",
|
|
187
|
-
crv: key.publicJwk.crv,
|
|
188
|
-
kid: key.kid,
|
|
189
|
-
kty: key.publicJwk.kty,
|
|
190
|
-
use: "sig",
|
|
191
|
-
x: key.publicJwk.x,
|
|
192
|
-
y: key.publicJwk.y
|
|
193
|
-
});
|
|
194
|
-
var verifyJwt = async (token, publicJwk) => {
|
|
195
|
-
const [headerSegment, payloadSegment, signatureSegment] = token.split(".");
|
|
196
|
-
if (headerSegment === undefined || payloadSegment === undefined || signatureSegment === undefined) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const key = await crypto.subtle.importKey("jwk", publicJwk, KEY_PARAMS, false, ["verify"]);
|
|
200
|
-
const valid = await crypto.subtle.verify(ES256, key, fromBase64Url(signatureSegment), ENCODER.encode(`${headerSegment}.${payloadSegment}`));
|
|
201
|
-
if (!valid)
|
|
202
|
-
return;
|
|
203
|
-
return {
|
|
204
|
-
header: decodeSegment(headerSegment),
|
|
205
|
-
payload: decodeSegment(payloadSegment)
|
|
206
|
-
};
|
|
207
|
-
};
|
|
208
|
-
|
|
209
1223
|
// src/oidc/dpop.ts
|
|
210
1224
|
init_constants();
|
|
211
1225
|
var DEFAULT_MAX_AGE_MS = 60000;
|
|
@@ -449,6 +1463,67 @@ var failureResponse = (config, failure, requiredScopes) => new Response(JSON.str
|
|
|
449
1463
|
},
|
|
450
1464
|
status: failure.code === "Forbidden" ? 403 : 401
|
|
451
1465
|
});
|
|
1466
|
+
var json = (value, status = 200) => new Response(JSON.stringify(value), {
|
|
1467
|
+
headers: {
|
|
1468
|
+
"cache-control": "no-store",
|
|
1469
|
+
"content-type": "application/json"
|
|
1470
|
+
},
|
|
1471
|
+
status
|
|
1472
|
+
});
|
|
1473
|
+
var recordBody = (body) => {
|
|
1474
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
return Object.fromEntries(Object.entries(body));
|
|
1478
|
+
};
|
|
1479
|
+
var registrationResponse = (result) => {
|
|
1480
|
+
const body = {
|
|
1481
|
+
...result.assertionExpires > 0 ? {
|
|
1482
|
+
assertion_expires: new Date(result.assertionExpires).toISOString()
|
|
1483
|
+
} : {},
|
|
1484
|
+
...result.claim === undefined ? {} : { claim: result.claim },
|
|
1485
|
+
...result.claimToken === undefined ? {} : { claim_token: result.claimToken },
|
|
1486
|
+
...result.claimTokenExpires === undefined ? {} : {
|
|
1487
|
+
claim_token_expires: new Date(result.claimTokenExpires).toISOString()
|
|
1488
|
+
},
|
|
1489
|
+
...result.identityAssertion === undefined ? {} : { identity_assertion: result.identityAssertion },
|
|
1490
|
+
...result.preClaimScopes === undefined ? {} : { pre_claim_scopes: result.preClaimScopes },
|
|
1491
|
+
post_claim_scopes: result.postClaimScopes,
|
|
1492
|
+
registration_id: result.registrationId,
|
|
1493
|
+
registration_type: result.registrationType
|
|
1494
|
+
};
|
|
1495
|
+
if (result.registrationType === "identity_assertion" && result.identityAssertion === undefined) {
|
|
1496
|
+
return json({
|
|
1497
|
+
...body,
|
|
1498
|
+
error: "interaction_required",
|
|
1499
|
+
error_description: "Authenticate at the service and confirm the account link."
|
|
1500
|
+
}, 401);
|
|
1501
|
+
}
|
|
1502
|
+
return json(body);
|
|
1503
|
+
};
|
|
1504
|
+
var parseRegistrationInput = (value) => {
|
|
1505
|
+
if (value.type === "anonymous") {
|
|
1506
|
+
const input2 = { type: "anonymous" };
|
|
1507
|
+
return input2;
|
|
1508
|
+
}
|
|
1509
|
+
if (value.type === "service_auth" && typeof value.login_hint === "string") {
|
|
1510
|
+
const input2 = {
|
|
1511
|
+
loginHint: value.login_hint,
|
|
1512
|
+
type: "service_auth"
|
|
1513
|
+
};
|
|
1514
|
+
return input2;
|
|
1515
|
+
}
|
|
1516
|
+
if (value.type !== "identity_assertion" || value.assertion_type !== AGENT_IDENTITY_ASSERTION_TYPE || typeof value.assertion !== "string") {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const input = {
|
|
1520
|
+
assertion: value.assertion,
|
|
1521
|
+
assertionType: AGENT_IDENTITY_ASSERTION_TYPE,
|
|
1522
|
+
type: "identity_assertion"
|
|
1523
|
+
};
|
|
1524
|
+
return input;
|
|
1525
|
+
};
|
|
1526
|
+
var escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
452
1527
|
var agentAuthPlugin = (config) => {
|
|
453
1528
|
const plugin = new Elysia().derive(({ request }) => ({
|
|
454
1529
|
protectAgent: async (requiredScopes, handleAuth, handleAuthFail) => {
|
|
@@ -479,9 +1554,83 @@ var agentAuthPlugin = (config) => {
|
|
|
479
1554
|
}));
|
|
480
1555
|
if (config === undefined)
|
|
481
1556
|
return plugin.as("global");
|
|
482
|
-
|
|
1557
|
+
if (config.agentRegistration === undefined) {
|
|
1558
|
+
return plugin.get(config.metadataRoute ?? DEFAULT_AGENT_RESOURCE_METADATA_ROUTE, () => agentProtectedResourceMetadata(config)).as("global");
|
|
1559
|
+
}
|
|
1560
|
+
const registration = config.agentRegistration;
|
|
1561
|
+
const identityRoute = registration.identityRoute ?? "/agent/identity";
|
|
1562
|
+
const claimRoute = registration.claimRoute ?? "/agent/identity/claim";
|
|
1563
|
+
const completeRoute = registration.completeRoute ?? "/agent/identity/claim/complete";
|
|
1564
|
+
const guideRoute = registration.guideRoute ?? "/auth.md";
|
|
1565
|
+
return plugin.get(config.metadataRoute ?? DEFAULT_AGENT_RESOURCE_METADATA_ROUTE, () => agentProtectedResourceMetadata(config)).get(guideRoute, () => new Response(generateAgentRegistrationGuide(config), {
|
|
1566
|
+
headers: {
|
|
1567
|
+
"cache-control": "public, max-age=300",
|
|
1568
|
+
"content-type": "text/markdown; charset=utf-8"
|
|
1569
|
+
}
|
|
1570
|
+
})).get(completeRoute, ({ query }) => {
|
|
1571
|
+
const token = typeof query.claim_attempt_token === "string" ? query.claim_attempt_token : "";
|
|
1572
|
+
if (token.length === 0)
|
|
1573
|
+
return new Response("Invalid claim link", { status: 400 });
|
|
1574
|
+
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>`, {
|
|
1575
|
+
headers: {
|
|
1576
|
+
"cache-control": "no-store",
|
|
1577
|
+
"content-security-policy": "default-src 'none'; form-action 'self'; style-src 'none'; base-uri 'none'; frame-ancestors 'none'",
|
|
1578
|
+
"content-type": "text/html; charset=utf-8",
|
|
1579
|
+
"x-content-type-options": "nosniff"
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
}).post(identityRoute, async ({ body }) => {
|
|
1583
|
+
const value = recordBody(body);
|
|
1584
|
+
if (value === undefined || typeof value.type !== "string") {
|
|
1585
|
+
return json({ error: "invalid_request" }, 400);
|
|
1586
|
+
}
|
|
1587
|
+
const input = parseRegistrationInput(value);
|
|
1588
|
+
if (input === undefined)
|
|
1589
|
+
return json({ error: "invalid_request" }, 400);
|
|
1590
|
+
const result = await startAgentRegistration(config, input);
|
|
1591
|
+
if ("error" in result) {
|
|
1592
|
+
return json({
|
|
1593
|
+
error: result.error,
|
|
1594
|
+
...result.message === undefined ? {} : { error_description: result.message }
|
|
1595
|
+
}, result.status);
|
|
1596
|
+
}
|
|
1597
|
+
return registrationResponse(result);
|
|
1598
|
+
}).post(claimRoute, async ({ body }) => {
|
|
1599
|
+
const value = recordBody(body);
|
|
1600
|
+
if (value === undefined || typeof value.claim_token !== "string" || typeof value.email !== "string") {
|
|
1601
|
+
return json({ error: "invalid_request" }, 400);
|
|
1602
|
+
}
|
|
1603
|
+
const result = await beginAgentClaim(config, {
|
|
1604
|
+
claimToken: value.claim_token,
|
|
1605
|
+
email: value.email
|
|
1606
|
+
});
|
|
1607
|
+
if ("error" in result)
|
|
1608
|
+
return json({ error: result.error }, result.status);
|
|
1609
|
+
return json({ claim_attempt: result.claimAttempt });
|
|
1610
|
+
}).post(completeRoute, async ({ body, request }) => {
|
|
1611
|
+
let value = recordBody(body);
|
|
1612
|
+
if (value === undefined && typeof body === "string") {
|
|
1613
|
+
value = Object.fromEntries(new URLSearchParams(body));
|
|
1614
|
+
}
|
|
1615
|
+
if (value === undefined || typeof value.claim_attempt_token !== "string" || typeof value.user_code !== "string") {
|
|
1616
|
+
return json({ error: "invalid_request" }, 400);
|
|
1617
|
+
}
|
|
1618
|
+
const result = await completeAgentClaim(config, {
|
|
1619
|
+
attemptToken: value.claim_attempt_token,
|
|
1620
|
+
request,
|
|
1621
|
+
userCode: value.user_code
|
|
1622
|
+
});
|
|
1623
|
+
if ("error" in result)
|
|
1624
|
+
return json({ error: result.error }, result.status);
|
|
1625
|
+
return new Response(null, { status: 204 });
|
|
1626
|
+
}).as("global");
|
|
483
1627
|
};
|
|
484
1628
|
// src/agents/inMemoryStores.ts
|
|
1629
|
+
var cloneIdentityRegistration = (value) => ({
|
|
1630
|
+
...value,
|
|
1631
|
+
claimAttempt: value.claimAttempt === undefined ? undefined : { ...value.claimAttempt },
|
|
1632
|
+
upstream: value.upstream === undefined ? undefined : { ...value.upstream }
|
|
1633
|
+
});
|
|
485
1634
|
var cloneRegistration = (value) => ({
|
|
486
1635
|
...value,
|
|
487
1636
|
allowedScopes: [...value.allowedScopes],
|
|
@@ -516,6 +1665,48 @@ var createInMemoryAgentDelegationStore = () => {
|
|
|
516
1665
|
}
|
|
517
1666
|
};
|
|
518
1667
|
};
|
|
1668
|
+
var createInMemoryAgentIdentityRegistrationStore = () => {
|
|
1669
|
+
const registrations = new Map;
|
|
1670
|
+
return {
|
|
1671
|
+
create: async (registration) => {
|
|
1672
|
+
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);
|
|
1673
|
+
if (conflicts)
|
|
1674
|
+
return false;
|
|
1675
|
+
registrations.set(registration.registrationId, cloneIdentityRegistration(registration));
|
|
1676
|
+
return true;
|
|
1677
|
+
},
|
|
1678
|
+
findByAgentId: async (agentId) => {
|
|
1679
|
+
const value = [...registrations.values()].find((registration) => registration.agentId === agentId);
|
|
1680
|
+
return value === undefined ? undefined : cloneIdentityRegistration(value);
|
|
1681
|
+
},
|
|
1682
|
+
findByAttemptTokenHash: async (attemptTokenHash) => {
|
|
1683
|
+
const value = [...registrations.values()].find((registration) => registration.claimAttempt?.tokenHash === attemptTokenHash);
|
|
1684
|
+
return value === undefined ? undefined : cloneIdentityRegistration(value);
|
|
1685
|
+
},
|
|
1686
|
+
findByClaimTokenHash: async (claimTokenHash) => {
|
|
1687
|
+
const value = [...registrations.values()].find((registration) => registration.claimTokenHash === claimTokenHash);
|
|
1688
|
+
return value === undefined ? undefined : cloneIdentityRegistration(value);
|
|
1689
|
+
},
|
|
1690
|
+
findByRegistrationId: async (registrationId) => {
|
|
1691
|
+
const value = registrations.get(registrationId);
|
|
1692
|
+
return value === undefined ? undefined : cloneIdentityRegistration(value);
|
|
1693
|
+
},
|
|
1694
|
+
findByUpstreamIdentity: async ({ clientId, issuer, subject }) => {
|
|
1695
|
+
const value = [...registrations.values()].find((registration) => registration.upstream?.clientId === clientId && registration.upstream?.issuer === issuer && registration.upstream.subject === subject);
|
|
1696
|
+
return value === undefined ? undefined : cloneIdentityRegistration(value);
|
|
1697
|
+
},
|
|
1698
|
+
replace: async (registration, expectedVersion) => {
|
|
1699
|
+
const current = registrations.get(registration.registrationId);
|
|
1700
|
+
if (current?.version !== expectedVersion)
|
|
1701
|
+
return false;
|
|
1702
|
+
registrations.set(registration.registrationId, cloneIdentityRegistration({
|
|
1703
|
+
...registration,
|
|
1704
|
+
version: expectedVersion + 1
|
|
1705
|
+
}));
|
|
1706
|
+
return true;
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
};
|
|
519
1710
|
var createInMemoryAgentRegistrationStore = () => {
|
|
520
1711
|
const registrations = new Map;
|
|
521
1712
|
return {
|
|
@@ -1824,7 +3015,7 @@ function getColumnNameAndConfig2(a, b) {
|
|
|
1824
3015
|
config: typeof a === "object" ? a : b
|
|
1825
3016
|
};
|
|
1826
3017
|
}
|
|
1827
|
-
var
|
|
3018
|
+
var textDecoder3 = typeof TextDecoder === "undefined" ? null : new TextDecoder;
|
|
1828
3019
|
function assertUnreachable2(_x) {
|
|
1829
3020
|
throw new Error("Didn't expect to get here");
|
|
1830
3021
|
}
|
|
@@ -2875,7 +4066,7 @@ var PgJson = class extends PgColumn {
|
|
|
2875
4066
|
return "json";
|
|
2876
4067
|
}
|
|
2877
4068
|
};
|
|
2878
|
-
function
|
|
4069
|
+
function json2(name2) {
|
|
2879
4070
|
return new PgJsonBuilder(name2 ?? "");
|
|
2880
4071
|
}
|
|
2881
4072
|
|
|
@@ -3412,7 +4603,7 @@ function getPgColumnBuilders() {
|
|
|
3412
4603
|
inet,
|
|
3413
4604
|
integer,
|
|
3414
4605
|
interval,
|
|
3415
|
-
json,
|
|
4606
|
+
json: json2,
|
|
3416
4607
|
jsonb,
|
|
3417
4608
|
line,
|
|
3418
4609
|
macaddr,
|
|
@@ -3629,6 +4820,9 @@ var Index = class {
|
|
|
3629
4820
|
function index(name2) {
|
|
3630
4821
|
return new IndexBuilderOn(false, name2);
|
|
3631
4822
|
}
|
|
4823
|
+
function uniqueIndex(name2) {
|
|
4824
|
+
return new IndexBuilderOn(true, name2);
|
|
4825
|
+
}
|
|
3632
4826
|
// node_modules/drizzle-orm/pg-core/checks.js
|
|
3633
4827
|
var CheckBuilder = class {
|
|
3634
4828
|
static [entityKind] = "PgCheckBuilder";
|
|
@@ -11377,6 +12571,38 @@ var agentDelegationsTable = pgTable("auth_agent_delegations", {
|
|
|
11377
12571
|
updated_at_ms: bigint("updated_at_ms", { mode: "number" }).notNull(),
|
|
11378
12572
|
user_id: varchar("user_id", { length: ID_LENGTH }).notNull()
|
|
11379
12573
|
});
|
|
12574
|
+
var agentIdentityRegistrationsTable = pgTable("auth_agent_identity_registrations", {
|
|
12575
|
+
agent_id: varchar("agent_id", { length: ID_LENGTH }).notNull().unique(),
|
|
12576
|
+
claim_attempt: jsonb("claim_attempt").$type(),
|
|
12577
|
+
claim_attempt_token_hash: varchar("claim_attempt_token_hash", {
|
|
12578
|
+
length: ID_LENGTH
|
|
12579
|
+
}).unique(),
|
|
12580
|
+
claim_expires_at_ms: bigint("claim_expires_at_ms", {
|
|
12581
|
+
mode: "number"
|
|
12582
|
+
}).notNull(),
|
|
12583
|
+
claim_token_hash: varchar("claim_token_hash", {
|
|
12584
|
+
length: ID_LENGTH
|
|
12585
|
+
}).notNull().unique(),
|
|
12586
|
+
created_at_ms: bigint("created_at_ms", { mode: "number" }).notNull(),
|
|
12587
|
+
expires_at_ms: bigint("expires_at_ms", { mode: "number" }).notNull(),
|
|
12588
|
+
kind: varchar("kind", { length: 32 }).$type().notNull(),
|
|
12589
|
+
last_polled_at_ms: bigint("last_polled_at_ms", { mode: "number" }),
|
|
12590
|
+
login_hint: varchar("login_hint", { length: ID_LENGTH }),
|
|
12591
|
+
registration_id: varchar("registration_id", {
|
|
12592
|
+
length: ID_LENGTH
|
|
12593
|
+
}).primaryKey(),
|
|
12594
|
+
status: varchar("status", { length: STATUS_LENGTH }).$type().notNull(),
|
|
12595
|
+
updated_at_ms: bigint("updated_at_ms", { mode: "number" }).notNull(),
|
|
12596
|
+
upstream_client_id: varchar("upstream_client_id", {
|
|
12597
|
+
length: ID_LENGTH
|
|
12598
|
+
}),
|
|
12599
|
+
upstream_issuer: varchar("upstream_issuer", { length: ID_LENGTH }),
|
|
12600
|
+
upstream_subject: varchar("upstream_subject", { length: ID_LENGTH }),
|
|
12601
|
+
user_id: varchar("user_id", { length: ID_LENGTH }),
|
|
12602
|
+
version: integer("version").notNull()
|
|
12603
|
+
}, (table) => [
|
|
12604
|
+
uniqueIndex("auth_agent_identity_upstream_unique").on(table.upstream_issuer, table.upstream_subject, table.upstream_client_id)
|
|
12605
|
+
]);
|
|
11380
12606
|
var agentRegistrationsTable = pgTable("auth_agent_registrations", {
|
|
11381
12607
|
agent_id: varchar("agent_id", { length: ID_LENGTH }).primaryKey(),
|
|
11382
12608
|
allowed_scopes: jsonb("allowed_scopes").$type().notNull().default([]),
|
|
@@ -11409,7 +12635,49 @@ var toDelegation = (row) => ({
|
|
|
11409
12635
|
updatedAt: row.updated_at_ms,
|
|
11410
12636
|
userId: row.user_id
|
|
11411
12637
|
});
|
|
12638
|
+
var toIdentityRegistration = (row) => ({
|
|
12639
|
+
agentId: row.agent_id,
|
|
12640
|
+
claimAttempt: row.claim_attempt ?? undefined,
|
|
12641
|
+
claimExpiresAt: row.claim_expires_at_ms,
|
|
12642
|
+
claimTokenHash: row.claim_token_hash,
|
|
12643
|
+
createdAt: row.created_at_ms,
|
|
12644
|
+
expiresAt: row.expires_at_ms,
|
|
12645
|
+
kind: row.kind,
|
|
12646
|
+
lastPolledAt: row.last_polled_at_ms ?? undefined,
|
|
12647
|
+
loginHint: row.login_hint ?? undefined,
|
|
12648
|
+
registrationId: row.registration_id,
|
|
12649
|
+
status: row.status,
|
|
12650
|
+
updatedAt: row.updated_at_ms,
|
|
12651
|
+
upstream: row.upstream_client_id === null || row.upstream_issuer === null || row.upstream_subject === null ? undefined : {
|
|
12652
|
+
clientId: row.upstream_client_id,
|
|
12653
|
+
issuer: row.upstream_issuer,
|
|
12654
|
+
subject: row.upstream_subject
|
|
12655
|
+
},
|
|
12656
|
+
userId: row.user_id ?? undefined,
|
|
12657
|
+
version: row.version
|
|
12658
|
+
});
|
|
12659
|
+
var identityRegistrationValues = (registration) => ({
|
|
12660
|
+
agent_id: registration.agentId,
|
|
12661
|
+
claim_attempt: registration.claimAttempt ?? null,
|
|
12662
|
+
claim_attempt_token_hash: registration.claimAttempt?.tokenHash ?? null,
|
|
12663
|
+
claim_expires_at_ms: registration.claimExpiresAt,
|
|
12664
|
+
claim_token_hash: registration.claimTokenHash,
|
|
12665
|
+
created_at_ms: registration.createdAt,
|
|
12666
|
+
expires_at_ms: registration.expiresAt,
|
|
12667
|
+
kind: registration.kind,
|
|
12668
|
+
last_polled_at_ms: registration.lastPolledAt ?? null,
|
|
12669
|
+
login_hint: registration.loginHint ?? null,
|
|
12670
|
+
registration_id: registration.registrationId,
|
|
12671
|
+
status: registration.status,
|
|
12672
|
+
updated_at_ms: registration.updatedAt,
|
|
12673
|
+
upstream_client_id: registration.upstream?.clientId ?? null,
|
|
12674
|
+
upstream_issuer: registration.upstream?.issuer ?? null,
|
|
12675
|
+
upstream_subject: registration.upstream?.subject ?? null,
|
|
12676
|
+
user_id: registration.userId ?? null,
|
|
12677
|
+
version: registration.version
|
|
12678
|
+
});
|
|
11412
12679
|
var createNeonAgentDelegationStore = (databaseUrl) => createPostgresAgentDelegationStore(createNeonDatabase(databaseUrl));
|
|
12680
|
+
var createNeonAgentIdentityRegistrationStore = (databaseUrl) => createPostgresAgentIdentityRegistrationStore(createNeonDatabase(databaseUrl));
|
|
11413
12681
|
var createNeonAgentRegistrationStore = (databaseUrl) => createPostgresAgentRegistrationStore(createNeonDatabase(databaseUrl));
|
|
11414
12682
|
var createPostgresAgentDelegationStore = (db) => ({
|
|
11415
12683
|
findActiveDelegation: async ({
|
|
@@ -11450,6 +12718,41 @@ var createPostgresAgentDelegationStore = (db) => ({
|
|
|
11450
12718
|
});
|
|
11451
12719
|
}
|
|
11452
12720
|
});
|
|
12721
|
+
var createPostgresAgentIdentityRegistrationStore = (db) => ({
|
|
12722
|
+
create: async (registration) => {
|
|
12723
|
+
const rows = await db.insert(agentIdentityRegistrationsTable).values(identityRegistrationValues(registration)).onConflictDoNothing().returning({ id: agentIdentityRegistrationsTable.registration_id });
|
|
12724
|
+
return rows.length === 1;
|
|
12725
|
+
},
|
|
12726
|
+
findByAgentId: async (agentId) => {
|
|
12727
|
+
const [row] = await db.select().from(agentIdentityRegistrationsTable).where(eq(agentIdentityRegistrationsTable.agent_id, agentId)).limit(1);
|
|
12728
|
+
return row === undefined ? undefined : toIdentityRegistration(row);
|
|
12729
|
+
},
|
|
12730
|
+
findByAttemptTokenHash: async (attemptTokenHash) => {
|
|
12731
|
+
const [row] = await db.select().from(agentIdentityRegistrationsTable).where(eq(agentIdentityRegistrationsTable.claim_attempt_token_hash, attemptTokenHash)).limit(1);
|
|
12732
|
+
return row === undefined ? undefined : toIdentityRegistration(row);
|
|
12733
|
+
},
|
|
12734
|
+
findByClaimTokenHash: async (claimTokenHash) => {
|
|
12735
|
+
const [row] = await db.select().from(agentIdentityRegistrationsTable).where(eq(agentIdentityRegistrationsTable.claim_token_hash, claimTokenHash)).limit(1);
|
|
12736
|
+
return row === undefined ? undefined : toIdentityRegistration(row);
|
|
12737
|
+
},
|
|
12738
|
+
findByRegistrationId: async (registrationId) => {
|
|
12739
|
+
const [row] = await db.select().from(agentIdentityRegistrationsTable).where(eq(agentIdentityRegistrationsTable.registration_id, registrationId)).limit(1);
|
|
12740
|
+
return row === undefined ? undefined : toIdentityRegistration(row);
|
|
12741
|
+
},
|
|
12742
|
+
findByUpstreamIdentity: async ({ clientId, issuer, subject }) => {
|
|
12743
|
+
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);
|
|
12744
|
+
return row === undefined ? undefined : toIdentityRegistration(row);
|
|
12745
|
+
},
|
|
12746
|
+
replace: async (registration, expectedVersion) => {
|
|
12747
|
+
const next = {
|
|
12748
|
+
...registration,
|
|
12749
|
+
version: expectedVersion + 1
|
|
12750
|
+
};
|
|
12751
|
+
const values = identityRegistrationValues(next);
|
|
12752
|
+
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 });
|
|
12753
|
+
return rows.length === 1;
|
|
12754
|
+
}
|
|
12755
|
+
});
|
|
11453
12756
|
var createPostgresAgentRegistrationStore = (db) => ({
|
|
11454
12757
|
findByAgentId: async (agentId) => {
|
|
11455
12758
|
const [row] = await db.select().from(agentRegistrationsTable).where(eq(agentRegistrationsTable.agent_id, agentId)).limit(1);
|
|
@@ -11482,24 +12785,46 @@ var createPostgresAgentRegistrationStore = (db) => ({
|
|
|
11482
12785
|
});
|
|
11483
12786
|
export {
|
|
11484
12787
|
validateClientIdMetadataDocument,
|
|
12788
|
+
startAgentRegistration,
|
|
12789
|
+
revokeAgentIdentityRegistration,
|
|
11485
12790
|
resolveAgentPrincipal,
|
|
12791
|
+
issueAgentServiceAssertion,
|
|
12792
|
+
issueAgentIdentityAssertion,
|
|
12793
|
+
handleAgentTokenGrant,
|
|
12794
|
+
generateAgentRegistrationGuide,
|
|
12795
|
+
discoverAgentRegistration,
|
|
11486
12796
|
createPostgresAgentRegistrationStore,
|
|
12797
|
+
createPostgresAgentIdentityRegistrationStore,
|
|
11487
12798
|
createPostgresAgentDelegationStore,
|
|
11488
12799
|
createOidcAgentCredentialVerifier,
|
|
11489
12800
|
createNeonAgentRegistrationStore,
|
|
12801
|
+
createNeonAgentIdentityRegistrationStore,
|
|
11490
12802
|
createNeonAgentDelegationStore,
|
|
11491
12803
|
createInMemoryAgentRegistrationStore,
|
|
12804
|
+
createInMemoryAgentIdentityRegistrationStore,
|
|
12805
|
+
createInMemoryAgentIdentityAssertionJtiStore,
|
|
11492
12806
|
createInMemoryAgentDelegationStore,
|
|
11493
12807
|
createClientIdMetadataResolver,
|
|
12808
|
+
createAgentRegistrationCredentialVerifier,
|
|
12809
|
+
createAgentRegistrationClient,
|
|
12810
|
+
createAgentIdentityAssertionVerifier,
|
|
12811
|
+
completeAgentClaim,
|
|
11494
12812
|
clientIdMetadataToOAuthClient,
|
|
12813
|
+
beginAgentClaim,
|
|
11495
12814
|
agentRegistrationsTable,
|
|
12815
|
+
agentRegistrationEndpoints,
|
|
12816
|
+
agentRegistrationDiscoveryMetadata,
|
|
11496
12817
|
agentProtectedResourceMetadata,
|
|
12818
|
+
agentIdentityRegistrationsTable,
|
|
11497
12819
|
agentHasScopes,
|
|
11498
12820
|
agentDelegationsTable,
|
|
11499
12821
|
agentAuthPlugin,
|
|
11500
12822
|
agentAuthChallenge,
|
|
11501
|
-
DEFAULT_AGENT_RESOURCE_METADATA_ROUTE
|
|
12823
|
+
DEFAULT_AGENT_RESOURCE_METADATA_ROUTE,
|
|
12824
|
+
AGENT_IDENTITY_ASSERTION_TYPE,
|
|
12825
|
+
AGENT_IDENTITY_ASSERTION_GRANT_TYPE,
|
|
12826
|
+
AGENT_CLAIM_GRANT_TYPE
|
|
11502
12827
|
};
|
|
11503
12828
|
|
|
11504
|
-
//# debugId=
|
|
12829
|
+
//# debugId=4AABAB4CBDEE339264756E2164756E21
|
|
11505
12830
|
//# sourceMappingURL=index.js.map
|