clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/fix-sync-enrollment-startup
lofi / package / schema / conformance_test.ts
conformance_test.ts 27.7K
1// Column-type conformance: every data shape the schema facade exposes must2// round-trip through the real pinned Jazz engine — insert, read-back, where3// filtering, update, modifiers, and policy matching. A member that only4// compiles is not supported; this suite is what fails when an alpha bump5// changes engine behavior for a column type the facade exports.6//7// Uses only the facade surface (`s` from ./mod.ts) so the suite proves what an8// application author can actually reach.9import { createPolicyTestApp } from "jazz-tools/testing";10import { assert } from "../runtime/test-assert.ts";11import {12 type ArrayColumn,13 EncryptedColumnError,14 type IntColumn,15 s,16 type StringColumn,17} from "./mod.ts";18import { clearEncryptedColumnKey, setEncryptedColumnKey } from "./encrypted.ts";1920const app = s.defineApp({21 parents: s.table({ name: s.string() }),22 strings: s.table({ value: s.string() }),23 booleans: s.table({ value: s.boolean() }),24 ints: s.table({ value: s.int() }),25 floats: s.table({ value: s.float() }),26 timestamps: s.table({ value: s.timestamp() }),27 blobs: s.table({ value: s.bytes() }),28 jsons: s.table({ value: s.json() }),29 enums: s.table({ value: s.enum("draft", "published", "archived") }),30 refs: s.table({ parentId: s.ref("parents"), label: s.string() }),31 arrays: s.table({ value: s.array(s.string()) }),32 modifiers: s.table({33 required: s.string(),34 note: s.string().optional(),35 label: s.string().default("untitled"),36 status: s.enum("open", "done", "dropped").default("open"),37 // CONFORMANCE FINDING (alpha.53): `.merge()` returns the untyped builder38 // because the legacy `merge(): this` signature shadows the typed overload,39 // which poisons the whole table's row types. The cast restores the typed40 // column; the runtime object is correct without it.41 total: s.int().default(0).merge("counter") as unknown as IntColumn<false, true>,42 }),43 transformed: s.table({44 // Stored as comma-joined TEXT, viewed as string[]. The untyped45 // `transform(): ColumnBuilder` signature shadows the typed overload the46 // same way `.merge()` does, so the cast restores the transformed value47 // type.48 tags: s.string().transform({49 from: (value: string) => value.split(","),50 to: (value: string[]) => value.join(","),51 }) as unknown as StringColumn<false, false, string[]>,52 label: s.string(),53 }),54 gated: s.table({ visible: s.boolean(), title: s.string() }),55 sealed: s.table({56 body: s.encryptedText("sealed.body"),57 meta: s.encryptedJson<{ note: string }>("sealed.meta"),58 amount: s.encryptedNumber("sealed.amount"),59 at: s.encryptedDate("sealed.at"),60 label: s.string(),61 }),62});6364// CONFORMANCE FINDING (alpha.53): a `g-set` column anywhere in a schema makes65// writes to *other* tables in the same app hang forever (bisected via a66// cumulative table-by-table probe; boots fine, first cross-table write never67// resolves). The g-set coverage therefore lives in its own single-table app,68// and the hang itself is pinned by the subprocess canary test below.69const gsetApp = s.defineApp({70 tagged: s.table({71 name: s.string(),72 tags: s.array(s.string()).merge("g-set") as unknown as ArrayColumn<"TEXT">,73 }),74});75const gsetPermissions = s.definePermissions(gsetApp, ({ policy }) => {76 policy.tagged.allowInsert.always();77 policy.tagged.allowRead.always();78 policy.tagged.allowUpdate.always();79 policy.tagged.allowDelete.always();80});8182const allowAllTables = [83 "parents",84 "strings",85 "booleans",86 "ints",87 "floats",88 "timestamps",89 "blobs",90 "jsons",91 "enums",92 "refs",93 "arrays",94 "modifiers",95 "sealed",96 "transformed",97] as const;9899type RuleSet = {100 always(): void;101 where(condition: Record<string, unknown>): void;102};103type TableRules = Record<"allowInsert" | "allowRead" | "allowUpdate" | "allowDelete", RuleSet>;104105const permissions = s.definePermissions(app, ({ policy, session, anyOf }) => {106 const rules = policy as unknown as Record<string, TableRules>;107 for (const table of allowAllTables) {108 rules[table].allowInsert.always();109 rules[table].allowRead.always();110 rules[table].allowUpdate.always();111 rules[table].allowDelete.always();112 }113 // Policy conditions must match over a typed column, not only magic columns,114 // and anyOf must compose alternatives. Delete is deliberately denied for115 // visible rows so the permission introspection columns have something to116 // discriminate.117 policy.gated.allowInsert.always();118 policy.gated.allowRead.where(anyOf([{ visible: true }, { $createdBy: session.user_id }]));119 policy.gated.allowUpdate.always();120 policy.gated.allowDelete.where({ visible: false });121});122123type ThrowExpectation = {124 toThrow(expected?: unknown): void;125 not: { toThrow(expected?: unknown): void };126};127128function expectLike(value: unknown): ThrowExpectation {129 const run = (): unknown => {130 if (typeof value !== "function") throw new Error("expectLike requires a callback");131 try {132 value();133 return undefined;134 } catch (error) {135 return error ?? new Error("threw undefined");136 }137 };138 const matches = (error: unknown, expected: unknown): boolean => {139 if (expected === undefined) return error !== undefined;140 const message = error instanceof Error ? error.message : String(error);141 return typeof expected === "string"142 ? message.includes(expected)143 : error instanceof (expected as typeof Error);144 };145 return {146 toThrow(expected?: unknown) {147 const error = run();148 if (!matches(error, expected)) {149 throw new Error(`expected callback to throw ${String(expected)}`);150 }151 },152 not: {153 toThrow(expected?: unknown) {154 const error = run();155 if (error !== undefined && (expected === undefined || matches(error, expected))) {156 const message = error instanceof Error ? error.message : String(error);157 throw new Error(`expected callback not to throw, received: ${message}`);158 }159 },160 },161 };162}163164const session = (user_id: string) => ({ user_id, claims: {}, authMode: "local-first" as const });165166function sameValue(actual: unknown, expected: unknown): boolean {167 if (expected instanceof Date) {168 return actual instanceof Date169 ? actual.getTime() === expected.getTime()170 : actual === expected.getTime();171 }172 if (expected instanceof Uint8Array) {173 return actual instanceof Uint8Array &&174 actual.length === expected.length &&175 expected.every((byte, index) => actual[index] === byte);176 }177 if (Array.isArray(expected)) {178 return Array.isArray(actual) &&179 actual.length === expected.length &&180 expected.every((item, index) => sameValue(actual[index], item));181 }182 if (expected !== null && typeof expected === "object") {183 if (actual === null || typeof actual !== "object") return false;184 const expectedEntries = Object.entries(expected);185 return expectedEntries.length === Object.keys(actual).length &&186 expectedEntries.every(([key, item]) =>187 sameValue((actual as Record<string, unknown>)[key], item)188 );189 }190 return actual === expected;191}192193function describeValue(value: unknown): string {194 if (value instanceof Uint8Array) return `Uint8Array(${value.length})[${value.join(",")}]`;195 if (value instanceof Date) return `Date(${value.toISOString()})`;196 return JSON.stringify(value);197}198199// One representative and one contrasting value per scalar column family. The200// contrasting value proves where-equality actually filters instead of201// matching everything.202const scalarCases = [203 { table: "strings", value: "héllo wörld", other: "" },204 { table: "booleans", value: true, other: false },205 // CONFORMANCE FINDING (alpha.53): int columns are i32 at runtime despite206 // the `number` static type — see the i64 rejection pin below.207 { table: "ints", value: 2_147_483_640, other: -1 },208 { table: "floats", value: 3.141592653589793, other: -0.5 },209 // PINNED (alpha.53): where-equality on a timestamp column matches every210 // row (verified with distinct non-epoch dates). When the pinned count211 // below reads 1 again, upstream fixed the comparison — clear the flag.212 {213 table: "timestamps",214 value: new Date("2026-07-18T12:34:56.789Z"),215 other: new Date("2001-02-03T04:05:06.007Z"),216 brokenWhereEquality: true,217 },218 { table: "enums", value: "published", other: "draft" },219] as const;220221// The scalar sweep iterates tables dynamically, which steps outside the typed222// query surface; these loose structural views cover only the sweep. The223// structured-columns test below stays fully typed.224type Row = { id: string } & Record<string, unknown>;225type LooseHandle = { where(condition: Record<string, unknown>): unknown };226type LooseDb = {227 insert(table: unknown, values: unknown): { wait(opts: { tier: "global" }): Promise<Row> };228 update(table: unknown, id: string, patch: unknown): { wait(opts: { tier: "global" }): unknown };229 all(query: unknown): Promise<Row[]>;230};231232Deno.test("facade structured columns (bytes, json, array, ref) round-trip through the real engine", async () => {233 const testApp = await createPolicyTestApp(app, permissions, expectLike);234 try {235 const db = testApp.as(session("author"));236237 const payload = new Uint8Array(32).map((_, index) => (index * 37) % 256);238 const blob = await db.insert(app.blobs, { value: payload }).wait({ tier: "global" });239 const blobRows = await db.all(app.blobs.where({ id: blob.id }));240 assert(241 blobRows.length === 1 && sameValue(blobRows[0].value, payload),242 `bytes: read back ${describeValue(blobRows[0]?.value)}`,243 );244245 const document = { nested: { list: [1, "two", null], flag: true }, empty: {} };246 const json = await db.insert(app.jsons, { value: document }).wait({ tier: "global" });247 const jsonRows = await db.all(app.jsons.where({ id: json.id }));248 assert(249 jsonRows.length === 1 && sameValue(jsonRows[0].value, document),250 `json: read back ${describeValue(jsonRows[0]?.value)}`,251 );252253 const items = ["alpha", "beta", "beta"];254 const array = await db.insert(app.arrays, { value: items }).wait({ tier: "global" });255 const arrayRows = await db.all(app.arrays.where({ id: array.id }));256 assert(257 arrayRows.length === 1 && sameValue(arrayRows[0].value, items),258 `array: read back ${describeValue(arrayRows[0]?.value)}`,259 );260261 const parent = await db.insert(app.parents, { name: "parent" }).wait({ tier: "global" });262 const decoy = await db.insert(app.parents, { name: "decoy" }).wait({ tier: "global" });263 await db.insert(app.refs, { parentId: parent.id, label: "child" }).wait({ tier: "global" });264 await db.insert(app.refs, { parentId: decoy.id, label: "other" }).wait({ tier: "global" });265 const children = await db.all(app.refs.where({ parentId: parent.id }));266 assert(267 children.length === 1 && children[0].label === "child",268 `ref: where on the foreign key returned ${children.length} rows`,269 );270 } finally {271 await testApp.shutdown();272 }273});274275Deno.test("facade column modifiers (optional, default, counter, g-set) behave through the real engine", async () => {276 const testApp = await createPolicyTestApp(app, permissions, expectLike);277 try {278 const db = testApp.as(session("author"));279280 const row = await db.insert(app.modifiers, { required: "present" })281 .wait({ tier: "global" });282 const readBack = (await db.all(app.modifiers.where({ id: row.id })))[0];283 assert(readBack !== undefined, "modifiers: row lost after insert");284 assert(285 readBack.note === null || readBack.note === undefined,286 `optional: omitted column read back as ${describeValue(readBack.note)}`,287 );288 assert(289 readBack.label === "untitled",290 `default: omitted column read back as ${describeValue(readBack.label)}`,291 );292 assert(293 readBack.status === "open",294 `enum default: omitted column read back as ${describeValue(readBack.status)}`,295 );296 await db.update(app.modifiers, row.id, { status: "done" }).wait({ tier: "global" });297 const transitioned = (await db.all(app.modifiers.where({ id: row.id })))[0];298 assert(299 transitioned?.status === "done",300 `enum: update read back ${describeValue(transitioned?.status)}`,301 );302 assert(303 readBack.total === 0,304 `counter default: read back ${describeValue(readBack.total)}`,305 );306307 // Single-session semantics only; concurrent-writer merge behavior is308 // pinned in ./merge_sync_test.ts, which runs first in its own process.309 await db.update(app.modifiers, row.id, { total: 5 }).wait({ tier: "global" });310 const bumped = (await db.all(app.modifiers.where({ id: row.id })))[0];311 assert(bumped !== undefined, "modifiers: row lost after update");312 assert(bumped.total === 5, `counter: update read back ${describeValue(bumped.total)}`);313314 await db.update(app.modifiers, row.id, { note: "written" }).wait({ tier: "global" });315 const cleared = (await db.all(app.modifiers.where({ id: row.id })))[0];316 assert(cleared?.note === "written", "optional: column did not accept a value");317 } finally {318 await testApp.shutdown();319 }320});321322Deno.test("g-set columns round-trip inside their own single-table app", async () => {323 const testApp = await createPolicyTestApp(gsetApp, gsetPermissions, expectLike);324 try {325 const db = testApp.as(session("author"));326 const row = await db.insert(gsetApp.tagged, { name: "first", tags: ["a"] })327 .wait({ tier: "global" });328 await db.update(gsetApp.tagged, row.id, { tags: ["a", "b"] }).wait({ tier: "global" });329 const readBack = (await db.all(gsetApp.tagged.where({ id: row.id })))[0];330 assert(readBack !== undefined, "g-set: row lost after update");331 assert(332 sameValue(readBack.tags, ["a", "b"]),333 `g-set: update read back ${describeValue(readBack.tags)}`,334 );335 } finally {336 await testApp.shutdown();337 }338});339340async function runGsetCanary(mode: "sibling-first" | "gset-first"): Promise<string> {341 const script = new URL("../testdata/gset_hang_canary.ts", import.meta.url);342 const repoRoot = new URL("../../", import.meta.url);343 const command = new Deno.Command(Deno.execPath(), {344 args: [345 "run",346 "--allow-env",347 "--allow-net=127.0.0.1",348 "--allow-read",349 "--allow-write",350 "--allow-run",351 "--allow-ffi",352 script.pathname,353 mode,354 ],355 cwd: repoRoot.pathname,356 stdout: "piped",357 stderr: "piped",358 });359 const output = await command.output();360 return new TextDecoder().decode(output.stdout);361}362363Deno.test("CANARY alpha.53: short byte payloads are unreliable", async () => {364 // A 2-byte insert has been observed succeeding, failing with the365 // flat-history encoder error ("data too short for column value"), and366 // hanging — context-dependent, so it runs behind a race in a child process367 // and every known mode is accepted. An unknown error message is the only368 // failure: it means the encoder changed and the bytes coverage should be369 // revisited (ideally widened to short payloads).370 const script = new URL("../testdata/bytes_short_payload_canary.ts", import.meta.url);371 const repoRoot = new URL("../../", import.meta.url);372 const command = new Deno.Command(Deno.execPath(), {373 args: [374 "run",375 "--allow-env",376 "--allow-net=127.0.0.1",377 "--allow-read",378 "--allow-write",379 "--allow-run",380 "--allow-ffi",381 script.pathname,382 ],383 cwd: repoRoot.pathname,384 stdout: "piped",385 stderr: "piped",386 });387 const output = await command.output();388 const stdout = new TextDecoder().decode(output.stdout);389 const known = stdout.includes("short-bytes:OK") ||390 stdout.includes("short-bytes:HUNG") ||391 (stdout.includes("short-bytes:ERROR") && stdout.includes("data too short"));392 assert(known, `bytes canary: unknown short-payload behavior: ${stdout}`);393 console.log(`[conformance] ${stdout.trim()}`);394});395396Deno.test("CANARY alpha.53: g-set cross-table writes are unreliable", async () => {397 // Writing a sibling table before a g-set table's first write hangs398 // reproducibly inside a process that has already booted policy test399 // harnesses (five independent bisection runs), but not always in a fresh400 // process — the wedge depends on process state, like the timestamp-query401 // wedge documented on the scalar test. Both modes therefore run in child402 // processes that force-exit, and either outcome is accepted and reported;403 // only an outright error is a failure. The g-set coverage stays isolated in404 // its own single-table app until upstream stabilizes this.405 const siblingFirst = await runGsetCanary("sibling-first");406 assert(407 siblingFirst.includes("sibling-table-write:OK") ||408 siblingFirst.includes("sibling-table-write:HUNG"),409 `g-set canary: unexpected sibling-first outcome: ${siblingFirst}`,410 );411 console.log(`[conformance] g-set sibling-first: ${siblingFirst.trim()}`);412413 const gsetFirst = await runGsetCanary("gset-first");414 assert(415 gsetFirst.includes("gset-table-write:OK") && gsetFirst.includes("sibling-table-write:OK"),416 `g-set canary: seeding the g-set table first no longer unblocks sibling writes: ${gsetFirst}`,417 );418});419420Deno.test("transformed columns apply the TypeScript-boundary transform on insert, read, and update", async () => {421 const testApp = await createPolicyTestApp(app, permissions, expectLike);422 try {423 const db = testApp.as(session("author"));424 const row = await db.insert(app.transformed, { tags: ["a", "b"], label: "first" })425 .wait({ tier: "global" });426 assert(427 sameValue(row.tags, ["a", "b"]),428 `transformed: insert returned ${describeValue(row.tags)}, not the view value`,429 );430 const readBack = (await db.all(app.transformed.where({ id: row.id })))[0];431 assert(readBack !== undefined, "transformed: row lost after insert");432 assert(433 sameValue(readBack.tags, ["a", "b"]),434 `transformed: read back ${describeValue(readBack.tags)}, not the view value`,435 );436437 await db.update(app.transformed, row.id, { tags: ["a", "b", "c"] }).wait({ tier: "global" });438 const updated = (await db.all(app.transformed.where({ id: row.id })))[0];439 assert(440 sameValue(updated?.tags, ["a", "b", "c"]),441 `transformed: update read back ${describeValue(updated?.tags)}`,442 );443444 // Filters address the stored representation, not the view: the where type445 // for a transformed column is the stored type, and the runtime agrees.446 await db.insert(app.transformed, { tags: ["z"], label: "decoy" }).wait({ tier: "global" });447 const byStored = await db.all(app.transformed.where({ tags: "a,b,c" }));448 assert(449 byStored.length === 1 && byStored[0].label === "first",450 `transformed: where on the stored value returned ${byStored.length} rows`,451 );452 } finally {453 await testApp.shutdown();454 }455});456457Deno.test("policies filter on typed columns, not only magic columns", async () => {458 const testApp = await createPolicyTestApp(app, permissions, expectLike);459 try {460 const db = testApp.as(session("author"));461 await db.insert(app.gated, { visible: true, title: "shown" }).wait({ tier: "global" });462 await db.insert(app.gated, { visible: false, title: "hidden" }).wait({ tier: "global" });463 // The author matches the $createdBy alternative and sees both rows; a464 // second identity only matches the typed-column alternative.465 const authorView = await db.all(app.gated.where({}));466 assert(467 authorView.length === 2,468 `gated: anyOf alternatives exposed ${authorView.length} rows to the author, expected 2`,469 );470 const viewer = testApp.as(session("viewer"));471 const viewerView = await viewer.all(app.gated.where({}));472 assert(473 viewerView.length === 1 && viewerView[0].title === "shown",474 `gated: policy on a boolean column exposed ${viewerView.length} rows to a non-author`,475 );476477 const introspected = await db.all(478 app.gated.select("title", "visible", ...s.permissionIntrospectionColumns).where({}),479 );480 for (const row of introspected) {481 assert(482 row.$canRead === true && row.$canEdit === true && row.$canDelete === !row.visible,483 `introspection: permission columns read back ${JSON.stringify(row)}, expected ` +484 `canDelete to mirror the visible-only delete rule`,485 );486 }487 } finally {488 await testApp.shutdown();489 }490});491492// CONFORMANCE FINDING (alpha.53): this test runs LAST deliberately. Running493// the broken timestamp where-equality query (pinned below) leaves the FFI494// driver wedged: the next createPolicyTestApp boot in the same process hangs495// on its first write. Observed across three orderings; tests that run before496// this one are unaffected.497Deno.test("facade scalar columns round-trip, filter, and update through the real engine", async () => {498 const testApp = await createPolicyTestApp(app, permissions, expectLike);499 try {500 const db = testApp.as(session("author")) as unknown as LooseDb;501 const handles = app as unknown as Record<string, LooseHandle>;502 for (const { table, value, other } of scalarCases) {503 const handle = handles[table];504 const inserted = await db.insert(handle, { value }).wait({ tier: "global" });505 await db.insert(handle, { value: other }).wait({ tier: "global" });506507 const byId = await db.all(handle.where({ id: inserted.id }));508 assert(byId.length === 1, `${table}: row lost after insert`);509 assert(510 sameValue(byId[0].value, value),511 `${table}: read back ${describeValue(byId[0].value)}, inserted ${describeValue(value)}`,512 );513514 const byValue = await db.all(handle.where({ value }));515 if ("brokenWhereEquality" in scalarCases.find((c) => c.table === table)!) {516 assert(517 byValue.length === 2,518 `${table}: pinned broken where-equality changed — returned ${byValue.length} rows; ` +519 `if 1, upstream fixed the filter: remove the pin`,520 );521 } else {522 assert(523 byValue.length === 1 && byValue[0].id === inserted.id,524 `${table}: where-equality on the typed column returned ${byValue.length} rows`,525 );526 }527528 await db.update(handle, inserted.id, { value: other }).wait({ tier: "global" });529 const afterUpdate = await db.all(handle.where({ id: inserted.id }));530 assert(531 afterUpdate.length === 1 && sameValue(afterUpdate[0].value, other),532 `${table}: update read back ${describeValue(afterUpdate[0]?.value)}`,533 );534 }535536 // PINNED (alpha.53): despite the `number` static type, int columns reject537 // values outside i32 with InvalidArg. When this pin fails, upstream538 // widened the runtime type — update the docs and the scalar case above.539 let i64Error = "";540 try {541 await db.insert(handles.ints, { value: 2 ** 40 + 7 }).wait({ tier: "global" });542 } catch (error) {543 i64Error = error instanceof Error ? error.message : String(error);544 }545 assert(546 i64Error.includes("expected i32"),547 `ints: pinned i32 limit changed: ${i64Error || "no error thrown"}`,548 );549 } finally {550 await testApp.shutdown();551 }552});553554Deno.test("encrypted columns round-trip through the engine and store only ciphertext", async () => {555 const testApp = await createPolicyTestApp(app, permissions, expectLike);556 setEncryptedColumnKey(new Uint8Array(32).map((_, index) => index + 1));557 try {558 const db = testApp.as(session("author"));559 const body = "the sealed plaintext body";560 // The number exceeds the pinned i32 int-column limit on purpose: sealed561 // numbers ride TEXT and must not inherit it.562 const amount = 2 ** 40 + 0.5;563 const at = new Date("2026-07-19T12:34:56.789Z");564 const row = await db.insert(app.sealed, {565 body,566 meta: { note: "sealed json" },567 amount,568 at,569 label: "first",570 }).wait({ tier: "global" });571 assert(row.body === body, `sealed: insert returned ${describeValue(row.body)}`);572 const readBack = (await db.all(app.sealed.where({ id: row.id })))[0];573 assert(readBack !== undefined, "sealed: row lost after insert");574 assert(readBack.body === body, `sealed: read back ${describeValue(readBack.body)}`);575 assert(576 sameValue(readBack.meta, { note: "sealed json" }),577 `sealed: json view was ${describeValue(readBack.meta)}`,578 );579 assert(580 readBack.amount === amount,581 `sealed: number view was ${describeValue(readBack.amount)}`,582 );583 assert(584 readBack.at instanceof Date && readBack.at.getTime() === at.getTime(),585 `sealed: date view was ${describeValue(readBack.at)}`,586 );587588 // Filters address the stored representation: the plaintext must match589 // nothing, because the engine never held it.590 const byPlaintext = await db.all(591 app.sealed.where({ body } as never),592 );593 assert(594 byPlaintext.length === 0,595 `sealed: a where on the plaintext matched ${byPlaintext.length} rows — the store saw plaintext`,596 );597 // CONFORMANCE FINDING (alpha.53): a where on a sealed number column with598 // the plaintext number throws in the query adapter (a number value599 // against a TEXT-stored column is a type mismatch), rather than matching600 // zero rows like the string case above. Pinned: loud is acceptable;601 // plaintext must never match.602 let plainNumberRefused = false;603 try {604 await db.all(app.sealed.where({ amount } as never));605 } catch {606 plainNumberRefused = true;607 }608 assert(609 plainNumberRefused,610 "sealed: pinned engine behavior changed — a plaintext-number where no longer throws",611 );612613 await db.update(app.sealed, row.id, { body: "rewritten sealed body" }).wait({614 tier: "global",615 });616 const updated = (await db.all(app.sealed.where({ id: row.id })))[0];617 assert(618 updated?.body === "rewritten sealed body",619 `sealed: update read back ${describeValue(updated?.body)}`,620 );621622 // The wrong account key must refuse loudly, never return garbage.623 setEncryptedColumnKey(new Uint8Array(32).map((_, index) => 255 - index));624 let refused = false;625 try {626 await db.all(app.sealed.where({ id: row.id }));627 } catch (error) {628 refused = error instanceof EncryptedColumnError && error.code === "corrupt";629 }630 assert(refused, "sealed: a foreign account key must fail authentication");631 } finally {632 clearEncryptedColumnKey();633 await testApp.shutdown();634 }635});636637// privateTable end-to-end: sealed-by-default columns round-trip through the638// real engine, the plain-marked column still filters, and the sealed columns639// registered for the policy guard.640const privApp = s.defineApp({641 privateNotes: s.privateTable("privateNotes", {642 body: s.string(),643 score: s.int(),644 title: s.plain(s.string()),645 }),646});647const privPermissions = s.definePermissions(privApp, ({ policy }) => {648 policy.privateNotes.allowInsert.always();649 policy.privateNotes.allowRead.always();650 policy.privateNotes.allowUpdate.always();651 policy.privateNotes.allowDelete.always();652});653654Deno.test("privateTable seals by default through the engine", async () => {655 const testApp = await createPolicyTestApp(privApp, privPermissions, expectLike);656 setEncryptedColumnKey(new Uint8Array(32).map((_, index) => index + 1));657 try {658 const db = testApp.as(session("author"));659 const row = await db.insert(privApp.privateNotes, {660 body: "sealed by default",661 score: 2 ** 34,662 title: "plaintext title",663 } as never).wait({ tier: "global" });664 const readBack = (await db.all(privApp.privateNotes.where({ id: row.id })))[0] as {665 body: string;666 score: number;667 title: string;668 };669 assert(readBack.body === "sealed by default", "sealed text did not round-trip");670 assert(readBack.score === 2 ** 34, "sealed number did not round-trip");671 const byTitle = await db.all(privApp.privateNotes.where({ title: "plaintext title" }));672 assert(byTitle.length === 1, "the plain-marked column must stay filterable");673 const byBody = await db.all(674 privApp.privateNotes.where({ body: "sealed by default" } as never),675 );676 assert(byBody.length === 0, "a plaintext filter on a sealed column must match nothing");677 } finally {678 clearEncryptedColumnKey();679 await testApp.shutdown();680 }681});