import { z } from 'zod'; import type { ResolutionLevel } from './resolve'; import type { Grant, GrantSubjectType } from './types'; /** * Wire schemas and views for the grant-management API (`/ponds/:id/grants`, * issue #52). The structural rules beyond shape (pond_admin only at pond * scope, subject/scope id presence, personal-pond single admin) live in * {@link ./validate} and are applied by the service after parsing. */ export const GRANT_SUBJECT_TYPES = ['user', 'authenticated', 'public'] as const; export const GRANT_ROLES = ['pond_admin', 'editor', 'reader'] as const; export const GRANT_SCOPE_TYPES = ['pond', 'label', 'page'] as const; export const GRANT_EFFECTS = ['allow', 'deny'] as const; export const createGrantInputSchema = z.object({ subjectType: z.enum(GRANT_SUBJECT_TYPES), /** Required for `user` subjects, absent otherwise (validate.ts checks). */ subjectId: z.string().min(1).nullish(), role: z.enum(GRANT_ROLES), scopeType: z.enum(GRANT_SCOPE_TYPES), /** The label/page id for those scopes, absent at pond scope. */ scopeId: z.string().min(1).nullish(), effect: z.enum(GRANT_EFFECTS), }); export type CreateGrantInput = z.infer; /** A stored grant as the API serves it. */ export interface GrantView extends Grant { id: string; pondId: string; createdBy: string; createdAt: string; } /** * A grant enriched with the display names the access-rules UI renders as * sentences (issue #55): the subject's display name and the scoped label/page * name. Names are resolved server-side so the client needs no id lookups. */ export interface AccessRuleView extends GrantView { /** The user subject's display name; `null` for `authenticated`/`public`. */ subjectName: string | null; /** Label name or page title for label/page scope; `null` at pond scope. */ scopeName: string | null; } /** Query for the effective-permissions inspector (issue #57). */ export const inspectSubjectSchema = z.object({ subjectType: z.enum(GRANT_SUBJECT_TYPES), /** Required for `user`, absent for `authenticated`/`public`. */ subjectId: z.string().min(1).optional(), /** Optional page to resolve against; omit for the pond-level base capability. */ pageId: z.string().min(1).optional(), }); export type InspectSubjectQuery = z.infer; /** A grant enriched with the names needed to render it as a sentence — the * shape the inspector highlights (no id: the decider is shown, not managed). */ export interface DecidingRule extends Grant { subjectName: string | null; scopeName: string | null; } /** One resolved capability (read or write) with the rule that decided it. */ export interface DecisionView { outcome: 'allow' | 'deny'; /** Which specificity level settled it (permissions.md §Resolution). */ decidedBy: ResolutionLevel; /** The deciding grant, enriched with names; `null` for site-admin/default. */ decidingRule: DecidingRule | null; } /** The inspector's answer to "what can this subject do here?" (issue #57). */ export interface EffectivePermissionView { subject: { type: GrantSubjectType; id: string | null; name: string | null }; page: { id: string; title: string } | null; read: DecisionView; write: DecisionView; } /** The parsed input as the resolver's grant shape (nullish → null). */ export function grantOfInput(input: CreateGrantInput): Grant { return { subjectType: input.subjectType, subjectId: input.subjectId ?? null, role: input.role, scopeType: input.scopeType, scopeId: input.scopeId ?? null, effect: input.effect, }; }