axis-platform-sdk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,218 +1,390 @@
1
- # axis-platform-sdk
2
-
3
- The **platform side** of AXIS. The bouncer at the door.
4
-
5
- The [`axis-protocol-sdk`](https://github.com/MachinesOfDesire/axis-protocol-sdk) is what an *agent* uses to prove who it is. This SDK is the other end of the wire: what a **consuming platform** uses when an AXIS agent shows up and wants to do something, so the platform can **verify** it, check its **scope**, and decide to **accept** or **boot** it.
6
-
7
- > Status: v0.2. Built for the full-loop demo and as the adoption surface for third-party platforms. Owyhee "The Door" is the first production consumer (it depends on this package rather than vendoring it).
8
-
9
- ## Why this exists
10
-
11
- The registry already does the hard part. `GET /verify?token=<AIT>` checks the signature, checks revocation, and walks the delegation chain server-side, returning the trustworthy `effective_scope`. This SDK packages the verdict + your platform's policy (audience, required scopes, blocked operators, minimum tier) into one call, so you integrate in a few lines instead of hand-rolling it.
12
-
13
- ## Install (once published)
14
-
15
- ```
16
- npm install axis-platform-sdk
17
- ```
18
-
19
- Zero dependencies. Runs in Node 20+, Cloudflare Workers, and modern browsers.
20
-
21
- ## Quickstart gate a Worker endpoint
22
-
23
- ```js
24
- import { aitGate, denialResponse } from 'axis-platform-sdk';
25
-
26
- const gate = aitGate({
27
- audience: 'comments.mysite.com', // your platform's stable audience id
28
- requireScopes: ['comments:write'], // what the agent must be allowed to do
29
- // minTier: 'domain', // optionally require domain-verified+
30
- // blockedOperators: ['axis:spammer:operator'],
31
- });
32
-
33
- export default {
34
- async fetch(request) {
35
- const verdict = await gate(request);
36
- if (!verdict.accepted) return denialResponse(verdict); // 401/403 + reason
37
- // verdict.agent_id is verified. Proceed.
38
- return Response.json({ ok: true, by: verdict.agent_id });
39
- },
40
- };
41
- ```
42
-
43
- The gate pulls the AIT from `Authorization: Bearer <ait>`, `X-AXIS-Token`, or `?ait=`.
44
-
45
- ## Or call the verifier directly
46
-
47
- ```js
48
- import { verifyAgent } from 'axis-platform-sdk';
49
-
50
- const verdict = await verifyAgent(token, {
51
- audience: 'comments.mysite.com',
52
- requireScopes: ['comments:write'],
53
- });
54
- // -> { accepted: true, agent_id, operator_id, effective_scope, delegation_valid, tier, expires_at }
55
- // or { accepted: false, code, reason, ... }
56
- ```
57
-
58
- ## API
59
-
60
- - **`verifyAgent(token, opts)`** the core. Verifies against the registry and applies your policy. Returns a structured verdict.
61
- - `audience` your platform id. The AIT's `aud` must equal it. (Matched locally: the registry only checks that `aud` is non-empty, not that it equals you. That check is yours.)
62
- - `requireScopes` checked against the trustworthy `effective_scope`.
63
- - `minTier` `email | domain | verified | kyb_individual | kyb_organization`.
64
- - `blockedOperators` / `approvedOperators` — deny/allow lists by operator id.
65
- - `registryBaseUrl` — defaults to `https://registry.axisprime.ai`.
66
- - **`aitGate(opts)`** — returns `(request) => Promise<verdict>`; binds your policy to a request gate.
67
- - **`denialResponse(verdict)`** — turns a denied verdict into a 401/403 `Response`.
68
- - **`scopeCovers(granted, required)` / `coversAll(granted, required[])`** — the AXIS scope matcher (ported verbatim from the Governor's, so operator and platform sides agree).
69
- - **`enrich(agentId, token, opts)`** — fetch the agent's presentation layer (display name, tier) for a console UI.
70
- - **`loadAccessPolicy(platformBaseUrl)`** — read a platform's published `/.well-known/axis-access` door policy.
71
- - **`decodeAitPayload(token)`**read the AIT payload (claims) without verifying. For the `aud` check; never trust it for authorization.
72
- - **`AccessLedger` / `MemoryLedgerStore` / `loggedGate(gate, ledger, opts)` / `recordEntry(verdict, opts)`** — the access ledger (who showed up). `loggedGate` wraps a gate so every verdict is logged.
73
- - **`Blocklist` / `MemoryBlocklistStore` / `gatedWithBlocklist(gate, blocklist)`** the runtime block list (by operator and by agent). `blockOperator` / `blockAgent` / `unblock*` / `isAgentBlocked` / `blockedOperatorIds` / `checkVerdict`.
74
- - **`reportFlag(args, opts)` / `blockAndReport(blocklist, args, opts)`** — sign a negative Trust Attestation and send it to a reputation index (OFF by default).
75
- - **`getPlatformKey(opts)` / `buildAttestation` / `signAttestation` / `verifyAttestation` / `MemoryKeyStore`** the platform's Ed25519 key + TA build/sign/verify primitives (WebCrypto, zero-dep).
76
-
77
- ## Gates as policy: `SwitchAuthorizer` (the free-tier engine)
78
-
79
- Identity verification is fixed and core. The authorization *decision* is a pluggable layer — the **Authorizer port**. `SwitchAuthorizer` is the free-tier implementation: config-driven on/off gates. Its `policy` object is exactly what a console's "door policy" screen edits and saves.
80
-
81
- ```js
82
- import { SwitchAuthorizer, denialResponse } from 'axis-platform-sdk';
83
-
84
- const door = new SwitchAuthorizer({
85
- audience: 'comments.mysite.com',
86
- defaultAllow: false,
87
- gates: {
88
- 'comments:write': { enabled: true, requireScopes: ['comments:write'], minTier: 'domain' },
89
- },
90
- });
91
-
92
- const verdict = await door.gate('comments:write')(request);
93
- if (!verdict.accepted) return denialResponse(verdict);
94
- ```
95
-
96
- Flip `enabled: false` and the gate closes, no code change. The port is engine-agnostic: a paid `EngineAuthorizer` (Permify / OpenFGA sidecar) for granular relationship/attribute rules drops into the same slot with the same `authorize(token, gateId, ctx)` shape. The demo and free tier need no engine.
97
-
98
- ## Stateful half: ledger, blocklist, reputation report-back
99
-
100
- `verifyAgent` / `aitGate` are stateless verdict machines. A self-hosting
101
- platform also needs **state it owns**: a record of who showed up, a runtime
102
- block list, and a way to report a bad actor onward. These three modules add that,
103
- all zero-infra — the platform runs the stores in its OWN store (default
104
- in-memory; plug in D1 / SQLite / Postgres via a documented adapter shape).
105
-
106
- ### Access ledger — "who's using my platform"
107
-
108
- ```js
109
- import { aitGate, AccessLedger, loggedGate } from 'axis-platform-sdk';
110
-
111
- const ledger = new AccessLedger(); // default in-memory store
112
- const gate = loggedGate(aitGate({ audience }), ledger, { audience });
113
-
114
- const verdict = await gate(request); // every verdict is logged
115
- // ...
116
- await ledger.recent({ limit: 25 }); // newest-first arrivals
117
- await ledger.byOperator('axis:acme:op'); // arrivals from one operator
118
- ```
119
-
120
- Each entry records `{ agent_id, operator_id, created_at, tier, delegation_valid,
121
- effective_scope, gate_id, requested_action, display_name, decision, reason,
122
- audience }`the same shape as Owyhee "The Door"'s `arrivals` record, so the
123
- SDK and the product share one arrival definition (see "Single source of truth"
124
- below). `decision` is `auto_allow | denied | held | approved | booted`;
125
- `created_at` is epoch ms. Only the trustworthy `effective_scope` is recorded,
126
- never the AIT's self-declared scope. A ledger write failure never changes the
127
- verdict.
128
-
129
- ### Persistent block / allow list runtime, by operator AND by agent
130
-
131
- The static `blockedOperators` / `approvedOperators` are config-time policy. The
132
- `Blocklist` is runtime policy the platform mutates without a redeploy, and it
133
- adds **agent-level** blocking (boot one agent without booting its operator).
134
-
135
- ```js
136
- import { Blocklist, verifyAgent } from 'axis-platform-sdk';
137
-
138
- const blocklist = new Blocklist();
139
- await blocklist.blockAgent('axis:acme:bot', 'spammed'); // agent-level
140
- await blocklist.blockOperator('axis:bad:op', 'whole op'); // operator-level
141
-
142
- // Inject dynamic operator blocks into verify, then catch agent-level post-verify:
143
- let verdict = await verifyAgent(token, {
144
- audience,
145
- blockedOperators: [...staticBlocked, ...(await blocklist.blockedOperatorIds())],
146
- });
147
- verdict = await blocklist.checkVerdict(verdict); // flips to denied if agent/op blocked
148
- ```
149
-
150
- ### Reputation report-back sign a negative Trust Attestation and send it onward
151
-
152
- When you boot an agent, you know something the network doesn't. `reportFlag`
153
- builds a protocol-shaped **Trust Attestation** (AXIS Layer 3; SPEC §4.5), signs
154
- it with the platform's own Ed25519 key (generated + persisted on first use via
155
- `getPlatformKey`, WebCrypto), and POSTs `{ attestation, platform_public_key,
156
- signature }` to a configurable reputation index.
157
-
158
- ```js
159
- import { reportFlag, blockAndReport, Blocklist } from 'axis-platform-sdk';
160
-
161
- // Report-back is OFF by default. Unconfigured -> graceful no-op (never throws).
162
- await reportFlag(
163
- { platformId: 'axis:my-platform:door', agentId: 'axis:acme:bot', category: 'abuse:spam', reason: 'flooded comments' },
164
- { reputationUrl: 'https://axis-reputation.example/attestations' } // when the index exists
165
- );
166
-
167
- // Convenience: block locally AND report (local block is authoritative; report is best-effort).
168
- const blocklist = new Blocklist();
169
- await blockAndReport(blocklist, { platformId: 'axis:my-platform:door', agentId: 'axis:acme:bot', category: 'abuse', reason: 'spam' });
170
- ```
171
-
172
- The reputation **index is a separate, future, commercial service** — NOT the
173
- canonical registry (which stays identity-only, Layer 1 + Layer 2). The
174
- [`axis-reputation`](https://github.com/MachinesOfDesire/axis-reputation) stub
175
- receiver accepts-and-discards (verifies signature, returns 202) until the real
176
- index is built. See `examples/bouncer-worker.js` for a reference admin surface
177
- over the ledger + blocklist.
178
-
179
- ### Single source of truth: the SDK is the port, The Door is the adapter
180
-
181
- The ledger and blocklist are a **port + in-memory default**, not a competing
182
- implementation. Owyhee **"The Door"** (the inbound surface of the Owyhee
183
- console) is the first production **adapter**:
184
-
185
- | SDK (this package, the port) | The Door (the D1-backed product adapter) |
186
- | ----------------------------------- | ----------------------------------------------- |
187
- | `AccessLedger` + `recordEntry` | `arrivals` table + `recordArrival()` |
188
- | `Blocklist` (operator-level) | `operator_blocks` table + `blockedOperators()` |
189
- | `SwitchAuthorizer` `policy` | `door_policy` table (serialized policy) |
190
- | `Blocklist` agent-level *(superset)* | *(not yet an additive `agent_blocks` table)* |
191
- | `reportFlag` / reputation emit *(new)* | *(not yet — the open emit half is here)* |
192
-
193
- The entry/meta shapes above are deliberately byte-compatible with The Door's
194
- columns (`created_at` epoch ms; the `auto_allow|denied|held|approved|booted`
195
- decision vocabulary) so there is **one** arrival/block record across the SDK and
196
- the product fold in, don't duplicate. A platform that needs its own store
197
- implements the documented adapter shape; The Door is the worked, deployed
198
- example of doing exactly that over Cloudflare D1.
199
-
200
- ## Trust model (read this)
201
-
202
- - **`effective_scope` is the only trustworthy scope.** It's the registry's server-side chain-walk result, returned when a valid delegation is presented. The AIT's self-declared `scope` is NOT trusted and is never used for `requireScopes`.
203
- - **A direct AIT with no valid delegation has no proven scope.** Any non-empty `requireScopes` will deny it. That's intentional.
204
- - **Audience matching is the platform's job.** The registry guarantees `aud` exists; you guarantee it's *you*.
205
-
206
- ## Where it sits
207
-
208
- ```
209
- agent (axis-protocol-sdk) ──presents AIT──> YOUR PLATFORM (axis-platform-sdk)
210
-
211
- └── GET /verify ──> registry (does the crypto)
212
- ```
213
-
214
- This is distinct from the Governor (the operator-side outbound gateway) and from generic AI gateways (TrueFoundry, Portkey, etc.), which govern an operator's *outbound* LLM calls. This SDK is the *inbound* identity gate.
215
-
216
- ## License
217
-
218
- Apache-2.0. (c) Kipple Labs, Inc.
1
+ # axis-platform-sdk
2
+
3
+ **Let verified agents into your platform. Boot the bad ones. Free, drop-in, no account required.**
4
+
5
+ AI agents are starting to show up at your platform to post, to buy, to call your
6
+ API on a human's behalf. An API key can't tell you *which human is behind this
7
+ agent*, *whether they're allowed to do this*, or *let you revoke one bad agent
8
+ without nuking the key everyone shares*.
9
+
10
+ `axis-platform-sdk` is the **bouncer at your door**. When an AXIS agent shows up
11
+ and presents a token, this verifies cryptographically, against a public
12
+ registry — **who it is, who's accountable for it, and exactly what it's been
13
+ authorized to do**, then lets it through or bounces it. A few lines of code.
14
+
15
+ On a Node/Express server:
16
+
17
+ ```js
18
+ import { axisGate } from 'axis-platform-sdk/express';
19
+
20
+ app.post('/comments',
21
+ axisGate({ audience: 'comments.mysite.com', requireScopes: ['content:comment'] }),
22
+ (req, res) => res.json({ ok: true, by: req.axis.agent_id })); // verified agent; proceed
23
+ ```
24
+
25
+ On a Cloudflare Worker (or any `fetch` handler):
26
+
27
+ ```js
28
+ import { aitGate, denialResponse } from 'axis-platform-sdk';
29
+
30
+ const gate = aitGate({ audience: 'comments.mysite.com', requireScopes: ['content:comment'] });
31
+
32
+ export default {
33
+ async fetch(request) {
34
+ const verdict = await gate(request);
35
+ if (!verdict.accepted) return denialResponse(verdict); // 401/403 + a real reason
36
+ return Response.json({ ok: true, by: verdict.agent_id }); // verified agent; proceed
37
+ },
38
+ };
39
+ ```
40
+
41
+ That's the whole integration. No SDK account, no API key from us, no infra to run.
42
+
43
+ ---
44
+
45
+ ## Why it's free, and what "no account required" means
46
+
47
+ The hard part — checking the signature, checking revocation, walking the
48
+ delegation chain to compute what the agent is *actually* allowed to do — is done
49
+ **server-side by the public AXIS registry** (`registry.axisprime.ai`). This SDK
50
+ is the thin, zero-dependency client that calls it and applies *your* policy. Your
51
+ platform makes **one outbound HTTPS call** and gets back a trustworthy verdict.
52
+
53
+ - **Zero dependencies.** Runs on Node 20+, Cloudflare Workers, and modern browsers.
54
+ - **Nothing to host.** No database, no key management, no service to deploy.
55
+ - **No relationship with us.** You verify against the public registry directly —
56
+ no signup, no key. The only thing that reaches us is the verification call your
57
+ server makes (the agent's token); we never see your content or your users.
58
+ - **Apache-2.0.** Use it, fork it, ship it.
59
+
60
+ Self-host is the whole product today. A cloud-hosted version a hosted console,
61
+ durable arrival history, and a richer policy engine is in alpha testing, planned
62
+ for release in Q3 2026, for teams who'd rather not run it themselves. You will
63
+ never need it to run the self-host path. See [Self-host today / cloud-hosted in
64
+ alpha](#self-host-today--cloud-hosted-in-alpha).
65
+
66
+ ---
67
+
68
+ ## Gate your platform in 10 minutes
69
+
70
+ There's a step-by-step guide in **[QUICKSTART.md](QUICKSTART.md)**, and two
71
+ complete, runnable drop-in starterscopy the one that matches your stack:
72
+
73
+ | Your stack | Starter | What it is |
74
+ | --- | --- | --- |
75
+ | Node / Express (or any Node HTTP server) | **[`templates/node-express/`](templates/node-express/)** | The `axisGate(...)` middleware from `axis-platform-sdk/express` (one import, one line) + a full worked server (door policy, arrivals ledger, boot console). `npm install && npm start`. |
76
+ | Cloudflare Workers | **[`templates/cloudflare-worker/`](templates/cloudflare-worker/)** | The same, as a deployable Worker. `npx wrangler dev`. |
77
+
78
+ Both wrap the identical engine. **You do not need to adopt Cloudflare to use
79
+ AXIS** — the Worker template is just one runtime we provide a starter for. If you
80
+ run Node, Python, Go, or anything else, the integration is the same shape: pull
81
+ the token off the request, call the verifier, act on the verdict.
82
+
83
+ The Express starter ships a `smoke` test you can run right now to watch the gate
84
+ turn away an unidentified agent:
85
+
86
+ ```
87
+ $ cd templates/node-express && npm install && npm run smoke
88
+ PASS no AIT -> 401 no_token
89
+ PASS — invalid AIT -> 403 denied
90
+ All checks passed.
91
+ ```
92
+
93
+ ---
94
+
95
+ ## What a verdict gives you
96
+
97
+ `verifyAgent(token, opts)` (and the `aitGate` / middleware that wrap it) return a
98
+ single structured verdict:
99
+
100
+ ```js
101
+ // accepted:
102
+ { accepted: true, agent_id, operator_id, effective_scope, delegation_valid, tier, expires_at }
103
+ // or denied:
104
+ { accepted: false, code, reason, ... } // code is stable: no_token | audience_mismatch |
105
+ // agent_revoked | insufficient_scope | insufficient_tier | ...
106
+ ```
107
+
108
+ You decide the policy; the SDK enforces it:
109
+
110
+ - **`audience`** — the AIT's `aud` must equal *you*, so an agent's token for some
111
+ other site can't be replayed at yours.
112
+ - **`requireScopes`** checked against the trustworthy **`effective_scope`** (the
113
+ registry's chain-walked result), never the token's self-declared scope.
114
+ - **`minTier`** require `email` / `domain` / `verified` / `kyb_individual` /
115
+ `kyb_organization`-level operator verification.
116
+ - **`blockedOperators` / `approvedOperators`** deny-list or allow-list by operator.
117
+
118
+ And the stateful half a real bouncer needs (all zero-infra by default):
119
+
120
+ - **Access ledger** log every arrival, accepted or denied: "who's been using my platform."
121
+ - **Runtime blocklist** — boot one agent, or a whole operator, **without a deploy**.
122
+ - **Reputation report-back** when you boot a bad actor, optionally sign a Trust
123
+ Attestation and emit it onward (off by default).
124
+
125
+ ---
126
+
127
+ ## Self-host today / cloud-hosted in alpha
128
+
129
+ Today this SDK *is* the product: you run it in your own backend, free. A
130
+ cloud-hosted version is in alpha testing (planned for release in Q3 2026) for
131
+ teams who'd rather not host the console and the state themselves.
132
+
133
+ | | **This SDK (free, self-hosted) — available now** | **Cloud-hosted alpha, Q3 2026** |
134
+ | --- | --- | --- |
135
+ | Identity verification | ✅ full (against the public registry) | same engine |
136
+ | Scope / tier / operator policy | ✅ `SwitchAuthorizer` (on/off gates) | + granular relationship/attribute rules |
137
+ | Arrivals + blocklist | ✅ your store (in-memory default; D1/SQLite/Postgres adapter) | hosted, durable, multi-tenant |
138
+ | Admin console | ✅ a reference HTML page you own | a hosted console |
139
+ | Cost / setup | free, nothing to run | a hosted product (alpha) |
140
+
141
+ The SDK is designed as the **port**; the cloud-hosted version is built as an
142
+ **adapter** over the same engine, with byte-compatible arrival/block record
143
+ shapes. That's a deliberate design choice so that when the cloud version ships,
144
+ moving to it is a lift, not a rewrite — and you are never forced up.
145
+
146
+ ---
147
+
148
+ ## Adoption tiers: start small, add as you need
149
+
150
+ You don't have to do everything at once. Most platforms start at **A** and stop
151
+ there; regulated or higher-stakes platforms add **B** and **C**.
152
+
153
+ | Tier | What you do | What it takes |
154
+ | --- | --- | --- |
155
+ | **A Identity acceptance** | Accept any AXIS-verified agent the way you accept a signed-in human. Pass/fail at request time. | `verifyAgent(token, { audience })` — that's it. |
156
+ | **B Access policy** | Also publish your requirements at `/.well-known/axis-access`, so agents and operators can check before they even call you. | A small JSON doc (the starters serve it). |
157
+ | **C — Scope + tier enforcement** | Additionally require specific permissions and a minimum verification level. | Add `requireScopes` / `minTier` (and `blockedOperators`) to the gate. |
158
+
159
+ All three are the same `verifyAgent` call with more of its options set — moving up
160
+ a tier is adding arguments, not re-architecting. (For an upstream you can't put the
161
+ SDK inside a legacy service, a non-Node runtime the separate
162
+ [`axis-gateway`](https://github.com/MachinesOfDesire/axis-gateway) reverse proxy
163
+ enforces Tier C in front of it.)
164
+
165
+ ---
166
+
167
+ ## Trust model (read this)
168
+
169
+ - **`effective_scope` is the only trustworthy scope.** It's the registry's
170
+ server-side chain-walk result, returned only when a valid delegation is
171
+ presented. The AIT's self-declared `scope` is **not** trusted and is never used
172
+ for `requireScopes`.
173
+ - **A direct AIT with no valid delegation has no proven scope.** Any non-empty
174
+ `requireScopes` will deny it. That's intentional.
175
+ - **Audience matching is the platform's job.** The registry guarantees `aud`
176
+ exists; *you* guarantee it equals you. (The starters do this for you.)
177
+
178
+ ### Forward compatibility (gating signals coming later)
179
+
180
+ Today you can gate on **scope**, **operator verification tier**, and
181
+ **operator allow/block lists**. Richer **provenance** signals operator account
182
+ age, signup method, prior abuse flags are defined in the protocol but **not yet
183
+ exposed by the registry**, so they aren't available to gate on right now.
184
+
185
+ When they ship, they arrive **additively and backward-compatibly**:
186
+
187
+ - The verdict object only *gains* fields; it never changes existing ones. Your code
188
+ keeps working untouched.
189
+ - Provenance gating will be **new optional `verifyAgent` options** (e.g. a minimum
190
+ account age), exactly like `minTier` is today. Unknown options are ignored, so an
191
+ older integration is never broken by a newer registry.
192
+ - You opt in when you want it: bump the SDK and set the new option. Platforms that
193
+ don't care do nothing and are unaffected.
194
+
195
+ So adding provenance later requires a **registry update** (to populate and expose
196
+ the fields) and an **SDK minor** (to read them) but **no breaking change and no
197
+ forced migration** for platforms already running. Build to Tier A/B/C now; the
198
+ provenance knobs slot into the same gate when they land.
199
+
200
+ ---
201
+
202
+ ## Where it sits
203
+
204
+ ```
205
+ agent (axis-protocol-sdk) ──presents AIT──> YOUR PLATFORM (axis-platform-sdk)
206
+
207
+ └── GET /verify ──> registry (does the crypto)
208
+ ```
209
+
210
+ The [`axis-protocol-sdk`](https://github.com/MachinesOfDesire/axis-protocol-sdk)
211
+ and the [AXIS Prime MCP](https://github.com/MachinesOfDesire/axis-mcp) are what an
212
+ *agent* uses to get and present an identity. **This** SDK is the other end of the
213
+ wire — the inbound identity gate. It is distinct from the operator-side
214
+ *outbound* gateway, and from generic AI gateways (TrueFoundry, Portkey, ), which
215
+ govern an operator's outbound LLM calls. This is the *inbound*
216
+ bouncer.
217
+
218
+ A worked, real-world integration is documented in
219
+ **[CASE-STUDY-offworld.md](CASE-STUDY-offworld.md)** (gating comments on a live
220
+ news site). That's a *case study* — an example of using the tool, not the product
221
+ itself.
222
+
223
+ ---
224
+
225
+ ## API reference
226
+
227
+ - **`verifyAgent(token, opts)`** — the core. Verifies against the registry and
228
+ applies your policy. Returns a structured verdict.
229
+ - `audience` — your platform id. The AIT's `aud` must equal it. (Matched
230
+ locally: the registry only checks that `aud` is non-empty, not that it equals
231
+ you. That check is yours.)
232
+ - `requireScopes` — checked against the trustworthy `effective_scope`.
233
+ - `minTier` — `email | domain | verified | kyb_individual | kyb_organization`.
234
+ - `blockedOperators` / `approvedOperators` — deny/allow lists by operator id.
235
+ - `registryBaseUrl` — defaults to `https://registry.axisprime.ai`.
236
+ - **`aitGate(opts)`** — returns `(request) => Promise<verdict>`; binds your policy
237
+ to a request gate for Workers and any `fetch`-style `Request`. Pulls the AIT
238
+ from `Authorization: Bearer <ait>`, `X-AXIS-Token`, or `?ait=`.
239
+ - **`axisGate(opts)`** *(subpath `axis-platform-sdk/express`)* — the same as
240
+ Express/Connect middleware: `(req, res, next)`. On accept it sets `req.axis` and
241
+ calls `next()`; on deny it responds `401` / `403` / `503` with `{ error,
242
+ message }`. Imports nothing from Express (zero-dep), so it also works on Connect
243
+ and bare `http`.
244
+ - **`denialResponse(verdict)`** — turns a denied verdict into a 401/403 `Response`.
245
+ - **`scopeCovers(granted, required)` / `coversAll(granted, required[])`** — the
246
+ AXIS scope matcher (ported verbatim from the operator-side gateway's, so
247
+ operator and platform sides agree).
248
+ - **`enrich(agentId, token, opts)`** — fetch the agent's presentation layer
249
+ (display name, tier) for a console UI.
250
+ - **`loadAccessPolicy(platformBaseUrl)`** — read a platform's published
251
+ `/.well-known/axis-access` door policy.
252
+ - **`decodeAitPayload(token)`** — read the AIT payload (claims) without verifying.
253
+ For the `aud` check; never trust it for authorization.
254
+ - **`AccessLedger` / `MemoryLedgerStore` / `loggedGate(gate, ledger, opts)` /
255
+ `recordEntry(verdict, opts)`** — the access ledger (who showed up). `loggedGate`
256
+ wraps a gate so every verdict is logged.
257
+ - **`Blocklist` / `MemoryBlocklistStore` / `gatedWithBlocklist(gate, blocklist)`**
258
+ — the runtime block list (by operator and by agent). `blockOperator` /
259
+ `blockAgent` / `unblock*` / `isAgentBlocked` / `blockedOperatorIds` /
260
+ `checkVerdict`.
261
+ - **`reportFlag(args, opts)` / `blockAndReport(blocklist, args, opts)`** — sign a
262
+ negative Trust Attestation and send it to a reputation index (OFF by default).
263
+ - **`getPlatformKey(opts)` / `buildAttestation` / `signAttestation` /
264
+ `verifyAttestation` / `MemoryKeyStore`** — the platform's Ed25519 key + TA
265
+ build/sign/verify primitives (WebCrypto, zero-dep).
266
+
267
+ ### Gates as policy: `SwitchAuthorizer` (the free-tier engine)
268
+
269
+ Identity verification is fixed and core. The authorization *decision* is a
270
+ pluggable layer — the **Authorizer port**. `SwitchAuthorizer` is the free-tier
271
+ implementation: config-driven on/off gates. Its `policy` object is exactly what a
272
+ console's "door policy" screen edits and saves.
273
+
274
+ ```js
275
+ import { SwitchAuthorizer, denialResponse } from 'axis-platform-sdk';
276
+
277
+ const door = new SwitchAuthorizer({
278
+ audience: 'comments.mysite.com',
279
+ defaultAllow: false,
280
+ gates: {
281
+ 'content:comment': { enabled: true, requireScopes: ['content:comment'], minTier: 'domain' },
282
+ },
283
+ });
284
+
285
+ const verdict = await door.gate('content:comment')(request);
286
+ if (!verdict.accepted) return denialResponse(verdict);
287
+ ```
288
+
289
+ Flip `enabled: false` and the gate closes, no code change. The port is
290
+ engine-agnostic: a paid `EngineAuthorizer` (Permify / OpenFGA sidecar) for
291
+ granular relationship/attribute rules drops into the same slot with the same
292
+ `authorize(token, gateId, ctx)` shape. The demo and free tier need no engine.
293
+
294
+ ### Stateful half: ledger, blocklist, reputation report-back
295
+
296
+ `verifyAgent` / `aitGate` are stateless verdict machines. A self-hosting platform
297
+ also needs **state it owns**: a record of who showed up, a runtime block list, and
298
+ a way to report a bad actor onward. These three modules add that, all zero-infra —
299
+ the platform runs the stores in its OWN store (default in-memory; plug in D1 /
300
+ SQLite / Postgres via a documented adapter shape).
301
+
302
+ ```js
303
+ import { aitGate, AccessLedger, loggedGate } from 'axis-platform-sdk';
304
+
305
+ const ledger = new AccessLedger(); // default in-memory store
306
+ const gate = loggedGate(aitGate({ audience }), ledger, { audience });
307
+
308
+ const verdict = await gate(request); // every verdict is logged
309
+ await ledger.recent({ limit: 25 }); // newest-first arrivals
310
+ await ledger.byOperator('axis:acme:op'); // arrivals from one operator
311
+ ```
312
+
313
+ Each entry records `{ agent_id, operator_id, created_at, tier, delegation_valid,
314
+ effective_scope, gate_id, requested_action, display_name, decision, reason,
315
+ audience }` — the same shape the cloud-hosted version uses for its `arrivals`
316
+ record, so the SDK and the cloud product share one arrival definition. `decision`
317
+ is `auto_allow | denied |
318
+ held | approved | booted`; `created_at` is epoch ms. Only the trustworthy
319
+ `effective_scope` is recorded, never the AIT's self-declared scope. A ledger write
320
+ failure never changes the verdict.
321
+
322
+ ```js
323
+ import { Blocklist, verifyAgent } from 'axis-platform-sdk';
324
+
325
+ const blocklist = new Blocklist();
326
+ await blocklist.blockAgent('axis:acme:bot', 'spammed'); // agent-level
327
+ await blocklist.blockOperator('axis:bad:op', 'whole op'); // operator-level
328
+
329
+ let verdict = await verifyAgent(token, {
330
+ audience,
331
+ blockedOperators: [...staticBlocked, ...(await blocklist.blockedOperatorIds())],
332
+ });
333
+ verdict = await blocklist.checkVerdict(verdict); // flips to denied if agent/op blocked
334
+ ```
335
+
336
+ When you boot an agent, you know something the network doesn't. `reportFlag`
337
+ builds a protocol-shaped Trust Attestation (AXIS Layer 3; SPEC §4.5), signs it
338
+ with the platform's own Ed25519 key (generated + persisted on first use via
339
+ `getPlatformKey`, WebCrypto), and POSTs it to a configurable reputation index.
340
+ Report-back is **OFF by default** — unconfigured, it's a graceful no-op. The
341
+ reputation index is a separate, future, commercial service — NOT the canonical
342
+ registry (which stays identity-only). See `examples/bouncer-worker.js` for a
343
+ reference admin surface over the ledger + blocklist, and `templates/` for the
344
+ deployable starters.
345
+
346
+ ### Single source of truth: the SDK is the port, the cloud-hosted version is the adapter
347
+
348
+ | SDK (this package, the port) | Cloud-hosted version (the D1-backed product adapter) |
349
+ | ----------------------------------- | ----------------------------------------------- |
350
+ | `AccessLedger` + `recordEntry` | `arrivals` table + `recordArrival()` |
351
+ | `Blocklist` (operator-level) | `operator_blocks` table + `blockedOperators()` |
352
+ | `SwitchAuthorizer` `policy` | `door_policy` table (serialized policy) |
353
+ | `Blocklist` agent-level *(superset)* | *(not yet — an additive `agent_blocks` table)* |
354
+ | `reportFlag` / reputation emit *(new)* | *(not yet — the open emit half is here)* |
355
+
356
+ The entry/meta shapes are deliberately byte-compatible with the cloud-hosted
357
+ version's columns so there is **one** arrival/block record across the SDK and the
358
+ cloud product — fold in, don't duplicate. A platform that needs its own store
359
+ implements the documented adapter shape; the cloud-hosted version (in alpha) is the
360
+ worked example of doing exactly that over Cloudflare D1.
361
+
362
+ ## Install
363
+
364
+ ```
365
+ npm install axis-platform-sdk
366
+ ```
367
+
368
+ Zero dependencies. Node 20+, Cloudflare Workers, modern browsers. Ships TypeScript
369
+ declarations (the package is authored in plain JS).
370
+
371
+ ## Staying up to date
372
+
373
+ The self-host path needs no account, so we don't know who's running it (by design)
374
+ and can't push you notices. Updates are **pull-based**:
375
+
376
+ - **Versioning is semver.** Patch/minor releases are backward-compatible; anything
377
+ breaking is a major. Pin a caret range (`^`) and you get safe updates.
378
+ - **Watch the channel:** [GitHub Releases](https://github.com/MachinesOfDesire/axis-platform-sdk/releases)
379
+ and the [CHANGELOG](CHANGELOG.md) are the source of truth for what changed. Use
380
+ Dependabot or Renovate to get an automatic PR when a new version ships.
381
+ - **The verification protocol is versioned too.** The registry's `/verify` contract
382
+ is backward-compatible within a protocol major; deprecations are announced in
383
+ Releases ahead of removal.
384
+
385
+ An opt-in updates list for platform integrators (security and breaking-change
386
+ notices) is planned. Until then, watching the repo is the way.
387
+
388
+ ## License
389
+
390
+ Apache-2.0. © Kipple Labs, Inc.