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 / access / shared_directory_sync_test.ts
shared_directory_sync_test.ts 8.8K
1// The sharedFieldAccess template against the real pinned server: the2// compiled directory policy is world-readable with self-only writes, and3// encrypted columns are refused in the directory's template positions.45import { createDb, schema as jazzSchema, schema as s } from "jazz-tools";6import { deploy, startLocalJazzServer } from "jazz-tools/testing";7import { defineAccessPolicies, sharedFieldAccess, sharedFieldDirectoryTable } from "./mod.ts";8import { s as lofiSchema } from "../schema/mod.ts";9import { clearEncryptedColumnRegistry } from "../schema/encrypted.ts";10import { assert } from "../runtime/test-assert.ts";1112const app = s.defineApp({13 keyDirectory: sharedFieldDirectoryTable(),14});15const permissions = defineAccessPolicies(app, [16 sharedFieldAccess({ directory: app.keyDirectory }),17]);1819function secret(fill: number): string {20 let binary = "";21 for (const byte of new Uint8Array(32).fill(fill)) binary += String.fromCharCode(byte);22 return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");23}2425async function within<T>(operation: Promise<T>, label: string, milliseconds = 10_000): Promise<T> {26 let timer: ReturnType<typeof setTimeout> | undefined;27 try {28 return await Promise.race([29 operation,30 new Promise<never>((_resolve, reject) => {31 timer = setTimeout(() => reject(new Error(`${label} timed out`)), milliseconds);32 }),33 ]);34 } finally {35 if (timer !== undefined) clearTimeout(timer);36 }37}3839Deno.test("the compiled directory policy is world-readable with self-only writes", async () => {40 const server = await startLocalJazzServer({ allowLocalFirstAuth: true });41 await deploy({42 appId: server.appId,43 serverUrl: server.url,44 adminSecret: server.adminSecret,45 schema: app,46 permissions,47 });48 const alice = await createDb({49 appId: server.appId,50 serverUrl: server.url,51 secret: secret(1),52 userBranch: "main",53 driver: { type: "memory" },54 });55 const bob = await createDb({56 appId: server.appId,57 serverUrl: server.url,58 secret: secret(2),59 userBranch: "main",60 driver: { type: "memory" },61 });62 try {63 const aliceId = alice.getAuthState().session?.user_id as string;64 const bobId = bob.getAuthState().session?.user_id as string;65 assert(Boolean(aliceId && bobId && aliceId !== bobId), "test principals were not isolated");6667 await within(68 alice.insert(app.keyDirectory, {69 user_id: aliceId,70 algo: "x25519-v1",71 public_key: "alice-pub",72 fingerprint: "alice-fp",73 }).wait({ tier: "global" }),74 "alice publishes",75 );76 const seenByBob = await within(77 bob.all(app.keyDirectory.where({ user_id: aliceId })),78 "bob reads alice's row",79 );80 assert(81 seenByBob.length === 1 && seenByBob[0].public_key === "alice-pub",82 `the directory must be world-readable (saw ${seenByBob.length})`,83 );8485 let denied = false;86 try {87 await within(88 bob.insert(app.keyDirectory, {89 user_id: aliceId,90 algo: "x25519-v1",91 public_key: "evil-pub",92 fingerprint: "evil-fp",93 }).wait({ tier: "global" }),94 "bob impersonates alice",95 );96 } catch {97 denied = true;98 }99 assert(denied, "a foreign user_id insert must be denied");100 } finally {101 await Promise.allSettled([102 within(alice.logout(), "Alice cleanup", 3_000),103 within(bob.logout(), "Bob cleanup", 3_000),104 ]);105 await within(server.stop(), "server cleanup", 3_000).catch(() => undefined);106 }107});108109Deno.test("a directory with a sealed column fails configuration", () => {110 clearEncryptedColumnRegistry();111 try {112 const sealedApp = lofiSchema.defineApp({113 keyDirectory: lofiSchema.table({114 user_id: lofiSchema.encryptedText("keyDirectory.user_id"),115 algo: lofiSchema.string(),116 public_key: lofiSchema.string(),117 fingerprint: lofiSchema.string(),118 }),119 });120 let message = "";121 try {122 defineAccessPolicies(sealedApp, [123 sharedFieldAccess({ directory: sealedApp.keyDirectory }),124 ]);125 } catch (error) {126 message = error instanceof Error ? error.message : String(error);127 }128 assert(129 message.includes("encrypted") && message.includes("user_id"),130 `a sealed directory column compiled: ${message || "no error"}`,131 );132 } finally {133 clearEncryptedColumnRegistry();134 }135});136137// ENGINE CANARY (alpha.53): rows whose read policy is an exists-based138// function condition never propagate to other accounts over real sync, while139// always() and direct object conditions propagate immediately. The140// collaboration templates' cross-account reads are bounded by this; the141// shared-field design routes around it (wrapped keys use a direct recipient142// condition, the directory reads always()). This test pins the gap: when an143// engine bump fixes propagation, it fails loudly — revisit the read-side144// guidance and the templates then.145Deno.test("engine canary: exists-gated rows do not propagate cross-account", async () => {146 const canaryApp = jazzSchema.defineApp({147 rooms: jazzSchema.table({ name: jazzSchema.string() }),148 roomMembers: jazzSchema.table({149 groupId: jazzSchema.ref("rooms"),150 user_id: jazzSchema.string(),151 }),152 gated: jazzSchema.table({ roomId: jazzSchema.ref("rooms"), title: jazzSchema.string() }),153 open: jazzSchema.table({ roomId: jazzSchema.ref("rooms"), title: jazzSchema.string() }),154 });155 const canaryPermissions = jazzSchema.definePermissions(canaryApp, (jazzContext) => {156 const { policy, session } = jazzContext as unknown as {157 policy: Record<158 string,159 Record<string, { where(input: unknown): unknown; always(): unknown }> & {160 exists: { where(input: unknown): unknown };161 }162 >;163 session: { user_id: unknown };164 };165 for (const table of ["rooms", "roomMembers", "open"]) {166 policy[table].allowRead.always();167 policy[table].allowInsert.always();168 policy[table].allowUpdate.always();169 policy[table].allowDelete.always();170 }171 policy.gated.allowRead.where((row: Record<string, unknown>) =>172 policy.roomMembers.exists.where({ groupId: row.roomId, user_id: session.user_id })173 );174 policy.gated.allowInsert.always();175 policy.gated.allowUpdate.always();176 policy.gated.allowDelete.always();177 });178179 const server = await startLocalJazzServer({ allowLocalFirstAuth: true });180 await deploy({181 appId: server.appId,182 serverUrl: server.url,183 adminSecret: server.adminSecret,184 schema: canaryApp,185 permissions: canaryPermissions,186 });187 const alice = await createDb({188 appId: server.appId,189 serverUrl: server.url,190 secret: secret(11),191 userBranch: "main",192 driver: { type: "memory" },193 });194 const bob = await createDb({195 appId: server.appId,196 serverUrl: server.url,197 secret: secret(12),198 userBranch: "main",199 driver: { type: "memory" },200 });201 try {202 const aliceId = alice.getAuthState().session?.user_id as string;203 const bobId = bob.getAuthState().session?.user_id as string;204 const room = await within(205 alice.insert(canaryApp.rooms, { name: "canary" }).wait({ tier: "global" }),206 "room insert",207 );208 for (const memberId of [aliceId, bobId]) {209 await within(210 alice.insert(canaryApp.roomMembers, { groupId: room.id, user_id: memberId })211 .wait({ tier: "global" }),212 "membership insert",213 );214 }215 const gated = await within(216 alice.insert(canaryApp.gated, { roomId: room.id, title: "gated" })217 .wait({ tier: "global" }),218 "gated insert",219 );220 const open = await within(221 alice.insert(canaryApp.open, { roomId: room.id, title: "open" }).wait({ tier: "global" }),222 "open insert",223 );224225 // The always()-gated control row arrives promptly...226 const deadline = Date.now() + 5_000;227 let openSeen = 0;228 while (openSeen === 0 && Date.now() < deadline) {229 openSeen = (await bob.all(canaryApp.open.where({ id: open.id }))).length;230 if (openSeen === 0) await new Promise((resolve) => setTimeout(resolve, 100));231 }232 assert(openSeen === 1, "the always()-gated control row must propagate to bob");233234 // ...while the exists-gated row stays absent well past the control's235 // arrival. This is the pinned gap, not the desired behavior.236 await new Promise((resolve) => setTimeout(resolve, 1_500));237 const gatedSeen = await bob.all(canaryApp.gated.where({ id: gated.id }));238 assert(239 gatedSeen.length === 0,240 "pinned engine behavior changed: exists-gated rows now propagate cross-account — " +241 "revisit the read-side guidance, the collaboration templates, and this canary",242 );243 } finally {244 await Promise.allSettled([245 within(alice.logout(), "Alice cleanup", 3_000),246 within(bob.logout(), "Bob cleanup", 3_000),247 ]);248 await within(server.stop(), "server cleanup", 3_000).catch(() => undefined);249 }250});