proof-harness 0.1.0-next.12 → 0.1.0-next.13
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/COMPATIBILITY.md +22 -3
- package/README.md +6 -2
- package/dist/playwright.d.ts +4 -0
- package/dist/playwright.js +40 -11
- package/dist/playwright.js.map +1 -1
- package/package.json +1 -1
package/COMPATIBILITY.md
CHANGED
|
@@ -27,10 +27,29 @@ versions rather than guessing how to interpret them.
|
|
|
27
27
|
|
|
28
28
|
## Prerelease migration notes
|
|
29
29
|
|
|
30
|
-
### 0.1.0-next.
|
|
30
|
+
### 0.1.0-next.13 — column-restricted tenant isolation (release candidate)
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
and
|
|
32
|
+
Prepared for release. After publication, pin exactly `proof-harness@0.1.0-next.13`
|
|
33
|
+
and update the lockfile. Add `columns` only where column grants require it, then
|
|
34
|
+
rerun the baseline, coverage, mutation and control checks against the real database.
|
|
35
|
+
Existing consumers that omit this option need no API changes.
|
|
36
|
+
|
|
37
|
+
`assert.tenantIsolation` accepts optional `columns: ["id", "account_id"]` for
|
|
38
|
+
private tables that grant customers only selected columns. The SDK uses the
|
|
39
|
+
same projection for service ground truth, owner controls, and outsider probes.
|
|
40
|
+
Only distinct plain column names are allowed; empty lists, wildcards, aliases,
|
|
41
|
+
aggregates, casts and joins are rejected before fixture setup. Omitting the
|
|
42
|
+
option retains the existing all-column query.
|
|
43
|
+
|
|
44
|
+
This proves row isolation for the selected columns. Consumers must separately
|
|
45
|
+
prove that private columns remain unreadable; do not grant extra columns to
|
|
46
|
+
make a fixture pass. Existing positive-control, bidirectional, non-vacuity and
|
|
47
|
+
provenance requirements are unchanged. No trace, mission, or health protocol migration is
|
|
48
|
+
required. Adopt only an exact reviewed published package version.
|
|
49
|
+
|
|
50
|
+
### 0.1.0-next.12 — independent isolation fixtures and column ACL recovery
|
|
51
|
+
|
|
52
|
+
Published release. Pin exactly `proof-harness@0.1.0-next.12` when adopting these changes.
|
|
34
53
|
|
|
35
54
|
`assert.tenantIsolation` now accepts a caller-owned `isolationFixture` instead of
|
|
36
55
|
workspace seeding. Supply a stable fixture `id`, the matching `table`, an explicit
|
package/README.md
CHANGED
|
@@ -157,6 +157,10 @@ privileged writes and requires action-boundary evidence even without workspace
|
|
|
157
157
|
inputs or for internal actions. See [required migration steps](COMPATIBILITY.md)
|
|
158
158
|
before adopting the exact version.
|
|
159
159
|
|
|
160
|
-
Release
|
|
160
|
+
Release `0.1.0-next.12` adds caller-owned account/user isolation fixtures
|
|
161
161
|
and restores column grants and grant options after table-privilege mutations.
|
|
162
|
-
|
|
162
|
+
See [migration requirements](COMPATIBILITY.md).
|
|
163
|
+
|
|
164
|
+
Release candidate `0.1.0-next.13` adds optional readable-column selection to
|
|
165
|
+
`assert.tenantIsolation` for tables with column grants. Private-column denial
|
|
166
|
+
remains a separate consumer proof; see [migration requirements](COMPATIBILITY.md).
|
package/dist/playwright.d.ts
CHANGED
|
@@ -97,6 +97,10 @@ declare const actAsUser: {
|
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
interface TenantIsolationBaseOptions {
|
|
100
|
+
/** Plain readable column names for column-restricted tables. Defaults to all
|
|
101
|
+
* columns. The same projection is used for ground truth and both actors.
|
|
102
|
+
* This proves row isolation only; test secret-column denial separately. */
|
|
103
|
+
columns?: readonly string[];
|
|
100
104
|
/**
|
|
101
105
|
* Table to query as userB. RLS on the table is what is being tested.
|
|
102
106
|
* Must be a table governed by a tenant-scoped RLS policy — either
|
package/dist/playwright.js
CHANGED
|
@@ -1765,9 +1765,22 @@ async function runRlsProbe(probe, actorCreds) {
|
|
|
1765
1765
|
detail: `${actorCreds.label} UPDATE on ${probe.table} left ${columns.join(", ")} unchanged across ${before.length} targeted row(s)${error ? ` (${error.message})` : ""}; verified by service-role re-read (RLS honored)`
|
|
1766
1766
|
});
|
|
1767
1767
|
}
|
|
1768
|
-
|
|
1768
|
+
function tenantColumnSelection(columns) {
|
|
1769
|
+
if (columns === void 0) return "*";
|
|
1770
|
+
if (!Array.isArray(columns) || columns.length === 0 || columns.some(
|
|
1771
|
+
(column) => typeof column !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)
|
|
1772
|
+
) || new Set(columns).size !== columns.length) {
|
|
1773
|
+
throw proofFail(
|
|
1774
|
+
"tenant_isolation_setup",
|
|
1775
|
+
"a non-empty list of distinct plain column names",
|
|
1776
|
+
"invalid tenant column projection"
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
return columns.join(",");
|
|
1780
|
+
}
|
|
1781
|
+
async function countRowsAsService(sb, table, filter, selection) {
|
|
1769
1782
|
const { count, error } = await applyEq(
|
|
1770
|
-
sb.from(table).select(
|
|
1783
|
+
sb.from(table).select(selection, { count: "exact", head: true }),
|
|
1771
1784
|
filter
|
|
1772
1785
|
);
|
|
1773
1786
|
if (error) {
|
|
@@ -1800,8 +1813,13 @@ async function probeTenantIsolation(args) {
|
|
|
1800
1813
|
[scopeColumn]: scopeValue,
|
|
1801
1814
|
...criterion?.where ?? {}
|
|
1802
1815
|
};
|
|
1803
|
-
const criterionDetail = criterion ? ` matching planner criterion "${criterion.description}" (${JSON.stringify(criterion.where)})` : "";
|
|
1804
|
-
const actualRowCount = await countRowsAsService(
|
|
1816
|
+
const criterionDetail = (criterion ? ` matching planner criterion "${criterion.description}" (${JSON.stringify(criterion.where)})` : "") + (args.selection === "*" ? "" : ` selecting columns ${args.selection}`);
|
|
1817
|
+
const actualRowCount = await countRowsAsService(
|
|
1818
|
+
sb,
|
|
1819
|
+
table,
|
|
1820
|
+
filter,
|
|
1821
|
+
args.selection
|
|
1822
|
+
);
|
|
1805
1823
|
if (actualRowCount === 0) {
|
|
1806
1824
|
return "skipped_no_data";
|
|
1807
1825
|
}
|
|
@@ -1813,7 +1831,7 @@ async function probeTenantIsolation(args) {
|
|
|
1813
1831
|
data: ownRows,
|
|
1814
1832
|
error: ownError,
|
|
1815
1833
|
status: ownStatus
|
|
1816
|
-
} = await applyEq(ownerClient.from(table).select(
|
|
1834
|
+
} = await applyEq(ownerClient.from(table).select(args.selection), filter);
|
|
1817
1835
|
failIfInfrastructure(
|
|
1818
1836
|
ownError,
|
|
1819
1837
|
ownStatus,
|
|
@@ -1860,7 +1878,7 @@ async function probeTenantIsolation(args) {
|
|
|
1860
1878
|
data: leakedRows,
|
|
1861
1879
|
error: queryError,
|
|
1862
1880
|
status: queryStatus
|
|
1863
|
-
} = await applyEq(client.from(table).select(
|
|
1881
|
+
} = await applyEq(client.from(table).select(args.selection), filter);
|
|
1864
1882
|
failIfInfrastructure(
|
|
1865
1883
|
queryError,
|
|
1866
1884
|
queryStatus,
|
|
@@ -1902,7 +1920,9 @@ async function probeTenantIsolation(args) {
|
|
|
1902
1920
|
);
|
|
1903
1921
|
}
|
|
1904
1922
|
if (leakedRows && leakedRows.length > 0) {
|
|
1905
|
-
const sampleIds = leakedRows.slice(0, 3).map(
|
|
1923
|
+
const sampleIds = leakedRows.slice(0, 3).map(
|
|
1924
|
+
(r) => r !== null && typeof r === "object" && "id" in r ? String(r.id) : "?"
|
|
1925
|
+
).join(", ");
|
|
1906
1926
|
recordAssertion({
|
|
1907
1927
|
kind: "tenant_isolation",
|
|
1908
1928
|
target: table,
|
|
@@ -2037,6 +2057,7 @@ var assertMethods = {
|
|
|
2037
2057
|
*/
|
|
2038
2058
|
async tenantIsolation(opts) {
|
|
2039
2059
|
const { table, page } = opts;
|
|
2060
|
+
const selection = tenantColumnSelection(opts.columns);
|
|
2040
2061
|
if (opts.isolationFixture) {
|
|
2041
2062
|
const fixture = await withoutAssertionProvenance(async () => {
|
|
2042
2063
|
const supplied = opts.isolationFixture;
|
|
@@ -2078,10 +2099,15 @@ var assertMethods = {
|
|
|
2078
2099
|
});
|
|
2079
2100
|
const sb = createProofServiceClient();
|
|
2080
2101
|
for (const side of participants) {
|
|
2081
|
-
const count = await countRowsAsService(
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2102
|
+
const count = await countRowsAsService(
|
|
2103
|
+
sb,
|
|
2104
|
+
table,
|
|
2105
|
+
{
|
|
2106
|
+
[opts.scopeColumn]: side.scopeValue,
|
|
2107
|
+
...opts.criterion?.where ?? {}
|
|
2108
|
+
},
|
|
2109
|
+
selection
|
|
2110
|
+
);
|
|
2085
2111
|
if (!count)
|
|
2086
2112
|
throw proofFail(
|
|
2087
2113
|
"tenant_isolation_vacuous",
|
|
@@ -2094,6 +2120,7 @@ var assertMethods = {
|
|
|
2094
2120
|
[participants[1], participants[0]]
|
|
2095
2121
|
]) {
|
|
2096
2122
|
const outcome = await probeTenantIsolation({
|
|
2123
|
+
selection,
|
|
2097
2124
|
table,
|
|
2098
2125
|
owner: owner.actor,
|
|
2099
2126
|
viewer: viewer.actor,
|
|
@@ -2256,6 +2283,7 @@ var assertMethods = {
|
|
|
2256
2283
|
);
|
|
2257
2284
|
}
|
|
2258
2285
|
const forward = await probeTenantIsolation({
|
|
2286
|
+
selection,
|
|
2259
2287
|
table,
|
|
2260
2288
|
viewer: userB,
|
|
2261
2289
|
viewerLabel: "user B",
|
|
@@ -2291,6 +2319,7 @@ var assertMethods = {
|
|
|
2291
2319
|
);
|
|
2292
2320
|
}
|
|
2293
2321
|
const reverse = await probeTenantIsolation({
|
|
2322
|
+
selection,
|
|
2294
2323
|
table,
|
|
2295
2324
|
viewer: userA,
|
|
2296
2325
|
viewerLabel: "user A",
|