axis-platform-sdk 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,97 @@
1
+ # Changelog
2
+
3
+ All notable changes to `axis-platform-sdk`. Pre-release; not yet published to npm.
4
+
5
+ ## [0.2.0] — 2026-06-25 (unreleased)
6
+
7
+ First npm publish + public repo. Adds the stateful half (ledger, blocklist,
8
+ reputation report-back) and reconciles it with Owyhee "The Door" (governor#27)
9
+ so the SDK and the product share one arrival/block shape.
10
+
11
+ ### Added — the stateful half (ledger, blocklist, reputation report-back)
12
+
13
+ - **`AccessLedger` (`src/ledger.js`)** — the platform's "who showed up" record.
14
+ Logs every verdict to a pluggable store (default in-memory `MemoryLedgerStore`;
15
+ documented adapter shape for D1 / SQLite / Postgres). `recent()` / `byOperator()`
16
+ query helpers. `loggedGate(gate, ledger, fields)` wraps any gate so verdicts are
17
+ logged as a side effect (a store failure never changes the verdict). Only the
18
+ trustworthy `effective_scope` is recorded.
19
+ - **`Blocklist` (`src/blocklist.js`)** — a runtime, stateful block list over the
20
+ same adapter shape. Blocks by `operator_id` AND by `agent_id` (agent-level
21
+ blocking is the SDK's superset over The Door's operator-only `operator_blocks`).
22
+ `blockedOperatorIds()` feeds verifyAgent's `blockedOperators`; `checkVerdict()`
23
+ enforces agent-level blocks post-verify (needs the resolved agent_id).
24
+ `gatedWithBlocklist(gate, blocklist)` wraps a gate.
25
+
26
+ ### Reconciled with The Door (single source of truth, no duplication)
27
+
28
+ - The ledger entry shape is now byte-compatible with The Door's `ArrivalRecord`
29
+ / `arrivals` columns: carries `tier`, `delegation_valid`, `gate_id`,
30
+ `requested_action`, `display_name`; `created_at` is epoch ms (was an ISO `ts`);
31
+ `decision` uses the `auto_allow | denied | held | approved | booted` vocabulary
32
+ (was `accepted | denied`), with the manual-review states available via a
33
+ `recordEntry(..., { decision })` override.
34
+ - Blocklist meta is `{ reason, created_at }` (epoch ms), matching `operator_blocks`.
35
+ - The SDK is positioned as the **port + in-memory default**; The Door is the
36
+ canonical **D1-backed adapter**. README documents the mapping table. The SDK
37
+ ships the genuinely-new pieces The Door lacks (agent-level blocking, reputation
38
+ emit); The Door keeps its own D1 state layer and, post-publish, depends on this
39
+ package instead of vendoring it.
40
+ - **Reputation report-back (`src/reportback.js`)** — `reportFlag()` builds a
41
+ protocol-shaped negative **Trust Attestation** (AXIS Layer 3; SPEC §4.5),
42
+ signs it with the platform's own Ed25519 key (`getPlatformKey()`, WebCrypto,
43
+ generated + persisted via a pluggable key store), and POSTs `{ attestation,
44
+ platform_public_key, signature }` to a configurable `reputationUrl`. OFF by
45
+ default — unconfigured is a graceful no-op (never throws). `blockAndReport()`
46
+ blocks locally + reports. `buildAttestation` / `signAttestation` /
47
+ `verifyAttestation` exposed. Zero-dep (no Buffer; WebCrypto + inline base64url
48
+ + inline JCS, byte-for-byte matching axis-protocol-sdk's signing convention).
49
+ - **`examples/bouncer-worker.js`** — reference stateful bouncer: a Worker admin
50
+ over the ledger + blocklist (`/admin/arrivals` enriched for display,
51
+ `/admin/boot` = block + report, plus a tiny HTML console). A reference, not a
52
+ product; in-memory stores.
53
+
54
+ ### Notes
55
+
56
+ - The reputation index is a SEPARATE, future, commercial service — NOT the
57
+ canonical registry, which stays identity-only (Layer 1 + Layer 2). The
58
+ `axis-reputation` stub receiver accepts-and-discards until the index is built.
59
+ - New tests for ledger / blocklist / reportback + the Door-compatible shape.
60
+ Full suite: 40 passing (`node --test`).
61
+
62
+ ## [0.1.0] — 2026-06-16 (unreleased)
63
+
64
+ First cut of the platform/verifier ("bouncer") side of AXIS.
65
+
66
+ ### Added
67
+
68
+ - **`verifyAgent(token, opts)`** — verifies an AIT against the registry
69
+ (signature + revocation + delegation chain, all server-side) and applies a
70
+ platform's policy (audience match, required scopes against the trustworthy
71
+ `effective_scope`, blocked/approved operators, minimum verification tier).
72
+ Returns one structured verdict.
73
+ - **`aitGate(opts)` / `extractToken()` / `denialResponse()`** — a drop-in
74
+ request gate for Cloudflare Workers (and Request-like objects). Pulls the AIT
75
+ from `Authorization: Bearer`, `X-AXIS-Token`, or `?ait=`.
76
+ - **`SwitchAuthorizer`** — the free-tier gate engine and the first implementation
77
+ of the Authorizer port. Config-driven on/off gates with optional minimum tier,
78
+ required scopes, and operator allow/block lists. Its `policy` object is what a
79
+ "Door policy" screen edits and saves. The port is engine-agnostic so a paid
80
+ `EngineAuthorizer` (Permify / OpenFGA sidecar) can drop into the same slot.
81
+ - **`scopeCovers` / `coversAll`** — AXIS scope matcher, ported verbatim from the
82
+ operator-side gateway so both sides agree on scope semantics.
83
+ - **`enrich()`**, **`loadAccessPolicy()`**, **`decodeAitPayload()`** helpers.
84
+ - Worked example: `examples/toy-platform-worker.js` (a bouncer comments service
85
+ gated by a `SwitchAuthorizer`).
86
+
87
+ ### Tested
88
+
89
+ - 15 unit tests (`node --test`) across scope matching, the verify verdict
90
+ matrix, and the SwitchAuthorizer gate logic. `loadAccessPolicy` additionally
91
+ verified live against `registry.axisprime.ai`.
92
+
93
+ ### Notes
94
+
95
+ - Zero runtime dependencies; runs in Node 20+, Cloudflare Workers, browsers.
96
+ - Identity verification is fixed/core; only the authorization decision is
97
+ pluggable. The demo and the free tier need no external policy engine.
package/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ Copyright (c) Kipple Labs, Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
14
+
15
+ NOTE (pre-release): drop the full Apache-2.0 license text into this file
16
+ before publishing, to match the sibling axis-* repos.
package/NOTICE ADDED
@@ -0,0 +1,11 @@
1
+ axis-platform-sdk
2
+ Copyright 2026 Kipple Labs, Inc.
3
+
4
+ This product was originally developed by Joshua Ashcroft prior
5
+ to the formation of Kipple Labs, Inc. All intellectual property
6
+ was assigned to Kipple Labs, Inc. at corporate formation under
7
+ the Confidential Information and Invention Assignment Agreement
8
+ dated April 24, 2026.
9
+
10
+ The "AXIS," "AXIS Protocol," "AXIS Prime," "N7," and "Kipple Labs"
11
+ names are trademarks of Kipple Labs, Inc.
package/README.md ADDED
@@ -0,0 +1,218 @@
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.
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Reference bouncer — the STATEFUL demo.
3
+ *
4
+ * The toy-platform-worker shows the stateless verdict path (verify + door
5
+ * policy). THIS worker adds the stateful half: an access ledger ("who showed
6
+ * up") and a runtime blocklist ("boot this one"), plus a tiny admin surface
7
+ * over both. It's a REFERENCE — wiring you'd copy into a real platform — not a
8
+ * product. The stores here are in-memory, so state resets when the isolate
9
+ * recycles; a real deployment plugs D1 / KV / Postgres into the same adapter
10
+ * shape (see ledger.js / blocklist.js).
11
+ *
12
+ * Run locally: wrangler dev examples/bouncer-worker.js
13
+ *
14
+ * Endpoints:
15
+ * POST /comment the gated action (verify + ledger + blocklist)
16
+ * GET /admin/arrivals recent agents, enriched for display
17
+ * POST /admin/boot { agent_id | operator_id, reason } -> block + report
18
+ * GET /admin a tiny HTML page over the two above
19
+ */
20
+ import {
21
+ SwitchAuthorizer,
22
+ verifyAgent,
23
+ extractToken,
24
+ denialResponse,
25
+ AccessLedger,
26
+ Blocklist,
27
+ enrich,
28
+ blockAndReport,
29
+ } from '../src/index.js';
30
+
31
+ const AUDIENCE = 'comments.demo-platform.example';
32
+ const PLATFORM_ID = 'axis:demo-platform:door'; // this platform's AXIS id, the attestor on reports
33
+ // const REPUTATION_URL = 'https://axis-reputation.example/attestations'; // OFF by default
34
+
35
+ // Stateless verdict engine (the editable "Door policy").
36
+ const door = new SwitchAuthorizer({
37
+ audience: AUDIENCE,
38
+ defaultAllow: false,
39
+ gates: { 'comments:write': { enabled: true, requireScopes: ['comments:write'] } },
40
+ });
41
+
42
+ // Stateful stores (in-memory reference; swap in D1/KV/Postgres in production).
43
+ const ledger = new AccessLedger();
44
+ const blocklist = new Blocklist();
45
+
46
+ export default {
47
+ async fetch(request) {
48
+ const url = new URL(request.url);
49
+
50
+ // --- the gated action ---------------------------------------------------
51
+ if (url.pathname === '/comment' && request.method === 'POST') {
52
+ // Inject runtime operator-blocks into the policy, then verify.
53
+ const dynBlocked = await blocklist.blockedOperatorIds();
54
+ const base = door.optsForGate('comments:write');
55
+ let verdict = await verifyAgent(extractToken(request), {
56
+ ...base,
57
+ blockedOperators: [...(base.blockedOperators || []), ...dynBlocked],
58
+ });
59
+ // Agent-level runtime block (needs the resolved agent_id from the verdict).
60
+ verdict = await blocklist.checkVerdict(verdict);
61
+
62
+ // Log the arrival regardless of decision.
63
+ await ledger.record(verdict, { audience: AUDIENCE }).catch(() => {});
64
+
65
+ if (!verdict.accepted) return denialResponse(verdict);
66
+ const body = await request.json().catch(() => ({}));
67
+ return Response.json({ ok: true, posted_by: verdict.agent_id, comment: body.text || '' });
68
+ }
69
+
70
+ // --- admin: recent arrivals (enriched for display) ----------------------
71
+ if (url.pathname === '/admin/arrivals' && request.method === 'GET') {
72
+ const limit = Number(url.searchParams.get('limit') || 25);
73
+ const rows = await ledger.recent({ limit });
74
+ // Enrich accepted arrivals with display name / tier for the console. We
75
+ // have no AIT here (presentation layer needs one), so this resolves only
76
+ // the public layer — display_name/tier may be null. That's expected.
77
+ const enriched = await Promise.all(
78
+ rows.map(async (r) => {
79
+ let display_name = null;
80
+ let tier = null;
81
+ if (r.agent_id) {
82
+ try {
83
+ const info = await enrich(r.agent_id, null);
84
+ display_name = info.display_name;
85
+ tier = info.tier;
86
+ } catch {
87
+ /* registry unreachable; show raw id */
88
+ }
89
+ }
90
+ return { ...r, display_name, tier };
91
+ })
92
+ );
93
+ return Response.json({ arrivals: enriched });
94
+ }
95
+
96
+ // --- admin: boot an agent or operator (block + best-effort report) ------
97
+ if (url.pathname === '/admin/boot' && request.method === 'POST') {
98
+ const body = await request.json().catch(() => ({}));
99
+ const reason = body.reason || 'booted by platform admin';
100
+ if (body.agent_id) {
101
+ // Block the agent locally AND report it (no-op send unless REPUTATION_URL set).
102
+ const out = await blockAndReport(
103
+ blocklist,
104
+ { platformId: PLATFORM_ID, agentId: body.agent_id, operatorId: body.operator_id, category: body.category || 'abuse', reason },
105
+ { /* reputationUrl: REPUTATION_URL */ }
106
+ );
107
+ return Response.json({ ok: true, ...out });
108
+ }
109
+ if (body.operator_id) {
110
+ await blocklist.blockOperator(body.operator_id, reason);
111
+ return Response.json({ ok: true, blocked_operator: body.operator_id });
112
+ }
113
+ return Response.json({ ok: false, error: 'provide agent_id or operator_id' }, { status: 400 });
114
+ }
115
+
116
+ // --- admin: tiny HTML console ------------------------------------------
117
+ if (url.pathname === '/admin') {
118
+ return new Response(ADMIN_HTML, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
119
+ }
120
+
121
+ return new Response('AXIS stateful bouncer reference. See /admin.\n', { status: 200 });
122
+ },
123
+ };
124
+
125
+ const ADMIN_HTML =`<!doctype html><meta charset=utf-8>
126
+ <title>AXIS bouncer — admin</title>
127
+ <style>
128
+ body{font:14px/1.5 system-ui,sans-serif;margin:2rem;max-width:60rem}
129
+ h1{font-size:1.2rem} table{border-collapse:collapse;width:100%;margin:1rem 0}
130
+ th,td{border:1px solid #ccc;padding:.35rem .5rem;text-align:left;font-size:13px}
131
+ .denied,.booted{color:#b00} .auto_allow,.approved{color:#070} .held{color:#a60}
132
+ button{cursor:pointer} code{background:#f3f3f3;padding:0 .25rem}
133
+ </style>
134
+ <h1>AXIS bouncer — arrivals</h1>
135
+ <p>Who showed up at <code>${AUDIENCE}</code>. Click <b>boot</b> to block + report an agent.</p>
136
+ <table id=t><thead><tr><th>when</th><th>agent</th><th>operator</th><th>scope</th><th>decision</th><th></th></tr></thead><tbody></tbody></table>
137
+ <script>
138
+ async function load(){
139
+ const r=await fetch('/admin/arrivals?limit=50');const {arrivals}=await r.json();
140
+ const tb=document.querySelector('#t tbody');tb.innerHTML='';
141
+ for(const a of arrivals){
142
+ const tr=document.createElement('tr');
143
+ const name=a.display_name?a.display_name+' ':'';
144
+ const when=a.created_at?new Date(a.created_at).toISOString().replace('T',' ').slice(0,19):'';
145
+ tr.innerHTML='<td>'+when+'</td><td>'+name+'<code>'+(a.agent_id||'?')+'</code></td>'+
146
+ '<td><code>'+(a.operator_id||'?')+'</code></td><td>'+(a.effective_scope||[]).join(', ')+'</td>'+
147
+ '<td class="'+a.decision+'">'+a.decision+'</td>'+
148
+ '<td>'+(a.agent_id?'<button data-a="'+a.agent_id+'">boot</button>':'')+'</td>';
149
+ tb.appendChild(tr);
150
+ }
151
+ tb.querySelectorAll('button').forEach(b=>b.onclick=async()=>{
152
+ await fetch('/admin/boot',{method:'POST',headers:{'Content-Type':'application/json'},
153
+ body:JSON.stringify({agent_id:b.dataset.a,reason:'booted from console',category:'abuse'})});
154
+ load();
155
+ });
156
+ }
157
+ load();
158
+ </script>`;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Toy "bouncer" platform — the backend for the full-loop demo.
3
+ *
4
+ * A pretend comments service that only accepts AXIS-verified agents holding
5
+ * `comments:write`. It gates with a SwitchAuthorizer driven by a `door` policy
6
+ * object — which is exactly what a "Door policy" screen in Owyhee edits and
7
+ * saves. Flip `enabled` to false and the gate closes with no code change.
8
+ *
9
+ * Run locally: wrangler dev examples/toy-platform-worker.js
10
+ */
11
+ import { SwitchAuthorizer, denialResponse } from '../src/index.js';
12
+
13
+ const AUDIENCE = 'comments.demo-platform.example';
14
+
15
+ // The free-tier gate engine. This object is the editable "Door policy".
16
+ const door = new SwitchAuthorizer({
17
+ audience: AUDIENCE,
18
+ defaultAllow: false,
19
+ gates: {
20
+ 'comments:write': {
21
+ enabled: true,
22
+ requireScopes: ['comments:write'],
23
+ // minTier: 'domain', // require domain-verified+ to comment
24
+ // blockedOperators: ['axis:spammer:operator'],
25
+ },
26
+ },
27
+ });
28
+
29
+ export default {
30
+ async fetch(request) {
31
+ const url = new URL(request.url);
32
+
33
+ // The gated action. The bouncer verifies + applies the door policy.
34
+ if (url.pathname === '/comment' && request.method === 'POST') {
35
+ const verdict = await door.gate('comments:write')(request);
36
+ if (!verdict.accepted) return denialResponse(verdict); // 401/403 + reason
37
+
38
+ const body = await request.json().catch(() => ({}));
39
+ return Response.json({
40
+ ok: true,
41
+ posted_by: verdict.agent_id,
42
+ operator: verdict.operator_id,
43
+ scope: verdict.effective_scope,
44
+ comment: body.text || '',
45
+ });
46
+ }
47
+
48
+ // Publish our door policy so AIT issuers know our audience + requirements.
49
+ if (url.pathname === '/.well-known/axis-access') {
50
+ return Response.json({
51
+ axis_version: '0.3',
52
+ platform_id: AUDIENCE,
53
+ audience: AUDIENCE,
54
+ access_policy: { minimum_verification_level: 'email', required_scopes: ['comments:write'], allow_unverified: false },
55
+ });
56
+ }
57
+
58
+ return new Response('AXIS bouncer demo. POST /comment with an AIT to get in.\n', { status: 200 });
59
+ },
60
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "axis-platform-sdk",
3
+ "version": "0.2.0",
4
+ "description": "Platform-side SDK for the AXIS protocol. The verifier/'bouncer' side: when an AXIS agent shows up at your platform, verify its identity + delegation + scope and decide whether to accept, scope, or boot it. Zero runtime dependencies; runs in Node 20+, Cloudflare Workers, and modern browsers.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./scope": "./src/scope.js",
10
+ "./gate": "./src/gate.js",
11
+ "./authorizer": "./src/authorizer.js",
12
+ "./ledger": "./src/ledger.js",
13
+ "./blocklist": "./src/blocklist.js",
14
+ "./reportback": "./src/reportback.js"
15
+ },
16
+ "scripts": {
17
+ "test": "node --test test/scope.test.js test/verify.test.js test/authorizer.test.js test/ledger.test.js test/blocklist.test.js test/reportback.test.js"
18
+ },
19
+ "keywords": [
20
+ "axis",
21
+ "axis-protocol",
22
+ "agent-identity",
23
+ "verifier",
24
+ "platform",
25
+ "authorization"
26
+ ],
27
+ "author": "Kipple Labs, Inc.",
28
+ "license": "Apache-2.0",
29
+ "engines": {
30
+ "node": ">=20.0.0"
31
+ },
32
+ "homepage": "https://github.com/MachinesOfDesire/axis-platform-sdk",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/MachinesOfDesire/axis-platform-sdk.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/MachinesOfDesire/axis-platform-sdk/issues"
39
+ },
40
+ "files": [
41
+ "src/",
42
+ "examples/",
43
+ "README.md",
44
+ "CHANGELOG.md",
45
+ "LICENSE",
46
+ "NOTICE"
47
+ ]
48
+ }