@pidgeon-ai/issuer 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import { KeyObject } from 'node:crypto';
1
2
  import { Server } from 'node:http';
2
3
 
3
4
  /**
4
5
  * Who the agents are. The issuer holds no list of its own: the company's
5
- * identity provider is the record (Keycloak first; `keycloak.ts`), read
6
+ * identity provider is the record (`keycloak.ts`, `auth0.ts`, `okta.ts`), read
6
7
  * through this interface, and the issuer turns what it says into cards,
7
8
  * keys in the policy, and grants. Pidgeon is the format and the check,
8
9
  * never the identity provider.
@@ -84,8 +85,8 @@ interface PersonOptions {
84
85
 
85
86
  /**
86
87
  * The issuer's signing key, behind one interface so custody never touches
87
- * the protocol: a file today; AWS KMS or an HSM over PKCS#11 implement the
88
- * same three members and nothing else changes.
88
+ * the protocol: a file, AWS KMS (`kms.ts`), or an HSM over PKCS#11
89
+ * implementing the same three members, and nothing else changes.
89
90
  *
90
91
  * Everything the issuer signs is a JWS signing input (a card, an ID token,
91
92
  * a grant), a few hundred bytes, so a KMS that signs pure Ed25519 only up to
@@ -292,6 +293,135 @@ interface KeycloakOptions {
292
293
  }
293
294
  declare function keycloakDirectory(options: KeycloakOptions): Directory;
294
295
 
296
+ /**
297
+ * Auth0 as the directory: the issuer reads the tenant's Management API and
298
+ * never writes it.
299
+ *
300
+ * **An agent is a machine-to-machine application** (`app_type:
301
+ * non_interactive`) whose `client_metadata` says so. Auth0 metadata keys
302
+ * cannot hold a dot, so the names are Keycloak's with an underscore:
303
+ *
304
+ * | `client_metadata` | Becomes |
305
+ * | ------------------ | ------------------------------------------------------------- |
306
+ * | `pidgeon_key` | the agent's Ed25519 public key (`x`, base64url); required |
307
+ * | `pidgeon_address` | its address, at the issuer's domain; required |
308
+ * | `pidgeon_kind` | `agent` (default) or `service` |
309
+ * | `pidgeon_groups` | comma-separated, what the config's `grants` map |
310
+ * | `pidgeon_disabled` | `true`: not an agent, from its next sign-in |
311
+ *
312
+ * **Disabling.** An Auth0 application has no enabled switch, so an agent
313
+ * is disabled with `pidgeon_disabled` or by deleting the application.
314
+ * Whoever holds `update:clients` can set or clear it, so it is exactly as
315
+ * strong as that permission, the same people who could change its key.
316
+ *
317
+ * **One key, two uses is not possible here.** Auth0's Private Key JWT takes
318
+ * RS256, RS384 and PS256 with RSA keys only, not Ed25519, so the agent
319
+ * authenticates to Auth0 some other way and names its Pidgeon key in
320
+ * `pidgeon_key`.
321
+ *
322
+ * The card's `sub` is the `client_id`, which Auth0 generates and never lets
323
+ * anyone change; its documentation does not say whether a deleted
324
+ * application's id can come back. `created_at` is the application's.
325
+ *
326
+ * **The issuer's own credential** is a machine-to-machine application
327
+ * authorized for the Management API with `read:clients` only: never
328
+ * `read:client_keys` or `read:client_credentials`, which expose secrets.
329
+ * Its secret comes from the environment.
330
+ */
331
+ interface Auth0Options {
332
+ /** The tenant's domain: `acme.eu.auth0.com`, or its custom domain. */
333
+ tenant: string;
334
+ /** The issuer's own application. */
335
+ clientId: string;
336
+ clientSecret: string;
337
+ /** The domain agents' addresses are at. */
338
+ domain: string;
339
+ fetch?: typeof fetch;
340
+ /** Milliseconds since the epoch, for tests. */
341
+ now?: () => number;
342
+ }
343
+ declare function auth0Directory(options: Auth0Options): Directory;
344
+
345
+ /**
346
+ * Okta as the directory: the issuer reads the org's Apps API and never
347
+ * writes it.
348
+ *
349
+ * **An agent is an active OAuth 2.0 service app** (`application_type:
350
+ * service`, client credentials) whose `profile` names it. An Okta group
351
+ * holds people, never apps, so the agent's groups live in the profile too,
352
+ * as Okta's own `profile.groupallowlist` pattern does:
353
+ *
354
+ * ```json
355
+ * "profile": { "pidgeon": { "key": "<x>", "address": "support@acme.com", "kind": "agent", "groups": ["refunds"] } }
356
+ * ```
357
+ *
358
+ * A deactivated app (`status: INACTIVE`) or a deleted one is not an agent
359
+ * from its next sign-in. The card's `sub` is the app's id; `created_at` is
360
+ * the app's `created`.
361
+ *
362
+ * **One key, two uses is not possible here.** An Okta app's JWKS takes RSA
363
+ * and EC keys only, not Ed25519, so the agent authenticates to Okta with
364
+ * another key and names its Pidgeon key in the profile.
365
+ *
366
+ * **The issuer's own credential** is a service app of its own, granted the
367
+ * `okta.apps.read` scope and a read-only admin role (Okta requires both).
368
+ * Okta takes only Private Key JWT for its own scopes, so the issuer signs a
369
+ * client assertion with an RSA or P-256 key whose public half is in that
370
+ * app's JWKS; the private half comes from the environment.
371
+ */
372
+ interface OktaOptions {
373
+ /** The org: `https://acme.okta.com`. */
374
+ org: string;
375
+ /** The issuer's own service app. */
376
+ clientId: string;
377
+ /** Its RSA or P-256 private key, PEM, for the client assertion. */
378
+ privateKey: string | KeyObject;
379
+ /** That key's `kid` in the app's JWKS, when the app has more than one. */
380
+ keyId?: string;
381
+ /** The domain agents' addresses are at. */
382
+ domain: string;
383
+ fetch?: typeof fetch;
384
+ /** Milliseconds since the epoch, for tests. */
385
+ now?: () => number;
386
+ }
387
+ declare function oktaDirectory(options: OktaOptions): Directory;
388
+
389
+ /** The two KMS calls the issuer makes, as `@aws-sdk/client-kms` shapes them. */
390
+ interface KmsApi {
391
+ getPublicKey(input: {
392
+ KeyId: string;
393
+ }): Promise<{
394
+ KeyId?: string;
395
+ KeySpec?: string;
396
+ KeyUsage?: string;
397
+ SigningAlgorithms?: string[];
398
+ PublicKey?: Uint8Array;
399
+ }>;
400
+ sign(input: {
401
+ KeyId: string;
402
+ Message: Uint8Array;
403
+ MessageType: 'RAW';
404
+ SigningAlgorithm: string;
405
+ }): Promise<{
406
+ KeyId?: string;
407
+ Signature?: Uint8Array;
408
+ }>;
409
+ }
410
+ interface KmsSignerOptions {
411
+ /** The key's ARN, id, or an alias (`alias/pidgeon-issuer`), resolved once at start. */
412
+ keyId: string;
413
+ /** Default the SDK's: `AWS_REGION`, then the profile's. */
414
+ region?: string;
415
+ kms?: KmsApi;
416
+ }
417
+ declare function kmsSigner(options: KmsSignerOptions): Promise<Signer & {
418
+ keyArn: string;
419
+ }>;
420
+ /** A key in KMS the issuer no longer signs with, published until `notAfter`: its public half only. */
421
+ declare function kmsRetiredKey(options: KmsSignerOptions & {
422
+ notAfter: Date;
423
+ }): Promise<RetiredKey>;
424
+
295
425
  /**
296
426
  * `issuer.json`, the one file a company writes (`issuer.example.json`).
297
427
  * Secrets never go in it: the key is a file it names (mounted as a secret)
@@ -302,11 +432,15 @@ interface IssuerFile {
302
432
  domain: string;
303
433
  issuer?: string;
304
434
  organisation?: string;
305
- /** The key file, relative to this file. `PIDGEON_ISSUER_KEY` (the seed) wins when set. */
306
- key?: string;
435
+ /**
436
+ * The key file, relative to this file, or a key in AWS KMS (`{ "kms":
437
+ * "arn:aws:kms:…" }`, an `ECC_NIST_EDWARDS25519` key). `PIDGEON_ISSUER_KEY`
438
+ * (the seed) wins when set.
439
+ */
440
+ key?: KeyRef;
307
441
  /** Keys being withdrawn: published until `not_after`. */
