clone

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

main
lofi / package / access / schema.ts
schema.ts 3.8K
1import { schema as s } from "jazz-tools";2import type { BooleanColumn, DefinedTable, IntColumn, RefColumn, StringColumn } from "jazz-tools";3import { AccessError } from "./errors.ts";45function requireTableName(name: string, helper: string): void {6 if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(name)) {7 throw new AccessError(8 "configuration",9 `${helper} requires a declared Jazz table name, received ${JSON.stringify(name)}`,10 );11 }12}1314/** Creates the conventional relationship table used by `sharedAccess`. */15export function sharedGrantTable<const Resource extends string>(16 resource: Resource,17): DefinedTable<{18 resourceId: RefColumn<Resource>;19 user_id: StringColumn;20 can_edit: BooleanColumn;21}> {22 requireTableName(resource, "sharedGrantTable");23 return s.table({24 resourceId: s.ref(resource),25 user_id: s.string(),26 can_edit: s.boolean(),27 });28}2930/** Fixed Wave 2 group roles. Custom role systems remain a raw Jazz escape hatch. */31export const groupRoles = ["reader", "contributor", "writer", "admin"] as const;32/** Fixed group roles supported by the built-in group policy template. */33export type GroupRole = (typeof groupRoles)[number];3435/** Returns the persisted capability flags for a fixed group role. */36export function groupRoleCapabilities(role: GroupRole): {37 readonly role: GroupRole;38 readonly can_create: boolean;39 readonly can_edit_any: boolean;40 readonly can_manage: boolean;41} {42 return {43 role,44 can_create: role !== "reader",45 can_edit_any: role === "writer" || role === "admin",46 can_manage: role === "admin",47 } as const;48}4950/** Creates the conventional relationship table used by `groupAccess`. */51export function groupMembershipTable<const Group extends string>(52 group: Group,53): DefinedTable<{54 groupId: RefColumn<Group>;55 user_id: StringColumn;56 role: StringColumn;57 can_create: BooleanColumn;58 can_edit_any: BooleanColumn;59 can_manage: BooleanColumn;60}> {61 requireTableName(group, "groupMembershipTable");62 return s.table({63 groupId: s.ref(group),64 user_id: s.string(),65 role: s.string(),66 can_create: s.boolean(),67 can_edit_any: s.boolean(),68 can_manage: s.boolean(),69 });70}7172/**73 * Creates the shared-field key directory table: one row per account holding74 * its self-published x25519 public key. Declared once per app schema and75 * given to `sharedFieldAccess`, which compiles the policy that makes it76 * world-readable in the store with self-only writes. Public keys are public;77 * integrity comes from fingerprint pinning, not from hiding the rows.78 */79export function sharedFieldDirectoryTable(): DefinedTable<{80 user_id: StringColumn;81 algo: StringColumn;82 public_key: StringColumn;83 fingerprint: StringColumn;84}> {85 return s.table({86 user_id: s.string(),87 algo: s.string(),88 public_key: s.string(),89 fingerprint: s.string(),90 });91}9293/** Creates the wrapped-field-key table for one group table: one row per94 * (recipient, generation) holding the group's field key sealed to that95 * member's public key. Rows are ordinary synced data the server relays but96 * cannot open; the sender's static key inside the wrap is what makes a97 * server-minted row a detected forgery rather than a readable key. */98export function sharedFieldKeyTable<const Group extends string>(99 group: Group,100): DefinedTable<{101 groupId: RefColumn<Group>;102 recipient_user_id: StringColumn;103 sender_user_id: StringColumn;104 generation: IntColumn;105 wrapped_key: StringColumn;106 recipient_fingerprint: StringColumn;107 sender_fingerprint: StringColumn;108}> {109 requireTableName(group, "sharedFieldKeyTable");110 return s.table({111 groupId: s.ref(group),112 recipient_user_id: s.string(),113 sender_user_id: s.string(),114 generation: s.int(),115 wrapped_key: s.string(),116 recipient_fingerprint: s.string(),117 sender_fingerprint: s.string(),118 });119}