@substrat-run/contract-tests 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-expiry-suite.d.ts +8 -0
- package/dist/capability-expiry-suite.d.ts.map +1 -0
- package/dist/capability-expiry-suite.js +131 -0
- package/dist/capability-expiry-suite.js.map +1 -0
- package/dist/capability-suite.d.ts +3 -0
- package/dist/capability-suite.d.ts.map +1 -0
- package/dist/capability-suite.js +498 -0
- package/dist/capability-suite.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/job-run-suite.d.ts +24 -0
- package/dist/job-run-suite.d.ts.map +1 -0
- package/dist/job-run-suite.js +582 -0
- package/dist/job-run-suite.js.map +1 -0
- package/dist/modules.d.ts +469 -0
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js +449 -5
- package/dist/modules.js.map +1 -1
- package/dist/permission-suite.d.ts.map +1 -1
- package/dist/permission-suite.js +267 -2
- package/dist/permission-suite.js.map +1 -1
- package/dist/schedule-suite.d.ts +14 -0
- package/dist/schedule-suite.d.ts.map +1 -1
- package/dist/schedule-suite.js +77 -7
- package/dist/schedule-suite.js.map +1 -1
- package/dist/scope-host-suite.d.ts.map +1 -1
- package/dist/scope-host-suite.js +1195 -25
- package/dist/scope-host-suite.js.map +1 -1
- package/dist/system-switch-suite.d.ts +16 -0
- package/dist/system-switch-suite.d.ts.map +1 -0
- package/dist/system-switch-suite.js +215 -0
- package/dist/system-switch-suite.js.map +1 -0
- package/package.json +5 -4
package/dist/modules.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modules.d.ts","sourceRoot":"","sources":["../src/modules.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EASL,CAAC,EAIF,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAML,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAI9B,eAAO,MAAM,eAAe
|
|
1
|
+
{"version":3,"file":"modules.d.ts","sourceRoot":"","sources":["../src/modules.ts"],"names":[],"mappings":"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"}
|
package/dist/modules.js
CHANGED
|
@@ -37,7 +37,20 @@ export const scheduleModManifest = moduleManifest.parse({
|
|
|
37
37
|
migrations: { journalDir: './migrations', compatibleFrom: '1.0.0' },
|
|
38
38
|
attachmentTargets: [],
|
|
39
39
|
entitlementKey: 'sched',
|
|
40
|
-
schedules: [
|
|
40
|
+
schedules: [
|
|
41
|
+
{ operation: 'sched/tick', cadence: { everyMinutes: 60 }, permissions: ['sched:tick'] },
|
|
42
|
+
// #1288's regression fixture, and it is not a silly name: `scheduleSpec.operation`
|
|
43
|
+
// is `z.string().min(1)`, so this IS a legal schedule, and its key is byte-for-byte
|
|
44
|
+
// the key the freshness expectation below writes under. Until `kind` joined the
|
|
45
|
+
// primary key of `_substrat_schedule_state` the two shared one row — each pass
|
|
46
|
+
// overwrote the other's verdict, and each read back the other's time. Declared here
|
|
47
|
+
// rather than in a module of its own so BOTH adapters' schedule suites carry it.
|
|
48
|
+
{
|
|
49
|
+
operation: 'freshness:sched.ticked',
|
|
50
|
+
cadence: { everyMinutes: 60 },
|
|
51
|
+
permissions: ['sched:tick'],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
41
54
|
// #1232: a freshness expectation on the module's own emitted type — declared TWICE
|
|
42
55
|
// with different windows, deliberately: the evaluator must collapse duplicates to
|
|
43
56
|
// the tightest window (one row per (scope, eventType), or the drain dedupe eats one).
|
|
@@ -68,6 +81,51 @@ export const freshnessMod = {
|
|
|
68
81
|
migrations: [],
|
|
69
82
|
operations: {},
|
|
70
83
|
};
|
|
84
|
+
/**
|
|
85
|
+
* #1577: the module a resumable RUN acts through.
|
|
86
|
+
*
|
|
87
|
+
* The job's handler is NOT here — a job is host code (`registerJob`), so the suite
|
|
88
|
+
* hands the host a closure, which is precisely what an operation or consumer
|
|
89
|
+
* cannot be on the Cloudflare adapter. What has to be here is the operation the
|
|
90
|
+
* job's steps invoke, because the ScopeDO closes over a code-time module set and
|
|
91
|
+
* the step's write has to be a real write, through a real `ctx.check`, inside the
|
|
92
|
+
* scope — otherwise "no step before it repeated" is asserted against a counter in
|
|
93
|
+
* the test process and proves nothing about what survived a pass boundary.
|
|
94
|
+
*
|
|
95
|
+
* `jobs:write` is declared but NOT projected by any schedule: the module declares
|
|
96
|
+
* no schedules, so the suite grants it to `system:@test/jobs` explicitly with
|
|
97
|
+
* `grantToSystem`. That IS the shape a deployment uses — a job's authority is a
|
|
98
|
+
* grant a human can read in the permission diff and revoke per scope, same as a
|
|
99
|
+
* schedule's, and nothing about a job invents a second way in.
|
|
100
|
+
*/
|
|
101
|
+
export const jobsModManifest = moduleManifest.parse({
|
|
102
|
+
id: '@test/jobs',
|
|
103
|
+
version: '1.0.0',
|
|
104
|
+
kernelContract: '^0.0.1',
|
|
105
|
+
permissions: [{ key: 'jobs:write', description: 'record one walked item' }],
|
|
106
|
+
events: { emits: [], consumes: [] },
|
|
107
|
+
migrations: { journalDir: './migrations', compatibleFrom: '1.0.0' },
|
|
108
|
+
attachmentTargets: [],
|
|
109
|
+
entitlementKey: 'jobs',
|
|
110
|
+
});
|
|
111
|
+
export const jobsMod = {
|
|
112
|
+
manifest: jobsModManifest,
|
|
113
|
+
migrations: [
|
|
114
|
+
{ version: '0001-init', sql: 'CREATE TABLE job_items (item TEXT NOT NULL)' },
|
|
115
|
+
],
|
|
116
|
+
operations: {
|
|
117
|
+
// Deliberately NOT idempotent and NOT unique-keyed: a repeated step shows up as
|
|
118
|
+
// a DUPLICATE ROW rather than being silently absorbed, which is the only way the
|
|
119
|
+
// resume assertion can fail loudly when the memo stops working.
|
|
120
|
+
'jobs/record': (async (ctx, input) => {
|
|
121
|
+
assertAllowed(await ctx.check('jobs:write'));
|
|
122
|
+
ctx.sql.exec('INSERT INTO job_items (item) VALUES (?)', [input.item]);
|
|
123
|
+
}),
|
|
124
|
+
'jobs/items': ((ctx) => ctx.sql
|
|
125
|
+
.query('SELECT item FROM job_items ORDER BY rowid')
|
|
126
|
+
.map((r) => r.item)),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
71
129
|
export const flowModManifest = moduleManifest.parse({
|
|
72
130
|
id: '@test/flow',
|
|
73
131
|
version: '1.0.0',
|
|
@@ -228,6 +286,10 @@ export const permModManifest = moduleManifest.parse({
|
|
|
228
286
|
permissions: [
|
|
229
287
|
{ key: 'perm:use', description: 'use the thing' },
|
|
230
288
|
{ key: 'perm:read', description: 'read the thing' },
|
|
289
|
+
// #1655: exactly PERMISSION_KEY_MAX_LENGTH (41) bytes, the longest key a manifest
|
|
290
|
+
// accepts — so both adapters run a real check of it, and the Durable-Object one
|
|
291
|
+
// builds `granted:<key>%` at the 50-byte LIKE limit it enforces and node does not.
|
|
292
|
+
{ key: 'perm:long-key-at-the-like-pattern-limit01', description: 'the longest key' },
|
|
231
293
|
],
|
|
232
294
|
events: {
|
|
233
295
|
// #473: the attachment surface emits these on upload/remove; declaring them keeps the
|
|
@@ -235,8 +297,11 @@ export const permModManifest = moduleManifest.parse({
|
|
|
235
297
|
emits: [
|
|
236
298
|
{ type: 'attachment.added', schemaVersion: 1 },
|
|
237
299
|
{ type: 'attachment.removed', schemaVersion: 1 },
|
|
300
|
+
{ type: 'perm.check-requested', schemaVersion: 1 },
|
|
238
301
|
],
|
|
239
|
-
|
|
302
|
+
// #1642: a consumer runs under a SYSTEM actor, whose check allows without asking
|
|
303
|
+
// the checker — the one path a malformed key could slip through unrefused.
|
|
304
|
+
consumes: [{ type: 'perm.check-requested', schemaVersion: 1 }],
|
|
240
305
|
},
|
|
241
306
|
migrations: { journalDir: './migrations', compatibleFrom: '1.0.0' },
|
|
242
307
|
// #473: an attachment target on `item`, gated read=perm:read / write=perm:use — reusing
|
|
@@ -546,7 +611,90 @@ const authorizedReadOp = async (ctx, input) => {
|
|
|
546
611
|
return ctx.sql.query('SELECT COUNT(*) AS n FROM testmod_items')[0].n;
|
|
547
612
|
};
|
|
548
613
|
const readOutboxOp = (ctx) => ctx.sql.query('SELECT id, type, authorization, impersonation, operation FROM _substrat_outbox ORDER BY id');
|
|
549
|
-
|
|
614
|
+
// #1642: a check whose throw is CAUGHT AND IGNORED — the careless caller a
|
|
615
|
+
// fail-closed refusal has to survive. It reports what it was handed back, then emits
|
|
616
|
+
// anyway, so a test can read what the event's K-34 `authorization` says passed.
|
|
617
|
+
const swallowedCheckOp = async (ctx, input) => {
|
|
618
|
+
let allowed = false;
|
|
619
|
+
let threw = null;
|
|
620
|
+
try {
|
|
621
|
+
allowed = (await ctx.check(input.permission)).allowed;
|
|
622
|
+
}
|
|
623
|
+
catch (err) {
|
|
624
|
+
threw = err instanceof Error ? err.message : String(err);
|
|
625
|
+
}
|
|
626
|
+
ctx.emit({
|
|
627
|
+
type: 'perm.acted',
|
|
628
|
+
schemaVersion: 1,
|
|
629
|
+
entity: { entityType: 'test-thing', entityId: 'swallowed' },
|
|
630
|
+
piiClass: 'none',
|
|
631
|
+
payload: {},
|
|
632
|
+
});
|
|
633
|
+
return { allowed, threw };
|
|
634
|
+
};
|
|
635
|
+
// #1642: hand a key to the system-actor path. The consumer below does the check.
|
|
636
|
+
const requestCheckOp = (ctx, input) => {
|
|
637
|
+
ctx.emit({
|
|
638
|
+
type: 'perm.check-requested',
|
|
639
|
+
schemaVersion: 1,
|
|
640
|
+
entity: { entityType: 'test-thing', entityId: ulid() },
|
|
641
|
+
piiClass: 'none',
|
|
642
|
+
payload: input,
|
|
643
|
+
});
|
|
644
|
+
};
|
|
645
|
+
const CHECK_LOG_DDL = `CREATE TABLE IF NOT EXISTS perm_check_log (
|
|
646
|
+
event_id TEXT PRIMARY KEY, permission TEXT NOT NULL, allowed INTEGER NOT NULL,
|
|
647
|
+
threw TEXT, grant_threw TEXT)`;
|
|
648
|
+
/**
|
|
649
|
+
* Runs as the system actor, whose `ctx.check` is allowed by construction. With
|
|
650
|
+
* `swallow` it catches the check's throw — and a `ctx.grant` of the same key, which
|
|
651
|
+
* the system actor would otherwise write straight into `_substrat_tuples` — and logs
|
|
652
|
+
* the outcome; without it, the throw dead-letters the event.
|
|
653
|
+
*/
|
|
654
|
+
const checkRequestedConsumer = async (ctx, event) => {
|
|
655
|
+
const p = event.payload;
|
|
656
|
+
const permission = p.permission; // the cast under test
|
|
657
|
+
if (!p.swallow) {
|
|
658
|
+
assertAllowed(await ctx.check(permission));
|
|
659
|
+
ctx.sql.exec(CHECK_LOG_DDL);
|
|
660
|
+
ctx.sql.exec('INSERT INTO perm_check_log (event_id, permission, allowed) VALUES (?, ?, 1)', [
|
|
661
|
+
event.id,
|
|
662
|
+
p.permission,
|
|
663
|
+
]);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
let allowed = false;
|
|
667
|
+
let threw = null;
|
|
668
|
+
let grantThrew = null;
|
|
669
|
+
try {
|
|
670
|
+
allowed = (await ctx.check(permission)).allowed;
|
|
671
|
+
}
|
|
672
|
+
catch (err) {
|
|
673
|
+
threw = err instanceof Error ? err.message : String(err);
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
await ctx.grant(principalId.parse(p.principal), permission, {
|
|
677
|
+
entityType: 'item',
|
|
678
|
+
entityId: 'check-requested',
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
grantThrew = err instanceof Error ? err.message : String(err);
|
|
683
|
+
}
|
|
684
|
+
ctx.sql.exec(CHECK_LOG_DDL);
|
|
685
|
+
ctx.sql.exec('INSERT INTO perm_check_log (event_id, permission, allowed, threw, grant_threw) VALUES (?, ?, ?, ?, ?)', [event.id, p.permission, allowed ? 1 : 0, threw, grantThrew]);
|
|
686
|
+
};
|
|
687
|
+
const readCheckLogOp = (ctx) => {
|
|
688
|
+
ctx.sql.exec(CHECK_LOG_DDL);
|
|
689
|
+
return {
|
|
690
|
+
log: ctx.sql.query('SELECT event_id, permission, allowed, threw, grant_threw FROM perm_check_log ORDER BY event_id'),
|
|
691
|
+
deliveries: ctx.sql.query(`SELECT d.event_id, d.error, json_extract(o.payload, '$.permission') AS permission
|
|
692
|
+
FROM _substrat_deliveries d JOIN _substrat_outbox o ON o.id = d.event_id
|
|
693
|
+
WHERE o.type = 'perm.check-requested' ORDER BY d.event_id`),
|
|
694
|
+
tuples: ctx.sql.query(`SELECT subject, relation, object FROM _substrat_tuples WHERE object = 'item:check-requested'`),
|
|
695
|
+
};
|
|
696
|
+
};
|
|
697
|
+
const readDenialsOp = (ctx) => ctx.sql.query(`SELECT actor, permission, tenant_id, scope_id, operation, impersonation, invocation_id
|
|
550
698
|
FROM _substrat_denials ORDER BY id`);
|
|
551
699
|
// -- K-42 fixtures (#868) ----------------------------------------------------
|
|
552
700
|
// A read, a bare-SQL write, and an intent. The bare-SQL write is the one that
|
|
@@ -795,6 +943,12 @@ export const scheduleMod = {
|
|
|
795
943
|
payload: {},
|
|
796
944
|
});
|
|
797
945
|
}),
|
|
946
|
+
// #1288: the colliding schedule's handler. Deliberately writes nothing and emits
|
|
947
|
+
// nothing — its `_substrat_schedule_state` row IS the evidence it ran, which is
|
|
948
|
+
// the fact under test. It holds `sched:tick` because its schedule declares it.
|
|
949
|
+
'freshness:sched.ticked': (async (ctx) => {
|
|
950
|
+
assertAllowed(await ctx.check('sched:tick'));
|
|
951
|
+
}),
|
|
798
952
|
// Checks a permission the schedule never granted the system principal — invoked
|
|
799
953
|
// directly through the system door, it must be DENIED, proving ctx.check is the gate.
|
|
800
954
|
'sched/needs-admin': (async (ctx) => {
|
|
@@ -802,7 +956,11 @@ export const scheduleMod = {
|
|
|
802
956
|
}),
|
|
803
957
|
'sched/count': ((ctx) => ctx.sql.query('SELECT COUNT(*) AS n FROM sched_ticks')[0].n),
|
|
804
958
|
'sched/read-outbox': ((ctx) => ctx.sql.query('SELECT type, actor, operation FROM _substrat_outbox ORDER BY id')),
|
|
805
|
-
|
|
959
|
+
// #1288: `kind` is selected, and the order leads with it — two rows may now share
|
|
960
|
+
// a `schedule_op`, so ordering by that alone leaves the pair's order to the query
|
|
961
|
+
// planner and an assertion on the array would be flaky rather than wrong.
|
|
962
|
+
'sched/schedule-state': ((ctx) => ctx.sql.query(`SELECT kind, schedule_op, last_status FROM _substrat_schedule_state
|
|
963
|
+
ORDER BY kind, schedule_op`)),
|
|
806
964
|
},
|
|
807
965
|
};
|
|
808
966
|
export const flowMod = {
|
|
@@ -824,7 +982,7 @@ export const flowMod = {
|
|
|
824
982
|
});
|
|
825
983
|
}),
|
|
826
984
|
'flow/log': ((ctx) => ctx.sql.query('SELECT event_id, type FROM flow_log ORDER BY event_id')),
|
|
827
|
-
'flow/deliveries': ((ctx) => ctx.sql.query(`SELECT event_id, consumer_module, error FROM _substrat_deliveries
|
|
985
|
+
'flow/deliveries': ((ctx) => ctx.sql.query(`SELECT event_id, consumer_module, error, invocation_id FROM _substrat_deliveries
|
|
828
986
|
WHERE consumer_module = '@test/flow' ORDER BY event_id`)),
|
|
829
987
|
'flow/step2-actors': ((ctx) => ctx.sql.query(`SELECT actor, operation FROM _substrat_outbox WHERE type = 'flow.step2'`)),
|
|
830
988
|
// #1237: the causal edge, straight off the spine. Both types, so a test can see
|
|
@@ -935,6 +1093,10 @@ export const permMod = {
|
|
|
935
1093
|
'perm/authorized-read': authorizedReadOp,
|
|
936
1094
|
'perm/read-outbox': readOutboxOp,
|
|
937
1095
|
'perm/read-denials': readDenialsOp,
|
|
1096
|
+
// #1642
|
|
1097
|
+
'perm/swallowed-check': swallowedCheckOp,
|
|
1098
|
+
'perm/request-check': requestCheckOp,
|
|
1099
|
+
'perm/read-check-log': readCheckLogOp,
|
|
938
1100
|
// K-42 (#868)
|
|
939
1101
|
'perm/write-note': noteWriteOp,
|
|
940
1102
|
'perm/read-notes': noteReadOp,
|
|
@@ -973,6 +1135,9 @@ export const permMod = {
|
|
|
973
1135
|
return { revoked: true };
|
|
974
1136
|
}),
|
|
975
1137
|
},
|
|
1138
|
+
consumers: {
|
|
1139
|
+
'perm.check-requested': checkRequestedConsumer,
|
|
1140
|
+
},
|
|
976
1141
|
};
|
|
977
1142
|
export const connectorModManifest = moduleManifest.parse({
|
|
978
1143
|
id: '@test/connector',
|
|
@@ -1515,6 +1680,178 @@ export const idempotencyMod = {
|
|
|
1515
1680
|
operationConcurrency: operationConcurrencyOf(idempotencyDeclaration),
|
|
1516
1681
|
operationIdempotencyOptOuts: operationIdempotencyOptOutsOf(idempotencyDeclaration),
|
|
1517
1682
|
};
|
|
1683
|
+
// -- capabilities (#1672) ------------------------------------------------------
|
|
1684
|
+
/**
|
|
1685
|
+
* The fixture for the capability suite: a folder tree (`doc` → `folder`, `folder` →
|
|
1686
|
+
* `folder`), three keys, and operations shaped the way a document vertical's would be —
|
|
1687
|
+
* a read and a write that check ONE entity, so what a capability can reach is decided by
|
|
1688
|
+
* the checker's walk and nothing else.
|
|
1689
|
+
*
|
|
1690
|
+
* The minting operations are deliberately UNGUARDED, like `perm/share`: the guardrails
|
|
1691
|
+
* under test live inside `ctx.capabilities`, and an operation-level check in front of
|
|
1692
|
+
* them would only prove the check.
|
|
1693
|
+
*/
|
|
1694
|
+
export const capModManifest = moduleManifest.parse({
|
|
1695
|
+
id: '@cap/mod',
|
|
1696
|
+
version: '1.0.0',
|
|
1697
|
+
kernelContract: '^0.0.1',
|
|
1698
|
+
permissions: [
|
|
1699
|
+
{ key: 'cap:read', description: 'read a folder or a document' },
|
|
1700
|
+
{ key: 'cap:write', description: 'comment on a document' },
|
|
1701
|
+
{ key: 'cap:admin', description: 'arrange the folder tree' },
|
|
1702
|
+
],
|
|
1703
|
+
events: {
|
|
1704
|
+
emits: [
|
|
1705
|
+
{ type: 'cap.commented', schemaVersion: 1 },
|
|
1706
|
+
{ type: 'cap.mint-requested', schemaVersion: 1 },
|
|
1707
|
+
],
|
|
1708
|
+
consumes: [{ type: 'cap.mint-requested', schemaVersion: 1 }],
|
|
1709
|
+
},
|
|
1710
|
+
migrations: { journalDir: './migrations', compatibleFrom: '1.0.0' },
|
|
1711
|
+
attachmentTargets: [],
|
|
1712
|
+
entityRelations: [
|
|
1713
|
+
{ entityType: 'doc', parentType: 'folder' },
|
|
1714
|
+
{ entityType: 'folder', parentType: 'folder' },
|
|
1715
|
+
],
|
|
1716
|
+
entitlementKey: 'cap',
|
|
1717
|
+
});
|
|
1718
|
+
const CAP_READ = permissionKey.parse('cap:read');
|
|
1719
|
+
const CAP_WRITE = permissionKey.parse('cap:write');
|
|
1720
|
+
/** Every channel through which a module could persist its own minted secret by accident. */
|
|
1721
|
+
export const CAP_LEAK_CHANNELS = ['emit', 'key', 'entity', 'kind', 'intent', 'sql', 'bytes'];
|
|
1722
|
+
export const capMod = {
|
|
1723
|
+
manifest: capModManifest,
|
|
1724
|
+
migrations: [
|
|
1725
|
+
{ version: '0001-init', sql: 'CREATE TABLE cap_notes (id TEXT PRIMARY KEY, body TEXT NOT NULL)' },
|
|
1726
|
+
],
|
|
1727
|
+
operations: {
|
|
1728
|
+
'cap/link': linkOp,
|
|
1729
|
+
'cap/share': ((ctx, input) => ctx.capabilities.mint(input)),
|
|
1730
|
+
// The link a vertical would hand back — the secret spliced into a URL fragment, so the
|
|
1731
|
+
// idempotency redaction has to find it INSIDE a string, not only as a whole value.
|
|
1732
|
+
'cap/share-link': (async (ctx, input) => {
|
|
1733
|
+
const minted = await ctx.capabilities.mint(input);
|
|
1734
|
+
return { id: minted.id, link: `https://docs.example/#share=${minted.secret}` };
|
|
1735
|
+
}),
|
|
1736
|
+
// A module that mints and then writes something through ONE persisting channel. With
|
|
1737
|
+
// `leak: true` what it writes is the secret — each channel an accident could take, and
|
|
1738
|
+
// each must be refused, the refusal rolling the mint back with it. With `leak: false` it
|
|
1739
|
+
// writes a harmless value through the very same channel: the clean twin, which must go
|
|
1740
|
+
// through, so a guard that refused the channel outright would fail it.
|
|
1741
|
+
'cap/share-and-leak': (async (ctx, input) => {
|
|
1742
|
+
const i = input;
|
|
1743
|
+
const { via, leak = true, ...spec } = i;
|
|
1744
|
+
const minted = await ctx.capabilities.mint(spec);
|
|
1745
|
+
const value = leak ? minted.secret : `harmless-${minted.id}`;
|
|
1746
|
+
const note = (payload, entity = spec.entity) => ctx.emit({ type: 'cap.commented', schemaVersion: 1, entity, piiClass: 'none', payload });
|
|
1747
|
+
switch (via) {
|
|
1748
|
+
case 'emit': // a payload value, spliced into a string
|
|
1749
|
+
note({ note: `share it: ${value}` });
|
|
1750
|
+
break;
|
|
1751
|
+
case 'key': // an object KEY — JSON persists property names too
|
|
1752
|
+
note({ links: { [value]: true } });
|
|
1753
|
+
break;
|
|
1754
|
+
case 'entity': // the event's entity id — persisted in its own outbox column
|
|
1755
|
+
note({ note: 'filed under the link' }, { entityType: 'doc', entityId: value });
|
|
1756
|
+
break;
|
|
1757
|
+
case 'kind': // a platform intent's kind — persisted as surely as its payload
|
|
1758
|
+
ctx.requestPlatform({ kind: value, payload: {} });
|
|
1759
|
+
break;
|
|
1760
|
+
case 'intent': // a platform intent's payload
|
|
1761
|
+
ctx.requestPlatform({ kind: 'cap-note', payload: { body: value } });
|
|
1762
|
+
break;
|
|
1763
|
+
case 'sql': // a text SQL parameter
|
|
1764
|
+
ctx.sql.exec('INSERT INTO cap_notes (id, body) VALUES (?, ?)', [ulid(), value]);
|
|
1765
|
+
break;
|
|
1766
|
+
case 'bytes': // a BYTE SQL parameter — stored as a BLOB, still the plaintext
|
|
1767
|
+
ctx.sql.exec('INSERT INTO cap_notes (id, body) VALUES (?, ?)', [
|
|
1768
|
+
ulid(),
|
|
1769
|
+
new TextEncoder().encode(value),
|
|
1770
|
+
]);
|
|
1771
|
+
break;
|
|
1772
|
+
}
|
|
1773
|
+
return { id: minted.id };
|
|
1774
|
+
}),
|
|
1775
|
+
'cap/unshare': (async (ctx, input) => {
|
|
1776
|
+
await ctx.capabilities.revoke(input.id);
|
|
1777
|
+
return { revoked: true };
|
|
1778
|
+
}),
|
|
1779
|
+
'cap/list': ((ctx, input) => ctx.capabilities.list(input)),
|
|
1780
|
+
// A read of ONE entity — the proof comes back so a test can see what authorized it.
|
|
1781
|
+
'cap/read': (async (ctx, input) => {
|
|
1782
|
+
const entity = input.entity;
|
|
1783
|
+
const decision = await ctx.check(CAP_READ, entity);
|
|
1784
|
+
assertAllowed(decision);
|
|
1785
|
+
return { read: entity, proof: decision.proof };
|
|
1786
|
+
}),
|
|
1787
|
+
// A node-level read: a capability holds no node-level authority, so this refuses it.
|
|
1788
|
+
'cap/read-all': (async (ctx) => {
|
|
1789
|
+
assertAllowed(await ctx.check(CAP_READ));
|
|
1790
|
+
return { all: true };
|
|
1791
|
+
}),
|
|
1792
|
+
// A write of ONE entity, which emits — K-34 authorization and the actor land on it.
|
|
1793
|
+
'cap/comment': (async (ctx, input) => {
|
|
1794
|
+
const i = input;
|
|
1795
|
+
assertAllowed(await ctx.check(CAP_WRITE, i.doc));
|
|
1796
|
+
ctx.emit({
|
|
1797
|
+
type: 'cap.commented',
|
|
1798
|
+
schemaVersion: 1,
|
|
1799
|
+
entity: i.doc,
|
|
1800
|
+
piiClass: 'none',
|
|
1801
|
+
payload: { body: i.body },
|
|
1802
|
+
});
|
|
1803
|
+
return { commented: true };
|
|
1804
|
+
}),
|
|
1805
|
+
'cap/whoami': whoAmIOp,
|
|
1806
|
+
// Asks the CONSUMER below to mint. A consumer runs under an override actor whose
|
|
1807
|
+
// checks allow unconditionally, so "the minter holds every key" would be vacuous
|
|
1808
|
+
// there — the mint must be refused outright, whatever the check would have said.
|
|
1809
|
+
'cap/request-mint': (async (ctx, input) => {
|
|
1810
|
+
ctx.emit({
|
|
1811
|
+
type: 'cap.mint-requested',
|
|
1812
|
+
schemaVersion: 1,
|
|
1813
|
+
entity: input.entity,
|
|
1814
|
+
piiClass: 'none',
|
|
1815
|
+
payload: { entity: input.entity },
|
|
1816
|
+
});
|
|
1817
|
+
return { requested: true };
|
|
1818
|
+
}),
|
|
1819
|
+
'cap/notes': ((ctx) => ctx.sql.query('SELECT id, body FROM cap_notes ORDER BY id')),
|
|
1820
|
+
'cap/outbox': ((ctx) => ctx.sql.query('SELECT id, type, occurred_at, actor, authorization, operation, entity_type, entity_id, payload FROM _substrat_outbox ORDER BY id')),
|
|
1821
|
+
'cap/denials': readDenialsOp,
|
|
1822
|
+
// The session table's size — what the bounded prune is measured by.
|
|
1823
|
+
'cap/session-count': ((ctx) => ctx.sql.query('SELECT COUNT(*) AS n FROM _substrat_capability_sessions')[0]?.n ??
|
|
1824
|
+
0),
|
|
1825
|
+
// Every table in this scope, every row, as text — what "the secret appears in no stored
|
|
1826
|
+
// row" is checked against. Read through module-facing `ctx.sql` on purpose: this is
|
|
1827
|
+
// what a vertical (or a dump of the scope) can see.
|
|
1828
|
+
'cap/dump': ((ctx) => {
|
|
1829
|
+
const tables = ctx.sql.query("SELECT name FROM sqlite_master WHERE type = 'table' " +
|
|
1830
|
+
"AND substr(name, 1, 4) <> '_cf_' AND substr(name, 1, 7) <> 'sqlite_'");
|
|
1831
|
+
const out = {};
|
|
1832
|
+
for (const { name } of tables) {
|
|
1833
|
+
out[name] = JSON.stringify(ctx.sql.query(`SELECT * FROM "${name}"`));
|
|
1834
|
+
}
|
|
1835
|
+
return out;
|
|
1836
|
+
}),
|
|
1837
|
+
},
|
|
1838
|
+
consumers: {
|
|
1839
|
+
// Records what the mint answered rather than letting the delivery fail, so the test
|
|
1840
|
+
// reads an outcome instead of inferring one from a dead letter.
|
|
1841
|
+
'cap.mint-requested': (async (ctx, event) => {
|
|
1842
|
+
const entity = event.payload.entity;
|
|
1843
|
+
let outcome;
|
|
1844
|
+
try {
|
|
1845
|
+
const minted = await ctx.capabilities.mint({ entity, permissions: [CAP_READ] });
|
|
1846
|
+
outcome = `minted ${minted.id}`;
|
|
1847
|
+
}
|
|
1848
|
+
catch (err) {
|
|
1849
|
+
outcome = `refused ${err.code ?? err.message}`;
|
|
1850
|
+
}
|
|
1851
|
+
ctx.sql.exec('INSERT INTO cap_notes (id, body) VALUES (?, ?)', [event.id, outcome]);
|
|
1852
|
+
}),
|
|
1853
|
+
},
|
|
1854
|
+
};
|
|
1518
1855
|
export const contractTestModules = [
|
|
1519
1856
|
...contractTestInitialModules,
|
|
1520
1857
|
lateMod,
|
|
@@ -1526,13 +1863,120 @@ export const contractTestModules = [
|
|
|
1526
1863
|
impersonationEchoMod,
|
|
1527
1864
|
connectorMod,
|
|
1528
1865
|
scheduleMod,
|
|
1866
|
+
// #1577: the operation a resumable run's steps write through. Inert for every
|
|
1867
|
+
// other suite — nothing else reads `job_items`.
|
|
1868
|
+
jobsMod,
|
|
1529
1869
|
atomicMod,
|
|
1530
1870
|
searchMod,
|
|
1531
1871
|
listMod,
|
|
1532
1872
|
parseMod,
|
|
1533
1873
|
concurrencyMod,
|
|
1534
1874
|
idempotencyMod,
|
|
1875
|
+
// #1672: the capability suite's folder tree. Inert for every other suite — nothing else
|
|
1876
|
+
// reads `cap_notes` or invokes a `cap/*` operation.
|
|
1877
|
+
capMod,
|
|
1535
1878
|
];
|
|
1879
|
+
// -- live reads (#938) -------------------------------------------------------
|
|
1880
|
+
/**
|
|
1881
|
+
* A module for the live-read fan-out, with two entity types that differ in exactly
|
|
1882
|
+
* one respect: `note` is declared watchable and `ledger` is not.
|
|
1883
|
+
*
|
|
1884
|
+
* Deliberately NOT in `contractTestModules`. Every scope in the kit shares one module
|
|
1885
|
+
* set, and a module declaring `liveTargets` there would give every other suite's scope
|
|
1886
|
+
* a fan-out to run on every invoke — changing what those suites exercise in order to
|
|
1887
|
+
* test this one. Adapters that can host live reads mount it on a scope host of its own.
|
|
1888
|
+
*
|
|
1889
|
+
* The undeclared `ledger` is the fixture for the fail-closed half of the rule, and it
|
|
1890
|
+
* is the half worth having a fixture for: "an entity type nobody declared reaches
|
|
1891
|
+
* nobody" is a sentence that is trivially true of an implementation that also announces
|
|
1892
|
+
* nothing at all, so the suite proves it against a `note` that DOES arrive.
|
|
1893
|
+
*/
|
|
1894
|
+
export const liveModManifest = moduleManifest.parse({
|
|
1895
|
+
id: '@test/live',
|
|
1896
|
+
version: '1.0.0',
|
|
1897
|
+
kernelContract: '^0.0.1',
|
|
1898
|
+
permissions: [
|
|
1899
|
+
{ key: 'live:read', description: 'read a note — and therefore watch it change' },
|
|
1900
|
+
{ key: 'live:write', description: 'touch a note' },
|
|
1901
|
+
],
|
|
1902
|
+
events: {
|
|
1903
|
+
emits: [
|
|
1904
|
+
{ type: 'live.note-touched', schemaVersion: 1 },
|
|
1905
|
+
{ type: 'live.ledger-touched', schemaVersion: 1 },
|
|
1906
|
+
// Declared for the reason permMod declares them: an attachment target implies
|
|
1907
|
+
// these, and the module's emitted-event set should say so rather than have the
|
|
1908
|
+
// kernel emit a type the manifest never mentions.
|
|
1909
|
+
{ type: 'attachment.added', schemaVersion: 1 },
|
|
1910
|
+
{ type: 'attachment.removed', schemaVersion: 1 },
|
|
1911
|
+
],
|
|
1912
|
+
consumes: [],
|
|
1913
|
+
},
|
|
1914
|
+
migrations: { journalDir: './migrations', compatibleFrom: '1.0.0' },
|
|
1915
|
+
// `note` is attachable as well as watchable, and that overlap is the fixture for a
|
|
1916
|
+
// second committing path: `attachmentAdd`/`attachmentRemove` emit about the entity
|
|
1917
|
+
// too, so a watcher of `note` must hear those exactly as it hears an operation's
|
|
1918
|
+
// own event. Without this the attachment path would have had no live-read test at
|
|
1919
|
+
// all, which is how it came to be the one door the fan-out was not wired into.
|
|
1920
|
+
attachmentTargets: [
|
|
1921
|
+
{ entityType: 'note', readPermission: 'live:read', writePermission: 'live:write' },
|
|
1922
|
+
],
|
|
1923
|
+
// The declaration under test. `note` is watchable, gated by the key that reads it;
|
|
1924
|
+
// `ledger` is absent, and its absence is the assertion.
|
|
1925
|
+
liveTargets: [{ entityType: 'note', readPermission: 'live:read' }],
|
|
1926
|
+
entitlementKey: 'live',
|
|
1927
|
+
});
|
|
1928
|
+
/** Touch a note — one write, one event about ONE entity, which is what a subscriber hears. */
|
|
1929
|
+
const liveTouchOp = async (ctx, input) => {
|
|
1930
|
+
assertAllowed(await ctx.check(permissionKey.parse('live:write')));
|
|
1931
|
+
ctx.emit({
|
|
1932
|
+
type: 'live.note-touched',
|
|
1933
|
+
schemaVersion: 1,
|
|
1934
|
+
entity: { entityType: 'note', entityId: input.noteId },
|
|
1935
|
+
piiClass: 'none',
|
|
1936
|
+
payload: {},
|
|
1937
|
+
});
|
|
1938
|
+
return { noteId: input.noteId };
|
|
1939
|
+
};
|
|
1940
|
+
/** The same write against the entity type nobody declared watchable. */
|
|
1941
|
+
const liveTouchLedgerOp = async (ctx, input) => {
|
|
1942
|
+
assertAllowed(await ctx.check(permissionKey.parse('live:write')));
|
|
1943
|
+
ctx.emit({
|
|
1944
|
+
type: 'live.ledger-touched',
|
|
1945
|
+
schemaVersion: 1,
|
|
1946
|
+
entity: { entityType: 'ledger', entityId: input.ledgerId },
|
|
1947
|
+
piiClass: 'none',
|
|
1948
|
+
payload: {},
|
|
1949
|
+
});
|
|
1950
|
+
return { ledgerId: input.ledgerId };
|
|
1951
|
+
};
|
|
1952
|
+
/**
|
|
1953
|
+
* Withdraw a read grant on one note, the way an app un-shares a record.
|
|
1954
|
+
*
|
|
1955
|
+
* Deliberately UNGUARDED by an operation-level check, exactly as `perm/share` is and
|
|
1956
|
+
* for the same reason: the guardrail under test lives inside the verb — `ctx.revoke`
|
|
1957
|
+
* re-checks the caller's own decision on that entity — so wrapping it in a second
|
|
1958
|
+
* check here would only prove the wrapper.
|
|
1959
|
+
*
|
|
1960
|
+
* It exists so the suite can take authority away MID-SOCKET. There is no `HostAdmin`
|
|
1961
|
+
* verb that withdraws an entity-narrowed grant, and that is the right shape: narrowing
|
|
1962
|
+
* and un-narrowing are things an app does on a person's behalf, not things a platform
|
|
1963
|
+
* actor reaches in.
|
|
1964
|
+
*/
|
|
1965
|
+
const liveUnshareOp = async (ctx, input) => {
|
|
1966
|
+
await ctx.revoke(principalId.parse(input.principal), permissionKey.parse('live:read'), {
|
|
1967
|
+
entityType: 'note',
|
|
1968
|
+
entityId: input.noteId,
|
|
1969
|
+
});
|
|
1970
|
+
};
|
|
1971
|
+
export const liveMod = {
|
|
1972
|
+
manifest: liveModManifest,
|
|
1973
|
+
migrations: [],
|
|
1974
|
+
operations: {
|
|
1975
|
+
'live/touch': liveTouchOp,
|
|
1976
|
+
'live/touch-ledger': liveTouchLedgerOp,
|
|
1977
|
+
'live/unshare': liveUnshareOp,
|
|
1978
|
+
},
|
|
1979
|
+
};
|
|
1536
1980
|
export const brokenModManifest = moduleManifest.parse({
|
|
1537
1981
|
id: '@test/broken',
|
|
1538
1982
|
version: '1.0.0',
|