@substrat-run/contract-tests 0.72.0 → 0.73.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.
@@ -0,0 +1,102 @@
1
+ import type { EntityRef } from '@substrat-run/contracts';
2
+ /** What a vertical supplies so the kit can drive its operations. */
3
+ export interface EntityCheckFixture {
4
+ /**
5
+ * Create one entity of this declared type and return its id.
6
+ *
7
+ * Called for each case, so every case gets a world nobody else has touched —
8
+ * the operation under test may well delete the thing it is given.
9
+ */
10
+ createEntity(entityType: string): Promise<string>;
11
+ /**
12
+ * Grant `permission` to the probe principal, narrowed to exactly this entity.
13
+ *
14
+ * Deliberately NOT the vertical's own sharing operation: using `share-list` to
15
+ * set up the test for `share-list` would prove only that it agrees with
16
+ * itself. Reach for the admin grant.
17
+ */
18
+ grantOnEntity(permission: string, entity: EntityRef): Promise<void>;
19
+ /** Invoke as the probe principal — the one holding only narrowed grants. */
20
+ invoke(operation: string, input: Record<string, unknown>): Promise<unknown>;
21
+ /**
22
+ * Is this error a permission denial? Defaults to `PermissionDenied`, which is
23
+ * what a stub throws; a fixture driving HTTP would test for its 403 instead.
24
+ */
25
+ isDenial?(error: unknown): boolean;
26
+ }
27
+ export interface EntityCheckSuiteOptions {
28
+ /**
29
+ * Extra input fields per operation, beyond the entity id the kit supplies.
30
+ *
31
+ * Only needed where the operation's schema has REQUIRED fields besides the id
32
+ * — the kit reads the schema to find out, so an operation taking nothing but
33
+ * an id needs no entry here.
34
+ */
35
+ readonly inputs?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
36
+ /**
37
+ * Permissions an operation needs BEYOND the one it declares, granted on the
38
+ * same target entity — with the reason it needs them.
39
+ *
40
+ * The first vertical this kit ran against produced one immediately.
41
+ * `todo/share-list` declares `list:manage` and honours it, then calls
42
+ * `ctx.grant` to hand `list:contribute` to the invitee — and delegation only
43
+ * works for a permission the caller HOLDS. A principal granted `list:manage`
44
+ * alone is refused, correctly, by the second gate.
45
+ *
46
+ * So an operation's declared permission is the gate it opens with, not
47
+ * necessarily the whole authority it exercises. That gap is invisible in
48
+ * production here only because todo's bootstrap grant hands every owner both
49
+ * keys on their own entity, so nobody ever holds one without the other.
50
+ *
51
+ * `because` is required rather than a comment: this is the one place the gap
52
+ * gets written down, and an entry without a reason is indistinguishable from
53
+ * someone widening the grant until the test went green.
54
+ */
55
+ readonly alsoGrant?: Readonly<Record<string, {
56
+ readonly permissions: readonly string[];
57
+ readonly because: string;
58
+ }>>;
59
+ /**
60
+ * The in-scope operations this kit cannot generate, each with its reason.
61
+ *
62
+ * Asserted EXACTLY: an operation that becomes uncoverable, or one that stops
63
+ * being, fails until this list is updated. That is the point — it is the
64
+ * coverage gap made reviewable rather than invisible.
65
+ */
66
+ readonly uncovered?: Readonly<Record<string, string>>;
67
+ }
68
+ /** One operation the kit can drive, with the declaration it was read from. */
69
+ export interface PlannedCheck {
70
+ readonly name: string;
71
+ readonly key: string;
72
+ readonly entity: string;
73
+ readonly idFrom: string;
74
+ }
75
+ /**
76
+ * Partition an operation set into what this kit can drive and what it cannot.
77
+ *
78
+ * Exported and pure so the classification is testable on its own. It is the part
79
+ * that decides what counts as covered, and a bug here is invisible in the worst
80
+ * way — it would drop an operation from the suite while every remaining test
81
+ * still passed.
82
+ *
83
+ * Out of scope entirely (neither covered nor uncovered): an operation with a
84
+ * bare-key node check, or one declaring `narrows`. Neither claims an entity
85
+ * check, so neither has one to honour.
86
+ */
87
+ export declare function planEntityCheckCoverage(operations: Readonly<Record<string, object>>, inputs?: Readonly<Record<string, Readonly<Record<string, unknown>>>>): {
88
+ covered: PlannedCheck[];
89
+ uncovered: Record<string, string>;
90
+ };
91
+ /**
92
+ * Generate the conformance pair for every operation declaring an entity check.
93
+ *
94
+ * ```ts
95
+ * entityCheckConformanceSuite('todo', todoOperations, makeFixture, {
96
+ * inputs: { 'todo/rename-list': { name: 'renamed' } },
97
+ * uncovered: { 'todo/set-item-done': 'resolved — the id is not in the input' },
98
+ * });
99
+ * ```
100
+ */
101
+ export declare function entityCheckConformanceSuite(subjectName: string, operations: Readonly<Record<string, object>>, makeFixture: () => Promise<EntityCheckFixture>, options?: EntityCheckSuiteOptions): void;
102
+ //# sourceMappingURL=entity-check-suite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-check-suite.d.ts","sourceRoot":"","sources":["../src/entity-check-suite.ts"],"names":[],"mappings":"AA6DA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAGzD,oEAAoE;AACpE,MAAM,WAAW,kBAAkB;IACjC;;;;;OAKG;IACH,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD;;;;;;OAMG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,4EAA4E;IAC5E,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5E;;;OAGG;IACH,QAAQ,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;CACpC;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9E;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAC3B,MAAM,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CACtF,CAAC;IACF;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACvD;AA0CD,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC5C,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAM,GACvE;IAAE,OAAO,EAAE,YAAY,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAuBhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC5C,WAAW,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,EAC9C,OAAO,GAAE,uBAA4B,GACpC,IAAI,CAkGN"}
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Does the HANDLER honour the entity check its operation DECLARED? (#747)
3
+ *
4
+ * #746 made an operation say what its permission checks against — `{ key,
5
+ * entity, idFrom }` for one entity, a bare key for the node. Nothing verified
6
+ * it. A declaration of `entity: 'list'` beside a handler calling
7
+ * `ctx.check(perm)` typechecks perfectly and fails open: everyone holding the
8
+ * key anywhere in the scope passes, which in a sharing app is every member
9
+ * against every record.
10
+ *
11
+ * This generates the behavioural pair that tells the two apart, from the
12
+ * declaration itself, for every operation that declares one.
13
+ *
14
+ * ## The two cases, and exactly what each one catches
15
+ *
16
+ * Both use a probe principal who holds the key ONLY through entity-narrowed
17
+ * grants — never scope-wide.
18
+ *
19
+ * **Case 1 — grant on A, invoke against A. Expect no denial.**
20
+ * A correct entity check resolves the grant on A and allows. A node check asks
21
+ * whether the principal holds the key at the scope, and a narrowed grant does
22
+ * not widen (`permission-suite`: *"No node-level access: the narrow grant does
23
+ * not widen"*), so it denies. **This is the case that catches the node-check
24
+ * bug** — and note which way round it fails: the broken implementation is MORE
25
+ * restrictive for this principal, so in the wild it surfaces as a baffling
26
+ * denial rather than as a breach. It is the direction nobody files a security
27
+ * bug about.
28
+ *
29
+ * **Case 2 — grant on A, invoke against B. Expect a denial.**
30
+ * The breach direction: a member reaching a record that is not theirs. Catches a
31
+ * handler that checks nothing, one that checks a constant entity, and one that
32
+ * reads the id from the wrong input field.
33
+ *
34
+ * **What case 2 does NOT catch, stated because a kit that overclaims is worse
35
+ * than no kit:** it does not catch the node-check bug. Against B the correct
36
+ * implementation denies, and so does the node check — for a different reason,
37
+ * with the same outcome. Only case 1 separates them. Neither case, on its own,
38
+ * is the test; the pair is.
39
+ *
40
+ * ## Why case 1 tolerates a non-permission failure
41
+ *
42
+ * It asserts "was not denied", not "succeeded". The harness supplies plausible
43
+ * input, not a valid domain state — `protocol/sign` on a fresh instance may
44
+ * legitimately refuse for a business reason. That refusal is not a permission
45
+ * answer and must not be read as one. Case 2 is the opposite and is strict: it
46
+ * demands a permission denial specifically, so a business error there is a
47
+ * FAILURE, not a pass. That strictness also pins the ordering the platform
48
+ * requires — the check is the operation's first line, so it answers before a
49
+ * not-found ever can.
50
+ *
51
+ * ## Nothing is skipped quietly
52
+ *
53
+ * An operation declaring `resolved` cannot be generated: the id is not in the
54
+ * input, so the harness has no way to reach the entity. An operation whose input
55
+ * needs required fields the caller did not supply cannot be generated either.
56
+ * Both are reported as UNCOVERED and asserted against a list the caller writes
57
+ * down, so losing coverage — switching an `idFrom` to a `resolved`, say — turns
58
+ * CI red and appears in the diff. A conformance kit that quietly covers the easy
59
+ * half reads as "checked" when it is not.
60
+ */
61
+ import { describe, expect, it } from 'vitest';
62
+ import { PermissionDenied } from '@substrat-run/kernel';
63
+ /** An operation declaring an entity-narrowed check — the ones in scope here. */
64
+ function entityCheckOf(op) {
65
+ const permission = op.permission;
66
+ if (!permission || typeof permission === 'string')
67
+ return undefined;
68
+ return permission.entity ? permission : undefined;
69
+ }
70
+ /**
71
+ * The input fields an operation REQUIRES, beyond the one carrying the entity id.
72
+ *
73
+ * Read off the schema rather than asked for: an operation taking only an id
74
+ * needs no fixture entry, and one that needs more says so precisely instead of
75
+ * failing later as a validation error the reader has to decode.
76
+ */
77
+ function requiredExtras(op, idField) {
78
+ const shape = op.input?.shape;
79
+ if (!shape)
80
+ return [];
81
+ return Object.entries(shape)
82
+ .filter(([field]) => field !== idField)
83
+ .filter(([, schema]) => {
84
+ const parse = schema.safeParse;
85
+ // No `safeParse` means we cannot tell — treat it as required, because
86
+ // guessing "optional" is the answer that silently drops the operation.
87
+ return typeof parse !== 'function' || !parse.call(schema, undefined).success;
88
+ })
89
+ .map(([field]) => field);
90
+ }
91
+ /**
92
+ * Partition an operation set into what this kit can drive and what it cannot.
93
+ *
94
+ * Exported and pure so the classification is testable on its own. It is the part
95
+ * that decides what counts as covered, and a bug here is invisible in the worst
96
+ * way — it would drop an operation from the suite while every remaining test
97
+ * still passed.
98
+ *
99
+ * Out of scope entirely (neither covered nor uncovered): an operation with a
100
+ * bare-key node check, or one declaring `narrows`. Neither claims an entity
101
+ * check, so neither has one to honour.
102
+ */
103
+ export function planEntityCheckCoverage(operations, inputs = {}) {
104
+ const covered = [];
105
+ const uncovered = {};
106
+ for (const [name, raw] of Object.entries(operations).sort(([a], [b]) => a.localeCompare(b))) {
107
+ const op = raw;
108
+ const check = entityCheckOf(op);
109
+ if (!check)
110
+ continue;
111
+ if (!check.idFrom) {
112
+ uncovered[name] =
113
+ `declares 'resolved' (${check.resolved ?? 'no reason given'}) — the entity id is not ` +
114
+ 'in the input, so the harness cannot reach the entity';
115
+ continue;
116
+ }
117
+ const missing = requiredExtras(op, check.idFrom).filter((f) => inputs[name]?.[f] === undefined);
118
+ if (missing.length > 0) {
119
+ uncovered[name] = `no sample input for required field(s): ${missing.join(', ')}`;
120
+ continue;
121
+ }
122
+ covered.push({ name, key: check.key, entity: check.entity, idFrom: check.idFrom });
123
+ }
124
+ return { covered, uncovered };
125
+ }
126
+ /**
127
+ * Generate the conformance pair for every operation declaring an entity check.
128
+ *
129
+ * ```ts
130
+ * entityCheckConformanceSuite('todo', todoOperations, makeFixture, {
131
+ * inputs: { 'todo/rename-list': { name: 'renamed' } },
132
+ * uncovered: { 'todo/set-item-done': 'resolved — the id is not in the input' },
133
+ * });
134
+ * ```
135
+ */
136
+ export function entityCheckConformanceSuite(subjectName, operations, makeFixture, options = {}) {
137
+ const declaredUncovered = options.uncovered ?? {};
138
+ const supplied = options.inputs ?? {};
139
+ // Partitioned at collection time so the generated tests can be named.
140
+ const { covered, uncovered } = planEntityCheckCoverage(operations, supplied);
141
+ describe(`declared entity checks are honoured: ${subjectName}`, () => {
142
+ it('covers every entity check it can, and names every one it cannot', () => {
143
+ // The whole partition asserted at once. A new operation with an entity
144
+ // check joins `covered` silently (it is simply tested); one the kit cannot
145
+ // drive has to be written down here, where a reviewer sees it.
146
+ expect(uncovered).toEqual(declaredUncovered);
147
+ });
148
+ it('grants nothing extra to an operation it does not drive', () => {
149
+ // An `alsoGrant` entry naming an operation that is uncovered or absent is
150
+ // either a stale note or a grant widened against the wrong name. Both read
151
+ // as coverage that is not there.
152
+ const driven = new Set(covered.map((c) => c.name));
153
+ expect(Object.keys(options.alsoGrant ?? {}).filter((n) => !driven.has(n))).toEqual([]);
154
+ });
155
+ it('generated a pair for at least one operation', () => {
156
+ // A suite that generated nothing passes every assertion above it. This is
157
+ // the zero guard: silence must not read as success.
158
+ expect(covered.length).toBeGreaterThan(0);
159
+ });
160
+ for (const { name, key, entity, idFrom } of covered) {
161
+ describe(`${name} — ${key} on ${entity}, id from '${idFrom}'`, () => {
162
+ const extras = supplied[name] ?? {};
163
+ const extraKeys = options.alsoGrant?.[name]?.permissions ?? [];
164
+ /**
165
+ * The declared key on the target, plus anything the operation needs to
166
+ * delegate. All of them NARROWED to the one entity — never scope-wide,
167
+ * which is what keeps case 1 able to catch a node check.
168
+ */
169
+ const grantAllOn = async (fixture, entityId) => {
170
+ const ref = { entityType: entity, entityId };
171
+ await fixture.grantOnEntity(key, ref);
172
+ for (const extra of extraKeys)
173
+ await fixture.grantOnEntity(extra, ref);
174
+ };
175
+ const denialFrom = async (fixture, input) => {
176
+ const isDenial = fixture.isDenial ?? ((e) => e instanceof PermissionDenied);
177
+ try {
178
+ await fixture.invoke(name, input);
179
+ return undefined;
180
+ }
181
+ catch (error) {
182
+ if (isDenial(error))
183
+ return error;
184
+ // Not a permission answer. Case 1 tolerates it; case 2 must not, so
185
+ // it is handed back rather than swallowed.
186
+ return { notADenial: error };
187
+ }
188
+ };
189
+ it('allows a principal granted on THAT entity (the node-check catcher)', async () => {
190
+ const fixture = await makeFixture();
191
+ const target = await fixture.createEntity(entity);
192
+ await grantAllOn(fixture, target);
193
+ const outcome = await denialFrom(fixture, { ...extras, [idFrom]: target });
194
+ const denied = outcome !== undefined && !outcome.notADenial;
195
+ expect(denied, `${name} denied a principal holding ${key} on the very ${entity} it was ` +
196
+ 'invoked against — the handler is checking the node, not the entity').toBe(false);
197
+ });
198
+ it('denies the same principal against an entity they were NOT granted', async () => {
199
+ const fixture = await makeFixture();
200
+ // Granted somewhere, so the denial below is about THIS entity rather
201
+ // than about a principal who holds nothing at all.
202
+ const granted = await fixture.createEntity(entity);
203
+ await grantAllOn(fixture, granted);
204
+ const other = await fixture.createEntity(entity);
205
+ const outcome = await denialFrom(fixture, { ...extras, [idFrom]: other });
206
+ const notADenial = outcome?.notADenial;
207
+ expect(outcome, `${name} allowed a principal to reach a ${entity} they hold no grant on`).toBeDefined();
208
+ expect(notADenial, `${name} failed against an ungranted ${entity}, but not with a permission ` +
209
+ 'denial — the check must answer before anything else can').toBeUndefined();
210
+ });
211
+ });
212
+ }
213
+ });
214
+ }
215
+ //# sourceMappingURL=entity-check-suite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-check-suite.js","sourceRoot":"","sources":["../src/entity-check-suite.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export { scopeHostContractSuite } from './scope-host-suite.js';
2
2
  export type { ScopeHostFixture, ScopeHostSuiteOptions } from './scope-host-suite.js';
3
3
  export { permissionContractSuite } from './permission-suite.js';
4
4
  export { scheduleContractSuite } from './schedule-suite.js';
5
+ export { entityCheckConformanceSuite, planEntityCheckCoverage } from './entity-check-suite.js';
6
+ export type { EntityCheckFixture, EntityCheckSuiteOptions, PlannedCheck } from './entity-check-suite.js';
5
7
  export { billedMod, brokenMod, connectorMod, contractTestBareOps, contractTestInitialModules, contractTestModules, scheduleMod, } from './modules.js';
6
8
  export { connectorCalls, connectorTestFetch, resetConnectorCalls, type ConnectorCall, } from './connector-fixture.js';
7
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,YAAY,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EACL,SAAS,EACT,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,WAAW,GACZ,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,aAAa,GACnB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,YAAY,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAC/F,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACzG,OAAO,EACL,SAAS,EACT,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,WAAW,GACZ,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,aAAa,GACnB,MAAM,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { scopeHostContractSuite } from './scope-host-suite.js';
2
2
  export { permissionContractSuite } from './permission-suite.js';
3
3
  export { scheduleContractSuite } from './schedule-suite.js';
4
+ export { entityCheckConformanceSuite, planEntityCheckCoverage } from './entity-check-suite.js';
4
5
  export { billedMod, brokenMod, connectorMod, contractTestBareOps, contractTestInitialModules, contractTestModules, scheduleMod, } from './modules.js';
5
6
  export { connectorCalls, connectorTestFetch, resetConnectorCalls, } from './connector-fixture.js';
6
7
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EACL,SAAS,EACT,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,WAAW,GACZ,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,GAEpB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAE/F,OAAO,EACL,SAAS,EACT,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,WAAW,GACZ,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,mBAAmB,GAEpB,MAAM,wBAAwB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@substrat-run/contract-tests",
3
- "version": "0.72.0",
3
+ "version": "0.73.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": {
@@ -22,18 +22,24 @@
22
22
  "dist"
23
23
  ],
24
24
  "dependencies": {
25
- "vitest": "^3.0.0",
26
- "@substrat-run/contracts": "^0.72.0",
27
- "@substrat-run/kernel": "^0.72.0"
25
+ "vitest": "^3.2.7",
26
+ "@substrat-run/kernel": "^0.73.0",
27
+ "@substrat-run/contracts": "^0.73.0"
28
28
  },
29
29
  "devDependencies": {
30
- "typescript": "^7.0.0"
30
+ "@types/node": "^22.0.0",
31
+ "typescript": "^7.0.0",
32
+ "zod": "^4.4.3"
31
33
  },
32
34
  "publishConfig": {
33
35
  "access": "public"
34
36
  },
37
+ "peerDependencies": {
38
+ "zod": "^4.4.0"
39
+ },
35
40
  "scripts": {
36
41
  "build": "tsc -p tsconfig.json",
37
- "typecheck": "tsc -p tsconfig.json --noEmit"
42
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
43
+ "test": "vitest run"
38
44
  }
39
45
  }