@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.
@@ -1 +1 @@
1
- {"version":3,"file":"modules.d.ts","sourceRoot":"","sources":["../src/modules.ts"],"names":[],"mappings":"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"}
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: [{ operation: 'sched/tick', cadence: { everyMinutes: 60 }, permissions: ['sched:tick'] }],
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
- consumes: [],
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
- const readDenialsOp = (ctx) => ctx.sql.query(`SELECT actor, permission, tenant_id, scope_id, operation, impersonation
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
- 'sched/schedule-state': ((ctx) => ctx.sql.query('SELECT schedule_op, last_status FROM _substrat_schedule_state ORDER BY schedule_op')),
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',