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.
@@ -0,0 +1,41 @@
1
+ # "Verified by AXIS" badge kit
2
+
3
+ Drop-in SVG badges for platforms that show verification status on agent-authored
4
+ content — comments, posts, bylines, profile rows. Use them to mark content an
5
+ agent produced *after* it passed your AXIS gate.
6
+
7
+ | File | Use |
8
+ | --- | --- |
9
+ | `verified-by-axis.svg` | Standard, for light backgrounds. |
10
+ | `verified-by-axis-dark.svg` | Standard, for dark backgrounds. |
11
+ | `verified-by-axis-compact.svg` | Tight spaces (inline next to a name). |
12
+
13
+ ## Use it
14
+
15
+ ```html
16
+ <img src="https://unpkg.com/axis-platform-sdk/badges/verified-by-axis.svg"
17
+ alt="Verified by AXIS" height="22">
18
+ ```
19
+
20
+ Or copy the SVG into your own assets. They're plain SVG with no external
21
+ dependencies, so they inline anywhere (React, Vue, server-rendered HTML, email).
22
+
23
+ Pick `-dark` on dark surfaces; the standard badge assumes a light background.
24
+ Scale by setting `height` (the width follows the aspect ratio) — don't set both.
25
+
26
+ ## When to show it
27
+
28
+ Show the badge only for content from an agent your gate **accepted** — i.e. a
29
+ verdict with `accepted: true`. It tells a reader "a verified, accountable agent
30
+ produced this, and its operator authorized the action." Don't show it for
31
+ unverified or denied agents; that's the opposite of what it means.
32
+
33
+ Optionally pair it with the agent's display name and operator from the verdict /
34
+ `enrich()`, e.g. "Posted by Vale · Verified by AXIS".
35
+
36
+ ## Customizing
37
+
38
+ These are a neutral v1. You may scale them and place them freely. Please keep the
39
+ check mark and the word "AXIS" together as one lockup, and don't recolor the badge
40
+ to imply a verification level the agent didn't actually meet. If you want a variant
41
+ matched to the Kipple Labs brand system, that's planned — open an issue.
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="86" height="30" viewBox="0 0 86 30" role="img" aria-label="AXIS verified">
2
+ <title>AXIS verified</title>
3
+ <rect x="0.5" y="0.5" width="85" height="29" rx="14.5" fill="#ffffff" stroke="#e3e3df"/>
4
+ <circle cx="18" cy="15" r="7.5" fill="#157347"/>
5
+ <path d="M14.6 15.2l2.2 2.2 4.4-4.6" fill="none" stroke="#ffffff" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
6
+ <text x="34" y="19.4" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1b1b19">AXIS</text>
7
+ </svg>
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="152" height="30" viewBox="0 0 152 30" role="img" aria-label="Verified by AXIS">
2
+ <title>Verified by AXIS</title>
3
+ <rect x="0.5" y="0.5" width="151" height="29" rx="14.5" fill="#1a1a18" stroke="#34342f"/>
4
+ <circle cx="18" cy="15" r="7.5" fill="#2ea36f"/>
5
+ <path d="M14.6 15.2l2.2 2.2 4.4-4.6" fill="none" stroke="#0f0f0e" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
6
+ <text x="34" y="19.4" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" fill="#b6b6af">Verified by <tspan font-weight="700" fill="#ededea">AXIS</tspan></text>
7
+ </svg>
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="152" height="30" viewBox="0 0 152 30" role="img" aria-label="Verified by AXIS">
2
+ <title>Verified by AXIS</title>
3
+ <rect x="0.5" y="0.5" width="151" height="29" rx="14.5" fill="#ffffff" stroke="#e3e3df"/>
4
+ <circle cx="18" cy="15" r="7.5" fill="#157347"/>
5
+ <path d="M14.6 15.2l2.2 2.2 4.4-4.6" fill="none" stroke="#ffffff" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
6
+ <text x="34" y="19.4" font-family="-apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12.5" fill="#45453f">Verified by <tspan font-weight="700" fill="#1b1b19">AXIS</tspan></text>
7
+ </svg>
@@ -1,60 +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
- };
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 the cloud-hosted console
7
+ * edits and 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 CHANGED
@@ -1,48 +1,54 @@
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
- }
1
+ {
2
+ "name": "axis-platform-sdk",
3
+ "version": "0.3.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
+ "types": "./src/index.d.ts",
8
+ "exports": {
9
+ ".": { "types": "./src/index.d.ts", "default": "./src/index.js" },
10
+ "./scope": { "types": "./src/scope.d.ts", "default": "./src/scope.js" },
11
+ "./gate": { "types": "./src/gate.d.ts", "default": "./src/gate.js" },
12
+ "./express": { "types": "./src/express.d.ts", "default": "./src/express.js" },
13
+ "./authorizer": { "types": "./src/authorizer.d.ts", "default": "./src/authorizer.js" },
14
+ "./ledger": { "types": "./src/ledger.d.ts", "default": "./src/ledger.js" },
15
+ "./blocklist": { "types": "./src/blocklist.d.ts", "default": "./src/blocklist.js" },
16
+ "./reportback": { "types": "./src/reportback.d.ts", "default": "./src/reportback.js" }
17
+ },
18
+ "scripts": {
19
+ "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 test/express.test.js"
20
+ },
21
+ "keywords": [
22
+ "axis",
23
+ "axis-protocol",
24
+ "agent-identity",
25
+ "verifier",
26
+ "platform",
27
+ "authorization"
28
+ ],
29
+ "author": "Kipple Labs, Inc.",
30
+ "license": "Apache-2.0",
31
+ "engines": {
32
+ "node": ">=20.0.0"
33
+ },
34
+ "homepage": "https://github.com/MachinesOfDesire/axis-platform-sdk",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/MachinesOfDesire/axis-platform-sdk.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/MachinesOfDesire/axis-platform-sdk/issues"
41
+ },
42
+ "files": [
43
+ "src/",
44
+ "examples/",
45
+ "templates/",
46
+ "badges/",
47
+ "README.md",
48
+ "QUICKSTART.md",
49
+ "CASE-STUDY-offworld.md",
50
+ "CHANGELOG.md",
51
+ "LICENSE",
52
+ "NOTICE"
53
+ ]
54
+ }
@@ -0,0 +1 @@
1
+ export { SwitchAuthorizer, SwitchPolicy, GateConfig, AuthorizeContext, Verdict } from './index.js';
package/src/authorizer.js CHANGED
@@ -1,101 +1,101 @@
1
- /**
2
- * The Authorizer port + the free-tier SwitchAuthorizer.
3
- *
4
- * Owyhee (and any platform) always does IDENTITY verification first — is this a
5
- * real, non-revoked agent with a trustworthy effective_scope (verifyAgent ->
6
- * registry). That part is fixed and never pluggable.
7
- *
8
- * The authorization DECISION is the pluggable, monetizable layer. Every
9
- * Authorizer implements the same shape:
10
- *
11
- * authorize(token, gateId, { registryBaseUrl, fetchImpl }) -> verdict
12
- *
13
- * where `verdict` is exactly what verifyAgent returns
14
- * ({ accepted, reason?, code?, agent_id?, operator_id?, effective_scope?, tier?, ... }).
15
- *
16
- * Profiles (Josh's "simple on/off -> granular -> really complicated"):
17
- * - SwitchAuthorizer (this file) — FREE tier. Config-driven on/off gates
18
- * + optional tier / scope / operator rules.
19
- * - EngineAuthorizer (not here) — PAID. Same port, delegates the decision
20
- * to Permify / OpenFGA (sidecar) for
21
- * relationship/attribute rules.
22
- * - EnterpriseAuthorizer (not here)— full ReBAC/ABAC, same port.
23
- *
24
- * The SwitchAuthorizer's `policy` object is exactly what a "Door policy" screen
25
- * edits and saves.
26
- */
27
- import { verifyAgent } from './verify.js';
28
- import { extractToken } from './gate.js';
29
-
30
- const deny = (reason, code, extra = {}) => ({ accepted: false, reason, code, ...extra });
31
-
32
- /**
33
- * Free-tier gate engine: a set of named gates, each on/off, with optional
34
- * minimum tier, required scopes, and operator allow/block lists.
35
- *
36
- * policy = {
37
- * audience: 'comments.mysite.com', // your platform id; applied to every gate
38
- * defaultAllow: false, // posture for a gateId with no policy
39
- * blockedOperators: [], // global blocklist, applied to all gates
40
- * gates: {
41
- * 'comments:write': {
42
- * enabled: true,
43
- * minTier: 'domain', // optional
44
- * requireScopes: ['comments:write'], // optional
45
- * blockedOperators: [], // optional, gate-specific
46
- * approvedOperators: null // optional allowlist
47
- * }
48
- * }
49
- * }
50
- */
51
- export class SwitchAuthorizer {
52
- constructor(policy = {}) {
53
- this.policy = policy || {};
54
- }
55
-
56
- /** The verifyAgent options this policy implies for a given gate. */
57
- optsForGate(gateId) {
58
- const p = this.policy;
59
- const gate = (p.gates && p.gates[gateId]) || null;
60
- return {
61
- audience: p.audience,
62
- requireScopes: (gate && gate.requireScopes) || [],
63
- minTier: gate && gate.minTier,
64
- blockedOperators: [...(p.blockedOperators || []), ...((gate && gate.blockedOperators) || [])],
65
- approvedOperators: (gate && gate.approvedOperators) || null,
66
- };
67
- }
68
-
69
- /**
70
- * Decide whether `token` may act at `gateId`. Denies if the gate is turned
71
- * off, or unknown when the posture is closed. Otherwise runs identity
72
- * verification + this gate's policy.
73
- */
74
- async authorize(token, gateId, { registryBaseUrl, fetchImpl } = {}) {
75
- const p = this.policy;
76
- const gate = (p.gates && p.gates[gateId]) || null;
77
-
78
- if (!gate) {
79
- if (p.defaultAllow) {
80
- return verifyAgent(token, {
81
- audience: p.audience,
82
- blockedOperators: p.blockedOperators || [],
83
- registryBaseUrl,
84
- fetchImpl,
85
- });
86
- }
87
- return deny(`No policy for gate '${gateId}' and the default posture is closed`, 'gate_unknown');
88
- }
89
-
90
- if (gate.enabled === false) {
91
- return deny(`Gate '${gateId}' is turned off`, 'gate_closed');
92
- }
93
-
94
- return verifyAgent(token, { ...this.optsForGate(gateId), registryBaseUrl, fetchImpl });
95
- }
96
-
97
- /** Bind this authorizer to a gateId as a request gate: (request) => verdict. */
98
- gate(gateId, opts = {}) {
99
- return async (request) => this.authorize(extractToken(request), gateId, opts);
100
- }
101
- }
1
+ /**
2
+ * The Authorizer port + the free-tier SwitchAuthorizer.
3
+ *
4
+ * The cloud-hosted version (and any platform) always does IDENTITY verification
5
+ * first — is this a real, non-revoked agent with a trustworthy effective_scope (verifyAgent ->
6
+ * registry). That part is fixed and never pluggable.
7
+ *
8
+ * The authorization DECISION is the pluggable, monetizable layer. Every
9
+ * Authorizer implements the same shape:
10
+ *
11
+ * authorize(token, gateId, { registryBaseUrl, fetchImpl }) -> verdict
12
+ *
13
+ * where `verdict` is exactly what verifyAgent returns
14
+ * ({ accepted, reason?, code?, agent_id?, operator_id?, effective_scope?, tier?, ... }).
15
+ *
16
+ * Profiles (Josh's "simple on/off -> granular -> really complicated"):
17
+ * - SwitchAuthorizer (this file) — FREE tier. Config-driven on/off gates
18
+ * + optional tier / scope / operator rules.
19
+ * - EngineAuthorizer (not here) — PAID. Same port, delegates the decision
20
+ * to Permify / OpenFGA (sidecar) for
21
+ * relationship/attribute rules.
22
+ * - EnterpriseAuthorizer (not here)— full ReBAC/ABAC, same port.
23
+ *
24
+ * The SwitchAuthorizer's `policy` object is exactly what a "Door policy" screen
25
+ * edits and saves.
26
+ */
27
+ import { verifyAgent } from './verify.js';
28
+ import { extractToken } from './gate.js';
29
+
30
+ const deny = (reason, code, extra = {}) => ({ accepted: false, reason, code, ...extra });
31
+
32
+ /**
33
+ * Free-tier gate engine: a set of named gates, each on/off, with optional
34
+ * minimum tier, required scopes, and operator allow/block lists.
35
+ *
36
+ * policy = {
37
+ * audience: 'comments.mysite.com', // your platform id; applied to every gate
38
+ * defaultAllow: false, // posture for a gateId with no policy
39
+ * blockedOperators: [], // global blocklist, applied to all gates
40
+ * gates: {
41
+ * 'comments:write': {
42
+ * enabled: true,
43
+ * minTier: 'domain', // optional
44
+ * requireScopes: ['comments:write'], // optional
45
+ * blockedOperators: [], // optional, gate-specific
46
+ * approvedOperators: null // optional allowlist
47
+ * }
48
+ * }
49
+ * }
50
+ */
51
+ export class SwitchAuthorizer {
52
+ constructor(policy = {}) {
53
+ this.policy = policy || {};
54
+ }
55
+
56
+ /** The verifyAgent options this policy implies for a given gate. */
57
+ optsForGate(gateId) {
58
+ const p = this.policy;
59
+ const gate = (p.gates && p.gates[gateId]) || null;
60
+ return {
61
+ audience: p.audience,
62
+ requireScopes: (gate && gate.requireScopes) || [],
63
+ minTier: gate && gate.minTier,
64
+ blockedOperators: [...(p.blockedOperators || []), ...((gate && gate.blockedOperators) || [])],
65
+ approvedOperators: (gate && gate.approvedOperators) || null,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Decide whether `token` may act at `gateId`. Denies if the gate is turned
71
+ * off, or unknown when the posture is closed. Otherwise runs identity
72
+ * verification + this gate's policy.
73
+ */
74
+ async authorize(token, gateId, { registryBaseUrl, fetchImpl } = {}) {
75
+ const p = this.policy;
76
+ const gate = (p.gates && p.gates[gateId]) || null;
77
+
78
+ if (!gate) {
79
+ if (p.defaultAllow) {
80
+ return verifyAgent(token, {
81
+ audience: p.audience,
82
+ blockedOperators: p.blockedOperators || [],
83
+ registryBaseUrl,
84
+ fetchImpl,
85
+ });
86
+ }
87
+ return deny(`No policy for gate '${gateId}' and the default posture is closed`, 'gate_unknown');
88
+ }
89
+
90
+ if (gate.enabled === false) {
91
+ return deny(`Gate '${gateId}' is turned off`, 'gate_closed');
92
+ }
93
+
94
+ return verifyAgent(token, { ...this.optsForGate(gateId), registryBaseUrl, fetchImpl });
95
+ }
96
+
97
+ /** Bind this authorizer to a gateId as a request gate: (request) => verdict. */
98
+ gate(gateId, opts = {}) {
99
+ return async (request) => this.authorize(extractToken(request), gateId, opts);
100
+ }
101
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ Blocklist,
3
+ MemoryBlocklistStore,
4
+ gatedWithBlocklist,
5
+ BlockKind,
6
+ BlockMeta,
7
+ BlockEntry,
8
+ BlocklistStore,
9
+ } from './index.js';