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/badges/README.md
ADDED
|
@@ -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
|
|
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,49 +1,54 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "axis-platform-sdk",
|
|
3
|
-
"version": "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
|
-
"./
|
|
13
|
-
"./
|
|
14
|
-
"./
|
|
15
|
-
"./
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"axis
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
}
|
package/src/authorizer.js
CHANGED
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Authorizer port + the free-tier SwitchAuthorizer.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
+
}
|