@substrat-run/kernel 0.114.0 → 0.117.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/dist/capability.d.ts +220 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +537 -0
- package/dist/capability.js.map +1 -0
- package/dist/check-key.d.ts +30 -0
- package/dist/check-key.d.ts.map +1 -0
- package/dist/check-key.js +37 -0
- package/dist/check-key.js.map +1 -0
- package/dist/denial-query.d.ts +35 -2
- package/dist/denial-query.d.ts.map +1 -1
- package/dist/denial-query.js +70 -28
- package/dist/denial-query.js.map +1 -1
- package/dist/index.d.ts +23 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/dist/job-run.d.ts +493 -0
- package/dist/job-run.d.ts.map +1 -0
- package/dist/job-run.js +655 -0
- package/dist/job-run.js.map +1 -0
- package/dist/outbox-event.d.ts +131 -0
- package/dist/outbox-event.d.ts.map +1 -0
- package/dist/outbox-event.js +185 -0
- package/dist/outbox-event.js.map +1 -0
- package/dist/permission-checker.d.ts +8 -1
- package/dist/permission-checker.d.ts.map +1 -1
- package/dist/permission-checker.js +18 -0
- package/dist/permission-checker.js.map +1 -1
- package/dist/permission-eval.d.ts +8 -0
- package/dist/permission-eval.d.ts.map +1 -1
- package/dist/permission-eval.js +164 -91
- package/dist/permission-eval.js.map +1 -1
- package/dist/platform-request-query.d.ts +58 -1
- package/dist/platform-request-query.d.ts.map +1 -1
- package/dist/platform-request-query.js +61 -1
- package/dist/platform-request-query.js.map +1 -1
- package/dist/platform-sweep.d.ts +180 -8
- package/dist/platform-sweep.d.ts.map +1 -1
- package/dist/platform-sweep.js +243 -34
- package/dist/platform-sweep.js.map +1 -1
- package/dist/row-decode.d.ts +107 -0
- package/dist/row-decode.d.ts.map +1 -0
- package/dist/row-decode.js +93 -0
- package/dist/row-decode.js.map +1 -0
- package/dist/scope-host.d.ts +444 -9
- package/dist/scope-host.d.ts.map +1 -1
- package/dist/scope-host.js +33 -0
- package/dist/scope-host.js.map +1 -1
- package/dist/scope-tuple-seat.d.ts +72 -0
- package/dist/scope-tuple-seat.d.ts.map +1 -0
- package/dist/scope-tuple-seat.js +93 -0
- package/dist/scope-tuple-seat.js.map +1 -0
- package/dist/subject-redaction.d.ts +160 -0
- package/dist/subject-redaction.d.ts.map +1 -0
- package/dist/subject-redaction.js +210 -0
- package/dist/subject-redaction.js.map +1 -0
- package/dist/system-switch.d.ts +108 -0
- package/dist/system-switch.d.ts.map +1 -0
- package/dist/system-switch.js +145 -0
- package/dist/system-switch.js.map +1 -0
- package/dist/timeline.d.ts.map +1 -1
- package/dist/timeline.js +115 -52
- package/dist/timeline.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import { CAPABILITY_EXERCISED, CAPABILITY_MINTED, CAPABILITY_REVOKED, CAPABILITY_SECRET_PREFIX, CAPABILITY_SESSION_PREFIX, CAPABILITY_SESSION_TTL_MS, becomeCapabilityInput, capabilityAuthor, capabilityFilter, capabilityId as capabilityIdSchema, capabilityMintInput, capabilityRecord, permissionKey, principalId as principalIdSchema, substratError, } from '@substrat-run/contracts';
|
|
2
|
+
import { PermissionDenied } from './permission-checker.js';
|
|
3
|
+
import { ulid } from './ulid.js';
|
|
4
|
+
/**
|
|
5
|
+
* The two spine tables, kernel-owned so no vertical carries a migration for them. Shared by
|
|
6
|
+
* both adapters' `KERNEL_DDL` (interpolated, like `IDEMPOTENCY_DDL`), so the shape
|
|
7
|
+
* self-host builds and the shape production builds cannot part company.
|
|
8
|
+
*/
|
|
9
|
+
export const CAPABILITY_DDL = `
|
|
10
|
+
CREATE TABLE IF NOT EXISTS _substrat_capabilities (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
-- #1672: SHA-256 hex of the secret. The secret itself is stored NOWHERE — it leaves
|
|
13
|
+
-- the kernel once, in the mint's return value, and a lookup is this indexed equality.
|
|
14
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
15
|
+
-- 'act': the holder acts AS the capability ({ capability } on the spine), bounded by
|
|
16
|
+
-- entity + permissions (+ operations). 'become': exchanging it yields \`principal\`.
|
|
17
|
+
mode TEXT NOT NULL,
|
|
18
|
+
label TEXT,
|
|
19
|
+
entity_type TEXT,
|
|
20
|
+
entity_id TEXT,
|
|
21
|
+
-- JSON array of permission keys (act). The capability's whole authority: no
|
|
22
|
+
-- \`capability:\` tuple is ever written, so none can be forged through the tuple table.
|
|
23
|
+
permissions TEXT,
|
|
24
|
+
-- JSON array of operation names, or NULL = any operation the keys allow (act).
|
|
25
|
+
operations TEXT,
|
|
26
|
+
principal TEXT,
|
|
27
|
+
-- JSON capabilityAuthor: a principal id (a module minted it, and its authority is
|
|
28
|
+
-- re-checked on every use) or {"platform": …} (HostAdmin minted it).
|
|
29
|
+
minted_by TEXT NOT NULL,
|
|
30
|
+
minted_at TEXT NOT NULL,
|
|
31
|
+
expires_at TEXT,
|
|
32
|
+
-- NULL = unlimited. A use is an EXCHANGE of the secret, never an invocation.
|
|
33
|
+
max_uses INTEGER,
|
|
34
|
+
uses INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
last_used_at TEXT,
|
|
36
|
+
-- Tombstone, never a delete: a capability that once admitted someone is evidence
|
|
37
|
+
-- of why they were admitted (K-4, K-21).
|
|
38
|
+
revoked_at TEXT,
|
|
39
|
+
revoked_by TEXT
|
|
40
|
+
);
|
|
41
|
+
CREATE INDEX IF NOT EXISTS _substrat_capabilities_entity
|
|
42
|
+
ON _substrat_capabilities (entity_type, entity_id);
|
|
43
|
+
-- #1672: what an exchange hands out instead of the secret — a session token, of which
|
|
44
|
+
-- again only the hash is kept. Each session acts as its capability until the earlier of
|
|
45
|
+
-- its own expiry and the capability's; the capability is re-read on every invoke, so
|
|
46
|
+
-- revoking it ends every session at once — a revoke touches only the capability row,
|
|
47
|
+
-- and it is that per-invoke read, not a cascade here, that refuses the next call.
|
|
48
|
+
-- Expired rows are pruned on exchange, at most CAPABILITY_SESSION_PRUNE_BATCH at a time,
|
|
49
|
+
-- on the only path that adds one — so an exchange's write is bounded however many expired.
|
|
50
|
+
CREATE TABLE IF NOT EXISTS _substrat_capability_sessions (
|
|
51
|
+
token_hash TEXT PRIMARY KEY,
|
|
52
|
+
capability_id TEXT NOT NULL,
|
|
53
|
+
created_at TEXT NOT NULL,
|
|
54
|
+
expires_at TEXT NOT NULL
|
|
55
|
+
);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS _substrat_capability_sessions_expiry
|
|
57
|
+
ON _substrat_capability_sessions (expires_at);
|
|
58
|
+
`;
|
|
59
|
+
/** Every column a read returns — everything but `token_hash`, which no reader has a use for. */
|
|
60
|
+
export const CAPABILITY_COLUMNS = 'id, mode, label, entity_type, entity_id, permissions, operations, principal, minted_by, ' +
|
|
61
|
+
'minted_at, expires_at, max_uses, uses, last_used_at, revoked_at, revoked_by';
|
|
62
|
+
/** The row the permission checker reads for a capability subject (`ScopeTupleReader.capability`). */
|
|
63
|
+
export function capabilityByIdQuery(id) {
|
|
64
|
+
return {
|
|
65
|
+
sql: `SELECT ${CAPABILITY_COLUMNS} FROM _substrat_capabilities WHERE id = ?`,
|
|
66
|
+
params: [id],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Is this capability USABLE — not revoked, not expired? The ONE predicate for it: the
|
|
71
|
+
* permission checker, the session door and the exchange all call this, so the three can
|
|
72
|
+
* never disagree about whether a revoked or expired capability still acts. Same shape as
|
|
73
|
+
* the evaluator's `live()` for tuples, and ISO instants compare lexically.
|
|
74
|
+
*/
|
|
75
|
+
export const capabilityLive = (row, now) => row.revoked_at === null && (row.expires_at === null || row.expires_at > now);
|
|
76
|
+
/**
|
|
77
|
+
* May this capability's secret be EXCHANGED now — usable, and under its use limit? The use
|
|
78
|
+
* limit bounds exchanges only: a session handed out by an earlier exchange keeps acting
|
|
79
|
+
* after the limit is reached, until the capability expires or is revoked.
|
|
80
|
+
*/
|
|
81
|
+
export const capabilityExchangeable = (row, now) => capabilityLive(row, now) && (row.max_uses === null || row.uses < row.max_uses);
|
|
82
|
+
export function capabilityGrantOf(row) {
|
|
83
|
+
if (row.mode !== 'act' || row.entity_type === null || row.entity_id === null)
|
|
84
|
+
return null;
|
|
85
|
+
try {
|
|
86
|
+
const permissions = JSON.parse(row.permissions ?? 'null').map((p) => permissionKey.parse(p));
|
|
87
|
+
const author = capabilityAuthor.parse(JSON.parse(row.minted_by));
|
|
88
|
+
return {
|
|
89
|
+
entity: { entityType: row.entity_type, entityId: row.entity_id },
|
|
90
|
+
permissions,
|
|
91
|
+
mintedBy: typeof author === 'string' ? author : null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** The record a caller reads back — never the secret, never its hash. */
|
|
99
|
+
export function capabilityRecordOf(row) {
|
|
100
|
+
const common = {
|
|
101
|
+
id: row.id,
|
|
102
|
+
label: row.label,
|
|
103
|
+
mintedBy: JSON.parse(row.minted_by),
|
|
104
|
+
mintedAt: row.minted_at,
|
|
105
|
+
expiresAt: row.expires_at,
|
|
106
|
+
maxUses: row.max_uses,
|
|
107
|
+
uses: row.uses,
|
|
108
|
+
lastUsedAt: row.last_used_at,
|
|
109
|
+
revokedAt: row.revoked_at,
|
|
110
|
+
revokedBy: row.revoked_by === null ? null : JSON.parse(row.revoked_by),
|
|
111
|
+
};
|
|
112
|
+
return capabilityRecord.parse(row.mode === 'become'
|
|
113
|
+
? { mode: 'become', ...common, principal: row.principal }
|
|
114
|
+
: {
|
|
115
|
+
mode: 'act',
|
|
116
|
+
...common,
|
|
117
|
+
entity: { entityType: row.entity_type, entityId: row.entity_id },
|
|
118
|
+
permissions: JSON.parse(row.permissions ?? '[]'),
|
|
119
|
+
operations: row.operations === null ? null : JSON.parse(row.operations),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const b64url = (bytes) => {
|
|
123
|
+
let s = '';
|
|
124
|
+
for (const b of bytes)
|
|
125
|
+
s += String.fromCharCode(b);
|
|
126
|
+
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
127
|
+
};
|
|
128
|
+
/** A fresh capability secret: 256 bits, base64url, prefixed. Only its hash is ever stored. */
|
|
129
|
+
export function mintCapabilitySecret() {
|
|
130
|
+
return CAPABILITY_SECRET_PREFIX + b64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
131
|
+
}
|
|
132
|
+
/** A fresh session token — the same construction, its own prefix. */
|
|
133
|
+
export function mintCapabilitySessionToken() {
|
|
134
|
+
return CAPABILITY_SESSION_PREFIX + b64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* SHA-256 hex of a secret or session token (Web Crypto — the same call in workerd, node and
|
|
138
|
+
* browsers). Unkeyed on purpose: with 256 bits of entropy there is no dictionary to defend
|
|
139
|
+
* against, and a key would be one more secret to hold and rotate.
|
|
140
|
+
*/
|
|
141
|
+
export async function capabilityTokenHash(token) {
|
|
142
|
+
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(token));
|
|
143
|
+
let hex = '';
|
|
144
|
+
for (const b of new Uint8Array(buf))
|
|
145
|
+
hex += b.toString(16).padStart(2, '0');
|
|
146
|
+
return hex;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* How many expired sessions one exchange prunes, at most. An unlimited link can hand out any
|
|
150
|
+
* number of sessions, so pruning every expired row at once would make one visitor's request
|
|
151
|
+
* an unbounded write — and a Durable Object's transaction a large one. A fixed batch keeps
|
|
152
|
+
* each exchange's cost constant, and the table still drains: every exchange takes another
|
|
153
|
+
* batch, and an expired session acts as nobody whether or not it has been pruned yet.
|
|
154
|
+
*/
|
|
155
|
+
export const CAPABILITY_SESSION_PRUNE_BATCH = 100;
|
|
156
|
+
/** A secret or token that cannot be one of ours is refused before it is ever hashed. */
|
|
157
|
+
const TOKEN_MAX_LENGTH = 128;
|
|
158
|
+
const plausible = (token, prefix) => typeof token === 'string' && token.startsWith(prefix) && token.length <= TOKEN_MAX_LENGTH;
|
|
159
|
+
const decodeBytes = (bytes) => new TextDecoder().decode(bytes);
|
|
160
|
+
/**
|
|
161
|
+
* The text a value would persist as — ONE serialization of the COMPLETE record, keys
|
|
162
|
+
* included, byte values decoded as UTF-8. The guard scans this rather than walking chosen
|
|
163
|
+
* fields, so a secret used as an entity id, a request kind, an object key or a BLOB
|
|
164
|
+
* parameter is found exactly as one in a payload value is: there is no field list to
|
|
165
|
+
* forget a field from. The secret's alphabet (base64url behind `sbcap_`) contains nothing
|
|
166
|
+
* JSON escapes, so the serialization contains the secret exactly when the value does.
|
|
167
|
+
*/
|
|
168
|
+
export function persistedText(value) {
|
|
169
|
+
if (typeof value === 'string')
|
|
170
|
+
return value;
|
|
171
|
+
if (value instanceof Uint8Array)
|
|
172
|
+
return decodeBytes(value);
|
|
173
|
+
return (JSON.stringify(value, (_key, v) => v instanceof Uint8Array ? decodeBytes(v) : typeof v === 'bigint' ? v.toString() : v) ?? '');
|
|
174
|
+
}
|
|
175
|
+
/** Does `value`, serialized as it would persist (`persistedText`), contain any of `secrets`? */
|
|
176
|
+
export function carriesSecret(value, secrets) {
|
|
177
|
+
if (secrets.length === 0)
|
|
178
|
+
return false;
|
|
179
|
+
const text = persistedText(value);
|
|
180
|
+
return secrets.some((s) => text.includes(s));
|
|
181
|
+
}
|
|
182
|
+
/** What a recorded response carries in place of a minted secret. */
|
|
183
|
+
export const WITHHELD_SECRET = '[capability secret withheld]';
|
|
184
|
+
/**
|
|
185
|
+
* `value` with every minted secret replaced by `WITHHELD_SECRET` — what an idempotency
|
|
186
|
+
* recording stores. A replayed mint therefore returns the placeholder, never the secret:
|
|
187
|
+
* the caller that lost the first response mints again, which is the honest outcome, since
|
|
188
|
+
* the alternative is a plaintext secret in a spine table for a day.
|
|
189
|
+
*
|
|
190
|
+
* Done on the serialization, like the guard, so a secret in a key is withheld as surely as
|
|
191
|
+
* one in a value. The recording is JSON already, so round-tripping through it changes
|
|
192
|
+
* nothing the recording would have kept — and a value carrying no secret is returned as is.
|
|
193
|
+
*/
|
|
194
|
+
export function redactSecrets(value, secrets) {
|
|
195
|
+
if (value === undefined || !carriesSecret(value, secrets))
|
|
196
|
+
return value;
|
|
197
|
+
let text = persistedText(value);
|
|
198
|
+
for (const s of secrets)
|
|
199
|
+
text = text.split(s).join(WITHHELD_SECRET);
|
|
200
|
+
return JSON.parse(text);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The tripwire (see above): refuse a write whose COMPLETE record, as it would persist,
|
|
204
|
+
* carries a secret this invocation minted. Thrown from `ctx.emit` (the whole parsed event),
|
|
205
|
+
* `ctx.requestPlatform` (the whole parsed request) and `ctx.sql` (the statement and every
|
|
206
|
+
* parameter, bytes decoded) while the minting invocation runs.
|
|
207
|
+
*/
|
|
208
|
+
export function assertNoSecret(where, record, secrets) {
|
|
209
|
+
if (carriesSecret(record, secrets)) {
|
|
210
|
+
throw substratError('forbidden', `${where} would store a capability secret. Only its hash is ever kept — return the ` +
|
|
211
|
+
'secret to the caller once, and never write it to a row or an event.', { reason: 'capability_secret' });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** `ctx.sql` with every statement — its text and all its parameters — held to `assertNoSecret`. */
|
|
215
|
+
export function guardSecrets(inner, secrets) {
|
|
216
|
+
return {
|
|
217
|
+
query: (sql, params) => {
|
|
218
|
+
assertNoSecret('ctx.sql', [sql, ...(params ?? [])], secrets);
|
|
219
|
+
return inner.query(sql, params);
|
|
220
|
+
},
|
|
221
|
+
exec: (sql, params) => {
|
|
222
|
+
assertNoSecret('ctx.sql', [sql, ...(params ?? [])], secrets);
|
|
223
|
+
return inner.exec(sql, params);
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const refToString = (e) => `${e.entityType}:${e.entityId}`;
|
|
228
|
+
function principalOf(subject, verb) {
|
|
229
|
+
if (subject.kind !== 'principal') {
|
|
230
|
+
throw substratError('forbidden', `${verb}: only a principal may ${verb.endsWith('mint') ? 'mint' : 'revoke'} a capability — ` +
|
|
231
|
+
`a ${subject.kind} actor cannot delegate authority it holds on someone else's behalf`, { reason: 'capability_delegation' });
|
|
232
|
+
}
|
|
233
|
+
return subject.id;
|
|
234
|
+
}
|
|
235
|
+
export function createCapabilityVerbs(deps) {
|
|
236
|
+
const readRow = (id) => {
|
|
237
|
+
const q = capabilityByIdQuery(id);
|
|
238
|
+
return deps.sql.query(q.sql, q.params)[0];
|
|
239
|
+
};
|
|
240
|
+
return {
|
|
241
|
+
async mint(raw) {
|
|
242
|
+
deps.assertWrites('ctx.capabilities.mint');
|
|
243
|
+
const minter = principalOf(deps.subject, 'ctx.capabilities.mint');
|
|
244
|
+
const input = capabilityMintInput.parse(raw);
|
|
245
|
+
const permissions = [...new Set(input.permissions)];
|
|
246
|
+
const operations = input.operations ? [...new Set(input.operations)] : null;
|
|
247
|
+
for (const op of operations ?? []) {
|
|
248
|
+
if (!deps.isOperation(op)) {
|
|
249
|
+
throw substratError('validation_failed', `ctx.capabilities.mint: '${op}' is not an operation on this host — an allowlist ` +
|
|
250
|
+
'naming nothing would read as a narrowing and narrow nothing');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (input.expiresAt !== undefined && input.expiresAt <= deps.now) {
|
|
254
|
+
throw substratError('validation_failed', `ctx.capabilities.mint: expiresAt ${input.expiresAt} is not in the future`);
|
|
255
|
+
}
|
|
256
|
+
// Delegation, never elevation — the SAME check the operation passes, per key, on the
|
|
257
|
+
// entity. A plain `PermissionDenied` with no detail, like `ctx.grant`'s refusal: it is
|
|
258
|
+
// the module's attempt that failed, and K-35 records enforced checks, not this.
|
|
259
|
+
for (const permission of permissions) {
|
|
260
|
+
const held = await deps.check(permission, input.entity);
|
|
261
|
+
if (!held.allowed) {
|
|
262
|
+
throw new PermissionDenied(`cannot mint a capability carrying '${permission}' on ${refToString(input.entity)} — ` +
|
|
263
|
+
'the caller does not hold it there (a capability delegates, it never elevates)');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const id = capabilityIdSchema.parse(ulid());
|
|
267
|
+
const secret = mintCapabilitySecret();
|
|
268
|
+
const tokenHash = await capabilityTokenHash(secret);
|
|
269
|
+
deps.sql.exec(`INSERT INTO _substrat_capabilities
|
|
270
|
+
(id, token_hash, mode, label, entity_type, entity_id, permissions, operations,
|
|
271
|
+
principal, minted_by, minted_at, expires_at, max_uses, uses)
|
|
272
|
+
VALUES (?, ?, 'act', ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, 0)`, [
|
|
273
|
+
id,
|
|
274
|
+
tokenHash,
|
|
275
|
+
input.label ?? null,
|
|
276
|
+
input.entity.entityType,
|
|
277
|
+
input.entity.entityId,
|
|
278
|
+
JSON.stringify(permissions),
|
|
279
|
+
operations === null ? null : JSON.stringify(operations),
|
|
280
|
+
JSON.stringify(minter),
|
|
281
|
+
deps.now,
|
|
282
|
+
input.expiresAt ?? null,
|
|
283
|
+
input.maxUses ?? null,
|
|
284
|
+
]);
|
|
285
|
+
deps.minted.push(secret);
|
|
286
|
+
const payload = {
|
|
287
|
+
capabilityId: id,
|
|
288
|
+
entity: input.entity,
|
|
289
|
+
permissions,
|
|
290
|
+
operations,
|
|
291
|
+
expiresAt: input.expiresAt ?? null,
|
|
292
|
+
maxUses: input.maxUses ?? null,
|
|
293
|
+
label: input.label ?? null,
|
|
294
|
+
mintedBy: minter,
|
|
295
|
+
};
|
|
296
|
+
deps.emit({
|
|
297
|
+
type: CAPABILITY_MINTED,
|
|
298
|
+
schemaVersion: 1,
|
|
299
|
+
entity: input.entity,
|
|
300
|
+
piiClass: 'none',
|
|
301
|
+
payload,
|
|
302
|
+
});
|
|
303
|
+
return { id, secret, expiresAt: input.expiresAt ?? null };
|
|
304
|
+
},
|
|
305
|
+
async revoke(rawId) {
|
|
306
|
+
deps.assertWrites('ctx.capabilities.revoke');
|
|
307
|
+
const revoker = principalOf(deps.subject, 'ctx.capabilities.revoke');
|
|
308
|
+
const id = capabilityIdSchema.parse(rawId);
|
|
309
|
+
const row = readRow(id);
|
|
310
|
+
if (!row)
|
|
311
|
+
throw substratError('not_found', `no capability ${id} in this scope`);
|
|
312
|
+
const grant = capabilityGrantOf(row);
|
|
313
|
+
if (!grant) {
|
|
314
|
+
throw substratError('forbidden', `capability ${id} was minted by the platform and is revoked through HostAdmin`, { reason: 'capability_platform_minted' });
|
|
315
|
+
}
|
|
316
|
+
if (row.revoked_at !== null)
|
|
317
|
+
return;
|
|
318
|
+
for (const permission of grant.permissions) {
|
|
319
|
+
const held = await deps.check(permission, grant.entity);
|
|
320
|
+
if (!held.allowed) {
|
|
321
|
+
throw new PermissionDenied(`cannot revoke capability ${id} — the caller does not hold '${permission}' on ` +
|
|
322
|
+
`${refToString(grant.entity)} (a revoke is open to whoever could have minted it)`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
deps.sql.exec(`UPDATE _substrat_capabilities SET revoked_at = ?, revoked_by = ?
|
|
326
|
+
WHERE id = ? AND revoked_at IS NULL`, [deps.now, JSON.stringify(revoker), id]);
|
|
327
|
+
const payload = {
|
|
328
|
+
capabilityId: id,
|
|
329
|
+
entity: grant.entity,
|
|
330
|
+
revokedBy: revoker,
|
|
331
|
+
};
|
|
332
|
+
deps.emit({
|
|
333
|
+
type: CAPABILITY_REVOKED,
|
|
334
|
+
schemaVersion: 1,
|
|
335
|
+
entity: grant.entity,
|
|
336
|
+
piiClass: 'none',
|
|
337
|
+
payload,
|
|
338
|
+
});
|
|
339
|
+
},
|
|
340
|
+
list(raw) {
|
|
341
|
+
const filter = capabilityFilter.parse(raw ?? {});
|
|
342
|
+
const where = [];
|
|
343
|
+
const params = [];
|
|
344
|
+
if (filter.entity) {
|
|
345
|
+
where.push('entity_type = ? AND entity_id = ?');
|
|
346
|
+
params.push(filter.entity.entityType, filter.entity.entityId);
|
|
347
|
+
}
|
|
348
|
+
if (!filter.includeRevoked)
|
|
349
|
+
where.push('revoked_at IS NULL');
|
|
350
|
+
params.push(filter.limit ?? 50);
|
|
351
|
+
const rows = deps.sql.query(`SELECT ${CAPABILITY_COLUMNS} FROM _substrat_capabilities
|
|
352
|
+
${where.length ? `WHERE ${where.join(' AND ')}` : ''}
|
|
353
|
+
ORDER BY id DESC LIMIT ?`, params);
|
|
354
|
+
return rows.map(capabilityRecordOf);
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// The host-level verbs: exchange, the session door, and the platform's mint/revoke.
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
/** The pseudo-operation an exchange's spine event names, on `attachments.upload`'s pattern. */
|
|
362
|
+
export const CAPABILITY_EXCHANGE_OPERATION = 'capabilities.exchange';
|
|
363
|
+
/**
|
|
364
|
+
* Trade a secret for what it grants — ONE counted use. Runs inside the scope's
|
|
365
|
+
* serialization domain and a transaction the adapter opened; `emit` writes the
|
|
366
|
+
* `capability.exercised` event with the capability as its actor.
|
|
367
|
+
*
|
|
368
|
+
* The use is taken with an `UPDATE … WHERE uses = ?` on the row just read, so it is atomic
|
|
369
|
+
* even without the scope's single-writer guarantee: two exchanges racing for a single-use
|
|
370
|
+
* capability cannot both see `uses = 0` and both win.
|
|
371
|
+
*
|
|
372
|
+
* `null` for an unknown, expired, revoked or used-up secret — one answer for all four, so
|
|
373
|
+
* a caller probing for secrets learns nothing it can act on. Refused exchanges are not
|
|
374
|
+
* recorded: their volume is whatever an attacker chooses, and with 256 bits of entropy
|
|
375
|
+
* there is nothing a record of guesses would ever show (#130 is the rate control).
|
|
376
|
+
*/
|
|
377
|
+
export async function exchangeCapability(deps, secret,
|
|
378
|
+
/**
|
|
379
|
+
* The only mode the caller can handle. A secret of the OTHER mode answers `null` without
|
|
380
|
+
* taking a use: a claim link pasted into a share-link exchange must not be spent by a
|
|
381
|
+
* route that would then throw away the principal it yielded.
|
|
382
|
+
*/
|
|
383
|
+
mode) {
|
|
384
|
+
if (!plausible(secret, CAPABILITY_SECRET_PREFIX))
|
|
385
|
+
return null;
|
|
386
|
+
const tokenHash = await capabilityTokenHash(secret);
|
|
387
|
+
const row = deps.sql.query(`SELECT ${CAPABILITY_COLUMNS} FROM _substrat_capabilities WHERE token_hash = ?`, [tokenHash])[0];
|
|
388
|
+
if (!row || !capabilityExchangeable(row, deps.now))
|
|
389
|
+
return null;
|
|
390
|
+
if (mode !== undefined && row.mode !== mode)
|
|
391
|
+
return null;
|
|
392
|
+
const taken = deps.sql.exec(`UPDATE _substrat_capabilities SET uses = uses + 1, last_used_at = ?
|
|
393
|
+
WHERE id = ? AND uses = ?`, [deps.now, row.id, row.uses]);
|
|
394
|
+
if (taken.changes !== 1)
|
|
395
|
+
return null;
|
|
396
|
+
const id = capabilityIdSchema.parse(row.id);
|
|
397
|
+
const uses = row.uses + 1;
|
|
398
|
+
if (row.mode === 'become') {
|
|
399
|
+
const principal = principalIdSchema.parse(row.principal);
|
|
400
|
+
const payload = {
|
|
401
|
+
capabilityId: id,
|
|
402
|
+
mode: 'become',
|
|
403
|
+
uses,
|
|
404
|
+
maxUses: row.max_uses,
|
|
405
|
+
sessionExpiresAt: null,
|
|
406
|
+
principal,
|
|
407
|
+
};
|
|
408
|
+
deps.emit(id, {
|
|
409
|
+
type: CAPABILITY_EXERCISED,
|
|
410
|
+
schemaVersion: 1,
|
|
411
|
+
entity: { entityType: 'capability', entityId: id },
|
|
412
|
+
piiClass: 'none',
|
|
413
|
+
payload,
|
|
414
|
+
});
|
|
415
|
+
return { kind: 'principal', capabilityId: id, principal };
|
|
416
|
+
}
|
|
417
|
+
const grant = capabilityGrantOf(row);
|
|
418
|
+
if (!grant)
|
|
419
|
+
return null;
|
|
420
|
+
const ttlEnd = new Date(Date.parse(deps.now) + CAPABILITY_SESSION_TTL_MS).toISOString();
|
|
421
|
+
const expiresAt = (row.expires_at !== null && row.expires_at < ttlEnd
|
|
422
|
+
? row.expires_at
|
|
423
|
+
: ttlEnd);
|
|
424
|
+
const sessionToken = mintCapabilitySessionToken();
|
|
425
|
+
// Bounded (see CAPABILITY_SESSION_PRUNE_BATCH) — a subquery LIMIT rather than
|
|
426
|
+
// `DELETE … LIMIT`, which SQLite accepts only when built with an option neither host
|
|
427
|
+
// promises. The expiry index makes the inner select a range seek.
|
|
428
|
+
deps.sql.exec(`DELETE FROM _substrat_capability_sessions WHERE token_hash IN (
|
|
429
|
+
SELECT token_hash FROM _substrat_capability_sessions WHERE expires_at <= ? LIMIT ?)`, [deps.now, CAPABILITY_SESSION_PRUNE_BATCH]);
|
|
430
|
+
deps.sql.exec(`INSERT INTO _substrat_capability_sessions (token_hash, capability_id, created_at, expires_at)
|
|
431
|
+
VALUES (?, ?, ?, ?)`, [await capabilityTokenHash(sessionToken), id, deps.now, expiresAt]);
|
|
432
|
+
const payload = {
|
|
433
|
+
capabilityId: id,
|
|
434
|
+
mode: 'act',
|
|
435
|
+
uses,
|
|
436
|
+
maxUses: row.max_uses,
|
|
437
|
+
sessionExpiresAt: expiresAt,
|
|
438
|
+
principal: null,
|
|
439
|
+
};
|
|
440
|
+
deps.emit(id, {
|
|
441
|
+
type: CAPABILITY_EXERCISED,
|
|
442
|
+
schemaVersion: 1,
|
|
443
|
+
entity: grant.entity,
|
|
444
|
+
piiClass: 'none',
|
|
445
|
+
payload,
|
|
446
|
+
});
|
|
447
|
+
return { kind: 'session', capabilityId: id, sessionToken, expiresAt, entity: grant.entity };
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* The session door's decision, re-made on EVERY invoke inside the operation's transaction
|
|
451
|
+
* (impersonation's per-invoke re-read, for the same reason): a stub is a capability of its
|
|
452
|
+
* own and nothing takes it away, so a revocation checked only when the stub was minted
|
|
453
|
+
* would stop everyone except the one caller holding it.
|
|
454
|
+
*
|
|
455
|
+
* Refuses an unknown or expired session and a revoked or expired capability as
|
|
456
|
+
* `unauthenticated` — the credential no longer works, and re-opening a live link is the
|
|
457
|
+
* remedy — and an operation outside the capability's allowlist as `forbidden`. None of
|
|
458
|
+
* these is a K-35 denial: no permission key was checked, and the denial log records
|
|
459
|
+
* enforced checks. The handler's own checks, once it runs, are.
|
|
460
|
+
*/
|
|
461
|
+
export function resolveCapabilitySession(sql, sessionHash, now, operation) {
|
|
462
|
+
const session = sql.query(`SELECT capability_id, expires_at FROM _substrat_capability_sessions WHERE token_hash = ?`, [sessionHash])[0];
|
|
463
|
+
if (!session || session.expires_at <= now) {
|
|
464
|
+
throw substratError('unauthenticated', 'capability session is unknown or has expired');
|
|
465
|
+
}
|
|
466
|
+
const q = capabilityByIdQuery(session.capability_id);
|
|
467
|
+
const row = sql.query(q.sql, q.params)[0];
|
|
468
|
+
if (!row || row.mode !== 'act' || !capabilityLive(row, now)) {
|
|
469
|
+
throw substratError('unauthenticated', 'capability has been revoked or has expired');
|
|
470
|
+
}
|
|
471
|
+
if (row.operations !== null) {
|
|
472
|
+
const allowed = JSON.parse(row.operations);
|
|
473
|
+
if (!allowed.includes(operation)) {
|
|
474
|
+
throw substratError('forbidden', `capability ${row.id} may not invoke '${operation}' — it is not on the capability's ` +
|
|
475
|
+
'operation list', { reason: 'capability_operation' });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return capabilityIdSchema.parse(row.id);
|
|
479
|
+
}
|
|
480
|
+
/** Is `token` shaped like a session token at all? Checked before it is hashed or sent anywhere. */
|
|
481
|
+
export const plausibleSessionToken = (token) => plausible(token, CAPABILITY_SESSION_PREFIX);
|
|
482
|
+
/**
|
|
483
|
+
* The platform's mint (`HostAdmin.mintCapability`) — a `become` capability, which is the
|
|
484
|
+
* only kind the platform mints and the only way one is minted in this first cut (see
|
|
485
|
+
* `becomeCapabilityInput`). No spine event: a platform actor's act is the admin log's to
|
|
486
|
+
* record, and the adapter records it there.
|
|
487
|
+
*/
|
|
488
|
+
/**
|
|
489
|
+
* A platform mint's input, parsed and held to its one rule beyond the shape: an expiry in
|
|
490
|
+
* the future. Its own function so a host whose storage sits across an RPC boundary can run
|
|
491
|
+
* it on the near side, where a typed refusal still reaches the caller as one — the same
|
|
492
|
+
* check `mintBecomeCapability` makes, not a second copy of it.
|
|
493
|
+
*/
|
|
494
|
+
export function checkBecomeInput(raw, now) {
|
|
495
|
+
const input = becomeCapabilityInput.parse(raw);
|
|
496
|
+
if (input.expiresAt <= now) {
|
|
497
|
+
throw substratError('validation_failed', `mintCapability: expiresAt ${input.expiresAt} is not in the future`);
|
|
498
|
+
}
|
|
499
|
+
return input;
|
|
500
|
+
}
|
|
501
|
+
export async function mintBecomeCapability(sql, raw, actor, now) {
|
|
502
|
+
const input = checkBecomeInput(raw, now);
|
|
503
|
+
const id = capabilityIdSchema.parse(ulid());
|
|
504
|
+
const secret = mintCapabilitySecret();
|
|
505
|
+
const author = { platform: actor };
|
|
506
|
+
sql.exec(`INSERT INTO _substrat_capabilities
|
|
507
|
+
(id, token_hash, mode, label, entity_type, entity_id, permissions, operations,
|
|
508
|
+
principal, minted_by, minted_at, expires_at, max_uses, uses)
|
|
509
|
+
VALUES (?, ?, 'become', ?, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?, 0)`, [
|
|
510
|
+
id,
|
|
511
|
+
await capabilityTokenHash(secret),
|
|
512
|
+
input.label ?? null,
|
|
513
|
+
input.principal,
|
|
514
|
+
JSON.stringify(author),
|
|
515
|
+
now,
|
|
516
|
+
input.expiresAt,
|
|
517
|
+
input.maxUses,
|
|
518
|
+
]);
|
|
519
|
+
return { id, secret, expiresAt: input.expiresAt };
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* The platform's revoke (`HostAdmin.revokeCapability`) — of any capability in the scope,
|
|
523
|
+
* the operator's lever for a leaked link. Returns the row as it stood before, for the admin
|
|
524
|
+
* log, or `undefined` when there is no such capability. Idempotent.
|
|
525
|
+
*/
|
|
526
|
+
export function revokeCapabilityAsPlatform(sql, rawId, actor, now) {
|
|
527
|
+
const id = capabilityIdSchema.parse(rawId);
|
|
528
|
+
const q = capabilityByIdQuery(id);
|
|
529
|
+
const row = sql.query(q.sql, q.params)[0];
|
|
530
|
+
if (!row)
|
|
531
|
+
return undefined;
|
|
532
|
+
const author = { platform: actor };
|
|
533
|
+
sql.exec(`UPDATE _substrat_capabilities SET revoked_at = ?, revoked_by = ?
|
|
534
|
+
WHERE id = ? AND revoked_at IS NULL`, [now, JSON.stringify(author), id]);
|
|
535
|
+
return capabilityRecordOf(row);
|
|
536
|
+
}
|
|
537
|
+
//# sourceMappingURL=capability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability.js","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime half of `ctx.check`'s `PermissionKey` parameter (#1642).
|
|
3
|
+
*
|
|
4
|
+
* The brand is compile-time only, so a module that casts (`'Workorder:Read' as
|
|
5
|
+
* PermissionKey`) hands the host a string no manifest and no role could ever carry.
|
|
6
|
+
* Until this, both adapters took it on trust: the checker refused it (nothing holds
|
|
7
|
+
* it), the refusal was recorded in the denial log under a key that is not a
|
|
8
|
+
* permission, and the author read `permission denied` for what is a typo. On the
|
|
9
|
+
* system-actor path, which allows without consulting a grant, it went further: the
|
|
10
|
+
* key became the relation of the proof the check returned, and `ctx.grant` wrote it
|
|
11
|
+
* into `_substrat_tuples`.
|
|
12
|
+
*
|
|
13
|
+
* So both adapters call this FIRST in `runCheck` — above the system-actor early
|
|
14
|
+
* return, which is the path that never reaches the checker — and `ctx.grant` /
|
|
15
|
+
* `ctx.revoke` inherit it because they re-check through `runCheck` before writing.
|
|
16
|
+
*
|
|
17
|
+
* **Why a throw and not a denial.** Both refuse, so neither lets the call through:
|
|
18
|
+
* a throw returns no `Decision`, so there is nothing for `assertAllowed` to pass and
|
|
19
|
+
* nothing is added to the K-34 `authorization` the operation's events carry. A
|
|
20
|
+
* caller that catches it and carries on has skipped a check, exactly as one that
|
|
21
|
+
* never called `ctx.check` has — no more than that. What differs is who is told. A
|
|
22
|
+
* denial is a statement about a principal ("was refused X"), and X here is not a
|
|
23
|
+
* permission; recording it fills the denial log with rows that read as an
|
|
24
|
+
* authorization problem, and on the system-actor path a denial is not even an
|
|
25
|
+
* available answer. It is a bug in the module, so it is `internal` — a 500 whose
|
|
26
|
+
* `detail` never leaves the server — and the message names the key for the log.
|
|
27
|
+
*/
|
|
28
|
+
import { type PermissionKey } from '@substrat-run/contracts';
|
|
29
|
+
export declare function assertPermissionKey(permission: unknown): PermissionKey;
|
|
30
|
+
//# sourceMappingURL=check-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-key.d.ts","sourceRoot":"","sources":["../src/check-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAgC,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE3F,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,CAStE"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime half of `ctx.check`'s `PermissionKey` parameter (#1642).
|
|
3
|
+
*
|
|
4
|
+
* The brand is compile-time only, so a module that casts (`'Workorder:Read' as
|
|
5
|
+
* PermissionKey`) hands the host a string no manifest and no role could ever carry.
|
|
6
|
+
* Until this, both adapters took it on trust: the checker refused it (nothing holds
|
|
7
|
+
* it), the refusal was recorded in the denial log under a key that is not a
|
|
8
|
+
* permission, and the author read `permission denied` for what is a typo. On the
|
|
9
|
+
* system-actor path, which allows without consulting a grant, it went further: the
|
|
10
|
+
* key became the relation of the proof the check returned, and `ctx.grant` wrote it
|
|
11
|
+
* into `_substrat_tuples`.
|
|
12
|
+
*
|
|
13
|
+
* So both adapters call this FIRST in `runCheck` — above the system-actor early
|
|
14
|
+
* return, which is the path that never reaches the checker — and `ctx.grant` /
|
|
15
|
+
* `ctx.revoke` inherit it because they re-check through `runCheck` before writing.
|
|
16
|
+
*
|
|
17
|
+
* **Why a throw and not a denial.** Both refuse, so neither lets the call through:
|
|
18
|
+
* a throw returns no `Decision`, so there is nothing for `assertAllowed` to pass and
|
|
19
|
+
* nothing is added to the K-34 `authorization` the operation's events carry. A
|
|
20
|
+
* caller that catches it and carries on has skipped a check, exactly as one that
|
|
21
|
+
* never called `ctx.check` has — no more than that. What differs is who is told. A
|
|
22
|
+
* denial is a statement about a principal ("was refused X"), and X here is not a
|
|
23
|
+
* permission; recording it fills the denial log with rows that read as an
|
|
24
|
+
* authorization problem, and on the system-actor path a denial is not even an
|
|
25
|
+
* available answer. It is a bug in the module, so it is `internal` — a 500 whose
|
|
26
|
+
* `detail` never leaves the server — and the message names the key for the log.
|
|
27
|
+
*/
|
|
28
|
+
import { permissionKey, substratError } from '@substrat-run/contracts';
|
|
29
|
+
export function assertPermissionKey(permission) {
|
|
30
|
+
const parsed = permissionKey.safeParse(permission);
|
|
31
|
+
if (parsed.success)
|
|
32
|
+
return parsed.data;
|
|
33
|
+
throw substratError('internal', `ctx.check was handed ${JSON.stringify(permission)}, which is not a permission key ` +
|
|
34
|
+
`(${parsed.error.issues.map((i) => i.message).join('; ')}) — a bug in the calling ` +
|
|
35
|
+
'module, not a refusal: nothing was checked and no denial was recorded');
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=check-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-key.js","sourceRoot":"","sources":["../src/check-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAsB,MAAM,yBAAyB,CAAC;AAE3F,MAAM,UAAU,mBAAmB,CAAC,UAAmB;IACrD,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IACnD,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IACvC,MAAM,aAAa,CACjB,UAAU,EACV,wBAAwB,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,kCAAkC;QAClF,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,2BAA2B;QACnF,uEAAuE,CAC1E,CAAC;AACJ,CAAC"}
|