308
442
  retired?: {
309
- key: string;
443
+ key: KeyRef;
310
444
  not_after: string;
311
445
  }[];
312
446
  directory: {
@@ -322,6 +456,26 @@ interface IssuerFile {
322
456
  /** The group whose members are agents. Default `agents`. */
323
457
  group?: string;
324
458
  };
459
+ } | {
460
+ auth0: {
461
+ /** The tenant's domain: `acme.eu.auth0.com`. */
462
+ tenant: string;
463
+ /** The issuer's own machine-to-machine application, with `read:clients` only. */
464
+ client_id: string;
465
+ /** The environment variable holding its secret. Default `AUTH0_CLIENT_SECRET`. */
466
+ client_secret_env?: string;
467
+ };
468
+ } | {
469
+ okta: {
470
+ /** The org: `https://acme.okta.com`. */
471
+ org: string;
472
+ /** The issuer's own service app, with `okta.apps.read` and a read-only admin role. */
473
+ client_id: string;
474
+ /** The environment variable holding its RSA or P-256 private key, PEM. Default `OKTA_PRIVATE_KEY`. */
475
+ private_key_env?: string;
476
+ /** That key's `kid` in the app's JWKS. */
477
+ key_id?: string;
478
+ };
325
479
  };
326
480
  /**
327
481
  * A person's sign-in, through the company's OpenID Connect provider, with
@@ -347,6 +501,11 @@ interface IssuerFile {
347
501
  /** How long the list of agents is held for the public reads, in seconds. Default 60. */
