axis-platform-sdk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CASE-STUDY-offworld.md +56 -0
- package/CHANGELOG.md +179 -97
- 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 -48
- package/src/authorizer.d.ts +1 -0
- package/src/authorizer.js +101 -101
- package/src/blocklist.d.ts +9 -0
- package/src/blocklist.js +183 -183
- package/src/express.d.ts +30 -0
- package/src/express.js +68 -0
- package/src/gate.d.ts +1 -0
- package/src/index.d.ts +288 -0
- package/src/ledger.d.ts +10 -0
- package/src/ledger.js +170 -171
- package/src/reportback.d.ts +15 -0
- package/src/scope.d.ts +1 -0
- 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/index.d.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// Type declarations for axis-platform-sdk (the package is authored in plain JS).
|
|
2
|
+
// Covers the full main-entry surface. Subpath entry points (./scope, ./gate,
|
|
3
|
+
// ./authorizer, ./ledger, ./blocklist, ./reportback) re-export their slice of
|
|
4
|
+
// this declaration. Keep in sync with src/*.js.
|
|
5
|
+
|
|
6
|
+
export type Tier = 'email' | 'domain' | 'verified' | 'kyb_individual' | 'kyb_organization';
|
|
7
|
+
|
|
8
|
+
// --- verify -----------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/** The single structured verdict verifyAgent / SwitchAuthorizer.authorize return. */
|
|
11
|
+
export interface Verdict {
|
|
12
|
+
accepted: boolean;
|
|
13
|
+
code?: string;
|
|
14
|
+
reason?: string;
|
|
15
|
+
agent_id?: string;
|
|
16
|
+
operator_id?: string;
|
|
17
|
+
effective_scope?: string[];
|
|
18
|
+
delegation_valid?: boolean;
|
|
19
|
+
tier?: Tier | null;
|
|
20
|
+
expires_at?: number | null;
|
|
21
|
+
/** present on an insufficient_scope denial */
|
|
22
|
+
missing?: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface VerifyOptions {
|
|
26
|
+
audience?: string;
|
|
27
|
+
requireScopes?: string[];
|
|
28
|
+
minTier?: Tier;
|
|
29
|
+
blockedOperators?: string[];
|
|
30
|
+
approvedOperators?: string[] | null;
|
|
31
|
+
registryBaseUrl?: string;
|
|
32
|
+
fetchImpl?: typeof fetch;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function verifyAgent(token: string, opts?: VerifyOptions): Promise<Verdict>;
|
|
36
|
+
|
|
37
|
+
// --- authorizer (the pluggable gate engine; SwitchAuthorizer = free tier) ----
|
|
38
|
+
|
|
39
|
+
export interface GateConfig {
|
|
40
|
+
enabled?: boolean;
|
|
41
|
+
minTier?: Tier;
|
|
42
|
+
requireScopes?: string[];
|
|
43
|
+
blockedOperators?: string[];
|
|
44
|
+
approvedOperators?: string[] | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Exactly what the "Door policy" screen edits + saves (door_policy.policy_json). */
|
|
48
|
+
export interface SwitchPolicy {
|
|
49
|
+
audience?: string;
|
|
50
|
+
defaultAllow?: boolean;
|
|
51
|
+
blockedOperators?: string[];
|
|
52
|
+
gates?: Record<string, GateConfig>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AuthorizeContext {
|
|
56
|
+
registryBaseUrl?: string;
|
|
57
|
+
fetchImpl?: typeof fetch;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class SwitchAuthorizer {
|
|
61
|
+
constructor(policy?: SwitchPolicy);
|
|
62
|
+
policy: SwitchPolicy;
|
|
63
|
+
optsForGate(gateId: string): VerifyOptions;
|
|
64
|
+
authorize(token: string, gateId: string, ctx?: AuthorizeContext): Promise<Verdict>;
|
|
65
|
+
gate(gateId: string, opts?: AuthorizeContext): (request: Request) => Promise<Verdict>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- scope ------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
export function scopeCovers(granted: string, required: string): boolean;
|
|
71
|
+
/** Returns { ok, missing } — NOT a boolean. */
|
|
72
|
+
export function coversAll(granted: string[], required: string[]): { ok: boolean; missing: string[] };
|
|
73
|
+
|
|
74
|
+
// --- gate (Worker request middleware) ---------------------------------------
|
|
75
|
+
|
|
76
|
+
export function aitGate(opts: VerifyOptions): (request: Request) => Promise<Verdict>;
|
|
77
|
+
export function extractToken(request: Request): string | null;
|
|
78
|
+
export function denialResponse(verdict: Verdict): Response;
|
|
79
|
+
|
|
80
|
+
// --- client (registry-call helpers) -----------------------------------------
|
|
81
|
+
|
|
82
|
+
export interface EnrichResult {
|
|
83
|
+
agent_id: string;
|
|
84
|
+
did?: string | null;
|
|
85
|
+
display_name?: string | null;
|
|
86
|
+
tier?: Tier | null;
|
|
87
|
+
operator_id?: string;
|
|
88
|
+
status?: string | null;
|
|
89
|
+
raw?: unknown;
|
|
90
|
+
[k: string]: unknown;
|
|
91
|
+
}
|
|
92
|
+
export function enrich(
|
|
93
|
+
agentId: string,
|
|
94
|
+
token: string | null,
|
|
95
|
+
opts?: { registryBaseUrl?: string; fetchImpl?: typeof fetch },
|
|
96
|
+
): Promise<EnrichResult>;
|
|
97
|
+
|
|
98
|
+
export function registryGet(
|
|
99
|
+
base: string,
|
|
100
|
+
path: string,
|
|
101
|
+
opts?: { headers?: Record<string, string>; fetchImpl?: typeof fetch },
|
|
102
|
+
): Promise<{ status: number; body: any }>;
|
|
103
|
+
|
|
104
|
+
/** Read an operator's verification tier from a resolved agent record. */
|
|
105
|
+
export function pickTier(agentBody: any): Tier | null;
|
|
106
|
+
|
|
107
|
+
/** Load a platform's published `/.well-known/axis-access` door policy. */
|
|
108
|
+
export function loadAccessPolicy(
|
|
109
|
+
platformBaseUrl: string,
|
|
110
|
+
opts?: { fetchImpl?: typeof fetch },
|
|
111
|
+
): Promise<SwitchPolicy & Record<string, unknown>>;
|
|
112
|
+
|
|
113
|
+
export function decodeAitPayload(token: string): Record<string, unknown> | null;
|
|
114
|
+
export const TIER_RANK: Record<string, number>;
|
|
115
|
+
export const DEFAULT_REGISTRY: string;
|
|
116
|
+
|
|
117
|
+
// --- ledger (the "who showed up" arrival record) ----------------------------
|
|
118
|
+
|
|
119
|
+
export type ArrivalDecision = 'auto_allow' | 'denied' | 'held' | 'approved' | 'booted';
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* One arrival record. Byte-compatible with the cloud-hosted version's `arrivals`
|
|
123
|
+
* columns / `ArrivalRecord` (minus the adapter's own id/org_id PKs).
|
|
124
|
+
* `created_at` is epoch ms.
|
|
125
|
+
*/
|
|
126
|
+
export interface ArrivalEntry {
|
|
127
|
+
agent_id: string | null;
|
|
128
|
+
operator_id: string | null;
|
|
129
|
+
created_at: number;
|
|
130
|
+
tier: Tier | null;
|
|
131
|
+
delegation_valid: boolean;
|
|
132
|
+
effective_scope: string[];
|
|
133
|
+
gate_id: string | null;
|
|
134
|
+
requested_action: string | null;
|
|
135
|
+
display_name: string | null;
|
|
136
|
+
decision: ArrivalDecision;
|
|
137
|
+
reason: string | null;
|
|
138
|
+
audience: string | null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Optional fields stamped onto an entry alongside the verdict. */
|
|
142
|
+
export interface RecordFields {
|
|
143
|
+
audience?: string;
|
|
144
|
+
gate_id?: string;
|
|
145
|
+
requested_action?: string;
|
|
146
|
+
display_name?: string;
|
|
147
|
+
decision?: ArrivalDecision;
|
|
148
|
+
created_at?: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function recordEntry(verdict: Verdict, fields?: RecordFields): ArrivalEntry;
|
|
152
|
+
|
|
153
|
+
/** The pluggable ledger store port (the default is in-memory). */
|
|
154
|
+
export interface LedgerStore {
|
|
155
|
+
append(entry: ArrivalEntry): Promise<void>;
|
|
156
|
+
recent(opts?: { limit?: number }): Promise<ArrivalEntry[]>;
|
|
157
|
+
byOperator(operatorId: string, opts?: { limit?: number }): Promise<ArrivalEntry[]>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class MemoryLedgerStore implements LedgerStore {
|
|
161
|
+
constructor(opts?: { max?: number });
|
|
162
|
+
append(entry: ArrivalEntry): Promise<void>;
|
|
163
|
+
recent(opts?: { limit?: number }): Promise<ArrivalEntry[]>;
|
|
164
|
+
byOperator(operatorId: string, opts?: { limit?: number }): Promise<ArrivalEntry[]>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export class AccessLedger {
|
|
168
|
+
constructor(opts?: { store?: LedgerStore });
|
|
169
|
+
store: LedgerStore;
|
|
170
|
+
record(verdict: Verdict, fields?: RecordFields): Promise<ArrivalEntry>;
|
|
171
|
+
recent(opts?: { limit?: number }): Promise<ArrivalEntry[]>;
|
|
172
|
+
byOperator(operatorId: string, opts?: { limit?: number }): Promise<ArrivalEntry[]>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function loggedGate(
|
|
176
|
+
gate: (request: Request) => Promise<Verdict>,
|
|
177
|
+
ledger: AccessLedger,
|
|
178
|
+
fields?: RecordFields,
|
|
179
|
+
): (request: Request) => Promise<Verdict>;
|
|
180
|
+
|
|
181
|
+
// --- blocklist (runtime block/allow, by operator AND agent) -----------------
|
|
182
|
+
|
|
183
|
+
export type BlockKind = 'operator' | 'agent';
|
|
184
|
+
export interface BlockMeta { reason?: string; created_at?: number; [k: string]: unknown; }
|
|
185
|
+
export interface BlockEntry { id: string; meta: BlockMeta; }
|
|
186
|
+
|
|
187
|
+
/** The pluggable block-store port (the default is in-memory). */
|
|
188
|
+
export interface BlocklistStore {
|
|
189
|
+
add(kind: BlockKind, id: string, meta?: BlockMeta): Promise<void>;
|
|
190
|
+
remove(kind: BlockKind, id: string): Promise<void>;
|
|
191
|
+
has(kind: BlockKind, id: string): Promise<boolean>;
|
|
192
|
+
list(kind: BlockKind): Promise<BlockEntry[]>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export class MemoryBlocklistStore implements BlocklistStore {
|
|
196
|
+
add(kind: BlockKind, id: string, meta?: BlockMeta): Promise<void>;
|
|
197
|
+
remove(kind: BlockKind, id: string): Promise<void>;
|
|
198
|
+
has(kind: BlockKind, id: string): Promise<boolean>;
|
|
199
|
+
list(kind: BlockKind): Promise<BlockEntry[]>;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export class Blocklist {
|
|
203
|
+
constructor(opts?: { store?: BlocklistStore });
|
|
204
|
+
store: BlocklistStore;
|
|
205
|
+
blockOperator(operatorId: string, reason?: string): Promise<void>;
|
|
206
|
+
blockAgent(agentId: string, reason?: string): Promise<void>;
|
|
207
|
+
unblockOperator(operatorId: string): Promise<void>;
|
|
208
|
+
unblockAgent(agentId: string): Promise<void>;
|
|
209
|
+
isOperatorBlocked(operatorId: string): Promise<boolean>;
|
|
210
|
+
isAgentBlocked(agentId: string): Promise<boolean>;
|
|
211
|
+
blockedOperatorIds(): Promise<string[]>;
|
|
212
|
+
listOperators(): Promise<BlockEntry[]>;
|
|
213
|
+
listAgents(): Promise<BlockEntry[]>;
|
|
214
|
+
verifyOpts(): Promise<{ blockedOperators: string[] }>;
|
|
215
|
+
checkVerdict(verdict: Verdict): Promise<Verdict>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function gatedWithBlocklist(
|
|
219
|
+
gate: (request: Request) => Promise<Verdict>,
|
|
220
|
+
blocklist: Blocklist,
|
|
221
|
+
): (request: Request) => Promise<Verdict>;
|
|
222
|
+
|
|
223
|
+
// --- reportback (sign + emit a negative Trust Attestation) ------------------
|
|
224
|
+
|
|
225
|
+
export interface TrustAttestation {
|
|
226
|
+
axis_version: string;
|
|
227
|
+
type: 'TrustAttestation';
|
|
228
|
+
id: string;
|
|
229
|
+
issued_by: string;
|
|
230
|
+
subject: string;
|
|
231
|
+
issued_at: string;
|
|
232
|
+
scope: string;
|
|
233
|
+
statement: string;
|
|
234
|
+
signature?: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface KeyStore {
|
|
238
|
+
load(): Promise<JsonWebKey | null>;
|
|
239
|
+
save(jwk: JsonWebKey): Promise<void>;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export class MemoryKeyStore implements KeyStore {
|
|
243
|
+
load(): Promise<JsonWebKey | null>;
|
|
244
|
+
save(jwk: JsonWebKey): Promise<void>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function getPlatformKey(
|
|
248
|
+
opts?: { keyStore?: KeyStore },
|
|
249
|
+
): Promise<{ privateKey: CryptoKey; publicKeyB64: string; jwk: JsonWebKey }>;
|
|
250
|
+
|
|
251
|
+
export function buildAttestation(args: {
|
|
252
|
+
platformId: string;
|
|
253
|
+
agentId: string;
|
|
254
|
+
category?: string;
|
|
255
|
+
reason?: string;
|
|
256
|
+
issuedAt?: string;
|
|
257
|
+
}): TrustAttestation;
|
|
258
|
+
|
|
259
|
+
export function signAttestation(attestation: TrustAttestation, privateKey: CryptoKey): Promise<TrustAttestation>;
|
|
260
|
+
export function verifyAttestation(attestation: TrustAttestation, publicKeyB64: string): Promise<boolean>;
|
|
261
|
+
|
|
262
|
+
export interface ReportArgs {
|
|
263
|
+
platformId: string;
|
|
264
|
+
agentId: string;
|
|
265
|
+
operatorId?: string;
|
|
266
|
+
category: string;
|
|
267
|
+
reason: string;
|
|
268
|
+
}
|
|
269
|
+
export interface ReportResult {
|
|
270
|
+
sent: boolean;
|
|
271
|
+
status?: number;
|
|
272
|
+
attestation?: TrustAttestation;
|
|
273
|
+
reason?: string;
|
|
274
|
+
}
|
|
275
|
+
export interface ReportOptions {
|
|
276
|
+
reputationUrl?: string | null;
|
|
277
|
+
keyStore?: KeyStore;
|
|
278
|
+
fetchImpl?: typeof fetch;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function reportFlag(args: ReportArgs, opts?: ReportOptions): Promise<ReportResult>;
|
|
282
|
+
export function blockAndReport(
|
|
283
|
+
blocklist: Blocklist,
|
|
284
|
+
args: ReportArgs,
|
|
285
|
+
opts?: ReportOptions,
|
|
286
|
+
): Promise<{ blocked: boolean; agent_id: string; report: ReportResult }>;
|
|
287
|
+
|
|
288
|
+
export const DEFAULT_REPUTATION_URL: string | null;
|
package/src/ledger.d.ts
ADDED
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
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
reportFlag,
|
|
3
|
+
blockAndReport,
|
|
4
|
+
getPlatformKey,
|
|
5
|
+
buildAttestation,
|
|
6
|
+
signAttestation,
|
|
7
|
+
verifyAttestation,
|
|
8
|
+
MemoryKeyStore,
|
|
9
|
+
TrustAttestation,
|
|
10
|
+
KeyStore,
|
|
11
|
+
ReportArgs,
|
|
12
|
+
ReportResult,
|
|
13
|
+
ReportOptions,
|
|
14
|
+
DEFAULT_REPUTATION_URL,
|
|
15
|
+
} from './index.js';
|