@substrat-run/contract-tests 0.92.0 → 0.94.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/atomic-suite.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js +70 -0
- package/dist/modules.js.map +1 -1
- package/dist/permission-suite.d.ts.map +1 -1
- package/dist/permission-suite.js +22 -0
- package/dist/permission-suite.js.map +1 -1
- package/dist/scope-host-suite.d.ts.map +1 -1
- package/dist/scope-host-suite.js +110 -0
- package/dist/scope-host-suite.js.map +1 -1
- package/dist/spine-guard-suite.d.ts +3 -0
- package/dist/spine-guard-suite.d.ts.map +1 -0
- package/dist/spine-guard-suite.js +97 -0
- package/dist/spine-guard-suite.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spine-guard-suite.d.ts","sourceRoot":"","sources":["../src/spine-guard-suite.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAS9D,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,GAC3C,IAAI,CAwFN"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract suite for the spine guard on `ctx.sql` (#954).
|
|
3
|
+
*
|
|
4
|
+
* What it pins is one sentence: **module code cannot write a `_substrat_*` table
|
|
5
|
+
* through the connection the kernel hands it, and can still read one.**
|
|
6
|
+
*
|
|
7
|
+
* Before this existed the rule was a source scan — CLAUDE.md plus
|
|
8
|
+
* `tools/boundary-lint.mjs` — and boundary-lint does not run on the hosted push
|
|
9
|
+
* path, so a vertical that never passed through this repo's CI reached production
|
|
10
|
+
* with `INSERT INTO _substrat_tuples …` (a forged grant) fully available to it.
|
|
11
|
+
*
|
|
12
|
+
* It belongs in the contract suites rather than in one adapter's tests because
|
|
13
|
+
* the guard is only worth anything if BOTH module-facing connections carry it: a
|
|
14
|
+
* vertical is developed against the pure host and deployed onto the DO, so a
|
|
15
|
+
* refusal that exists in only one of them is a rule that changes meaning at
|
|
16
|
+
* deploy time. The guard itself lives in the kernel (`guardSpine`); each adapter
|
|
17
|
+
* proves it wrapped the connection module code actually holds.
|
|
18
|
+
*/
|
|
19
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
20
|
+
import { errorCodeOf, platformActorId, principalId, scopeId, tenantId } from '@substrat-run/contracts';
|
|
21
|
+
import { ulid } from '@substrat-run/kernel';
|
|
22
|
+
import { testMod } from './modules.js';
|
|
23
|
+
export function spineGuardContractSuite(adapterName, makeFixture) {
|
|
24
|
+
describe(`spine guard (ctx.sql): ${adapterName}`, () => {
|
|
25
|
+
let fixture;
|
|
26
|
+
let host;
|
|
27
|
+
let stub;
|
|
28
|
+
const t1 = tenantId.parse(ulid());
|
|
29
|
+
const s1 = scopeId.parse(ulid());
|
|
30
|
+
const alice = principalId.parse(ulid());
|
|
31
|
+
const staff = platformActorId.parse(ulid());
|
|
32
|
+
beforeAll(async () => {
|
|
33
|
+
fixture = await makeFixture();
|
|
34
|
+
host = fixture.host;
|
|
35
|
+
host.registerModule(testMod);
|
|
36
|
+
await host.admin.createTenant(staff, { id: t1, slug: 'spine-tenant', name: 'Spine Tenant' });
|
|
37
|
+
await host.admin.grantEntitlement(staff, t1, 'testmod');
|
|
38
|
+
await host.provisionScope(staff, { tenantId: t1, scopeId: s1, vertical: 'spine-vertical' });
|
|
39
|
+
await host.admin.activateScope(staff, t1, s1);
|
|
40
|
+
stub = await host.getScope(alice, t1, s1);
|
|
41
|
+
});
|
|
42
|
+
afterAll(async () => {
|
|
43
|
+
await fixture.cleanup();
|
|
44
|
+
});
|
|
45
|
+
// -- the refusals --------------------------------------------------------
|
|
46
|
+
const refused = [
|
|
47
|
+
['a forged tuple (INSERT)', 'testmod/forge-tuple'],
|
|
48
|
+
['a rewritten event (UPDATE)', 'testmod/forge-outbox'],
|
|
49
|
+
['a dropped migration journal (DROP)', 'testmod/forge-drop-journal'],
|
|
50
|
+
['a quoted identifier (DELETE FROM "_substrat_tuples")', 'testmod/forge-quoted'],
|
|
51
|
+
['a write dressed as a read (INSERT … RETURNING via ctx.sql.query)', 'testmod/forge-returning'],
|
|
52
|
+
['a trigger ON the outbox (CREATE TRIGGER … ON)', 'testmod/forge-trigger'],
|
|
53
|
+
];
|
|
54
|
+
for (const [what, operation] of refused) {
|
|
55
|
+
it(`refuses ${what}`, async () => {
|
|
56
|
+
await expect(stub.invoke(operation)).rejects.toThrow(/cannot write the platform spine/);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
it('refuses as a `forbidden`, so a transport answers 403 rather than 500', async () => {
|
|
60
|
+
const err = await stub.invoke('testmod/forge-tuple').then(() => undefined, (e) => e);
|
|
61
|
+
expect(errorCodeOf(err)).toBe('forbidden');
|
|
62
|
+
});
|
|
63
|
+
it('left no forged tuple behind — the whole operation rolled back', async () => {
|
|
64
|
+
const tuples = await stub.invoke('testmod/read-tuples');
|
|
65
|
+
expect(tuples.some((t) => t.subject === 'principal:forged')).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* The one that proves the refusal is a THROW and not a silent skip: the
|
|
69
|
+
* operation writes a legitimate row before it forges, so a guard that merely
|
|
70
|
+
* dropped the offending statement would leave the item committed.
|
|
71
|
+
*/
|
|
72
|
+
it('takes the operation down with it — the legitimate row before the forge is gone', async () => {
|
|
73
|
+
await expect(stub.invoke('testmod/forge-after-write', { id: 'forge-1' })).rejects.toThrow();
|
|
74
|
+
const items = await stub.invoke('testmod/read-items');
|
|
75
|
+
expect(items.map((i) => i.id)).not.toContain('forge-1');
|
|
76
|
+
});
|
|
77
|
+
// -- what stays open -----------------------------------------------------
|
|
78
|
+
it('still reads the migration journal', async () => {
|
|
79
|
+
const rows = await stub.invoke('testmod/read-journal');
|
|
80
|
+
expect(rows.length).toBeGreaterThan(0);
|
|
81
|
+
});
|
|
82
|
+
it('still reads the tuple store', async () => {
|
|
83
|
+
await expect(stub.invoke('testmod/read-tuples')).resolves.toBeInstanceOf(Array);
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* The projection CLAUDE.md blesses — a spine READ feeding a domain write. Only
|
|
87
|
+
* the write's TARGET is judged, so this must keep working; a guard that refused
|
|
88
|
+
* it would be a rule against timelines rather than against forging.
|
|
89
|
+
*/
|
|
90
|
+
it('still projects a spine table into a module table', async () => {
|
|
91
|
+
await expect(stub.invoke('testmod/project-journal')).resolves.toBeUndefined();
|
|
92
|
+
const notes = await stub.invoke('testmod/read-notes');
|
|
93
|
+
expect(notes.length).toBeGreaterThan(0);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=spine-guard-suite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spine-guard-suite.js","sourceRoot":"","sources":["../src/spine-guard-suite.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@substrat-run/contract-tests",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.94.0",
|
|
4
4
|
"description": "The suite every scope-host adapter must pass, forever (D-14)",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"vitest": "^3.2.7",
|
|
34
|
-
"@substrat-run/
|
|
35
|
-
"@substrat-run/
|
|
34
|
+
"@substrat-run/contracts": "^0.94.0",
|
|
35
|
+
"@substrat-run/kernel": "^0.94.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^22.0.0",
|