@fusionkit/plane 0.1.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.
Files changed (52) hide show
  1. package/dist/auth.d.ts +18 -0
  2. package/dist/auth.js +46 -0
  3. package/dist/claim-token-service.d.ts +23 -0
  4. package/dist/claim-token-service.js +54 -0
  5. package/dist/contract-service.d.ts +14 -0
  6. package/dist/contract-service.js +39 -0
  7. package/dist/domain-errors.d.ts +13 -0
  8. package/dist/domain-errors.js +31 -0
  9. package/dist/idp.d.ts +26 -0
  10. package/dist/idp.js +24 -0
  11. package/dist/index.d.ts +35 -0
  12. package/dist/index.js +21 -0
  13. package/dist/keys.d.ts +60 -0
  14. package/dist/keys.js +132 -0
  15. package/dist/logging.d.ts +21 -0
  16. package/dist/logging.js +42 -0
  17. package/dist/plane.d.ts +167 -0
  18. package/dist/plane.js +606 -0
  19. package/dist/policy.d.ts +23 -0
  20. package/dist/policy.js +92 -0
  21. package/dist/ratelimit.d.ts +40 -0
  22. package/dist/ratelimit.js +94 -0
  23. package/dist/receipt-service.d.ts +16 -0
  24. package/dist/receipt-service.js +17 -0
  25. package/dist/retention.d.ts +33 -0
  26. package/dist/retention.js +123 -0
  27. package/dist/run-lifecycle.d.ts +2 -0
  28. package/dist/run-lifecycle.js +19 -0
  29. package/dist/secrets.d.ts +25 -0
  30. package/dist/secrets.js +73 -0
  31. package/dist/server.d.ts +38 -0
  32. package/dist/server.js +418 -0
  33. package/dist/sqlite-store.d.ts +53 -0
  34. package/dist/sqlite-store.js +401 -0
  35. package/dist/store.d.ts +107 -0
  36. package/dist/store.js +9 -0
  37. package/dist/test/api.test.d.ts +1 -0
  38. package/dist/test/api.test.js +179 -0
  39. package/dist/test/hardening.test.d.ts +1 -0
  40. package/dist/test/hardening.test.js +259 -0
  41. package/dist/test/policy.test.d.ts +1 -0
  42. package/dist/test/policy.test.js +78 -0
  43. package/dist/test/server-hardening.test.d.ts +1 -0
  44. package/dist/test/server-hardening.test.js +192 -0
  45. package/dist/test/ui-parity.test.d.ts +1 -0
  46. package/dist/test/ui-parity.test.js +28 -0
  47. package/dist/validation.d.ts +326 -0
  48. package/dist/validation.js +178 -0
  49. package/package.json +34 -0
  50. package/ui/app.css +276 -0
  51. package/ui/app.js +483 -0
  52. package/ui/index.html +65 -0
