axis-platform-sdk 0.2.1 → 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/CASE-STUDY-offworld.md +56 -0
- package/CHANGELOG.md +179 -112
- package/QUICKSTART.md +168 -0
- package/README.md +390 -218
- package/badges/README.md +41 -0
- package/badges/verified-by-axis-compact.svg +7 -0
- package/badges/verified-by-axis-dark.svg +7 -0
- package/badges/verified-by-axis.svg +7 -0
- package/examples/toy-platform-worker.js +60 -60
- package/package.json +54 -49
- package/src/authorizer.js +101 -101
- package/src/blocklist.js +183 -183
- package/src/express.d.ts +30 -0
- package/src/express.js +68 -0
- package/src/index.d.ts +288 -288
- package/src/ledger.js +170 -171
- package/src/scope.js +46 -46
- package/templates/cloudflare-worker/README.md +79 -0
- package/templates/cloudflare-worker/package.json +17 -0
- package/templates/cloudflare-worker/src/worker.js +162 -0
- package/templates/cloudflare-worker/wrangler.toml +20 -0
- package/templates/node-express/README.md +113 -0
- package/templates/node-express/package.json +19 -0
- package/templates/node-express/server.js +182 -0
- package/templates/node-express/smoke.js +63 -0
package/src/ledger.js
CHANGED
|
@@ -1,171 +1,170 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Access ledger — the platform's "who showed up at my door" record.
|
|
3
|
-
*
|
|
4
|
-
* Every time an agent presents and the gate reaches a verdict, the platform can
|
|
5
|
-
* log the arrival here. This is the STATEFUL half of the bouncer: verifyAgent /
|
|
6
|
-
* aitGate are stateless verdict machines; the ledger is the platform's own
|
|
7
|
-
* append-only record of what those verdicts were, kept in the platform's OWN
|
|
8
|
-
* store. Zero-infra by default (in-memory); a real platform plugs in D1 /
|
|
9
|
-
* SQLite / Postgres via the adapter shape below.
|
|
10
|
-
*
|
|
11
|
-
* Canonical adapter:
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* async
|
|
27
|
-
* async
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this._entries.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
out
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* '
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* @param {object}
|
|
90
|
-
* @param {
|
|
91
|
-
* @param {string} [fields.
|
|
92
|
-
* @param {string} [fields.
|
|
93
|
-
* @param {string} [fields.
|
|
94
|
-
* @param {string} [fields.
|
|
95
|
-
* @param {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
*
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* const
|
|
153
|
-
* const
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Access ledger — the platform's "who showed up at my door" record.
|
|
3
|
+
*
|
|
4
|
+
* Every time an agent presents and the gate reaches a verdict, the platform can
|
|
5
|
+
* log the arrival here. This is the STATEFUL half of the bouncer: verifyAgent /
|
|
6
|
+
* aitGate are stateless verdict machines; the ledger is the platform's own
|
|
7
|
+
* append-only record of what those verdicts were, kept in the platform's OWN
|
|
8
|
+
* store. Zero-infra by default (in-memory); a real platform plugs in D1 /
|
|
9
|
+
* SQLite / Postgres via the adapter shape below.
|
|
10
|
+
*
|
|
11
|
+
* Canonical adapter: the cloud-hosted version is the D1-backed product instance
|
|
12
|
+
* of this port — its `arrivals` table + `recordArrival()` ARE this ledger over
|
|
13
|
+
* D1. This module is the library form (the shape + an in-memory default); the
|
|
14
|
+
* cloud-hosted version is the production adapter. The entry shape below is
|
|
15
|
+
* deliberately byte-compatible with its `ArrivalRecord` / `arrivals` columns so
|
|
16
|
+
* there is ONE arrival record across the SDK and the cloud product, not two.
|
|
17
|
+
*
|
|
18
|
+
* Trust note: the only scope worth recording is `effective_scope` (the
|
|
19
|
+
* registry's chain-walked, trustworthy scope), which is exactly what a verdict
|
|
20
|
+
* carries. We never persist the AIT's self-declared scope.
|
|
21
|
+
*
|
|
22
|
+
* --- Adapter shape -------------------------------------------------------
|
|
23
|
+
* A store is any object implementing:
|
|
24
|
+
*
|
|
25
|
+
* async append(entry) -> void // persist one arrival
|
|
26
|
+
* async recent({ limit }) -> entry[] // newest first
|
|
27
|
+
* async byOperator(operatorId, { limit }) -> entry[] // newest first
|
|
28
|
+
*
|
|
29
|
+
* `entry` is the record shape produced by `recordEntry()` — the same fields The
|
|
30
|
+
* Door's `arrivals` table holds (minus the adapter's own PK/org columns):
|
|
31
|
+
* {
|
|
32
|
+
* agent_id, operator_id, created_at, // created_at = epoch ms (Date.now())
|
|
33
|
+
* tier, delegation_valid, effective_scope: string[],
|
|
34
|
+
* gate_id, requested_action, display_name,
|
|
35
|
+
* decision: 'auto_allow'|'denied'|'held'|'approved'|'booted',
|
|
36
|
+
* reason, audience
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* The same adapter shape is shared with blocklist.js (a tiny CRUD port). A
|
|
40
|
+
* platform implements both against whatever it already runs. The defaults here
|
|
41
|
+
* keep the demo and the free tier zero-infra.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default in-memory ledger store. Newest-first iteration. Bounded so a
|
|
46
|
+
* long-running Worker isolate doesn't grow without limit; override `max` (0 =
|
|
47
|
+
* unbounded) when you want the full history and have a real store behind it.
|
|
48
|
+
*/
|
|
49
|
+
export class MemoryLedgerStore {
|
|
50
|
+
constructor({ max = 10000 } = {}) {
|
|
51
|
+
this._entries = []; // chronological; newest pushed to the end
|
|
52
|
+
this.max = max;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async append(entry) {
|
|
56
|
+
this._entries.push(entry);
|
|
57
|
+
if (this.max && this._entries.length > this.max) {
|
|
58
|
+
this._entries.splice(0, this._entries.length - this.max);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async recent({ limit = 50 } = {}) {
|
|
63
|
+
const out = this._entries.slice(-limit);
|
|
64
|
+
out.reverse();
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async byOperator(operatorId, { limit = 50 } = {}) {
|
|
69
|
+
const out = [];
|
|
70
|
+
for (let i = this._entries.length - 1; i >= 0 && out.length < limit; i--) {
|
|
71
|
+
if (this._entries[i].operator_id === operatorId) out.push(this._entries[i]);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Normalize a verdict (from verifyAgent) into a ledger entry. The output is
|
|
79
|
+
* byte-compatible with the cloud-hosted version's `ArrivalRecord` so the SDK and
|
|
80
|
+
* the cloud product share one arrival shape.
|
|
81
|
+
*
|
|
82
|
+
* `decision` defaults from the verdict (accepted -> 'auto_allow', else
|
|
83
|
+
* 'denied'); a caller may override it with a manual-review state a real bouncer
|
|
84
|
+
* needs ('held' | 'approved' | 'booted'). `created_at` is epoch ms (Date.now()),
|
|
85
|
+
* matching the cloud-hosted version's column; for an ISO string use
|
|
86
|
+
* `new Date(entry.created_at).toISOString()`.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} verdict A verifyAgent verdict.
|
|
89
|
+
* @param {object} [fields]
|
|
90
|
+
* @param {string} [fields.audience] Platform audience (optional metadata; the cloud-hosted version doesn't persist it).
|
|
91
|
+
* @param {string} [fields.gate_id] Which gate was requested.
|
|
92
|
+
* @param {string} [fields.requested_action] Human-facing action label.
|
|
93
|
+
* @param {string} [fields.display_name] Enriched presentation name.
|
|
94
|
+
* @param {string} [fields.decision] Override decision ('held'|'approved'|'booted').
|
|
95
|
+
* @param {number} [fields.created_at] Override timestamp (epoch ms).
|
|
96
|
+
*/
|
|
97
|
+
export function recordEntry(verdict, { audience, gate_id, requested_action, display_name, decision, created_at } = {}) {
|
|
98
|
+
const v = verdict || {};
|
|
99
|
+
return {
|
|
100
|
+
agent_id: v.agent_id || null,
|
|
101
|
+
operator_id: v.operator_id || null,
|
|
102
|
+
created_at: created_at || Date.now(),
|
|
103
|
+
tier: v.tier || null,
|
|
104
|
+
delegation_valid: v.delegation_valid === true,
|
|
105
|
+
effective_scope: Array.isArray(v.effective_scope) ? v.effective_scope : [],
|
|
106
|
+
gate_id: gate_id || null,
|
|
107
|
+
requested_action: requested_action || null,
|
|
108
|
+
display_name: display_name || null,
|
|
109
|
+
decision: decision || (v.accepted ? 'auto_allow' : 'denied'),
|
|
110
|
+
reason: v.accepted ? null : v.reason || v.code || 'denied',
|
|
111
|
+
audience: audience || null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The platform's access ledger. Wraps a store and gives you the log helpers a
|
|
117
|
+
* "who's using my platform" view needs.
|
|
118
|
+
*/
|
|
119
|
+
export class AccessLedger {
|
|
120
|
+
constructor({ store } = {}) {
|
|
121
|
+
this.store = store || new MemoryLedgerStore();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Log a verdict. Returns the persisted entry. `fields` carries the same
|
|
126
|
+
* optional arrival fields as `recordEntry` (audience, gate_id,
|
|
127
|
+
* requested_action, display_name, decision override).
|
|
128
|
+
*/
|
|
129
|
+
async record(verdict, fields = {}) {
|
|
130
|
+
const entry = recordEntry(verdict, fields);
|
|
131
|
+
await this.store.append(entry);
|
|
132
|
+
return entry;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Most recent arrivals, newest first. */
|
|
136
|
+
async recent(opts = {}) {
|
|
137
|
+
return this.store.recent(opts);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Recent arrivals from a single operator, newest first. */
|
|
141
|
+
async byOperator(operatorId, opts = {}) {
|
|
142
|
+
return this.store.byOperator(operatorId, opts);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Wrap a gate (or any `(request) => Promise<verdict>`) so every verdict is
|
|
148
|
+
* logged to a ledger before it's returned. Drop-in around `aitGate(...)` or
|
|
149
|
+
* `authorizer.gate(gateId)`:
|
|
150
|
+
*
|
|
151
|
+
* const ledger = new AccessLedger();
|
|
152
|
+
* const gate = loggedGate(aitGate({ audience }), ledger, { audience, gate_id });
|
|
153
|
+
* const verdict = await gate(request); // logged as a side effect
|
|
154
|
+
*
|
|
155
|
+
* `fields` are the static arrival fields to stamp on every entry (audience,
|
|
156
|
+
* gate_id, requested_action, display_name). Logging failures never block the
|
|
157
|
+
* request — a store hiccup must not turn an accepted agent away. The verdict is
|
|
158
|
+
* always returned.
|
|
159
|
+
*/
|
|
160
|
+
export function loggedGate(gate, ledger, fields = {}) {
|
|
161
|
+
return async function loggingGate(request) {
|
|
162
|
+
const verdict = await gate(request);
|
|
163
|
+
try {
|
|
164
|
+
await ledger.record(verdict, fields);
|
|
165
|
+
} catch {
|
|
166
|
+
/* never let a ledger write failure change the verdict */
|
|
167
|
+
}
|
|
168
|
+
return verdict;
|
|
169
|
+
};
|
|
170
|
+
}
|
package/src/scope.js
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
// Scope matching for the AXIS scope grammar (spec v0.2 §4.4).
|
|
2
|
-
//
|
|
3
|
-
// Ported verbatim from
|
|
4
|
-
// (
|
|
5
|
-
// semantics exactly. Colon-separated segments; `*` is a wildcard for ONE
|
|
6
|
-
// segment, not multi; no recursion. If the protocol grammar changes, change it
|
|
7
|
-
// in both places.
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Does `granted` cover `required`? I.e. can a caller granted `granted` claim
|
|
11
|
-
* the permission `required`?
|
|
12
|
-
*
|
|
13
|
-
* scopeCovers('admin:*', 'admin:users') === true
|
|
14
|
-
* scopeCovers('admin:users', 'admin:users') === true
|
|
15
|
-
* scopeCovers('admin:users', 'admin:roles') === false
|
|
16
|
-
* scopeCovers('admin:*', 'admin:users:delete') === false // * = 1 segment
|
|
17
|
-
* scopeCovers('anthropic:complete', 'anthropic:complete') === true
|
|
18
|
-
*/
|
|
19
|
-
export function scopeCovers(granted, required) {
|
|
20
|
-
if (!granted || !required) return false;
|
|
21
|
-
if (granted === required) return true;
|
|
22
|
-
const g = granted.split(':');
|
|
23
|
-
const r = required.split(':');
|
|
24
|
-
if (g.length !== r.length) return false;
|
|
25
|
-
for (let i = 0; i < g.length; i++) {
|
|
26
|
-
if (g[i] === '*') continue;
|
|
27
|
-
if (g[i] !== r[i]) return false;
|
|
28
|
-
}
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Do the granted scopes cover EVERY required scope? Empty `required` is
|
|
34
|
-
* trivially satisfied. Returns the missing required scopes on failure so a
|
|
35
|
-
* caller can put them in a denial reason.
|
|
36
|
-
*
|
|
37
|
-
* @returns {{ ok: boolean, missing: string[] }}
|
|
38
|
-
*/
|
|
39
|
-
export function coversAll(granted, required) {
|
|
40
|
-
const grantedList = Array.isArray(granted) ? granted : [];
|
|
41
|
-
const missing = [];
|
|
42
|
-
for (const req of required) {
|
|
43
|
-
if (!grantedList.some((g) => scopeCovers(g, req))) missing.push(req);
|
|
44
|
-
}
|
|
45
|
-
return { ok: missing.length === 0, missing };
|
|
46
|
-
}
|
|
1
|
+
// Scope matching for the AXIS scope grammar (spec v0.2 §4.4).
|
|
2
|
+
//
|
|
3
|
+
// Ported verbatim from the operator-side gateway's scope matcher so the operator
|
|
4
|
+
// side (the outbound proxy chain-walk) and the platform side (this SDK) agree on
|
|
5
|
+
// scope semantics exactly. Colon-separated segments; `*` is a wildcard for ONE
|
|
6
|
+
// segment, not multi; no recursion. If the protocol grammar changes, change it
|
|
7
|
+
// in both places.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Does `granted` cover `required`? I.e. can a caller granted `granted` claim
|
|
11
|
+
* the permission `required`?
|
|
12
|
+
*
|
|
13
|
+
* scopeCovers('admin:*', 'admin:users') === true
|
|
14
|
+
* scopeCovers('admin:users', 'admin:users') === true
|
|
15
|
+
* scopeCovers('admin:users', 'admin:roles') === false
|
|
16
|
+
* scopeCovers('admin:*', 'admin:users:delete') === false // * = 1 segment
|
|
17
|
+
* scopeCovers('anthropic:complete', 'anthropic:complete') === true
|
|
18
|
+
*/
|
|
19
|
+
export function scopeCovers(granted, required) {
|
|
20
|
+
if (!granted || !required) return false;
|
|
21
|
+
if (granted === required) return true;
|
|
22
|
+
const g = granted.split(':');
|
|
23
|
+
const r = required.split(':');
|
|
24
|
+
if (g.length !== r.length) return false;
|
|
25
|
+
for (let i = 0; i < g.length; i++) {
|
|
26
|
+
if (g[i] === '*') continue;
|
|
27
|
+
if (g[i] !== r[i]) return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Do the granted scopes cover EVERY required scope? Empty `required` is
|
|
34
|
+
* trivially satisfied. Returns the missing required scopes on failure so a
|
|
35
|
+
* caller can put them in a denial reason.
|
|
36
|
+
*
|
|
37
|
+
* @returns {{ ok: boolean, missing: string[] }}
|
|
38
|
+
*/
|
|
39
|
+
export function coversAll(granted, required) {
|
|
40
|
+
const grantedList = Array.isArray(granted) ? granted : [];
|
|
41
|
+
const missing = [];
|
|
42
|
+
for (const req of required) {
|
|
43
|
+
if (!grantedList.some((g) => scopeCovers(g, req))) missing.push(req);
|
|
44
|
+
}
|
|
45
|
+
return { ok: missing.length === 0, missing };
|
|
46
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# AXIS gate — Cloudflare Worker starter
|
|
2
|
+
|
|
3
|
+
Gate a Cloudflare Worker on **AXIS verified-agent identity**. When an AI agent
|
|
4
|
+
shows up and wants to act, this verifies *who it is* and *what it's allowed to
|
|
5
|
+
do* before your handler runs.
|
|
6
|
+
|
|
7
|
+
Use this template if your platform already runs on Workers. **You do not need to
|
|
8
|
+
adopt Cloudflare just to use AXIS** — if you run a Node backend, use the
|
|
9
|
+
`node-express` starter instead. Both wrap the same zero-dependency engine.
|
|
10
|
+
|
|
11
|
+
## Run it
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
npx wrangler dev # http://localhost:8787
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Your published door policy:
|
|
20
|
+
curl -s localhost:8787/.well-known/axis-access
|
|
21
|
+
|
|
22
|
+
# No AIT -> 401:
|
|
23
|
+
curl -i -X POST localhost:8787/comments -H 'content-type: application/json' -d '{"text":"hi"}'
|
|
24
|
+
|
|
25
|
+
# Arrivals + boot console:
|
|
26
|
+
open http://localhost:8787/admin
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Deploy it
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx wrangler deploy
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
No bindings are required for the free standalone path — the Worker's only
|
|
36
|
+
outbound call is one HTTPS GET to the public AXIS registry.
|
|
37
|
+
|
|
38
|
+
## The drop-in
|
|
39
|
+
|
|
40
|
+
The minimal version is a few lines:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import { aitGate, denialResponse } from 'axis-platform-sdk';
|
|
44
|
+
|
|
45
|
+
const gate = aitGate({ audience: 'comments.mysite.com', requireScopes: ['content:comment'] });
|
|
46
|
+
|
|
47
|
+
export default {
|
|
48
|
+
async fetch(request) {
|
|
49
|
+
const verdict = await gate(request);
|
|
50
|
+
if (!verdict.accepted) return denialResponse(verdict); // 401/403 + reason
|
|
51
|
+
// verdict.agent_id is verified. Proceed.
|
|
52
|
+
return Response.json({ ok: true, by: verdict.agent_id });
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`src/worker.js` is the fuller version: a `SwitchAuthorizer` door policy (named
|
|
58
|
+
on/off gates you can edit like config), an access ledger ("who showed up"), a
|
|
59
|
+
runtime blocklist ("boot this one"), and a tiny `/admin` console.
|
|
60
|
+
|
|
61
|
+
## Options
|
|
62
|
+
|
|
63
|
+
`aitGate(opts)` / `verifyAgent(token, opts)` take:
|
|
64
|
+
|
|
65
|
+
| option | meaning |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `audience` | Your platform's stable id. The AIT's `aud` must equal it. |
|
|
68
|
+
| `requireScopes` | Scopes checked against the trustworthy `effective_scope`. |
|
|
69
|
+
| `minTier` | `email` < `domain` < `verified` < `kyb_individual` < `kyb_organization`. |
|
|
70
|
+
| `blockedOperators` / `approvedOperators` | Deny / allow by operator id. |
|
|
71
|
+
| `registryBaseUrl` | Defaults to `https://registry.axisprime.ai`. |
|
|
72
|
+
|
|
73
|
+
## Persisting state in production
|
|
74
|
+
|
|
75
|
+
The in-memory ledger/blocklist reset when the isolate recycles. For durable
|
|
76
|
+
arrivals + blocks, add a D1 binding in `wrangler.toml` and implement the SDK's
|
|
77
|
+
documented store adapter shape against it (see `ledger.js` / `blocklist.js` in
|
|
78
|
+
the SDK). The arrival record shape is byte-compatible with the cloud-hosted version
|
|
79
|
+
(in alpha, Q3 2026), so moving to it later is a lift, not a rewrite.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "axis-gate-worker-starter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Drop-in starter: gate a Cloudflare Worker platform on AXIS verified-agent identity.",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "wrangler dev",
|
|
9
|
+
"deploy": "wrangler deploy"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"axis-platform-sdk": "^0.2.1"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"wrangler": "^3.90.0"
|
|
16
|
+
}
|
|
17
|
+
}
|