@pidgeon-ai/issuer 0.2.0 → 0.4.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.
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/directory.ts
4
+ function onePerAddress(agents) {
5
+ const count = /* @__PURE__ */ new Map();
6
+ for (const agent of agents) count.set(agent.address, (count.get(agent.address) ?? 0) + 1);
7
+ return agents.filter((agent) => count.get(agent.address) === 1);
8
+ }
4
9
  function staticDirectory(agents) {
5
10
  const list = agents.map((agent) => ({ ...agent, address: agent.address.toLowerCase() }));
6
11
  return {
@@ -25,6 +30,104 @@ function cachedDirectory(inner, seconds = 60, now = () => Date.now()) {
25
30
  };
26
31
  }
27
32
 
33
+ // src/rate-limit.ts
34
+ async function withBackoff(attempt, now = () => Date.now(), sleep = (ms) => new Promise((done) => setTimeout(done, ms))) {
35
+ for (let tries = 0; ; tries++) {
36
+ const response = await attempt();
37
+ if (response.status !== 429 || tries >= 2) return response;
38
+ const reset = Number(
39
+ response.headers.get("x-ratelimit-reset") ?? response.headers.get("x-rate-limit-reset")
40
+ );
41
+ const retryAfter = Number(response.headers.get("retry-after"));
42
+ const wait = Number.isFinite(reset) && reset > 0 ? reset * 1e3 - now() : Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1e3 : 1e3;
43
+ await sleep(Math.min(2e3, Math.max(250, wait)));
44
+ }
45
+ }
46
+
47
+ // src/auth0.ts
48
+ var PER_PAGE = 100;
49
+ function auth0Directory(options) {
50
+ const base = `https://${options.tenant.replace(/^https:\/\//, "").replace(/\/$/, "")}`;
51
+ const domain = options.domain.toLowerCase();
52
+ const fetcher = options.fetch ?? fetch;
53
+ const now = options.now ?? (() => Date.now());
54
+ let token = null;
55
+ async function accessToken() {
56
+ if (token && token.until > now()) return token.value;
57
+ const response = await fetcher(`${base}/oauth/token`, {
58
+ method: "POST",
59
+ headers: { "content-type": "application/x-www-form-urlencoded" },
60
+ body: new URLSearchParams({
61
+ grant_type: "client_credentials",
62
+ client_id: options.clientId,
63
+ client_secret: options.clientSecret,
64
+ audience: `${base}/api/v2/`
65
+ })
66
+ });
67
+ const body = await response.json().catch(() => ({}));
68
+ if (!response.ok || !body.access_token) {
69
+ throw new Error(
70
+ `Auth0 refused the issuer's application ${options.clientId} (${response.status}).`
71
+ );
72
+ }
73
+ token = { value: body.access_token, until: now() + ((body.expires_in ?? 120) - 60) * 1e3 };
74
+ return token.value;
75
+ }
76
+ async function management(path) {
77
+ const bearer = await accessToken();
78
+ const response = await withBackoff(
79
+ () => fetcher(`${base}/api/v2${path}`, {
80
+ headers: { authorization: `Bearer ${bearer}`, accept: "application/json" }
81
+ }),
82
+ now
83
+ );
84
+ if (response.status === 404) return null;
85
+ if (!response.ok) throw new Error(`Auth0 answered ${response.status} for ${path}.`);
86
+ return await response.json();
87
+ }
88
+ function agentOf(client) {
89
+ if (client.app_type !== "non_interactive") return null;
90
+ const metadata = client.client_metadata ?? {};
91
+ if (metadata.pidgeon_disabled?.trim().toLowerCase() === "true") return null;
92
+ const key = metadata.pidgeon_key?.trim();
93
+ if (!key || Buffer.from(key, "base64url").length !== 32) return null;
94
+ const address = metadata.pidgeon_address?.trim().toLowerCase();
95
+ if (!address?.endsWith(`@${domain}`)) return null;
96
+ return {
97
+ sub: client.client_id,
98
+ address,
99
+ kind: metadata.pidgeon_kind?.trim() === "service" ? "service" : "agent",
100
+ key,
101
+ groups: (metadata.pidgeon_groups ?? "").split(",").map((group) => group.trim()).filter(Boolean),
102
+ // An age Auth0 does not give is treated as new: the card says `new`, never older than it is.
103
+ createdAt: new Date(client.created_at ? Date.parse(client.created_at) : now()).toISOString()
104
+ };
105
+ }
106
+ async function all() {
107
+ const agents = [];
108
+ for (let page2 = 0; ; page2++) {
109
+ const body = await management(
110
+ `/clients?app_type=non_interactive&per_page=${PER_PAGE}&page=${page2}&include_totals=true`
111
+ );
112
+ const clients = body?.clients ?? [];
113
+ for (const client of clients) {
114
+ const agent = agentOf(client);
115
+ if (agent) agents.push(agent);
116
+ }
117
+ if (clients.length < PER_PAGE || (page2 + 1) * PER_PAGE >= (body?.total ?? 0)) break;
118
+ }
119
+ return agents;
120
+ }
121
+ return {
122
+ agents: async () => onePerAddress(await all()),
123
+ async agent(sub) {
124
+ const client = await management(`/clients/${encodeURIComponent(sub)}`);
125
+ const agent = client ? agentOf(client) : null;
126
+ return agent?.sub === sub ? agent : null;
127
+ }
128
+ };
129
+ }
130
+
28
131
  // src/keycloak.ts
29
132
  var SERVICE_ACCOUNT_PREFIX = "service-account-";
30
133
  var ATTRIBUTES = { key: "pidgeon.key", address: "pidgeon.address", kind: "pidgeon.kind" };
@@ -106,7 +209,7 @@ function keycloakDirectory(options) {
106
209
  const clients = await admin("/clients?first=0&max=1000") ?? [];
107
210
  const candidates = clients.filter((client) => keyOf(client) !== null);
108
211
  const agents = await Promise.all(candidates.map(agentOf));
109
- return agents.filter((agent) => agent !== null);
212
+ return onePerAddress(agents.filter((agent) => agent !== null));
110
213
  },
111
214
  /**
112
215
  * The user's record does not name its client (only a realm export
@@ -128,229 +231,129 @@ function keycloakDirectory(options) {
128
231
  };
129
232
  }
130
233
 
131
- // src/person.ts
132
- import {
133
- createCipheriv,
134
- createDecipheriv,
135
- createHash,
136
- createPublicKey,
137
- randomBytes,
138
- timingSafeEqual,
139
- verify
140
- } from "crypto";
141
- var MIN_SECRET = 32;
142
- var FLOW_COOKIE = "pidgeon_issuer_flow";
143
- var CODE_SECONDS = 60;
144
- var FLOW_SECONDS = 600;
145
- var SKEW = 60;
146
- var UPSTREAM_ALGS = ["RS256", "ES256", "EdDSA"];
147
- function checkSecret(secret) {
148
- if (!secret || secret.length < MIN_SECRET) {
149
- throw new Error(
150
- `A person's sign-in needs PIDGEON_ISSUER_SECRET, ${MIN_SECRET} characters or more. It seals the sign-in in progress and the code; there is no default.`
234
+ // src/okta.ts
235
+ import { createPrivateKey, randomUUID, sign } from "crypto";
236
+ var OKTA_SCOPE = "okta.apps.read";
237
+ function oktaDirectory(options) {
238
+ const org = options.org.replace(/\/$/, "");
239
+ const domain = options.domain.toLowerCase();
240
+ const fetcher = options.fetch ?? fetch;
241
+ const now = options.now ?? (() => Date.now());
242
+ const privateKey = typeof options.privateKey === "string" ? createPrivateKey(options.privateKey) : options.privateKey;
243
+ const alg = privateKey.asymmetricKeyType === "rsa" ? "RS256" : privateKey.asymmetricKeyType === "ec" && privateKey.asymmetricKeyDetails?.namedCurve === "prime256v1" ? "ES256" : null;
244
+ if (!alg) throw new Error("Okta's client assertion is signed with an RSA or P-256 key.");
245
+ let token = null;
246
+ function assertion(audience) {
247
+ const at = Math.floor(now() / 1e3);
248
+ const b64 = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
249
+ const input = `${b64({ alg, typ: "JWT", ...options.keyId ? { kid: options.keyId } : {} })}.${b64(
250
+ {
251
+ iss: options.clientId,
252
+ sub: options.clientId,
253
+ aud: audience,
254
+ iat: at,
255
+ exp: at + 300,
256
+ jti: randomUUID()
257
+ }
258
+ )}`;
259
+ const signature = sign(
260
+ "sha256",
261
+ Buffer.from(input),
262
+ alg === "ES256" ? { key: privateKey, dsaEncoding: "ieee-p1363" } : privateKey
151
263
  );
264
+ return `${input}.${signature.toString("base64url")}`;
152
265
  }
153
- return secret;
154
- }
155
- var b64url = (bytes) => Buffer.from(bytes).toString("base64url");
156
- function sealKey(secret, purpose) {
157
- return createHash("sha256").update(`pidgeon issuer seal\0${purpose}\0${secret}`).digest();
158
- }
159
- function seal(secret, purpose, value) {
160
- const iv = randomBytes(12);
161
- const cipher = createCipheriv("aes-256-gcm", sealKey(secret, purpose), iv);
162
- cipher.setAAD(Buffer.from(purpose));
163
- const body = Buffer.concat([cipher.update(JSON.stringify(value), "utf8"), cipher.final()]);
164
- return b64url(Buffer.concat([iv, body, cipher.getAuthTag()]));
165
- }
166
- function unseal(secret, purpose, token) {
167
- if (!token) return null;
168
- try {
169
- const raw = Buffer.from(token, "base64url");
170
- if (raw.length < 29) return null;
171
- const decipher = createDecipheriv("aes-256-gcm", sealKey(secret, purpose), raw.subarray(0, 12));
172
- decipher.setAAD(Buffer.from(purpose));
173
- decipher.setAuthTag(raw.subarray(raw.length - 16));
174
- const text2 = Buffer.concat([
175
- decipher.update(raw.subarray(12, raw.length - 16)),
176
- decipher.final()
177
- ]).toString("utf8");
178
- return JSON.parse(text2);
179
- } catch {
180
- return null;
181
- }
182
- }
183
- function pkceChallenge(verifier) {
184
- return b64url(createHash("sha256").update(verifier).digest());
185
- }
186
- function pkceHolds(verifier, challenge) {
187
- const a = Buffer.from(pkceChallenge(verifier));
188
- const b = Buffer.from(challenge);
189
- return a.length === b.length && timingSafeEqual(a, b);
190
- }
191
- function cookieOf(request, name) {
192
- for (const part of (request.headers.get("cookie") ?? "").split(";")) {
193
- const eq = part.indexOf("=");
194
- if (eq > 0 && part.slice(0, eq).trim() === name) {
195
- return decodeURIComponent(part.slice(eq + 1).trim());
196
- }
197
- }
198
- return null;
199
- }
200
- var ProviderRefused = class extends Error {
201
- };
202
- function provider(options, fetcher, now) {
203
- const issuer = options.issuer.replace(/\/$/, "");
204
- let configuration = null;
205
- let keys = null;
206
- function discover() {
207
- if (!configuration || configuration.at <= now() - 36e5) {
208
- const url = options.discoveryUrl ?? `${issuer}/.well-known/openid-configuration`;
209
- const value = (async () => {
210
- const response = await fetcher(url, { headers: { accept: "application/json" } });
211
- if (!response.ok) throw new Error(`${url} answered ${response.status}.`);
212
- const body = await response.json();
213
- if (body.issuer?.replace(/\/$/, "") !== issuer) {
214
- throw new Error(`${url} names the issuer ${body.issuer}, not ${issuer}.`);
215
- }
216
- return body;
217
- })();
218
- configuration = { at: now(), value };
219
- value.catch(() => {
220
- if (configuration?.value === value) configuration = null;
221
- });
222
- }
223
- return configuration.value;
224
- }
225
- async function key(kid) {
226
- const find = () => keys?.value.find((one) => (kid ? one.kid === kid : true) && one.use !== "enc") ?? null;
227
- const fresh = keys && keys.at > now() - 3e5;
228
- if (fresh && find()) return find();
229
- if (keys && keys.at > now() - 1e4) return null;
230
- const response = await fetcher((await discover()).jwks_uri);
231
- const body = await response.json().catch(() => ({}));
232
- keys = { at: now(), value: Array.isArray(body.keys) ? body.keys : [] };
233
- return find();
234
- }
235
- async function authorizeUrl(input) {
236
- const url = new URL((await discover()).authorization_endpoint);
237
- url.search = new URLSearchParams({
238
- response_type: "code",
239
- client_id: options.clientId,
240
- redirect_uri: input.redirectUri,
241
- scope: "openid email",
242
- state: input.state,
243
- nonce: input.nonce,
244
- code_challenge: pkceChallenge(input.verifier),
245
- code_challenge_method: "S256"
246
- }).toString();
247
- return url.toString();
248
- }
249
- async function redeem(input) {
250
- const form = new URLSearchParams({
251
- grant_type: "authorization_code",
252
- code: input.code,
253
- redirect_uri: input.redirectUri,
254
- code_verifier: input.verifier
255
- });
256
- const headers = {
257
- "content-type": "application/x-www-form-urlencoded",
258
- accept: "application/json"
259
- };
260
- if (options.tokenAuth === "client_secret_post") {
261
- form.set("client_id", options.clientId);
262
- form.set("client_secret", options.clientSecret);
263
- } else {
264
- const pair = `${encodeURIComponent(options.clientId)}:${encodeURIComponent(options.clientSecret)}`;
265
- headers.authorization = `Basic ${Buffer.from(pair).toString("base64")}`;
266
- }
267
- const response = await fetcher((await discover()).token_endpoint, {
266
+ async function accessToken() {
267
+ if (token && token.until > now()) return token.value;
268
+ const endpoint = `${org}/oauth2/v1/token`;
269
+ const response = await fetcher(endpoint, {
268
270
  method: "POST",
269
- headers,
270
- body: form
271
+ headers: {
272
+ "content-type": "application/x-www-form-urlencoded",
273
+ accept: "application/json"
274
+ },
275
+ body: new URLSearchParams({
276
+ grant_type: "client_credentials",
277
+ scope: OKTA_SCOPE,
278
+ client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
279
+ client_assertion: assertion(endpoint)
280
+ })
271
281
  });
272
282
  const body = await response.json().catch(() => ({}));
273
- if (!response.ok || typeof body.id_token !== "string") {
274
- throw new ProviderRefused(
275
- `The identity provider did not sign the person in (${body.error ?? response.status}).`
283
+ if (!response.ok || !body.access_token) {
284
+ throw new Error(
285
+ `Okta refused the issuer's app ${options.clientId} (${response.status}${body.error_description ? `: ${body.error_description}` : ""}).`
276
286
  );
277
287
  }
278
- return check(body.id_token, input.nonce);
288
+ token = { value: body.access_token, until: now() + ((body.expires_in ?? 120) - 60) * 1e3 };
289
+ return token.value;
279
290
  }
280
- async function check(token, nonce) {
281
- const parts = token.split(".");
282
- if (parts.length !== 3) throw new ProviderRefused("The provider sent no ID token.");
283
- let header;
284
- let claims;
285
- try {
286
- header = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8"));
287
- claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
288
- } catch {
289
- throw new ProviderRefused("The provider sent no ID token.");
290
- }
291
- if (!header.alg || !UPSTREAM_ALGS.includes(header.alg)) {
292
- throw new ProviderRefused(`The provider's ID token is signed with ${header.alg}.`);
293
- }
294
- const jwk = await key(header.kid);
295
- if (!jwk || !signatureHolds(header.alg, jwk, `${parts[0]}.${parts[1]}`, parts[2])) {
296
- throw new ProviderRefused("The provider's ID token is not signed by its keys.");
297
- }
298
- const at = Math.floor(now() / 1e3);
299
- if (typeof claims.iss !== "string" || claims.iss.replace(/\/$/, "") !== issuer) {
300
- throw new ProviderRefused("The provider's ID token is from another issuer.");
301
- }
302
- const aud = claims.aud;
303
- const audiences = Array.isArray(aud) ? aud : [aud];
304
- if (!audiences.includes(options.clientId)) {
305
- throw new ProviderRefused("The provider's ID token is for another client.");
306
- }
307
- if (audiences.length > 1 && claims.azp !== options.clientId) {
308
- throw new ProviderRefused("The provider's ID token is for another client.");
309
- }
310
- if (typeof claims.exp !== "number" || claims.exp + SKEW <= at) {
311
- throw new ProviderRefused("The provider's ID token has expired.");
312
- }
313
- if (typeof claims.iat === "number" && claims.iat > at + SKEW) {
314
- throw new ProviderRefused("The provider's ID token is from the future.");
315
- }
316
- if (claims.nonce !== nonce) {
317
- throw new ProviderRefused("The provider's ID token is for another sign-in.");
318
- }
319
- if (typeof claims.sub !== "string" || !claims.sub) {
320
- throw new ProviderRefused("The provider's ID token names nobody.");
321
- }
322
- if (claims.email_verified !== true || typeof claims.email !== "string") {
323
- throw new ProviderRefused("The identity provider has not verified this person\u2019s address.");
324
- }
291
+ async function api(url) {
292
+ const bearer = await accessToken();
293
+ const response = await withBackoff(
294
+ () => fetcher(url, {
295
+ headers: { authorization: `Bearer ${bearer}`, accept: "application/json" }
296
+ }),
297
+ now
298
+ );
299
+ if (!response.ok && response.status !== 404) {
300
+ throw new Error(`Okta answered ${response.status} for ${new URL(url).pathname}.`);
301
+ }
302
+ return response;
303
+ }
304
+ function agentOf(app) {
305
+ if (app.status !== "ACTIVE") return null;
306
+ if (app.settings?.oauthClient?.application_type !== "service") return null;
307
+ const pidgeon = app.profile?.pidgeon;
308
+ const key = typeof pidgeon?.key === "string" ? pidgeon.key.trim() : "";
309
+ if (!key || Buffer.from(key, "base64url").length !== 32) return null;
310
+ const address = typeof pidgeon?.address === "string" ? pidgeon.address.trim().toLowerCase() : "";
311
+ if (!address.endsWith(`@${domain}`)) return null;
325
312
  return {
326
- sub: claims.sub,
327
- email: claims.email.trim().toLowerCase(),
328
- authTime: typeof claims.auth_time === "number" ? claims.auth_time : at
313
+ sub: app.id,
314
+ address,
315
+ kind: pidgeon?.kind === "service" ? "service" : "agent",
316
+ key,
317
+ groups: Array.isArray(pidgeon?.groups) ? pidgeon.groups.filter((group) => typeof group === "string") : [],
318
+ createdAt: new Date(app.created ? Date.parse(app.created) : now()).toISOString()
329
319
  };
330
320
  }
331
- return { authorizeUrl, redeem };
332
- }
333
- function signatureHolds(alg, jwk, input, signature) {
334
- try {
335
- const key = createPublicKey({ key: jwk, format: "jwk" });
336
- const data = Buffer.from(input);
337
- const sig = Buffer.from(signature, "base64url");
338
- if (alg === "EdDSA") return jwk.kty === "OKP" && verify(null, data, key, sig);
339
- if (alg === "ES256") {
340
- return jwk.kty === "EC" && jwk.crv === "P-256" && verify("sha256", data, { key, dsaEncoding: "ieee-p1363" }, sig);
321
+ function nextPage(response) {
322
+ for (const part of (response.headers.get("link") ?? "").split(",")) {
323
+ const match = /<([^>]+)>\s*;\s*rel="next"/.exec(part);
324
+ if (match && new URL(match[1]).origin === new URL(org).origin) return match[1];
325
+ }
326
+ return null;
327
+ }
328
+ return {
329
+ async agents() {
330
+ const agents = [];
331
+ let url = `${org}/api/v1/apps?filter=${encodeURIComponent('status eq "ACTIVE"')}&limit=200`;
332
+ for (let pages = 0; url && pages < 100; pages++) {
333
+ const response = await api(url);
334
+ for (const app of await response.json() ?? []) {
335
+ const agent = agentOf(app);
336
+ if (agent) agents.push(agent);
337
+ }
338
+ url = nextPage(response);
339
+ }
340
+ return onePerAddress(agents);
341
+ },
342
+ async agent(sub) {
343
+ const response = await api(`${org}/api/v1/apps/${encodeURIComponent(sub)}`);
344
+ if (response.status === 404) return null;
345
+ const agent = agentOf(await response.json());
346
+ return agent?.sub === sub ? agent : null;
341
347
  }
342
- return jwk.kty === "RSA" && verify("sha256", data, key, sig);
343
- } catch {
344
- return false;
345
- }
348
+ };
346
349
  }
347
350
 
348
- // src/signer.ts
349
- import { readFile } from "fs/promises";
351
+ // src/kms.ts
352
+ import { createPublicKey, verify as verify2 } from "crypto";
350
353
 
351
354
  // ../id/dist/chunk-QDQDZSU7.js
352
- import { createHash as createHash2 } from "crypto";
353
- import { createHash as createHash22, randomBytes as randomBytes2 } from "crypto";
355
+ import { createHash } from "crypto";
356
+ import { createHash as createHash2, randomBytes } from "crypto";
354
357
  var __defProp = Object.defineProperty;
355
358
  var __export = (target, all) => {
356
359
  for (var name in all)
@@ -365,8 +368,8 @@ __export(post_core_wasm_exports, {
365
368
  policyDecision: () => policyDecision,
366
369
  policyMaxAge: () => policyMaxAge,
367
370
  publicKey: () => publicKey,
368
- sign: () => sign,
369
- verify: () => verify2
371
+ sign: () => sign2,
372
+ verify: () => verify
370
373
  });
371
374
  function generateSeed() {
372
375
  try {
@@ -430,7 +433,7 @@ function publicKey(seed) {
430
433
  wasm.__wbindgen_add_to_stack_pointer(16);
431
434
  }
432
435
  }
433
- function sign(seed, message) {
436
+ function sign2(seed, message) {
434
437
  try {
435
438
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
436
439
  const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_export3);
@@ -452,7 +455,7 @@ function sign(seed, message) {
452
455
  wasm.__wbindgen_add_to_stack_pointer(16);
453
456
  }
454
457
  }
455
- function verify2(public_key, message, signature) {
458
+ function verify(public_key, message, signature) {
456
459
  try {
457
460
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
458
461
  const ptr0 = passArray8ToWasm0(public_key, wasm.__wbindgen_export3);
@@ -697,9 +700,9 @@ function wasm2() {
697
700
  }
698
701
  return post_core_wasm_exports;
699
702
  }
700
- var b64url2 = (bytes) => Buffer.from(bytes).toString("base64url");
703
+ var b64url = (bytes) => Buffer.from(bytes).toString("base64url");
701
704
  function jwkThumbprint(publicKey2) {
702
- return createHash2("sha256").update(`{"crv":"Ed25519","kty":"OKP","x":"${b64url2(publicKey2)}"}`).digest("base64url");
705
+ return createHash("sha256").update(`{"crv":"Ed25519","kty":"OKP","x":"${b64url(publicKey2)}"}`).digest("base64url");
703
706
  }
704
707
  function verifyBytes(publicKey2, message, signature) {
705
708
  try {
@@ -732,7 +735,7 @@ function bytesOf(body) {
732
735
  return typeof body === "string" ? new TextEncoder().encode(body) : body;
733
736
  }
734
737
  function contentDigest(body) {
735
- return `sha-256=:${createHash22("sha256").update(bytesOf(body)).digest("base64")}:`;
738
+ return `sha-256=:${createHash2("sha256").update(bytesOf(body)).digest("base64")}:`;
736
739
  }
737
740
  async function verifyRequest(input, options) {
738
741
  const headers = /* @__PURE__ */ new Map();
@@ -841,7 +844,307 @@ function splitTopLevel(value) {
841
844
  return out;
842
845
  }
843
846
 
847
+ // src/kms.ts
848
+ var KMS_KEY_SPEC = "ECC_NIST_EDWARDS25519";
849
+ var KMS_SIGNING_ALGORITHM = "ED25519_SHA_512";
850
+ async function sdkClient(region) {
851
+ let sdk;
852
+ try {
853
+ sdk = await import("@aws-sdk/client-kms");
854
+ } catch {
855
+ throw new Error(
856
+ "A KMS key needs @aws-sdk/client-kms: `npm install @aws-sdk/client-kms` beside the issuer."
857
+ );
858
+ }
859
+ const client = new sdk.KMSClient(region ? { region } : {});
860
+ return {
861
+ getPublicKey: (input) => client.send(new sdk.GetPublicKeyCommand(input)),
862
+ sign: (input) => client.send(
863
+ new sdk.SignCommand({
864
+ ...input,
865
+ SigningAlgorithm: input.SigningAlgorithm
866
+ })
867
+ )
868
+ };
869
+ }
870
+ async function readKey(kms, keyId) {
871
+ let key;
872
+ try {
873
+ key = await kms.getPublicKey({ KeyId: keyId });
874
+ } catch (error) {
875
+ throw new Error(
876
+ `KMS would not give the issuer ${keyId}'s public key (${error instanceof Error ? error.message : error}). Its role needs kms:GetPublicKey and kms:Sign on that key.`
877
+ );
878
+ }
879
+ if (key.KeySpec !== KMS_KEY_SPEC) {
880
+ throw new Error(`${keyId} is a ${key.KeySpec} key. The issuer signs with ${KMS_KEY_SPEC}.`);
881
+ }
882
+ if (key.KeyUsage && key.KeyUsage !== "SIGN_VERIFY") {
883
+ throw new Error(`${keyId} is not a signing key.`);
884
+ }
885
+ if (!key.SigningAlgorithms?.includes(KMS_SIGNING_ALGORITHM)) {
886
+ throw new Error(`${keyId} does not sign with ${KMS_SIGNING_ALGORITHM}.`);
887
+ }
888
+ if (!key.KeyId || !key.PublicKey) throw new Error(`KMS gave no public key for ${keyId}.`);
889
+ const publicKey2 = createPublicKey({
890
+ key: Buffer.from(key.PublicKey),
891
+ format: "der",
892
+ type: "spki"
893
+ });
894
+ const x = publicKey2.export({ format: "jwk" }).x;
895
+ const raw = new Uint8Array(Buffer.from(x ?? "", "base64url"));
896
+ if (raw.length !== 32) throw new Error(`${keyId}'s public key is not an Ed25519 key.`);
897
+ return { arn: key.KeyId, publicKey: publicKey2, raw };
898
+ }
899
+ async function kmsSigner(options) {
900
+ const kms = options.kms ?? await sdkClient(options.region);
901
+ const { arn, publicKey: publicKey2, raw } = await readKey(kms, options.keyId);
902
+ return {
903
+ kid: jwkThumbprint(raw),
904
+ publicKey: raw,
905
+ keyArn: arn,
906
+ async sign(message) {
907
+ const signed = await kms.sign({
908
+ KeyId: arn,
909
+ Message: message,
910
+ MessageType: "RAW",
911
+ SigningAlgorithm: KMS_SIGNING_ALGORITHM
912
+ });
913
+ const signature = signed.Signature ? new Uint8Array(signed.Signature) : null;
914
+ if (!signature || !verify2(null, message, publicKey2, signature)) {
915
+ throw new Error(
916
+ `KMS answered for ${arn} with a signature its public key does not verify. Nothing was signed.`
917
+ );
918
+ }
919
+ return signature;
920
+ }
921
+ };
922
+ }
923
+ async function kmsRetiredKey(options) {
924
+ const kms = options.kms ?? await sdkClient(options.region);
925
+ const { raw } = await readKey(kms, options.keyId);
926
+ return { kid: jwkThumbprint(raw), publicKey: raw, notAfter: options.notAfter };
927
+ }
928
+
929
+ // src/person.ts
930
+ import {
931
+ createCipheriv,
932
+ createDecipheriv,
933
+ createHash as createHash3,
934
+ createPublicKey as createPublicKey2,
935
+ randomBytes as randomBytes2,
936
+ timingSafeEqual,
937
+ verify as verify3
938
+ } from "crypto";
939
+ var MIN_SECRET = 32;
940
+ var FLOW_COOKIE = "pidgeon_issuer_flow";
941
+ var CODE_SECONDS = 60;
942
+ var FLOW_SECONDS = 600;
943
+ var SKEW = 60;
944
+ var UPSTREAM_ALGS = ["RS256", "ES256", "EdDSA"];
945
+ function checkSecret(secret) {
946
+ if (!secret || secret.length < MIN_SECRET) {
947
+ throw new Error(
948
+ `A person's sign-in needs PIDGEON_ISSUER_SECRET, ${MIN_SECRET} characters or more. It seals the sign-in in progress and the code; there is no default.`
949
+ );
950
+ }
951
+ return secret;
952
+ }
953
+ var b64url2 = (bytes) => Buffer.from(bytes).toString("base64url");
954
+ function sealKey(secret, purpose) {
955
+ return createHash3("sha256").update(`pidgeon issuer seal\0${purpose}\0${secret}`).digest();
956
+ }
957
+ function seal(secret, purpose, value) {
958
+ const iv = randomBytes2(12);
959
+ const cipher = createCipheriv("aes-256-gcm", sealKey(secret, purpose), iv);
960
+ cipher.setAAD(Buffer.from(purpose));
961
+ const body = Buffer.concat([cipher.update(JSON.stringify(value), "utf8"), cipher.final()]);
962
+ return b64url2(Buffer.concat([iv, body, cipher.getAuthTag()]));
963
+ }
964
+ function unseal(secret, purpose, token) {
965
+ if (!token) return null;
966
+ try {
967
+ const raw = Buffer.from(token, "base64url");
968
+ if (raw.length < 29) return null;
969
+ const decipher = createDecipheriv("aes-256-gcm", sealKey(secret, purpose), raw.subarray(0, 12));
970
+ decipher.setAAD(Buffer.from(purpose));
971
+ decipher.setAuthTag(raw.subarray(raw.length - 16));
972
+ const text2 = Buffer.concat([
973
+ decipher.update(raw.subarray(12, raw.length - 16)),
974
+ decipher.final()
975
+ ]).toString("utf8");
976
+ return JSON.parse(text2);
977
+ } catch {
978
+ return null;
979
+ }
980
+ }
981
+ function pkceChallenge(verifier) {
982
+ return b64url2(createHash3("sha256").update(verifier).digest());
983
+ }
984
+ function pkceHolds(verifier, challenge) {
985
+ const a = Buffer.from(pkceChallenge(verifier));
986
+ const b = Buffer.from(challenge);
987
+ return a.length === b.length && timingSafeEqual(a, b);
988
+ }
989
+ function cookieOf(request, name) {
990
+ for (const part of (request.headers.get("cookie") ?? "").split(";")) {
991
+ const eq = part.indexOf("=");
992
+ if (eq > 0 && part.slice(0, eq).trim() === name) {
993
+ return decodeURIComponent(part.slice(eq + 1).trim());
994
+ }
995
+ }
996
+ return null;
997
+ }
998
+ var ProviderRefused = class extends Error {
999
+ };
1000
+ function provider(options, fetcher, now) {
1001
+ const issuer = options.issuer.replace(/\/$/, "");
1002
+ let configuration = null;
1003
+ let keys = null;
1004
+ function discover() {
1005
+ if (!configuration || configuration.at <= now() - 36e5) {
1006
+ const url = options.discoveryUrl ?? `${issuer}/.well-known/openid-configuration`;
1007
+ const value = (async () => {
1008
+ const response = await fetcher(url, { headers: { accept: "application/json" } });
1009
+ if (!response.ok) throw new Error(`${url} answered ${response.status}.`);
1010
+ const body = await response.json();
1011
+ if (body.issuer?.replace(/\/$/, "") !== issuer) {
1012
+ throw new Error(`${url} names the issuer ${body.issuer}, not ${issuer}.`);
1013
+ }
1014
+ return body;
1015
+ })();
1016
+ configuration = { at: now(), value };
1017
+ value.catch(() => {
1018
+ if (configuration?.value === value) configuration = null;
1019
+ });
1020
+ }
1021
+ return configuration.value;
1022
+ }
1023
+ async function key(kid) {
1024
+ const find = () => keys?.value.find((one) => (kid ? one.kid === kid : true) && one.use !== "enc") ?? null;
1025
+ const fresh = keys && keys.at > now() - 3e5;
1026
+ if (fresh && find()) return find();
1027
+ if (keys && keys.at > now() - 1e4) return null;
1028
+ const response = await fetcher((await discover()).jwks_uri);
1029
+ const body = await response.json().catch(() => ({}));
1030
+ keys = { at: now(), value: Array.isArray(body.keys) ? body.keys : [] };
1031
+ return find();
1032
+ }
1033
+ async function authorizeUrl(input) {
1034
+ const url = new URL((await discover()).authorization_endpoint);
1035
+ url.search = new URLSearchParams({
1036
+ response_type: "code",
1037
+ client_id: options.clientId,
1038
+ redirect_uri: input.redirectUri,
1039
+ scope: "openid email",
1040
+ state: input.state,
1041
+ nonce: input.nonce,
1042
+ code_challenge: pkceChallenge(input.verifier),
1043
+ code_challenge_method: "S256"
1044
+ }).toString();
1045
+ return url.toString();
1046
+ }
1047
+ async function redeem(input) {
1048
+ const form = new URLSearchParams({
1049
+ grant_type: "authorization_code",
1050
+ code: input.code,
1051
+ redirect_uri: input.redirectUri,
1052
+ code_verifier: input.verifier
1053
+ });
1054
+ const headers = {
1055
+ "content-type": "application/x-www-form-urlencoded",
1056
+ accept: "application/json"
1057
+ };
1058
+ if (options.tokenAuth === "client_secret_post") {
1059
+ form.set("client_id", options.clientId);
1060
+ form.set("client_secret", options.clientSecret);
1061
+ } else {
1062
+ const pair = `${encodeURIComponent(options.clientId)}:${encodeURIComponent(options.clientSecret)}`;
1063
+ headers.authorization = `Basic ${Buffer.from(pair).toString("base64")}`;
1064
+ }
1065
+ const response = await fetcher((await discover()).token_endpoint, {
1066
+ method: "POST",
1067
+ headers,
1068
+ body: form
1069
+ });
1070
+ const body = await response.json().catch(() => ({}));
1071
+ if (!response.ok || typeof body.id_token !== "string") {
1072
+ throw new ProviderRefused(
1073
+ `The identity provider did not sign the person in (${body.error ?? response.status}).`
1074
+ );
1075
+ }
1076
+ return check(body.id_token, input.nonce);
1077
+ }
1078
+ async function check(token, nonce) {
1079
+ const parts = token.split(".");
1080
+ if (parts.length !== 3) throw new ProviderRefused("The provider sent no ID token.");
1081
+ let header;
1082
+ let claims;
1083
+ try {
1084
+ header = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8"));
1085
+ claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1086
+ } catch {
1087
+ throw new ProviderRefused("The provider sent no ID token.");
1088
+ }
1089
+ if (!header.alg || !UPSTREAM_ALGS.includes(header.alg)) {
1090
+ throw new ProviderRefused(`The provider's ID token is signed with ${header.alg}.`);
1091
+ }
1092
+ const jwk = await key(header.kid);
1093
+ if (!jwk || !signatureHolds(header.alg, jwk, `${parts[0]}.${parts[1]}`, parts[2])) {
1094
+ throw new ProviderRefused("The provider's ID token is not signed by its keys.");
1095
+ }
1096
+ const at = Math.floor(now() / 1e3);
1097
+ if (typeof claims.iss !== "string" || claims.iss.replace(/\/$/, "") !== issuer) {
1098
+ throw new ProviderRefused("The provider's ID token is from another issuer.");
1099
+ }
1100
+ const aud = claims.aud;
1101
+ const audiences = Array.isArray(aud) ? aud : [aud];
1102
+ if (!audiences.includes(options.clientId)) {
1103
+ throw new ProviderRefused("The provider's ID token is for another client.");
1104
+ }
1105
+ if (audiences.length > 1 && claims.azp !== options.clientId) {
1106
+ throw new ProviderRefused("The provider's ID token is for another client.");
1107
+ }
1108
+ if (typeof claims.exp !== "number" || claims.exp + SKEW <= at) {
1109
+ throw new ProviderRefused("The provider's ID token has expired.");
1110
+ }
1111
+ if (typeof claims.iat === "number" && claims.iat > at + SKEW) {
1112
+ throw new ProviderRefused("The provider's ID token is from the future.");
1113
+ }
1114
+ if (claims.nonce !== nonce) {
1115
+ throw new ProviderRefused("The provider's ID token is for another sign-in.");
1116
+ }
1117
+ if (typeof claims.sub !== "string" || !claims.sub) {
1118
+ throw new ProviderRefused("The provider's ID token names nobody.");
1119
+ }
1120
+ if (claims.email_verified !== true || typeof claims.email !== "string") {
1121
+ throw new ProviderRefused("The identity provider has not verified this person\u2019s address.");
1122
+ }
1123
+ return {
1124
+ sub: claims.sub,
1125
+ email: claims.email.trim().toLowerCase(),
1126
+ authTime: typeof claims.auth_time === "number" ? claims.auth_time : at
1127
+ };
1128
+ }
1129
+ return { authorizeUrl, redeem };
1130
+ }
1131
+ function signatureHolds(alg, jwk, input, signature) {
1132
+ try {
1133
+ const key = createPublicKey2({ key: jwk, format: "jwk" });
1134
+ const data = Buffer.from(input);
1135
+ const sig = Buffer.from(signature, "base64url");
1136
+ if (alg === "EdDSA") return jwk.kty === "OKP" && verify3(null, data, key, sig);
1137
+ if (alg === "ES256") {
1138
+ return jwk.kty === "EC" && jwk.crv === "P-256" && verify3("sha256", data, { key, dsaEncoding: "ieee-p1363" }, sig);
1139
+ }
1140
+ return jwk.kty === "RSA" && verify3("sha256", data, key, sig);
1141
+ } catch {
1142
+ return false;
1143
+ }
1144
+ }
1145
+
844
1146
  // src/signer.ts
1147
+ import { readFile } from "fs/promises";
845
1148
  async function fileSigner(path) {
846
1149
  let file;
847
1150
  try {
@@ -884,12 +1187,21 @@ async function loadConfig(path, env = process.env) {
884
1187
  throw new Error(`${path}: \`domain\` is the domain whose agents this issues for.`);
885
1188
  }
886
1189
  const here = (relative) => resolve(dirname(path), relative);
887
- const signer = env.PIDGEON_ISSUER_KEY ? seedSigner(env.PIDGEON_ISSUER_KEY, { domain: file.domain }) : file.key ? await fileSigner(here(file.key)) : null;
888
- if (!signer) throw new Error(`${path}: name a \`key\` file, or set PIDGEON_ISSUER_KEY.`);
1190
+ const signer = env.PIDGEON_ISSUER_KEY ? seedSigner(env.PIDGEON_ISSUER_KEY, { domain: file.domain }) : typeof file.key === "string" ? await fileSigner(here(file.key)) : file.key?.kms ? await kmsSigner({ keyId: file.key.kms, region: file.key.region }) : null;
1191
+ if (!signer) {
1192
+ throw new Error(`${path}: name a \`key\` file or a KMS key, or set PIDGEON_ISSUER_KEY.`);
1193
+ }
889
1194
  const retired = [];
890
1195
  for (const entry of file.retired ?? []) {
891
- const old = await fileSigner(here(entry.key));
892
- retired.push({ kid: old.kid, publicKey: old.publicKey, notAfter: new Date(entry.not_after) });
1196
+ const notAfter = new Date(entry.not_after);
1197
+ if (typeof entry.key === "string") {
1198
+ const old = await fileSigner(here(entry.key));
1199
+ retired.push({ kid: old.kid, publicKey: old.publicKey, notAfter });
1200
+ } else {
1201
+ retired.push(
1202
+ await kmsRetiredKey({ keyId: entry.key.kms, region: entry.key.region, notAfter })
1203
+ );
1204
+ }
893
1205
  }
894
1206
  let person;
895
1207
  if (file.person) {
@@ -946,6 +1258,32 @@ function directoryFrom(file, path, env) {
946
1258
  group: keycloak.group
947
1259
  });
948
1260
  }
1261
+ if ("auth0" in directory) {
1262
+ const { auth0 } = directory;
1263
+ const secretName = auth0.client_secret_env ?? "AUTH0_CLIENT_SECRET";
1264
+ const secret = env[secretName];
1265
+ if (!secret) throw new Error(`${path}: set ${secretName}, the issuer's Auth0 client secret.`);
1266
+ return auth0Directory({
1267
+ tenant: auth0.tenant,
1268
+ clientId: auth0.client_id,
1269
+ clientSecret: secret,
1270
+ domain: file.domain
1271
+ });
1272
+ }
1273
+ if ("okta" in directory) {
1274
+ const { okta } = directory;
1275
+ const keyName = okta.private_key_env ?? "OKTA_PRIVATE_KEY";
1276
+ const privateKey = env[keyName];
1277
+ if (!privateKey)
1278
+ throw new Error(`${path}: set ${keyName}, the issuer's Okta private key (PEM).`);
1279
+ return oktaDirectory({
1280
+ org: okta.org,
1281
+ clientId: okta.client_id,
1282
+ privateKey,
1283
+ keyId: okta.key_id,
1284
+ domain: file.domain
1285
+ });
1286
+ }
949
1287
  if ("static" in directory && Array.isArray(directory.static)) {
950
1288
  for (const agent of directory.static) {
951
1289
  if (!agent.address?.toLowerCase().endsWith(`@${file.domain.toLowerCase()}`)) {
@@ -954,11 +1292,13 @@ function directoryFrom(file, path, env) {
954
1292
  }
955
1293
  return staticDirectory(directory.static);
956
1294
  }
957
- throw new Error(`${path}: \`directory\` is { "keycloak": {...} } or { "static": [...] }.`);
1295
+ throw new Error(
1296
+ `${path}: \`directory\` is { "keycloak": {...} }, { "auth0": {...} }, { "okta": {...} } or { "static": [...] }.`
1297
+ );
958
1298
  }
959
1299
 
960
1300
  // src/issuer.ts
961
- import { createHash as createHash3, createPublicKey as createPublicKey2, randomBytes as randomBytes3, randomUUID, verify as verify3 } from "crypto";
1301
+ import { createHash as createHash4, createPublicKey as createPublicKey3, randomBytes as randomBytes3, randomUUID as randomUUID2, verify as verify4 } from "crypto";
962
1302
  var TOKEN_EXCHANGE2 = "urn:ietf:params:oauth:grant-type:token-exchange";
963
1303
  var TOKEN_TYPE_JWT = "urn:ietf:params:oauth:token-type:jwt";
964
1304
  var TOKEN_TYPE_ID = "urn:ietf:params:oauth:token-type:id_token";
@@ -1029,10 +1369,10 @@ function createIssuer(options) {
1029
1369
  if (header?.alg !== "EdDSA" || header?.typ !== typ) return null;
1030
1370
  const key = publishedIssuerKeys().find((candidate) => candidate.kid === header.kid);
1031
1371
  if (!key) return null;
1032
- const signed = verify3(
1372
+ const signed = verify4(
1033
1373
  null,
1034
1374
  Buffer.from(`${parts[0]}.${parts[1]}`),
1035
- createPublicKey2({
1375
+ createPublicKey3({
1036
1376
  key: { kty: "OKP", crv: "Ed25519", x: b64url3(key.publicKey) },
1037
1377
  format: "jwk"
1038
1378
  }),
@@ -1152,7 +1492,7 @@ function createIssuer(options) {
1152
1492
  };
1153
1493
  }
1154
1494
  function record() {
1155
- const id = createHash3("sha256").update(`post policy id\0self-hosted\0${domain}`).digest("hex").slice(0, 16);
1495
+ const id = createHash4("sha256").update(`post policy id\0self-hosted\0${domain}`).digest("hex").slice(0, 16);
1156
1496
  return `v=post0; id=${id}`;
1157
1497
  }
1158
1498
  const cardUrl = (address) => `${issuer}${PATHS.card}?address=${encodeURIComponent(address)}`;
@@ -1189,7 +1529,7 @@ function createIssuer(options) {
1189
1529
  const claims = cardClaims(agent);
1190
1530
  return { jws: await jws({ alg: "EdDSA", typ: "agent-card+jwt" }, claims), card: claims };
1191
1531
  }
1192
- const grantId = (agent, rule) => `grt_${createHash3("sha256").update(
1532
+ const grantId = (agent, rule) => `grt_${createHash4("sha256").update(
1193
1533
  `${agent.sub}\0${rule.group}\0${rule.scope.name}\0${rule.subject.type}:${rule.subject.id}`
1194
1534
  ).digest("base64url").slice(0, 22)}`;
1195
1535
  const rulesFor = (agent) => (options.grants ?? []).filter((rule) => agent.groups.includes(rule.group));
@@ -1380,7 +1720,7 @@ function createIssuer(options) {
1380
1720
  }
1381
1721
  const audience = form.get("audience");
1382
1722
  const iat = nowSeconds();
1383
- const jti = randomUUID();
1723
+ const jti = randomUUID2();
1384
1724
  const cnf = { jwk: { kty: "OKP", crv: "Ed25519", x: live.key } };
1385
1725
  const fresh = now() - Date.parse(live.createdAt) < NEW_ADDRESS_DAYS * 864e5;
1386
1726
  const idToken = await jws(
@@ -1580,7 +1920,7 @@ function createIssuer(options) {
1580
1920
  });
1581
1921
  }
1582
1922
  const sealed = {
1583
- jti: randomUUID(),
1923
+ jti: randomUUID2(),
1584
1924
  clientId: flow.site.clientId,
1585
1925
  redirectUri: flow.site.redirectUri,
1586
1926
  challenge: flow.site.challenge,
@@ -1625,7 +1965,7 @@ function createIssuer(options) {
1625
1965
  exp: iat + ID_TOKEN_SECONDS,
1626
1966
  auth_time: code.authTime,
1627
1967
  ...code.nonce ? { nonce: code.nonce } : {},
1628
- jti: randomUUID(),
1968
+ jti: randomUUID2(),
1629
1969
  sub: code.sub,
1630
1970
  kind: "person",
1631
1971
  email: code.email,
@@ -1865,7 +2205,11 @@ ${HELP}`);
1865
2205
  export {
1866
2206
  staticDirectory,
1867
2207
  cachedDirectory,
2208
+ auth0Directory,
1868
2209
  keycloakDirectory,
2210
+ oktaDirectory,
2211
+ kmsSigner,
2212
+ kmsRetiredKey,
1869
2213
  fileSigner,
1870
2214
  seedSigner,
1871
2215
  loadConfig,
@@ -1877,4 +2221,4 @@ export {
1877
2221
  UsageError2 as UsageError,
1878
2222
  runIssuer
1879
2223
  };
1880
- //# sourceMappingURL=chunk-BXZ2EI2Y.js.map
2224
+ //# sourceMappingURL=chunk-IKGE6SNR.js.map