@absolutejs/auth 0.54.9 → 0.55.1
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 +26 -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 +1427 -95
- 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 +144 -1
- package/dist/agents/types.d.ts +59 -0
- package/dist/apikeys/routes.d.ts +1 -1
- package/dist/authInstance.d.ts +12 -0
- package/dist/authorization/protectPermission.d.ts +2 -2
- package/dist/cli/migrate.js +57 -8
- package/dist/cli/migrate.js.map +4 -4
- package/dist/index.d.ts +14 -131
- package/dist/index.js +1535 -238
- package/dist/index.js.map +16 -13
- package/dist/manifest.js +12 -6
- package/dist/manifest.js.map +4 -4
- package/dist/manifest.json +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 +8 -6
- package/dist/routes/protectRoute.d.ts +2 -2
- package/dist/server.d.ts +18 -0
- package/dist/server.js +29362 -0
- package/dist/server.js.map +287 -0
- package/dist/utils.d.ts +1 -1
- 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 +14 -6
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;
|
|
@@ -411,10 +1425,16 @@ var resolveAgentPrincipal = async (request, config) => {
|
|
|
411
1425
|
};
|
|
412
1426
|
// src/agents/routes.ts
|
|
413
1427
|
import { Elysia } from "elysia";
|
|
1428
|
+
var DELETE_CODE_POINT = 127;
|
|
1429
|
+
var HTTP_BAD_REQUEST = 400;
|
|
1430
|
+
var HTTP_FORBIDDEN = 403;
|
|
1431
|
+
var HTTP_OK = 200;
|
|
1432
|
+
var HTTP_UNAUTHORIZED = 401;
|
|
1433
|
+
var MINIMUM_PRINTABLE_CODE_POINT = 32;
|
|
414
1434
|
var quoteHeaderValue = (value) => {
|
|
415
1435
|
const printable = [...value].filter((character) => {
|
|
416
1436
|
const codePoint = character.codePointAt(0) ?? 0;
|
|
417
|
-
return codePoint >=
|
|
1437
|
+
return codePoint >= MINIMUM_PRINTABLE_CODE_POINT && codePoint !== DELETE_CODE_POINT;
|
|
418
1438
|
}).join("");
|
|
419
1439
|
return `"${printable.replace(/[\\"]/g, "\\$&")}"`;
|
|
420
1440
|
};
|
|
@@ -447,41 +1467,177 @@ var failureResponse = (config, failure, requiredScopes) => new Response(JSON.str
|
|
|
447
1467
|
requiredScopes
|
|
448
1468
|
})
|
|
449
1469
|
},
|
|
450
|
-
status: failure.code === "Forbidden" ?
|
|
1470
|
+
status: failure.code === "Forbidden" ? HTTP_FORBIDDEN : HTTP_UNAUTHORIZED
|
|
451
1471
|
});
|
|
452
|
-
var
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
1472
|
+
var json = (value, status = HTTP_OK) => new Response(JSON.stringify(value), {
|
|
1473
|
+
headers: {
|
|
1474
|
+
"cache-control": "no-store",
|
|
1475
|
+
"content-type": "application/json"
|
|
1476
|
+
},
|
|
1477
|
+
status
|
|
1478
|
+
});
|
|
1479
|
+
var recordBody = (body) => {
|
|
1480
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
return Object.fromEntries(Object.entries(body));
|
|
1484
|
+
};
|
|
1485
|
+
var registrationResponse = (result) => {
|
|
1486
|
+
const body = {
|
|
1487
|
+
...result.assertionExpires > 0 ? {
|
|
1488
|
+
assertion_expires: new Date(result.assertionExpires).toISOString()
|
|
1489
|
+
} : {},
|
|
1490
|
+
...result.claim === undefined ? {} : { claim: result.claim },
|
|
1491
|
+
...result.claimToken === undefined ? {} : { claim_token: result.claimToken },
|
|
1492
|
+
...result.claimTokenExpires === undefined ? {} : {
|
|
1493
|
+
claim_token_expires: new Date(result.claimTokenExpires).toISOString()
|
|
1494
|
+
},
|
|
1495
|
+
...result.identityAssertion === undefined ? {} : { identity_assertion: result.identityAssertion },
|
|
1496
|
+
...result.preClaimScopes === undefined ? {} : { pre_claim_scopes: result.preClaimScopes },
|
|
1497
|
+
post_claim_scopes: result.postClaimScopes,
|
|
1498
|
+
registration_id: result.registrationId,
|
|
1499
|
+
registration_type: result.registrationType
|
|
1500
|
+
};
|
|
1501
|
+
if (result.registrationType === "identity_assertion" && result.identityAssertion === undefined) {
|
|
1502
|
+
return json({
|
|
1503
|
+
...body,
|
|
1504
|
+
error: "interaction_required",
|
|
1505
|
+
error_description: "Authenticate at the service and confirm the account link."
|
|
1506
|
+
}, HTTP_UNAUTHORIZED);
|
|
1507
|
+
}
|
|
1508
|
+
return json(body);
|
|
1509
|
+
};
|
|
1510
|
+
var parseRegistrationInput = (value) => {
|
|
1511
|
+
if (value.type === "anonymous") {
|
|
1512
|
+
const input2 = { type: "anonymous" };
|
|
1513
|
+
return input2;
|
|
1514
|
+
}
|
|
1515
|
+
if (value.type === "service_auth" && typeof value.login_hint === "string") {
|
|
1516
|
+
const input2 = {
|
|
1517
|
+
loginHint: value.login_hint,
|
|
1518
|
+
type: "service_auth"
|
|
1519
|
+
};
|
|
1520
|
+
return input2;
|
|
1521
|
+
}
|
|
1522
|
+
if (value.type !== "identity_assertion" || value.assertion_type !== AGENT_IDENTITY_ASSERTION_TYPE || typeof value.assertion !== "string") {
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
const input = {
|
|
1526
|
+
assertion: value.assertion,
|
|
1527
|
+
assertionType: AGENT_IDENTITY_ASSERTION_TYPE,
|
|
1528
|
+
type: "identity_assertion"
|
|
1529
|
+
};
|
|
1530
|
+
return input;
|
|
1531
|
+
};
|
|
1532
|
+
var escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
1533
|
+
var agentAuthContextPlugin = (config) => new Elysia().derive(({ request }) => ({
|
|
1534
|
+
protectAgent: async (requiredScopes, handleAuth, handleAuthFail) => {
|
|
1535
|
+
if (config === undefined) {
|
|
1536
|
+
const failure = {
|
|
1537
|
+
code: "Unauthorized",
|
|
1538
|
+
message: "Agent is not authenticated"
|
|
1539
|
+
};
|
|
1540
|
+
return await handleAuthFail?.(failure) ?? new Response(failure.message, { status: 401 });
|
|
478
1541
|
}
|
|
479
|
-
|
|
1542
|
+
const principal = await resolveAgentPrincipal(request, config);
|
|
1543
|
+
if (principal === undefined) {
|
|
1544
|
+
const failure = {
|
|
1545
|
+
code: "Unauthorized",
|
|
1546
|
+
message: "Agent is not authenticated"
|
|
1547
|
+
};
|
|
1548
|
+
return await handleAuthFail?.(failure) ?? failureResponse(config, failure, requiredScopes);
|
|
1549
|
+
}
|
|
1550
|
+
if (!agentHasScopes(principal, requiredScopes)) {
|
|
1551
|
+
const failure = {
|
|
1552
|
+
code: "Forbidden",
|
|
1553
|
+
message: "Insufficient agent scopes"
|
|
1554
|
+
};
|
|
1555
|
+
return await handleAuthFail?.(failure) ?? failureResponse(config, failure, requiredScopes);
|
|
1556
|
+
}
|
|
1557
|
+
return handleAuth(principal);
|
|
1558
|
+
}
|
|
1559
|
+
}));
|
|
1560
|
+
var agentAuthPlugin = (config) => {
|
|
1561
|
+
const plugin = agentAuthContextPlugin(config);
|
|
480
1562
|
if (config === undefined)
|
|
481
1563
|
return plugin.as("global");
|
|
482
|
-
|
|
1564
|
+
if (config.agentRegistration === undefined) {
|
|
1565
|
+
return plugin.get(config.metadataRoute ?? DEFAULT_AGENT_RESOURCE_METADATA_ROUTE, () => agentProtectedResourceMetadata(config)).as("global");
|
|
1566
|
+
}
|
|
1567
|
+
const registration = config.agentRegistration;
|
|
1568
|
+
const identityRoute = registration.identityRoute ?? "/agent/identity";
|
|
1569
|
+
const claimRoute = registration.claimRoute ?? "/agent/identity/claim";
|
|
1570
|
+
const completeRoute = registration.completeRoute ?? "/agent/identity/claim/complete";
|
|
1571
|
+
const guideRoute = registration.guideRoute ?? "/auth.md";
|
|
1572
|
+
return plugin.get(config.metadataRoute ?? DEFAULT_AGENT_RESOURCE_METADATA_ROUTE, () => agentProtectedResourceMetadata(config)).get(guideRoute, () => new Response(generateAgentRegistrationGuide(config), {
|
|
1573
|
+
headers: {
|
|
1574
|
+
"cache-control": "public, max-age=300",
|
|
1575
|
+
"content-type": "text/markdown; charset=utf-8"
|
|
1576
|
+
}
|
|
1577
|
+
})).get(completeRoute, ({ query }) => {
|
|
1578
|
+
const token = typeof query.claim_attempt_token === "string" ? query.claim_attempt_token : "";
|
|
1579
|
+
if (token.length === 0)
|
|
1580
|
+
return new Response("Invalid claim link", { status: 400 });
|
|
1581
|
+
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>`, {
|
|
1582
|
+
headers: {
|
|
1583
|
+
"cache-control": "no-store",
|
|
1584
|
+
"content-security-policy": "default-src 'none'; form-action 'self'; style-src 'none'; base-uri 'none'; frame-ancestors 'none'",
|
|
1585
|
+
"content-type": "text/html; charset=utf-8",
|
|
1586
|
+
"x-content-type-options": "nosniff"
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
}).post(identityRoute, async ({ body }) => {
|
|
1590
|
+
const value = recordBody(body);
|
|
1591
|
+
if (value === undefined || typeof value.type !== "string") {
|
|
1592
|
+
return json({ error: "invalid_request" }, HTTP_BAD_REQUEST);
|
|
1593
|
+
}
|
|
1594
|
+
const input = parseRegistrationInput(value);
|
|
1595
|
+
if (input === undefined)
|
|
1596
|
+
return json({ error: "invalid_request" }, HTTP_BAD_REQUEST);
|
|
1597
|
+
const result = await startAgentRegistration(config, input);
|
|
1598
|
+
if ("error" in result) {
|
|
1599
|
+
return json({
|
|
1600
|
+
error: result.error,
|
|
1601
|
+
...result.message === undefined ? {} : { error_description: result.message }
|
|
1602
|
+
}, result.status);
|
|
1603
|
+
}
|
|
1604
|
+
return registrationResponse(result);
|
|
1605
|
+
}).post(claimRoute, async ({ body }) => {
|
|
1606
|
+
const value = recordBody(body);
|
|
1607
|
+
if (value === undefined || typeof value.claim_token !== "string" || typeof value.email !== "string") {
|
|
1608
|
+
return json({ error: "invalid_request" }, HTTP_BAD_REQUEST);
|
|
1609
|
+
}
|
|
1610
|
+
const result = await beginAgentClaim(config, {
|
|
1611
|
+
claimToken: value.claim_token,
|
|
1612
|
+
email: value.email
|
|
1613
|
+
});
|
|
1614
|
+
if ("error" in result)
|
|
1615
|
+
return json({ error: result.error }, result.status);
|
|
1616
|
+
return json({ claim_attempt: result.claimAttempt });
|
|
1617
|
+
}).post(completeRoute, async ({ body, request }) => {
|
|
1618
|
+
let value = recordBody(body);
|
|
1619
|
+
if (value === undefined && typeof body === "string") {
|
|
1620
|
+
value = Object.fromEntries(new URLSearchParams(body));
|
|
1621
|
+
}
|
|
1622
|
+
if (value === undefined || typeof value.claim_attempt_token !== "string" || typeof value.user_code !== "string") {
|
|
1623
|
+
return json({ error: "invalid_request" }, HTTP_BAD_REQUEST);
|
|
1624
|
+
}
|
|
1625
|
+
const result = await completeAgentClaim(config, {
|
|
1626
|
+
attemptToken: value.claim_attempt_token,
|
|
1627
|
+
request,
|
|
1628
|
+
userCode: value.user_code
|
|
1629
|
+
});
|
|
1630
|
+
if ("error" in result)
|
|
1631
|
+
return json({ error: result.error }, result.status);
|
|
1632
|
+
return new Response(null, { status: 204 });
|
|
1633
|
+
}).as("global");
|
|
483
1634
|
};
|
|
484
1635
|
// src/agents/inMemoryStores.ts
|
|
1636
|
+
var cloneIdentityRegistration = (value) => ({
|
|
1637
|
+
...value,
|
|
1638
|
+
claimAttempt: value.claimAttempt === undefined ? undefined : { ...value.claimAttempt },
|
|
1639
|
+
upstream: value.upstream === undefined ? undefined : { ...value.upstream }
|
|
1640
|
+
});
|
|
485
1641
|
var cloneRegistration = (value) => ({
|
|
486
1642
|
...value,
|
|
487
1643
|
allowedScopes: [...value.allowedScopes],
|
|
@@ -516,6 +1672,48 @@ var createInMemoryAgentDelegationStore = () => {
|
|
|
516
1672
|
}
|
|
517
1673
|
};
|
|
518
1674
|
};
|
|
1675
|
+
var createInMemoryAgentIdentityRegistrationStore = () => {
|
|
1676
|
+
const registrations = new Map;
|
|
1677
|
+
return {
|
|
1678
|
+
create: async (registration) => {
|
|
1679
|
+
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);
|
|
1680
|
+
if (conflicts)
|
|
1681
|
+
return false;
|
|
1682
|
+
registrations.set(registration.registrationId, cloneIdentityRegistration(registration));
|
|
1683
|
+
return true;
|
|
1684
|
+
},
|
|
1685
|
+
findByAgentId: async (agentId) => {
|
|
1686
|
+
const value = [...registrations.values()].find((registration) => registration.agentId === agentId);
|
|
1687
|
+
return value === undefined ? undefined : cloneIdentityRegistration(value);
|
|
1688
|
+
},
|
|
1689
|
+
findByAttemptTokenHash: async (attemptTokenHash) => {
|
|
1690
|
+
const value = [...registrations.values()].find((registration) => registration.claimAttempt?.tokenHash === attemptTokenHash);
|
|
1691
|
+
return value === undefined ? undefined : cloneIdentityRegistration(value);
|
|
1692
|
+
},
|
|
1693
|
+
findByClaimTokenHash: async (claimTokenHash) => {
|
|
1694
|
+
const value = [...registrations.values()].find((registration) => registration.claimTokenHash === claimTokenHash);
|
|
1695
|
+
return value === undefined ? undefined : cloneIdentityRegistration(value);
|
|
1696
|
+
},
|
|
1697
|
+
findByRegistrationId: async (registrationId) => {
|
|
1698
|
+
const value = registrations.get(registrationId);
|
|
1699
|
+
return value === undefined ? undefined : cloneIdentityRegistration(value);
|
|
1700
|
+
},
|
|
1701
|
+
findByUpstreamIdentity: async ({ clientId, issuer, subject }) => {
|
|
1702
|
+
const value = [...registrations.values()].find((registration) => registration.upstream?.clientId === clientId && registration.upstream?.issuer === issuer && registration.upstream.subject === subject);
|
|
1703
|
+
return value === undefined ? undefined : cloneIdentityRegistration(value);
|
|
1704
|
+
},
|
|
1705
|
+
replace: async (registration, expectedVersion) => {
|
|
1706
|
+
const current = registrations.get(registration.registrationId);
|
|
1707
|
+
if (current?.version !== expectedVersion)
|
|
1708
|
+
return false;
|
|
1709
|
+
registrations.set(registration.registrationId, cloneIdentityRegistration({
|
|
1710
|
+
...registration,
|
|
1711
|
+
version: expectedVersion + 1
|
|
1712
|
+
}));
|
|
1713
|
+
return true;
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
};
|
|
519
1717
|
var createInMemoryAgentRegistrationStore = () => {
|
|
520
1718
|
const registrations = new Map;
|
|
521
1719
|
return {
|
|
@@ -1824,7 +3022,7 @@ function getColumnNameAndConfig2(a, b) {
|
|
|
1824
3022
|
config: typeof a === "object" ? a : b
|
|
1825
3023
|
};
|
|
1826
3024
|
}
|
|
1827
|
-
var
|
|
3025
|
+
var textDecoder3 = typeof TextDecoder === "undefined" ? null : new TextDecoder;
|
|
1828
3026
|
function assertUnreachable2(_x) {
|
|
1829
3027
|
throw new Error("Didn't expect to get here");
|
|
1830
3028
|
}
|
|
@@ -2875,7 +4073,7 @@ var PgJson = class extends PgColumn {
|
|
|
2875
4073
|
return "json";
|
|
2876
4074
|
}
|
|
2877
4075
|
};
|
|
2878
|
-
function
|
|
4076
|
+
function json2(name2) {
|
|
2879
4077
|
return new PgJsonBuilder(name2 ?? "");
|
|
2880
4078
|
}
|
|
2881
4079
|
|
|
@@ -3412,7 +4610,7 @@ function getPgColumnBuilders() {
|
|
|
3412
4610
|
inet,
|
|
3413
4611
|
integer,
|
|
3414
4612
|
interval,
|
|
3415
|
-
json,
|
|
4613
|
+
json: json2,
|
|
3416
4614
|
jsonb,
|
|
3417
4615
|
line,
|
|
3418
4616
|
macaddr,
|
|
@@ -3629,6 +4827,9 @@ var Index = class {
|
|
|
3629
4827
|
function index(name2) {
|
|
3630
4828
|
return new IndexBuilderOn(false, name2);
|
|
3631
4829
|
}
|
|
4830
|
+
function uniqueIndex(name2) {
|
|
4831
|
+
return new IndexBuilderOn(true, name2);
|
|
4832
|
+
}
|
|
3632
4833
|
// node_modules/drizzle-orm/pg-core/checks.js
|
|
3633
4834
|
var CheckBuilder = class {
|
|
3634
4835
|
static [entityKind] = "PgCheckBuilder";
|
|
@@ -11377,6 +12578,38 @@ var agentDelegationsTable = pgTable("auth_agent_delegations", {
|
|
|
11377
12578
|
updated_at_ms: bigint("updated_at_ms", { mode: "number" }).notNull(),
|
|
11378
12579
|
user_id: varchar("user_id", { length: ID_LENGTH }).notNull()
|
|
11379
12580
|
});
|
|
12581
|
+
var agentIdentityRegistrationsTable = pgTable("auth_agent_identity_registrations", {
|
|
12582
|
+
agent_id: varchar("agent_id", { length: ID_LENGTH }).notNull().unique(),
|
|
12583
|
+
claim_attempt: jsonb("claim_attempt").$type(),
|
|
12584
|
+
claim_attempt_token_hash: varchar("claim_attempt_token_hash", {
|
|
12585
|
+
length: ID_LENGTH
|
|
12586
|
+
}).unique(),
|
|
12587
|
+
claim_expires_at_ms: bigint("claim_expires_at_ms", {
|
|
12588
|
+
mode: "number"
|
|
12589
|
+
}).notNull(),
|
|
12590
|
+
claim_token_hash: varchar("claim_token_hash", {
|
|
12591
|
+
length: ID_LENGTH
|
|
12592
|
+
}).notNull().unique(),
|
|
12593
|
+
created_at_ms: bigint("created_at_ms", { mode: "number" }).notNull(),
|
|
12594
|
+
expires_at_ms: bigint("expires_at_ms", { mode: "number" }).notNull(),
|
|
12595
|
+
kind: varchar("kind", { length: 32 }).$type().notNull(),
|
|
12596
|
+
last_polled_at_ms: bigint("last_polled_at_ms", { mode: "number" }),
|
|
12597
|
+
login_hint: varchar("login_hint", { length: ID_LENGTH }),
|
|
12598
|
+
registration_id: varchar("registration_id", {
|
|
12599
|
+
length: ID_LENGTH
|
|
12600
|
+
}).primaryKey(),
|
|
12601
|
+
status: varchar("status", { length: STATUS_LENGTH }).$type().notNull(),
|
|
12602
|
+
updated_at_ms: bigint("updated_at_ms", { mode: "number" }).notNull(),
|
|
12603
|
+
upstream_client_id: varchar("upstream_client_id", {
|
|
12604
|
+
length: ID_LENGTH
|
|
12605
|
+
}),
|
|
12606
|
+
upstream_issuer: varchar("upstream_issuer", { length: ID_LENGTH }),
|
|
12607
|
+
upstream_subject: varchar("upstream_subject", { length: ID_LENGTH }),
|
|
12608
|
+
user_id: varchar("user_id", { length: ID_LENGTH }),
|
|
12609
|
+
version: integer("version").notNull()
|
|
12610
|
+
}, (table) => [
|
|
12611
|
+
uniqueIndex("auth_agent_identity_upstream_unique").on(table.upstream_issuer, table.upstream_subject, table.upstream_client_id)
|
|
12612
|
+
]);
|
|
11380
12613
|
var agentRegistrationsTable = pgTable("auth_agent_registrations", {
|
|
11381
12614
|
agent_id: varchar("agent_id", { length: ID_LENGTH }).primaryKey(),
|
|
11382
12615
|
allowed_scopes: jsonb("allowed_scopes").$type().notNull().default([]),
|
|
@@ -11409,7 +12642,49 @@ var toDelegation = (row) => ({
|
|
|
11409
12642
|
updatedAt: row.updated_at_ms,
|
|
11410
12643
|
userId: row.user_id
|
|
11411
12644
|
});
|
|
12645
|
+
var toIdentityRegistration = (row) => ({
|
|
12646
|
+
agentId: row.agent_id,
|
|
12647
|
+
claimAttempt: row.claim_attempt ?? undefined,
|
|
12648
|
+
claimExpiresAt: row.claim_expires_at_ms,
|
|
12649
|
+
claimTokenHash: row.claim_token_hash,
|
|
12650
|
+
createdAt: row.created_at_ms,
|
|
12651
|
+
expiresAt: row.expires_at_ms,
|
|
12652
|
+
kind: row.kind,
|
|
12653
|
+
lastPolledAt: row.last_polled_at_ms ?? undefined,
|
|
12654
|
+
loginHint: row.login_hint ?? undefined,
|
|
12655
|
+
registrationId: row.registration_id,
|
|
12656
|
+
status: row.status,
|
|
12657
|
+
updatedAt: row.updated_at_ms,
|
|
12658
|
+
upstream: row.upstream_client_id === null || row.upstream_issuer === null || row.upstream_subject === null ? undefined : {
|
|
12659
|
+
clientId: row.upstream_client_id,
|
|
12660
|
+
issuer: row.upstream_issuer,
|
|
12661
|
+
subject: row.upstream_subject
|
|
12662
|
+
},
|
|
12663
|
+
userId: row.user_id ?? undefined,
|
|
12664
|
+
version: row.version
|
|
12665
|
+
});
|
|
12666
|
+
var identityRegistrationValues = (registration) => ({
|
|
12667
|
+
agent_id: registration.agentId,
|
|
12668
|
+
claim_attempt: registration.claimAttempt ?? null,
|
|
12669
|
+
claim_attempt_token_hash: registration.claimAttempt?.tokenHash ?? null,
|
|
12670
|
+
claim_expires_at_ms: registration.claimExpiresAt,
|
|
12671
|
+
claim_token_hash: registration.claimTokenHash,
|
|
12672
|
+
created_at_ms: registration.createdAt,
|
|
12673
|
+
expires_at_ms: registration.expiresAt,
|
|
12674
|
+
kind: registration.kind,
|
|
12675
|
+
last_polled_at_ms: registration.lastPolledAt ?? null,
|
|
12676
|
+
login_hint: registration.loginHint ?? null,
|
|
12677
|
+
registration_id: registration.registrationId,
|
|
12678
|
+
status: registration.status,
|
|
12679
|
+
updated_at_ms: registration.updatedAt,
|
|
12680
|
+
upstream_client_id: registration.upstream?.clientId ?? null,
|
|
12681
|
+
upstream_issuer: registration.upstream?.issuer ?? null,
|
|
12682
|
+
upstream_subject: registration.upstream?.subject ?? null,
|
|
12683
|
+
user_id: registration.userId ?? null,
|
|
12684
|
+
version: registration.version
|
|
12685
|
+
});
|
|
11412
12686
|
var createNeonAgentDelegationStore = (databaseUrl) => createPostgresAgentDelegationStore(createNeonDatabase(databaseUrl));
|
|
12687
|
+
var createNeonAgentIdentityRegistrationStore = (databaseUrl) => createPostgresAgentIdentityRegistrationStore(createNeonDatabase(databaseUrl));
|
|
11413
12688
|
var createNeonAgentRegistrationStore = (databaseUrl) => createPostgresAgentRegistrationStore(createNeonDatabase(databaseUrl));
|
|
11414
12689
|
var createPostgresAgentDelegationStore = (db) => ({
|
|
11415
12690
|
findActiveDelegation: async ({
|
|
@@ -11450,6 +12725,41 @@ var createPostgresAgentDelegationStore = (db) => ({
|
|
|
11450
12725
|
});
|
|
11451
12726
|
}
|
|
11452
12727
|
});
|
|
12728
|
+
var createPostgresAgentIdentityRegistrationStore = (db) => ({
|
|
12729
|
+
create: async (registration) => {
|
|
12730
|
+
const rows = await db.insert(agentIdentityRegistrationsTable).values(identityRegistrationValues(registration)).onConflictDoNothing().returning({ id: agentIdentityRegistrationsTable.registration_id });
|
|
12731
|
+
return rows.length === 1;
|
|
12732
|
+
},
|
|
12733
|
+
findByAgentId: async (agentId) => {
|
|
12734
|
+
const [row] = await db.select().from(agentIdentityRegistrationsTable).where(eq(agentIdentityRegistrationsTable.agent_id, agentId)).limit(1);
|
|
12735
|
+
return row === undefined ? undefined : toIdentityRegistration(row);
|
|
12736
|
+
},
|
|
12737
|
+
findByAttemptTokenHash: async (attemptTokenHash) => {
|
|
12738
|
+
const [row] = await db.select().from(agentIdentityRegistrationsTable).where(eq(agentIdentityRegistrationsTable.claim_attempt_token_hash, attemptTokenHash)).limit(1);
|
|
12739
|
+
return row === undefined ? undefined : toIdentityRegistration(row);
|
|
12740
|
+
},
|
|
12741
|
+
findByClaimTokenHash: async (claimTokenHash) => {
|
|
12742
|
+
const [row] = await db.select().from(agentIdentityRegistrationsTable).where(eq(agentIdentityRegistrationsTable.claim_token_hash, claimTokenHash)).limit(1);
|
|
12743
|
+
return row === undefined ? undefined : toIdentityRegistration(row);
|
|
12744
|
+
},
|
|
12745
|
+
findByRegistrationId: async (registrationId) => {
|
|
12746
|
+
const [row] = await db.select().from(agentIdentityRegistrationsTable).where(eq(agentIdentityRegistrationsTable.registration_id, registrationId)).limit(1);
|
|
12747
|
+
return row === undefined ? undefined : toIdentityRegistration(row);
|
|
12748
|
+
},
|
|
12749
|
+
findByUpstreamIdentity: async ({ clientId, issuer, subject }) => {
|
|
12750
|
+
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);
|
|
12751
|
+
return row === undefined ? undefined : toIdentityRegistration(row);
|
|
12752
|
+
},
|
|
12753
|
+
replace: async (registration, expectedVersion) => {
|
|
12754
|
+
const next = {
|
|
12755
|
+
...registration,
|
|
12756
|
+
version: expectedVersion + 1
|
|
12757
|
+
};
|
|
12758
|
+
const values = identityRegistrationValues(next);
|
|
12759
|
+
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 });
|
|
12760
|
+
return rows.length === 1;
|
|
12761
|
+
}
|
|
12762
|
+
});
|
|
11453
12763
|
var createPostgresAgentRegistrationStore = (db) => ({
|
|
11454
12764
|
findByAgentId: async (agentId) => {
|
|
11455
12765
|
const [row] = await db.select().from(agentRegistrationsTable).where(eq(agentRegistrationsTable.agent_id, agentId)).limit(1);
|
|
@@ -11482,24 +12792,46 @@ var createPostgresAgentRegistrationStore = (db) => ({
|
|
|
11482
12792
|
});
|
|
11483
12793
|
export {
|
|
11484
12794
|
validateClientIdMetadataDocument,
|
|
12795
|
+
startAgentRegistration,
|
|
12796
|
+
revokeAgentIdentityRegistration,
|
|
11485
12797
|
resolveAgentPrincipal,
|
|
12798
|
+
issueAgentServiceAssertion,
|
|
12799
|
+
issueAgentIdentityAssertion,
|
|
12800
|
+
handleAgentTokenGrant,
|
|
12801
|
+
generateAgentRegistrationGuide,
|
|
12802
|
+
discoverAgentRegistration,
|
|
11486
12803
|
createPostgresAgentRegistrationStore,
|
|
12804
|
+
createPostgresAgentIdentityRegistrationStore,
|
|
11487
12805
|
createPostgresAgentDelegationStore,
|
|
11488
12806
|
createOidcAgentCredentialVerifier,
|
|
11489
12807
|
createNeonAgentRegistrationStore,
|
|
12808
|
+
createNeonAgentIdentityRegistrationStore,
|
|
11490
12809
|
createNeonAgentDelegationStore,
|
|
11491
12810
|
createInMemoryAgentRegistrationStore,
|
|
12811
|
+
createInMemoryAgentIdentityRegistrationStore,
|
|
12812
|
+
createInMemoryAgentIdentityAssertionJtiStore,
|
|
11492
12813
|
createInMemoryAgentDelegationStore,
|
|
11493
12814
|
createClientIdMetadataResolver,
|
|
12815
|
+
createAgentRegistrationCredentialVerifier,
|
|
12816
|
+
createAgentRegistrationClient,
|
|
12817
|
+
createAgentIdentityAssertionVerifier,
|
|
12818
|
+
completeAgentClaim,
|
|
11494
12819
|
clientIdMetadataToOAuthClient,
|
|
12820
|
+
beginAgentClaim,
|
|
11495
12821
|
agentRegistrationsTable,
|
|
12822
|
+
agentRegistrationEndpoints,
|
|
12823
|
+
agentRegistrationDiscoveryMetadata,
|
|
11496
12824
|
agentProtectedResourceMetadata,
|
|
12825
|
+
agentIdentityRegistrationsTable,
|
|
11497
12826
|
agentHasScopes,
|
|
11498
12827
|
agentDelegationsTable,
|
|
11499
12828
|
agentAuthPlugin,
|
|
11500
12829
|
agentAuthChallenge,
|
|
11501
|
-
DEFAULT_AGENT_RESOURCE_METADATA_ROUTE
|
|
12830
|
+
DEFAULT_AGENT_RESOURCE_METADATA_ROUTE,
|
|
12831
|
+
AGENT_IDENTITY_ASSERTION_TYPE,
|
|
12832
|
+
AGENT_IDENTITY_ASSERTION_GRANT_TYPE,
|
|
12833
|
+
AGENT_CLAIM_GRANT_TYPE
|
|
11502
12834
|
};
|
|
11503
12835
|
|
|
11504
|
-
//# debugId=
|
|
12836
|
+
//# debugId=05CB90C7C290284064756E2164756E21
|
|
11505
12837
|
//# sourceMappingURL=index.js.map
|