import type { CollectionAccessSubject } from "../manifest/collections"; import type { ColineActorRef } from "./types"; /** Workspace membership and install consent are checked separately by the host. */ export interface ColineRecordAccess { workspace: "none" | "read" | "write"; readers: string[]; writers: string[]; } export const DEFAULT_RECORD_ACCESS: ColineRecordAccess = { workspace: "write", readers: [], writers: [], }; /** The stored form of a record creator: `user:`, `visitor:` or `app`. */ export function actorKey(actor: ColineActorRef): string { if (actor.kind === "user") return `user:${actor.userId}`; if (actor.kind === "visitor") return `visitor:${actor.visitorId}`; return "app"; } /** Parse a stored creator. Visitor details are not stored on records. */ export function parseActorKey(key: string): ColineActorRef { if (key.startsWith("user:")) return { kind: "user", userId: key.slice(5) }; if (key.startsWith("visitor:")) { return { kind: "visitor", visitorId: key.slice(8), signedIn: false, name: null, email: null }; } return { kind: "app" }; } /** Whether two actors are the same principal. */ export function sameActor(a: ColineActorRef, b: ColineActorRef): boolean { return actorKey(a) === actorKey(b); } /** * Record access for members and app logic. * * The owner is the exact creating principal: a user, the app, or a site * visitor. Visitors never pass this check, not even for records they * created. Their access is decided by the collection's declared rules * (`canVisitorAccess`), so a site can never reach a record through the * per-record ACL. */ export function canAccessRecord( access: ColineRecordAccess, creator: ColineActorRef, caller: ColineActorRef, action: "read" | "write" | "manage", ): boolean { if (caller.kind === "visitor") return false; if (sameActor(creator, caller)) return true; if (action === "manage") return false; if (access.workspace === "write") return true; const userId = caller.kind === "user" ? caller.userId : null; if (!userId) return false; if (access.writers.includes(userId)) return true; return action === "read" && (access.workspace === "read" || access.readers.includes(userId)); } /** * Whether a collection rule admits a visitor. `creator` is the record's * creator for update and delete, and null for create and for queries * (where `owner` narrows results to the visitor's own records instead). */ export function canVisitorAccess( subject: CollectionAccessSubject, visitor: { visitorId: string; signedIn: boolean }, creator: ColineActorRef | null, ): boolean { switch (subject) { case "public": case "visitor": return true; case "signedIn": return visitor.signedIn; case "owner": return creator?.kind === "visitor" && creator.visitorId === visitor.visitorId; case "team": return false; } } export interface ColineRecordWriteOptions { expectedVersion?: number; } export interface ColineRecordInsertOptions { id?: string; access?: ColineRecordAccess; }