@@ -0,0 +1,401 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { sha256Hex } from "@fusionkit/protocol";
5
+ import { isPrincipalRole } from "./store.js";
6
+ const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
7
+ export class SqliteStore {
8
+ db;
9
+ constructor(dbPath, options = {}) {
10
+ if (dbPath !== ":memory:")
11
+ mkdirSync(dirname(dbPath), { recursive: true });
12
+ this.db = new DatabaseSync(dbPath);
13
+ this.db.exec(`PRAGMA journal_mode = ${options.journalMode ?? "WAL"}`);
14
+ this.db.exec("PRAGMA foreign_keys = ON");
15
+ this.db.exec(`PRAGMA busy_timeout = ${options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS}`);
16
+ this.migrate();
17
+ }
18
+ migrate() {
19
+ this.db.exec(`
20
+ CREATE TABLE IF NOT EXISTS runs (
21
+ id TEXT PRIMARY KEY,
22
+ status TEXT NOT NULL,
23
+ pool TEXT NOT NULL,
24
+ created_at TEXT NOT NULL,
25
+ updated_at TEXT NOT NULL,
26
+ claimed_by TEXT,
27
+ record TEXT NOT NULL
28
+ );
29
+ CREATE INDEX IF NOT EXISTS idx_runs_pool_status ON runs(pool, status, created_at);
30
+ CREATE INDEX IF NOT EXISTS idx_runs_updated ON runs(updated_at);
31
+
32
+ CREATE TABLE IF NOT EXISTS events (
33
+ run_id TEXT NOT NULL,
34
+ seq INTEGER NOT NULL,
35
+ ts TEXT NOT NULL,
36
+ event TEXT NOT NULL,
37
+ PRIMARY KEY (run_id, seq)
38
+ );
39
+ CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts);
40
+
41
+ CREATE TABLE IF NOT EXISTS receipts (
42
+ run_id TEXT PRIMARY KEY,
43
+ receipt TEXT NOT NULL
44
+ );
45
+
46
+ CREATE TABLE IF NOT EXISTS blobs (
47
+ hash TEXT PRIMARY KEY,
48
+ content BLOB NOT NULL
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS runners (
52
+ runner_id TEXT PRIMARY KEY,
53
+ pool TEXT NOT NULL,
54
+ public_key_pem TEXT NOT NULL,
55
+ token_hash TEXT NOT NULL UNIQUE,
56
+ enrolled_at TEXT NOT NULL
57
+ );
58
+
59
+ CREATE TABLE IF NOT EXISTS principals (
60
+ principal_id TEXT PRIMARY KEY,
61
+ name TEXT NOT NULL UNIQUE,
62
+ role TEXT NOT NULL,
63
+ token_hash TEXT NOT NULL UNIQUE,
64
+ created_at TEXT NOT NULL,
65
+ revoked_at TEXT
66
+ );
67
+
68
+ CREATE TABLE IF NOT EXISTS enroll_tokens (
69
+ token_hash TEXT PRIMARY KEY,
70
+ pool TEXT,
71
+ created_at TEXT NOT NULL,
72
+ expires_at TEXT NOT NULL,
73
+ used_at TEXT
74
+ );
75
+
76
+ CREATE TABLE IF NOT EXISTS claim_nonces (
77
+ nonce TEXT PRIMARY KEY,
78
+ expires_at_ms INTEGER NOT NULL
79
+ );
80
+ `);
81
+ }
82
+ close() {
83
+ this.db.close();
84
+ }
85
+ // ---- Runs ----
86
+ saveRun(record) {
87
+ this.db
88
+ .prepare(`INSERT INTO runs (id, status, pool, created_at, updated_at, claimed_by, record)
89
+ VALUES (?, ?, ?, ?, ?, ?, ?)
90
+ ON CONFLICT(id) DO UPDATE SET
91
+ status = excluded.status,
92
+ pool = excluded.pool,
93
+ updated_at = excluded.updated_at,
94
+ claimed_by = excluded.claimed_by,
95
+ record = excluded.record`)
96
+ .run(record.id, record.status, record.request.pool, record.createdAt, record.updatedAt, record.claimedBy ?? null, JSON.stringify(record));
97
+ }
98
+ // Rows in this database are written exclusively by this store from typed
99
+ // records; the JSON casts on read trust our own writes, which is the same
100
+ // trust boundary as the database file itself. (A corrupted file fails at
101
+ // JSON.parse with a loud error, not silently.)
102
+ getRun(runId) {
103
+ const row = this.db
104
+ .prepare("SELECT record FROM runs WHERE id = ?")
105
+ .get(runId);
106
+ return row ? JSON.parse(row.record) : undefined;
107
+ }
108
+ listRuns() {
109
+ const rows = this.db
110
+ .prepare("SELECT record FROM runs ORDER BY created_at DESC")
111
+ .all();
112
+ return rows.map((r) => JSON.parse(r.record));
113
+ }
114
+ claimNextRun(pool, runnerId, now) {
115
+ this.db.exec("BEGIN IMMEDIATE");
116
+ try {
117
+ const row = this.db
118
+ .prepare(`SELECT record FROM runs
119
+ WHERE pool = ? AND status = 'created'
120
+ ORDER BY created_at ASC LIMIT 1`)
121
+ .get(pool);
122
+ if (!row) {
123
+ this.db.exec("COMMIT");
124
+ return undefined;
125
+ }
126
+ const record = JSON.parse(row.record);
127
+ record.status = "claimed";
128
+ record.claimedBy = runnerId;
129
+ record.updatedAt = now;
130
+ const result = this.db
131
+ .prepare(`UPDATE runs SET status = ?, claimed_by = ?, updated_at = ?, record = ?
132
+ WHERE id = ? AND status = 'created'`)
133
+ .run("claimed", runnerId, now, JSON.stringify(record), record.id);
134
+ // BEGIN IMMEDIATE means no other writer ran between SELECT and UPDATE,
135
+ // but verify the compare-and-set anyway so a logic regression surfaces
136
+ // as "no claim" rather than two runners holding the same run.
137
+ if (Number(result.changes) !== 1) {
138
+ this.db.exec("ROLLBACK");
139
+ return undefined;
140
+ }
141
+ this.db.exec("COMMIT");
142
+ return record;
143
+ }
144
+ catch (error) {
145
+ this.db.exec("ROLLBACK");
146
+ throw error;
147
+ }
148
+ }
149
+ // ---- Events ----
150
+ appendEvents(runId, events) {
151
+ if (events.length === 0)
152
+ return;
153
+ const stmt = this.db.prepare("INSERT INTO events (run_id, seq, ts, event) VALUES (?, ?, ?, ?)");
154
+ this.db.exec("BEGIN IMMEDIATE");
155
+ try {
156
+ for (const event of events) {
157
+ stmt.run(runId, event.seq, event.ts, JSON.stringify(event));
158
+ }
159
+ this.db.exec("COMMIT");
160
+ }
161
+ catch (error) {
162
+ this.db.exec("ROLLBACK");
163
+ throw error;
164
+ }
165
+ }
166
+ getEvents(runId) {
167
+ const rows = this.db
168
+ .prepare("SELECT event FROM events WHERE run_id = ? ORDER BY seq ASC")
169
+ .all(runId);
170
+ return rows.map((r) => JSON.parse(r.event));
171
+ }
172
+ exportEvents(sinceMs) {
173
+ // Filter in SQL on the indexed ts column. Event timestamps are canonical
174
+ // ISO-8601 UTC strings, so lexicographic comparison matches chronological
175
+ // order and the cutoff can be passed as an ISO string.
176
+ const sinceIso = new Date(Math.max(sinceMs, 0)).toISOString();
177
+ const rows = this.db
178
+ .prepare(`SELECT run_id, event FROM events
179
+ WHERE ts >= ? ORDER BY run_id ASC, seq ASC`)
180
+ .all(sinceIso);
181
+ return rows.map((row) => ({
182
+ runId: row.run_id,
183
+ event: JSON.parse(row.event)
184
+ }));
185
+ }
186
+ // ---- Receipts ----
187
+ saveReceipt(runId, receipt) {
188
+ this.db
189
+ .prepare(`INSERT INTO receipts (run_id, receipt) VALUES (?, ?)
190
+ ON CONFLICT(run_id) DO UPDATE SET receipt = excluded.receipt`)
191
+ .run(runId, JSON.stringify(receipt));
192
+ }
193
+ getReceipt(runId) {
194
+ const row = this.db
195
+ .prepare("SELECT receipt FROM receipts WHERE run_id = ?")
196
+ .get(runId);
197
+ return row ? JSON.parse(row.receipt) : undefined;
198
+ }
199
+ // ---- Blobs ----
200
+ putBlob(content) {
201
+ const hash = sha256Hex(content);
202
+ this.db
203
+ .prepare("INSERT OR IGNORE INTO blobs (hash, content) VALUES (?, ?)")
204
+ .run(hash, content);
205
+ return hash;
206
+ }
207
+ getBlob(hash) {
208
+ if (!/^[0-9a-f]{64}$/.test(hash))
209
+ return undefined;
210
+ const row = this.db
211
+ .prepare("SELECT content FROM blobs WHERE hash = ?")
212
+ .get(hash);
213
+ return row ? Buffer.from(row.content) : undefined;
214
+ }
215
+ // ---- Runners ----
216
+ saveRunner(record) {
217
+ this.db
218
+ .prepare(`INSERT INTO runners (runner_id, pool, public_key_pem, token_hash, enrolled_at)
219
+ VALUES (?, ?, ?, ?, ?)
220
+ ON CONFLICT(runner_id) DO UPDATE SET
221
+ pool = excluded.pool,
222
+ public_key_pem = excluded.public_key_pem,
223
+ token_hash = excluded.token_hash,
224
+ enrolled_at = excluded.enrolled_at`)
225
+ .run(record.runnerId, record.pool, record.publicKeyPem, record.tokenHash, record.enrolledAt);
226
+ }
227
+ runnerFromRow(row) {
228
+ return {
229
+ runnerId: row.runner_id,
230
+ pool: row.pool,
231
+ publicKeyPem: row.public_key_pem,
232
+ tokenHash: row.token_hash,
233
+ enrolledAt: row.enrolled_at
234
+ };
235
+ }
236
+ getRunnerByTokenHash(tokenHash) {
237
+ const row = this.db
238
+ .prepare("SELECT * FROM runners WHERE token_hash = ?")
239
+ .get(tokenHash);
240
+ return row ? this.runnerFromRow(row) : undefined;
241
+ }
242
+ getRunnerById(runnerId) {
243
+ const row = this.db
244
+ .prepare("SELECT * FROM runners WHERE runner_id = ?")
245
+ .get(runnerId);
246
+ return row ? this.runnerFromRow(row) : undefined;
247
+ }
248
+ listRunners() {
249
+ const rows = this.db
250
+ .prepare("SELECT * FROM runners ORDER BY enrolled_at ASC")
251
+ .all();
252
+ return rows.map((r) => this.runnerFromRow(r));
253
+ }
254
+ // ---- Principals ----
255
+ savePrincipal(record) {
256
+ this.db
257
+ .prepare(`INSERT INTO principals (principal_id, name, role, token_hash, created_at, revoked_at)
258
+ VALUES (?, ?, ?, ?, ?, ?)
259
+ ON CONFLICT(principal_id) DO UPDATE SET
260
+ name = excluded.name,
261
+ role = excluded.role,
262
+ token_hash = excluded.token_hash,
263
+ revoked_at = excluded.revoked_at`)
264
+ .run(record.principalId, record.name, record.role, record.tokenHash, record.createdAt, record.revokedAt ?? null);
265
+ }
266
+ principalFromRow(row) {
267
+ if (!isPrincipalRole(row.role)) {
268
+ throw new Error(`principal ${row.principal_id} has unknown role "${row.role}" in store`);
269
+ }
270
+ return {
271
+ principalId: row.principal_id,
272
+ name: row.name,
273
+ role: row.role,
274
+ tokenHash: row.token_hash,
275
+ createdAt: row.created_at,
276
+ ...(row.revoked_at ? { revokedAt: row.revoked_at } : {})
277
+ };
278
+ }
279
+ getPrincipalByTokenHash(tokenHash) {
280
+ const row = this.db
281
+ .prepare("SELECT * FROM principals WHERE token_hash = ?")
282
+ .get(tokenHash);
283
+ return row ? this.principalFromRow(row) : undefined;
284
+ }
285
+ getPrincipalByName(name) {
286
+ const row = this.db
287
+ .prepare("SELECT * FROM principals WHERE name = ?")
288
+ .get(name);
289
+ return row ? this.principalFromRow(row) : undefined;
290
+ }
291
+ listPrincipals() {
292
+ const rows = this.db
293
+ .prepare("SELECT * FROM principals ORDER BY created_at ASC")
294
+ .all();
295
+ return rows.map((r) => this.principalFromRow(r));
296
+ }
297
+ revokePrincipal(principalId, now) {
298
+ const result = this.db
299
+ .prepare("UPDATE principals SET revoked_at = ? WHERE principal_id = ? AND revoked_at IS NULL")
300
+ .run(now, principalId);
301
+ return result.changes > 0;
302
+ }
303
+ // ---- Enroll tokens ----
304
+ saveEnrollToken(record) {
305
+ this.db
306
+ .prepare(`INSERT INTO enroll_tokens (token_hash, pool, created_at, expires_at, used_at)
307
+ VALUES (?, ?, ?, ?, ?)`)
308
+ .run(record.tokenHash, record.pool ?? null, record.createdAt, record.expiresAt, record.usedAt ?? null);
309
+ }
310
+ consumeEnrollToken(tokenHash, now) {
311
+ this.db.exec("BEGIN IMMEDIATE");
312
+ try {
313
+ const row = this.db
314
+ .prepare("SELECT * FROM enroll_tokens WHERE token_hash = ?")
315
+ .get(tokenHash);
316
+ if (!row ||
317
+ row.used_at !== null ||
318
+ // Compare against the caller-supplied clock so tests can inject time.
319
+ new Date(row.expires_at).getTime() < new Date(now).getTime()) {
320
+ this.db.exec("COMMIT");
321
+ return undefined;
322
+ }
323
+ this.db
324
+ .prepare("UPDATE enroll_tokens SET used_at = ? WHERE token_hash = ?")
325
+ .run(now, tokenHash);
326
+ this.db.exec("COMMIT");
327
+ return {
328
+ tokenHash: row.token_hash,
329
+ ...(row.pool ? { pool: row.pool } : {}),
330
+ createdAt: row.created_at,
331
+ expiresAt: row.expires_at,
332
+ usedAt: now
333
+ };
334
+ }
335
+ catch (error) {
336
+ this.db.exec("ROLLBACK");
337
+ throw error;
338
+ }
339
+ }
340
+ // ---- Claim nonces ----
341
+ recordClaimNonce(nonce, expiresAtMs) {
342
+ const result = this.db
343
+ .prepare("INSERT OR IGNORE INTO claim_nonces (nonce, expires_at_ms) VALUES (?, ?)")
344
+ .run(nonce, expiresAtMs);
345
+ return result.changes > 0;
346
+ }
347
+ pruneClaimNonces(nowMs) {
348
+ const result = this.db
349
+ .prepare("DELETE FROM claim_nonces WHERE expires_at_ms <= ?")
350
+ .run(nowMs);
351
+ return Number(result.changes);
352
+ }
353
+ // ---- Retention / GC ----
354
+ deleteRunsUpdatedBefore(cutoffMs, terminalStatuses) {
355
+ const placeholders = terminalStatuses.map(() => "?").join(", ");
356
+ const rows = this.db
357
+ .prepare(`SELECT id, updated_at FROM runs WHERE status IN (${placeholders})`)
358
+ .all(...terminalStatuses);
359
+ const doomed = rows
360
+ .filter((r) => new Date(r.updated_at).getTime() < cutoffMs)
361
+ .map((r) => r.id);
362
+ if (doomed.length === 0)
363
+ return [];
364
+ this.db.exec("BEGIN IMMEDIATE");
365
+ try {
366
+ for (const id of doomed) {
367
+ this.db.prepare("DELETE FROM events WHERE run_id = ?").run(id);
368
+ this.db.prepare("DELETE FROM receipts WHERE run_id = ?").run(id);
369
+ this.db.prepare("DELETE FROM runs WHERE id = ?").run(id);
370
+ }
371
+ this.db.exec("COMMIT");
372
+ }
373
+ catch (error) {
374
+ this.db.exec("ROLLBACK");
375
+ throw error;
376
+ }
377
+ return doomed;
378
+ }
379
+ deleteBlobsExcept(keep) {
380
+ const hashes = this.db.prepare("SELECT hash FROM blobs").all().map((r) => r.hash);
381
+ const doomed = hashes.filter((h) => !keep.has(h));
382
+ if (doomed.length === 0)
383
+ return 0;
384
+ const stmt = this.db.prepare("DELETE FROM blobs WHERE hash = ?");
385
+ this.db.exec("BEGIN IMMEDIATE");
386
+ try {
387
+ for (const hash of doomed)
388
+ stmt.run(hash);
389
+ this.db.exec("COMMIT");
390
+ }
391
+ catch (error) {
392
+ this.db.exec("ROLLBACK");
393
+ throw error;
394
+ }
395
+ return doomed.length;
396
+ }
397
+ countBlobs() {
398
+ const row = this.db.prepare("SELECT COUNT(*) AS n FROM blobs").get();
399
+ return Number(row.n);
400
+ }
401
+ }
@@ -0,0 +1,107 @@
1
+ import type { ActorRef, ChainedEvent, ContinuationRef, Receipt, RunContract, RunRequest, RunStatus } from "@fusionkit/protocol";
2
+ export type { RunRequest };
3
+ export type RunRecord = {
4
+ id: string;
5
+ status: RunStatus;
6
+ createdAt: string;
7
+ updatedAt: string;
8
+ request: RunRequest;
9
+ consentRequirements: string[];
10
+ approvals: ApprovalRecord[];
11
+ contract?: RunContract;
12
+ claimedBy?: string;
13
+ failureMessage?: string;
14
+ };
15
+ export type ApprovalRecord = {
16
+ actor: ActorRef;
17
+ ts: string;
18
+ /** Present when the approval was backed by a verified IdP assertion. */
19
+ idpSubject?: string;
20
+ idpIssuer?: string;
21
+ };
22
+ export type RunnerRecord = {
23
+ runnerId: string;
24
+ pool: string;
25
+ publicKeyPem: string;
26
+ tokenHash: string;
27
+ enrolledAt: string;
28
+ };
29
+ /** A principal authorized to call the plane, identified by a hashed key. */
30
+ export type PrincipalRole = "admin" | "requester" | "approver" | "enroller";
31
+ export declare const PRINCIPAL_ROLES: readonly ["admin", "requester", "approver", "enroller"];
32
+ export declare function isPrincipalRole(value: string): value is PrincipalRole;
33
+ export type PrincipalRecord = {
34
+ principalId: string;
35
+ name: string;
36
+ role: PrincipalRole;
37
+ /** sha256 of the bearer token; the token itself is never stored. */
38
+ tokenHash: string;
39
+ createdAt: string;
40
+ /** Set when revoked; revoked principals never authenticate. */
41
+ revokedAt?: string;
42
+ };
43
+ /** A single-use, expiring runner enrollment token (hashed at rest). */
44
+ export type EnrollTokenRecord = {
45
+ tokenHash: string;
46
+ pool?: string;
47
+ createdAt: string;
48
+ expiresAt: string;
49
+ usedAt?: string;
50
+ };
51
+ export type RunSummaryRow = {
52
+ record: RunRecord;
53
+ hasReceipt: boolean;
54
+ };
55
+ /**
56
+ * Durable, transactional storage for the control plane. The default
57
+ * implementation ([sqlite-store.ts](sqlite-store.ts)) is backed by
58
+ * node:sqlite with WAL and immediate-transaction claims; the interface is
59
+ * deliberately narrow so a Postgres adapter can be added for multi-node
60
+ * deployments without touching the plane logic.
61
+ */
62
+ export interface PlaneStore {
63
+ /** Release any underlying handles. */
64
+ close(): void;
65
+ saveRun(record: RunRecord): void;
66
+ getRun(runId: string): RunRecord | undefined;
67
+ listRuns(): RunRecord[];
68
+ /**
69
+ * Atomically claim the oldest `created` run in a pool: transition it to
70
+ * `claimed` for the runner and return the updated record, or undefined if
71
+ * none is available. Must be a single transaction (compare-and-set).
72
+ */
73
+ claimNextRun(pool: string, runnerId: string, now: string): RunRecord | undefined;
74
+ appendEvents(runId: string, events: ChainedEvent[]): void;
75
+ getEvents(runId: string): ChainedEvent[];
76
+ /** All events across all runs at or after `sinceMs`, oldest first. */
77
+ exportEvents(sinceMs: number): {
78
+ runId: string;
79
+ event: ChainedEvent;
80
+ }[];
81
+ saveReceipt(runId: string, receipt: Receipt): void;
82
+ getReceipt(runId: string): Receipt | undefined;
83
+ putBlob(content: Buffer): string;
84
+ getBlob(hash: string): Buffer | undefined;
85
+ saveRunner(record: RunnerRecord): void;
86
+ getRunnerByTokenHash(tokenHash: string): RunnerRecord | undefined;
87
+ getRunnerById(runnerId: string): RunnerRecord | undefined;
88
+ listRunners(): RunnerRecord[];
89
+ savePrincipal(record: PrincipalRecord): void;
90
+ getPrincipalByTokenHash(tokenHash: string): PrincipalRecord | undefined;
91
+ getPrincipalByName(name: string): PrincipalRecord | undefined;
92
+ listPrincipals(): PrincipalRecord[];
93
+ revokePrincipal(principalId: string, now: string): boolean;
94
+ saveEnrollToken(record: EnrollTokenRecord): void;
95
+ /** Atomically consume an enroll token; returns it if valid and unused. */
96
+ consumeEnrollToken(tokenHash: string, now: string): EnrollTokenRecord | undefined;
97
+ /** Record a nonce; returns false if it was already present (a replay). */
98
+ recordClaimNonce(nonce: string, expiresAtMs: number): boolean;
99
+ /** Delete claim nonces whose expiry is at or before `nowMs`. */
100
+ pruneClaimNonces(nowMs: number): number;
101
+ /** Delete terminal runs (and their events/receipts) updated before the cutoff. */
102
+ deleteRunsUpdatedBefore(cutoffMs: number, terminalStatuses: RunStatus[]): string[];
103
+ /** Delete every blob whose hash is not in `keep`; returns the count removed. */
104
+ deleteBlobsExcept(keep: Set<string>): number;
105
+ countBlobs(): number;
106
+ }
107
+ export type ContinuationRefOrUndefined = ContinuationRef | undefined;
package/dist/store.js ADDED
@@ -0,0 +1,9 @@
1
+ export const PRINCIPAL_ROLES = [
2
+ "admin",
3
+ "requester",
4
+ "approver",
5
+ "enroller"
6
+ ];
7
+ export function isPrincipalRole(value) {
8
+ return PRINCIPAL_ROLES.includes(value);
9
+ }
@@ -0,0 +1 @@
1
+ export {};