348
502
  directory_cache_seconds?: number;
349
503
  }
504
+ /** A key file's path, or a KMS key: its ARN, id or alias, and its region when not the default. */
505
+ type KeyRef = string | {
506
+ kms: string;
507
+ region?: string;
508
+ };
350
509
  declare function loadConfig(path: string, env?: Record<string, string | undefined>): Promise<IssuerOptions>;
351
510
 
352
511
  /**
@@ -373,4 +532,4 @@ declare function runIssuer(args: string[], env?: Record<string, string | undefin
373
532
  log?: (line: string) => void;
374
533
  }): Promise<string>;
375
534
 
376
- export { type Directory, type DirectoryAgent, type GrantRule, type Issuer, type IssuerFile, type IssuerOptions, type KeycloakOptions, PATHS, type PersonOptions, type ReplayStore, type RetiredKey, type Signer, type StaticClient, TOKEN_EXCHANGE, UsageError, cachedDirectory, createIssuer, fileSigner, keycloakDirectory, loadConfig, memoryReplayStore, runIssuer, seedSigner, serve, staticDirectory };
535
+ export { type Auth0Options, type Directory, type DirectoryAgent, type GrantRule, type Issuer, type IssuerFile, type IssuerOptions, type KeyRef, type KeycloakOptions, type KmsApi, type KmsSignerOptions, type OktaOptions, PATHS, type PersonOptions, type ReplayStore, type RetiredKey, type Signer, type StaticClient, TOKEN_EXCHANGE, UsageError, auth0Directory, cachedDirectory, createIssuer, fileSigner, keycloakDirectory, kmsRetiredKey, kmsSigner, loadConfig, memoryReplayStore, oktaDirectory, runIssuer, seedSigner, serve, staticDirectory };
package/dist/index.js CHANGED
@@ -3,27 +3,35 @@ import {
3
3
  PATHS,
4
4
  TOKEN_EXCHANGE,
5
5
  UsageError,
6
+ auth0Directory,
6
7
  cachedDirectory,
7
8
  createIssuer,
8
9
  fileSigner,
9
10
  keycloakDirectory,
11
+ kmsRetiredKey,
12
+ kmsSigner,
10
13
  loadConfig,
11
14
  memoryReplayStore,
15
+ oktaDirectory,
12
16
  runIssuer,
13
17
  seedSigner,
14
18
  serve,
15
19
  staticDirectory
16
- } from "./chunk-BXZ2EI2Y.js";
20
+ } from "./chunk-5GZMAASA.js";
17
21
  export {
18
22
  PATHS,
19
23
  TOKEN_EXCHANGE,
20
24
  UsageError,
25
+ auth0Directory,
21
26
  cachedDirectory,
22
27
  createIssuer,
23
28
  fileSigner,
24
29
  keycloakDirectory,
30
+ kmsRetiredKey,
31
+ kmsSigner,
25
32
  loadConfig,
26
33
  memoryReplayStore,
34
+ oktaDirectory,
27
35
  runIssuer,
28
36
  seedSigner,
29
37
  serve,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pidgeon-ai/issuer",
3
- "version": "0.2.0",
4
- "description": "A Post issuer you run yourself, inside your own network: your domain signs your AI agents' cards and sign-ins with a key that never leaves it, for the agents your identity provider (Keycloak) says you have.",
3
+ "version": "0.4.0",
4
+ "description": "A Post issuer you run yourself, inside your own network: your domain signs your AI agents' cards and sign-ins with a key that never leaves it, for the agents your identity provider (Keycloak, Auth0, Okta) says you have.",
5
5
  "keywords": [
6
6
  "agent-identity",
7
7
  "ai-agents",
@@ -9,6 +9,8 @@
9
9
  "openid-connect",
10
10
  "token-exchange",
11
11
  "keycloak",
12
+ "auth0",
13
+ "okta",
12
14
  "web-bot-auth",
13
15
  "pidgeon",
14
16
  "post"
@@ -43,7 +45,16 @@
43
45
  "publishConfig": {
44
46
  "access": "public"
45
47
  },
48
+ "peerDependencies": {
49
+ "@aws-sdk/client-kms": "^3.1164.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "@aws-sdk/client-kms": {
53
+ "optional": true
54
+ }
55
+ },
46
56
  "devDependencies": {
57
+ "@aws-sdk/client-kms": "^3.1164.0",
47
58
  "tsup": "^8.5.0",
48
59
  "typescript": "^5",
49
60
  "@pidgeon-ai/id": "0.2.0"