@noy-db/test-format-conformance 0.7.0-pre.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/LICENSE +21 -0
- package/README.md +138 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vLannaAi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# @noy-db/test-format-conformance
|
|
2
|
+
|
|
3
|
+
The `as-*` export gate, published as an executable suite.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { runFormatConformanceTests } from '@noy-db/test-format-conformance'
|
|
7
|
+
|
|
8
|
+
runFormatConformanceTests('as-myformat', {
|
|
9
|
+
format: 'myformat',
|
|
10
|
+
vault: () => seededExportCapableVault(),
|
|
11
|
+
exports: [
|
|
12
|
+
{ name: 'toString', run: (v) => toString(v, opts) },
|
|
13
|
+
{ name: 'download', run: (v) => download(v, opts) },
|
|
14
|
+
{ name: 'write', run: (v) => write(v, path, { ...opts, acknowledgeRisks: true }) },
|
|
15
|
+
],
|
|
16
|
+
writeWithoutAcknowledgement: (v, path) => write(v, path, opts),
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## What it checks
|
|
21
|
+
|
|
22
|
+
`as-*` is the one place plaintext leaves the vault. Every package calls
|
|
23
|
+
`vault.assertCanExport('plaintext', <format>)`, and that call is the whole
|
|
24
|
+
boundary. Nine packages had converged on the same shape by convention, and
|
|
25
|
+
convention is what the next format author reads instead of a contract.
|
|
26
|
+
|
|
27
|
+
For **every** entry point the fixture lists:
|
|
28
|
+
|
|
29
|
+
- it **refuses** when `assertCanExport` denies, and
|
|
30
|
+
- it refuses **before reading a single record**.
|
|
31
|
+
|
|
32
|
+
Plus: the `write` path refuses without `acknowledgeRisks: true`.
|
|
33
|
+
|
|
34
|
+
## Gated is not the property. Gated BEFORE decrypting is.
|
|
35
|
+
|
|
36
|
+
A gate called after `exportStream` has run refuses the caller *and decrypts
|
|
37
|
+
anyway*. That is the property a delegation refactor breaks silently — move the
|
|
38
|
+
gate from `toObject` into `download` and every existing test still passes.
|
|
39
|
+
|
|
40
|
+
Proven, not asserted: moving as-csv's gate inside its `exportStream` loop
|
|
41
|
+
leaves both "REFUSES" cases green and fails all three "before reading" cases.
|
|
42
|
+
|
|
43
|
+
## The two ways this suite could have been vacuous
|
|
44
|
+
|
|
45
|
+
Both were real, and both were found by trying to break it rather than by
|
|
46
|
+
reading it.
|
|
47
|
+
|
|
48
|
+
**A refusal is only evidence when the same call would otherwise succeed.**
|
|
49
|
+
The kit's first case runs an entry point on the *ungated* vault and requires it
|
|
50
|
+
to resolve. On its first run it failed `as-zip` — whose export path opens a
|
|
51
|
+
blob slot, so the fixture vault was refusing on *blob storage*, not on the
|
|
52
|
+
export gate. **Six green cases in a file I had just declared passing were
|
|
53
|
+
passing for the wrong reason.**
|
|
54
|
+
|
|
55
|
+
**`rejects.toThrow()` is absorbed by whichever guard fires first.** The
|
|
56
|
+
acknowledgement case originally used a vault with no `exportCapability` grant,
|
|
57
|
+
so `write` refused at the export gate and never reached the flag — deleting the
|
|
58
|
+
acknowledgement guard from as-csv left the suite green. Fixed twice over: the
|
|
59
|
+
case now matches on `/acknowledgeRisks/`, and the fixture is required to supply
|
|
60
|
+
an export-capable vault.
|
|
61
|
+
|
|
62
|
+
## Why a Proxy and not a fake Vault
|
|
63
|
+
|
|
64
|
+
A hand-written double would drift from `Vault`, and would only ever exercise
|
|
65
|
+
the methods whoever wrote it thought of. The fixture supplies a **real** vault
|
|
66
|
+
and the kit wraps it, so an entry point reaching for some other decrypting
|
|
67
|
+
method is still observed.
|
|
68
|
+
|
|
69
|
+
## Mutation-checked
|
|
70
|
+
|
|
71
|
+
| mutation | result |
|
|
72
|
+
|---|---|
|
|
73
|
+
| as-csv: gate deleted | 6 fail |
|
|
74
|
+
| as-csv: gate moved after the read | 3 fail — the "before reading" cases only |
|
|
75
|
+
| as-csv: `acknowledgeRisks` guard deleted | 1 fails |
|
|
76
|
+
| as-zip: gate deleted | 6 fail |
|
|
77
|
+
|
|
78
|
+
## All nine formats bind it
|
|
79
|
+
|
|
80
|
+
`as-blob` · `as-csv` · `as-json` · `as-ndjson` · `as-noydb` · `as-sql` ·
|
|
81
|
+
`as-xlsx` · `as-xml` · `as-zip`.
|
|
82
|
+
|
|
83
|
+
Wiring them found that the family is **two capability tiers**, not one:
|
|
84
|
+
|
|
85
|
+
| tier | packages | gate |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `plaintext` | eight | `assertCanExport('plaintext', <format>)` |
|
|
88
|
+
| `bundle` | `as-noydb` | `assertCanExport('bundle')` — no format |
|
|
89
|
+
|
|
90
|
+
`as-noydb` emits an **encrypted** pod, so it also has **no `acknowledgeRisks`
|
|
91
|
+
gate**, and its source says so twice. Its fixture therefore declares no
|
|
92
|
+
acknowledgement case — and the suite prints
|
|
93
|
+
`write: SKIPPED — … UNVERIFIED here` rather than staying quiet, which is the
|
|
94
|
+
difference between a documented absence and a hole.
|
|
95
|
+
|
|
96
|
+
`as-aws-s3` is not in the list: it exports `asAwsS3(options)` and is a
|
|
97
|
+
**destination, not a format**.
|
|
98
|
+
|
|
99
|
+
## The vacuity guard earned its place twice
|
|
100
|
+
|
|
101
|
+
It fired on `as-zip` (no `withBlobs()`) and on `as-blob` (no blob attached to
|
|
102
|
+
the seeded record). In both cases six refusal assertions were green and
|
|
103
|
+
meaningless. Neither would have been visible from reading the output.
|
|
104
|
+
|
|
105
|
+
## This kit is for EGRESS, not only for `as-*`
|
|
106
|
+
|
|
107
|
+
The family prefix is not the criterion. **Anything that puts plaintext where the
|
|
108
|
+
vault no longer controls it is an export**, and belongs here — which includes
|
|
109
|
+
bindings that are not `as-*` packages at all.
|
|
110
|
+
|
|
111
|
+
| projection | egress? | gated |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `as-csv`, `as-xlsx`, `as-sql`, … | writes a file | ✅ |
|
|
114
|
+
| `as-aws-s3` | pushes to a bucket — a **destination**, not a format | ✅ |
|
|
115
|
+
| a `ui-*` binding rendering to a screen | the user already unlocked the vault | ❌ not egress |
|
|
116
|
+
| **a `ui-*` binding pushing into Google Sheets / Excel-web / Airtable / Retool** | **a third party persists and indexes it** | ✅ **use this kit** |
|
|
117
|
+
|
|
118
|
+
> **A UI that exports is an export.** Rendering locally is covered by unlock and
|
|
119
|
+
> the ACL. Handing plaintext to a service that keeps a copy is the same act
|
|
120
|
+
> `as-csv` performs, and it calls the same gate:
|
|
121
|
+
> `assertCanExport('plaintext', <your format id>)`.
|
|
122
|
+
|
|
123
|
+
Since `ExportFormat` is an **open** union, your id does not need to be one hub
|
|
124
|
+
ships — `{ plaintext: ['gsheet'] }` is a grantable capability. Before that
|
|
125
|
+
change a third-party id could be *checked* and never *granted*, so the only way
|
|
126
|
+
to authorise one was the `'*'` wildcard.
|
|
127
|
+
|
|
128
|
+
### What this contract is, honestly
|
|
129
|
+
|
|
130
|
+
`as-*` packages refuse because **hub's own export paths call the gate**. A
|
|
131
|
+
binding written outside this repo can simply not call it, and nothing stops it.
|
|
132
|
+
So this is a **convention plus an executable check**, not an enforcement
|
|
133
|
+
boundary — the same footing `to-*` runs on. Saying otherwise would repeat the
|
|
134
|
+
mistake this kit was built to correct: a security claim that reads as
|
|
135
|
+
enforcement and enforces nothing.
|
|
136
|
+
|
|
137
|
+
Passing this suite is what makes the claim checkable. See
|
|
138
|
+
`docs/adr/0005-no-ui-port.md` in the `noy-db` repo for the reasoning.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Vault, ExportFormat } from '@noy-db/hub';
|
|
2
|
+
|
|
3
|
+
/** One plaintext-producing entry point, named as a consumer would call it. */
|
|
4
|
+
interface FormatEntryPoint {
|
|
5
|
+
/** Exported function name, e.g. `'toString'`. Used in the test title. */
|
|
6
|
+
readonly name: string;
|
|
7
|
+
/** Call it against the supplied vault. Arguments are the fixture's business. */
|
|
8
|
+
run(vault: Vault): Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
/** Everything an `as-*` package must supply to be checked against the gate. */
|
|
11
|
+
interface FormatFixture {
|
|
12
|
+
/**
|
|
13
|
+
* The TIER the package passes to `assertCanExport`. The `as-*` family is
|
|
14
|
+
* two capability classes, not one — discovered by wiring `as-noydb`, which
|
|
15
|
+
* calls `assertCanExport('bundle')` and never mentions plaintext because it
|
|
16
|
+
* emits an encrypted pod. A kit that assumed one tier would have made that
|
|
17
|
+
* fixture describe itself wrongly while still passing.
|
|
18
|
+
*/
|
|
19
|
+
readonly tier: 'plaintext' | 'bundle';
|
|
20
|
+
/**
|
|
21
|
+
* The format tag, e.g. `'csv'`. REQUIRED for the plaintext tier and
|
|
22
|
+
* meaningless for `bundle` — hub itself throws when a plaintext check
|
|
23
|
+
* arrives without one, so the pairing is asserted rather than assumed.
|
|
24
|
+
*/
|
|
25
|
+
readonly format?: ExportFormat;
|
|
26
|
+
/**
|
|
27
|
+
* A REAL vault with at least one record. Built fresh per case, so an entry
|
|
28
|
+
* point that mutates it cannot leak into the next assertion.
|
|
29
|
+
*/
|
|
30
|
+
vault(): Promise<Vault>;
|
|
31
|
+
/**
|
|
32
|
+
* EVERY entry point that can produce plaintext — not a representative one.
|
|
33
|
+
* A format with four exports and one listed here reports a green suite for
|
|
34
|
+
* the three nobody checked.
|
|
35
|
+
*/
|
|
36
|
+
readonly exports: ReadonlyArray<FormatEntryPoint>;
|
|
37
|
+
/**
|
|
38
|
+
* The on-disk write path, if the package has one. It must refuse without
|
|
39
|
+
* `acknowledgeRisks: true`; pass a call that OMITS the flag.
|
|
40
|
+
*
|
|
41
|
+
* The vault this receives is the fixture's own — NOT the denying proxy —
|
|
42
|
+
* and it must be export-CAPABLE. A vault that would refuse the export
|
|
43
|
+
* anyway makes the case unfalsifiable: the refusal arrives from the gate
|
|
44
|
+
* upstream and the acknowledgement is never reached. That is not
|
|
45
|
+
* hypothetical; it is what the first version of this kit did, and deleting
|
|
46
|
+
* the acknowledgement guard from as-csv left the suite green.
|
|
47
|
+
*/
|
|
48
|
+
writeWithoutAcknowledgement?: (vault: Vault, path: string) => Promise<unknown>;
|
|
49
|
+
}
|
|
50
|
+
/** Thrown by the denying proxy so a refusal is attributable to the gate. */
|
|
51
|
+
declare class ExportDeniedByConformanceKit extends Error {
|
|
52
|
+
constructor(tier: string, format?: string);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Run the shared `as-*` export-gate contract against one format.
|
|
56
|
+
*
|
|
57
|
+
* @param name - shown in the suite title, e.g. `'as-csv'`.
|
|
58
|
+
*/
|
|
59
|
+
declare function runFormatConformanceTests(name: string, fixture: FormatFixture): void;
|
|
60
|
+
|
|
61
|
+
export { ExportDeniedByConformanceKit, type FormatEntryPoint, type FormatFixture, runFormatConformanceTests };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
var ExportDeniedByConformanceKit = class extends Error {
|
|
4
|
+
constructor(tier, format) {
|
|
5
|
+
super(`conformance: assertCanExport denied '${tier}'${format ? ` / '${format}'` : ""}`);
|
|
6
|
+
this.name = "ExportDeniedByConformanceKit";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function denyingVault(real, tier, format, seen) {
|
|
10
|
+
return new Proxy(real, {
|
|
11
|
+
get(target, prop, receiver) {
|
|
12
|
+
if (prop === "assertCanExport") {
|
|
13
|
+
return () => {
|
|
14
|
+
throw new ExportDeniedByConformanceKit(tier, format);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const value = Reflect.get(target, prop, receiver);
|
|
18
|
+
if (typeof value === "function" && typeof prop === "string") {
|
|
19
|
+
return (...args) => {
|
|
20
|
+
if (prop === "exportStream" || prop === "export" || prop === "snapshot") {
|
|
21
|
+
seen.decryptCalls.push(prop);
|
|
22
|
+
}
|
|
23
|
+
return value.apply(target, args);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function runFormatConformanceTests(name, fixture) {
|
|
31
|
+
describe(`${name} \u2014 as-* export gate conformance`, () => {
|
|
32
|
+
it("declares a tier, and a format iff the tier needs one", () => {
|
|
33
|
+
if (fixture.tier === "plaintext") {
|
|
34
|
+
expect(fixture.format, "the plaintext tier requires a format").toBeTruthy();
|
|
35
|
+
} else {
|
|
36
|
+
expect(fixture.format, `the '${fixture.tier}' tier takes no format`).toBeUndefined();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
it("declares at least one export entry point", () => {
|
|
40
|
+
expect(fixture.exports.length).toBeGreaterThan(0);
|
|
41
|
+
});
|
|
42
|
+
it("the fixture vault CAN export \u2014 otherwise every refusal below is free", async () => {
|
|
43
|
+
const vault = await fixture.vault();
|
|
44
|
+
await expect(
|
|
45
|
+
fixture.exports[0].run(vault),
|
|
46
|
+
`${fixture.exports[0].name} failed on an ungated vault \u2014 check the \`format\` tag and the exportCapability grant`
|
|
47
|
+
).resolves.toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
for (const entry of fixture.exports) {
|
|
50
|
+
it(`${entry.name}: REFUSES when assertCanExport denies`, async () => {
|
|
51
|
+
const seen = { decryptCalls: [] };
|
|
52
|
+
const vault = denyingVault(await fixture.vault(), fixture.tier, fixture.format, seen);
|
|
53
|
+
await expect(entry.run(vault)).rejects.toThrow();
|
|
54
|
+
});
|
|
55
|
+
it(`${entry.name}: refuses BEFORE reading any record`, async () => {
|
|
56
|
+
const seen = { decryptCalls: [] };
|
|
57
|
+
const vault = denyingVault(await fixture.vault(), fixture.tier, fixture.format, seen);
|
|
58
|
+
await expect(entry.run(vault)).rejects.toThrow();
|
|
59
|
+
expect(
|
|
60
|
+
seen.decryptCalls,
|
|
61
|
+
`${entry.name} read records before the export gate refused`
|
|
62
|
+
).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const writeTitle = fixture.writeWithoutAcknowledgement ? "write: REFUSES without acknowledgeRisks" : "write: SKIPPED \u2014 fixture declares no acknowledgement case, so the plaintext-on-disk gate is UNVERIFIED here";
|
|
66
|
+
it(writeTitle, async () => {
|
|
67
|
+
const write = fixture.writeWithoutAcknowledgement;
|
|
68
|
+
if (!write) {
|
|
69
|
+
expect(write).toBeUndefined();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const vault = await fixture.vault();
|
|
73
|
+
await expect(write(vault, "/tmp/conformance-should-not-exist")).rejects.toThrow(
|
|
74
|
+
/acknowledgeRisks/i
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
ExportDeniedByConformanceKit,
|
|
81
|
+
runFormatConformanceTests
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/test-format-conformance** — the `as-*` export gate, published as\n * an executable suite.\n *\n * The `as-*` family is the one place plaintext leaves the vault. Each package\n * calls `vault.assertCanExport('plaintext', <format>)` before producing\n * anything, and that call is the whole security boundary: a projection that\n * skips it hands out decrypted records to a caller the vault would have\n * refused.\n *\n * Nothing enforced it. Nine packages had converged on the same shape by\n * convention — `toString`/`toBytes`, `download`, `write` — and convention is\n * what the next format author reads instead of a contract.\n *\n * ## Gated is not the property. Gated BEFORE decrypting is.\n *\n * A gate called after `exportStream` has already run is a gate that refuses\n * the caller and decrypts anyway. So the suite asserts BOTH:\n *\n * - every export entry point REJECTS when `assertCanExport` throws, and\n * - it rejects having read NOTHING — `exportStream` is never called.\n *\n * The second is the one a delegation refactor breaks silently: move the gate\n * from `toObject` into `download` and every existing test still passes.\n *\n * ## Why a Proxy and not a fake Vault\n *\n * `Vault` is large, and a hand-written double would drift from it — and worse,\n * would only ever exercise the methods whoever wrote the double thought of.\n * The fixture supplies a REAL vault; the kit wraps it, so an entry point that\n * reaches for some other decrypting method is still observed.\n *\n * @packageDocumentation\n */\nimport { describe, it, expect } from 'vitest'\nimport type { Vault, ExportFormat } from '@noy-db/hub'\n\n/** One plaintext-producing entry point, named as a consumer would call it. */\nexport interface FormatEntryPoint {\n /** Exported function name, e.g. `'toString'`. Used in the test title. */\n readonly name: string\n /** Call it against the supplied vault. Arguments are the fixture's business. */\n run(vault: Vault): Promise<unknown>\n}\n\n/** Everything an `as-*` package must supply to be checked against the gate. */\nexport interface FormatFixture {\n /**\n * The TIER the package passes to `assertCanExport`. The `as-*` family is\n * two capability classes, not one — discovered by wiring `as-noydb`, which\n * calls `assertCanExport('bundle')` and never mentions plaintext because it\n * emits an encrypted pod. A kit that assumed one tier would have made that\n * fixture describe itself wrongly while still passing.\n */\n readonly tier: 'plaintext' | 'bundle'\n /**\n * The format tag, e.g. `'csv'`. REQUIRED for the plaintext tier and\n * meaningless for `bundle` — hub itself throws when a plaintext check\n * arrives without one, so the pairing is asserted rather than assumed.\n */\n readonly format?: ExportFormat\n /**\n * A REAL vault with at least one record. Built fresh per case, so an entry\n * point that mutates it cannot leak into the next assertion.\n */\n vault(): Promise<Vault>\n /**\n * EVERY entry point that can produce plaintext — not a representative one.\n * A format with four exports and one listed here reports a green suite for\n * the three nobody checked.\n */\n readonly exports: ReadonlyArray<FormatEntryPoint>\n /**\n * The on-disk write path, if the package has one. It must refuse without\n * `acknowledgeRisks: true`; pass a call that OMITS the flag.\n *\n * The vault this receives is the fixture's own — NOT the denying proxy —\n * and it must be export-CAPABLE. A vault that would refuse the export\n * anyway makes the case unfalsifiable: the refusal arrives from the gate\n * upstream and the acknowledgement is never reached. That is not\n * hypothetical; it is what the first version of this kit did, and deleting\n * the acknowledgement guard from as-csv left the suite green.\n */\n writeWithoutAcknowledgement?: (vault: Vault, path: string) => Promise<unknown>\n}\n\n/** Thrown by the denying proxy so a refusal is attributable to the gate. */\nexport class ExportDeniedByConformanceKit extends Error {\n constructor(tier: string, format?: string) {\n super(`conformance: assertCanExport denied '${tier}'${format ? ` / '${format}'` : ''}`)\n this.name = 'ExportDeniedByConformanceKit'\n }\n}\n\ninterface Observation {\n decryptCalls: string[]\n}\n\n/**\n * Wrap a real vault so `assertCanExport` denies, and every method that could\n * yield plaintext records is recorded.\n *\n * `exportStream` is named explicitly because it is the shared read path; the\n * catch-all records any other function property that gets invoked, so an entry\n * point taking a different route is still visible rather than silently\n * unobserved.\n */\nfunction denyingVault(real: Vault, tier: string, format: string | undefined, seen: Observation): Vault {\n return new Proxy(real, {\n get(target, prop, receiver) {\n if (prop === 'assertCanExport') {\n return () => {\n throw new ExportDeniedByConformanceKit(tier, format)\n }\n }\n const value = Reflect.get(target, prop, receiver) as unknown\n if (typeof value === 'function' && typeof prop === 'string') {\n return (...args: unknown[]) => {\n if (prop === 'exportStream' || prop === 'export' || prop === 'snapshot') {\n seen.decryptCalls.push(prop)\n }\n return (value as (...a: unknown[]) => unknown).apply(target, args)\n }\n }\n return value\n },\n }) as Vault\n}\n\n/**\n * Run the shared `as-*` export-gate contract against one format.\n *\n * @param name - shown in the suite title, e.g. `'as-csv'`.\n */\nexport function runFormatConformanceTests(name: string, fixture: FormatFixture): void {\n describe(`${name} — as-* export gate conformance`, () => {\n it('declares a tier, and a format iff the tier needs one', () => {\n // Hub throws on `assertCanExport('plaintext')` with no format, so a\n // fixture in that state describes a call the package cannot be making.\n if (fixture.tier === 'plaintext') {\n expect(fixture.format, 'the plaintext tier requires a format').toBeTruthy()\n } else {\n expect(fixture.format, `the '${fixture.tier}' tier takes no format`).toBeUndefined()\n }\n })\n\n it('declares at least one export entry point', () => {\n // A fixture with an empty list would pass every case below without\n // running anything — a live suite iterating an empty array.\n expect(fixture.exports.length).toBeGreaterThan(0)\n })\n\n it('the fixture vault CAN export — otherwise every refusal below is free', async () => {\n // The whole suite tests refusals, and a refusal is only evidence when\n // the same call would otherwise SUCCEED. A fixture whose `format` tag\n // does not match what the package passes to `assertCanExport` — or\n // which forgets the `exportCapability` grant — makes every case below\n // pass by refusing for the wrong reason, and nothing in the output\n // distinguishes that from a working gate.\n const vault = await fixture.vault()\n await expect(\n fixture.exports[0]!.run(vault),\n `${fixture.exports[0]!.name} failed on an ungated vault — check the \\`format\\` tag and the exportCapability grant`,\n ).resolves.toBeDefined()\n })\n\n for (const entry of fixture.exports) {\n it(`${entry.name}: REFUSES when assertCanExport denies`, async () => {\n const seen: Observation = { decryptCalls: [] }\n const vault = denyingVault(await fixture.vault(), fixture.tier, fixture.format, seen)\n await expect(entry.run(vault)).rejects.toThrow()\n })\n\n it(`${entry.name}: refuses BEFORE reading any record`, async () => {\n const seen: Observation = { decryptCalls: [] }\n const vault = denyingVault(await fixture.vault(), fixture.tier, fixture.format, seen)\n await expect(entry.run(vault)).rejects.toThrow()\n // The property that a delegation refactor breaks silently: a gate\n // moved downstream still refuses the caller, having already decrypted.\n expect(\n seen.decryptCalls,\n `${entry.name} read records before the export gate refused`,\n ).toEqual([])\n })\n }\n\n const writeTitle = fixture.writeWithoutAcknowledgement\n ? 'write: REFUSES without acknowledgeRisks'\n : 'write: SKIPPED — fixture declares no acknowledgement case, so the plaintext-on-disk gate is UNVERIFIED here'\n\n it(writeTitle, async () => {\n const write = fixture.writeWithoutAcknowledgement\n if (!write) {\n // Passes loudly. Omitting the case would make an unchecked security\n // gate indistinguishable from a checked one in the output.\n expect(write).toBeUndefined()\n return\n }\n const vault = await fixture.vault()\n // Matched on the MESSAGE, not merely on \"it threw\". `rejects.toThrow()`\n // alone passes when the export gate refuses first — which is exactly\n // what happened here before this line existed, and it made the case\n // unable to fail. The flag name is the one string every such message\n // contains by construction.\n await expect(write(vault, '/tmp/conformance-should-not-exist')).rejects.toThrow(\n /acknowledgeRisks/i,\n )\n })\n })\n}\n"],"mappings":";AAkCA,SAAS,UAAU,IAAI,cAAc;AAqD9B,IAAM,+BAAN,cAA2C,MAAM;AAAA,EACtD,YAAY,MAAc,QAAiB;AACzC,UAAM,wCAAwC,IAAI,IAAI,SAAS,OAAO,MAAM,MAAM,EAAE,EAAE;AACtF,SAAK,OAAO;AAAA,EACd;AACF;AAeA,SAAS,aAAa,MAAa,MAAc,QAA4B,MAA0B;AACrG,SAAO,IAAI,MAAM,MAAM;AAAA,IACrB,IAAI,QAAQ,MAAM,UAAU;AAC1B,UAAI,SAAS,mBAAmB;AAC9B,eAAO,MAAM;AACX,gBAAM,IAAI,6BAA6B,MAAM,MAAM;AAAA,QACrD;AAAA,MACF;AACA,YAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,QAAQ;AAChD,UAAI,OAAO,UAAU,cAAc,OAAO,SAAS,UAAU;AAC3D,eAAO,IAAI,SAAoB;AAC7B,cAAI,SAAS,kBAAkB,SAAS,YAAY,SAAS,YAAY;AACvE,iBAAK,aAAa,KAAK,IAAI;AAAA,UAC7B;AACA,iBAAQ,MAAuC,MAAM,QAAQ,IAAI;AAAA,QACnE;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AAOO,SAAS,0BAA0B,MAAc,SAA8B;AACpF,WAAS,GAAG,IAAI,wCAAmC,MAAM;AACvD,OAAG,wDAAwD,MAAM;AAG/D,UAAI,QAAQ,SAAS,aAAa;AAChC,eAAO,QAAQ,QAAQ,sCAAsC,EAAE,WAAW;AAAA,MAC5E,OAAO;AACL,eAAO,QAAQ,QAAQ,QAAQ,QAAQ,IAAI,wBAAwB,EAAE,cAAc;AAAA,MACrF;AAAA,IACF,CAAC;AAED,OAAG,4CAA4C,MAAM;AAGnD,aAAO,QAAQ,QAAQ,MAAM,EAAE,gBAAgB,CAAC;AAAA,IAClD,CAAC;AAED,OAAG,6EAAwE,YAAY;AAOrF,YAAM,QAAQ,MAAM,QAAQ,MAAM;AAClC,YAAM;AAAA,QACJ,QAAQ,QAAQ,CAAC,EAAG,IAAI,KAAK;AAAA,QAC7B,GAAG,QAAQ,QAAQ,CAAC,EAAG,IAAI;AAAA,MAC7B,EAAE,SAAS,YAAY;AAAA,IACzB,CAAC;AAED,eAAW,SAAS,QAAQ,SAAS;AACnC,SAAG,GAAG,MAAM,IAAI,yCAAyC,YAAY;AACnE,cAAM,OAAoB,EAAE,cAAc,CAAC,EAAE;AAC7C,cAAM,QAAQ,aAAa,MAAM,QAAQ,MAAM,GAAG,QAAQ,MAAM,QAAQ,QAAQ,IAAI;AACpF,cAAM,OAAO,MAAM,IAAI,KAAK,CAAC,EAAE,QAAQ,QAAQ;AAAA,MACjD,CAAC;AAED,SAAG,GAAG,MAAM,IAAI,uCAAuC,YAAY;AACjE,cAAM,OAAoB,EAAE,cAAc,CAAC,EAAE;AAC7C,cAAM,QAAQ,aAAa,MAAM,QAAQ,MAAM,GAAG,QAAQ,MAAM,QAAQ,QAAQ,IAAI;AACpF,cAAM,OAAO,MAAM,IAAI,KAAK,CAAC,EAAE,QAAQ,QAAQ;AAG/C;AAAA,UACE,KAAK;AAAA,UACL,GAAG,MAAM,IAAI;AAAA,QACf,EAAE,QAAQ,CAAC,CAAC;AAAA,MACd,CAAC;AAAA,IACH;AAEA,UAAM,aAAa,QAAQ,8BACvB,4CACA;AAEJ,OAAG,YAAY,YAAY;AACzB,YAAM,QAAQ,QAAQ;AACtB,UAAI,CAAC,OAAO;AAGV,eAAO,KAAK,EAAE,cAAc;AAC5B;AAAA,MACF;AACA,YAAM,QAAQ,MAAM,QAAQ,MAAM;AAMlC,YAAM,OAAO,MAAM,OAAO,mCAAmC,CAAC,EAAE,QAAQ;AAAA,QACtE;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@noy-db/test-format-conformance",
|
|
3
|
+
"version": "0.7.0-pre.0",
|
|
4
|
+
"description": "Parameterized contract tests for noy-db as-* formats — the export-gate suite every plaintext projection must pass",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "vLannaAi <vicio@lanna.ai>",
|
|
7
|
+
"homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/test-format-conformance#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/vLannaAi/noy-db.git",
|
|
11
|
+
"directory": "packages/test-format-conformance"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/vLannaAi/noy-db/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"vitest": "^3.0.0",
|
|
36
|
+
"@noy-db/hub": "0.7.0-pre.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"vitest": "^3.0.0",
|
|
40
|
+
"@noy-db/hub": "0.7.0-pre.0"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"noy-db",
|
|
44
|
+
"conformance",
|
|
45
|
+
"export",
|
|
46
|
+
"plaintext",
|
|
47
|
+
"testing"
|
|
48
|
+
],
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"test": "vitest run --passWithNoTests",
|
|
55
|
+
"typecheck": "tsc --noEmit"
|
|
56
|
+
}
|
|
57
|
+
}
|