@pidgeon-ai/issuer 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PCN Labs OÜ
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,122 @@
1
- # Temporary Holding Version
1
+ # @pidgeon-ai/issuer
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A Post issuer you run yourself, inside your own network. Your own domain
4
+ signs your AI agents' cards, sign-ins and grants, with a key that never
5
+ leaves you, for the agents your identity provider says you have. Nothing is
6
+ sent to Pidgeon: not the key, not the list of agents.
7
+
8
+ It is for companies that may not hand authentication to a third party. Pidgeon
9
+ Cloud runs the same issuer for everyone else. Both speak the same protocol, so
10
+ `@pidgeon-ai/id` (the agent's side) and `@pidgeon-ai/sign-in` (the site's)
11
+ work with either one unchanged.
12
+
13
+ ```bash
14
+ npx @pidgeon-ai/issuer keygen acme.com
15
+ ```
16
+
17
+ ## What runs where
18
+
19
+ | Where | What |
20
+ | ---------------------- | -------------------------------------------------------------------------- |
21
+ | Inside your network | The issuer (`pidgeon-issuer serve`, or the container), its key, its config |
22
+ | Your identity provider | Who the agents are: their addresses, keys and groups |
23
+ | `post.<your domain>` | The public documents: your keys, your agents' cards, OpenID discovery |
24
+ | `_post.<your domain>` | A TXT record, so verifiers find `post.<your domain>` |
25
+ | Each agent | Its own Ed25519 key, which signs its mail, its requests and its sign-ins |
26
+
27
+ The token endpoint only needs to reach your agents. Everything a stranger
28
+ reads is a document: publish it through your reverse proxy, or write it out
29
+ with `pidgeon-issuer export <dir>` and serve it from any static host.
30
+
31
+ ## Run it
32
+
33
+ 1. **The issuer's key**, once: `pidgeon-issuer keygen acme.com`. It writes
34
+ `pidgeon-issuer-key.json` (mode 0600). Mount it as a secret, or put the
35
+ seed in `PIDGEON_ISSUER_KEY`. Any `Signer` fits in its place, so a KMS or
36
+ an HSM needs no change to the rest.
37
+ 2. **Each agent's key**, on the agent's machine: `pidgeon-issuer keygen
38
+ acme.com --out ~/.config/pidgeon/id/acme.com.json`. Give the identity
39
+ provider the `key` line it prints; the private half stays with the agent.
40
+ 3. **`issuer.json`**: copy `issuer.example.json`. `directory.keycloak` reads
41
+ the agents from Keycloak (below); `directory.static` lists them by hand. `grants` maps a group to what its agents may do, signed by
42
+ your domain as the organisation. `clients` are the sites you let your
43
+ agents sign in to.
44
+ 4. **DNS**: `pidgeon-issuer dns` prints the `_post` record and where
45
+ `post.acme.com` must answer.
46
+ 5. **Serve**: `pidgeon-issuer serve` (port `PORT`, default 8080), or
47
+ `docker build -f packages/issuer/Dockerfile -t pidgeon-issuer .` from this
48
+ repository's root and run it with the config and the key mounted at
49
+ `/etc/pidgeon-issuer/`.
50
+
51
+ ## Keycloak
52
+
53
+ An agent is a Keycloak client with a service account, in the `agents` group.
54
+ Its key is the Ed25519 key Keycloak authenticates it with (Credentials →
55
+ Signed JWT, EdDSA), so the agent holds one key for Keycloak and for sites; a
56
+ client on a secret names its key in a `pidgeon.key` attribute instead.
57
+ `pidgeon.address` (default `<clientId>@<domain>`) and `pidgeon.kind` say the
58
+ rest, and its groups are what `grants` maps.
59
+
60
+ ```json
61
+ "directory": {
62
+ "keycloak": {
63
+ "url": "https://sso.acme.internal",
64
+ "realm": "acme",
65
+ "client_id": "pidgeon-issuer",
66
+ "group": "agents"
67
+ }
68
+ }
69
+ ```
70
+
71
+ `pidgeon-issuer` is a client of its own with a service account holding
72
+ `realm-management`'s `view-clients` and `view-users`, nothing that writes; its
73
+ secret is `KEYCLOAK_CLIENT_SECRET`. `examples/self-hosted-issuer` runs all of
74
+ it in Docker.
75
+
76
+ ## Sign in to a site
77
+
78
+ A site lists your issuer beside Pidgeon's:
79
+
80
+ ```ts
81
+ import { pidgeonSignIn } from '@pidgeon-ai/sign-in';
82
+
83
+ export const pidgeon = pidgeonSignIn({
84
+ clientId: 'client_shop',
85
+ url: 'https://shop.example',
86
+ secret: process.env.PIDGEON_SESSION_SECRET!,
87
+ issuers: ['https://post.acme.com'],
88
+ });
89
+ ```
90
+
91
+ and your agent signs in as it would through Pidgeon Cloud: its card comes from
92
+ `post.acme.com`, and it goes to the issuer that signed the card.
93
+
94
+ ```ts
95
+ import { loadIdentity, signInWithPidgeon } from '@pidgeon-ai/id';
96
+
97
+ const { site } = await signInWithPidgeon(await loadIdentity('acme.com'), {
98
+ agent: 'support-agent@acme.com',
99
+ site: 'https://shop.example/api/auth/pidgeon/token',
100
+ });
101
+ ```
102
+
103
+ ## What it checks before it signs anything
104
+
105
+ - The token request is signed by a key your identity provider gives an
106
+ agent, covers what it must, and is used once (one replica, or pass a
107
+ shared `replay` store).
108
+ - The card is one this issuer signed, current, that agent's, naming that key.
109
+ - The agent is live in the identity provider **now**: disable it there and
110
+ its next sign-in fails.
111
+ - A grant is one this issuer signed, that agent's, still held through its
112
+ group, and for that site.
113
+
114
+ ## Left out
115
+
116
+ The person's sign-in, userinfo, introspection and revocation endpoints (an ID
117
+ token lasts five minutes; revoking an agent is disabling it), dynamic client
118
+ registration, and Pidgeon's card log, which would mean sending us your agents.
119
+ Authorization stays yours: a grant is a relation (`customer:42#refunds.issue`
120
+ for an agent), so it can be written from the engine you already run.
121
+
122
+ Apache-2.0.
package/dist/bin.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ UsageError,
4
+ runIssuer
5
+ } from "./chunk-CCOFMK5K.js";
6
+
7
+ // src/bin.ts
8
+ async function main() {
9
+ try {
10
+ process.stdout.write(
11
+ `${await runIssuer(process.argv.slice(2), process.env, {
12
+ log: (line) => process.stderr.write(`${line}
13
+ `)
14
+ })}
15
+ `
16
+ );
17
+ } catch (error) {
18
+ process.stderr.write(`${error instanceof Error ? error.message : "Something went wrong."}
19
+ `);
20
+ process.exit(error instanceof UsageError ? 2 : 1);
21
+ }
22
+ }
23
+ void main();
24
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/bin.ts"],"sourcesContent":["import { runIssuer, UsageError } from './cli';\n\n/** `pidgeon-issuer <command>`. Nothing else in this package runs anything on import. */\nasync function main() {\n try {\n process.stdout.write(\n `${await runIssuer(process.argv.slice(2), process.env, {\n log: (line) => process.stderr.write(`${line}\\n`),\n })}\\n`,\n );\n } catch (error) {\n process.stderr.write(`${error instanceof Error ? error.message : 'Something went wrong.'}\\n`);\n process.exit(error instanceof UsageError ? 2 : 1);\n }\n}\n\nvoid main();\n"],"mappings":";;;;;;;AAGA,eAAe,OAAO;AACpB,MAAI;AACF,YAAQ,OAAO;AAAA,MACb,GAAG,MAAM,UAAU,QAAQ,KAAK,MAAM,CAAC,GAAG,QAAQ,KAAK;AAAA,QACrD,KAAK,CAAC,SAAS,QAAQ,OAAO,MAAM,GAAG,IAAI;AAAA,CAAI;AAAA,MACjD,CAAC,CAAC;AAAA;AAAA,IACJ;AAAA,EACF,SAAS,OAAO;AACd,YAAQ,OAAO,MAAM,GAAG,iBAAiB,QAAQ,MAAM,UAAU,uBAAuB;AAAA,CAAI;AAC5F,YAAQ,KAAK,iBAAiB,aAAa,IAAI,CAAC;AAAA,EAClD;AACF;AAEA,KAAK,KAAK;","names":[]}