All checks were successful
CD / Build and push images (push) Successful in 3m14s
CI / Lint, typecheck, test (push) Successful in 2m31s
CI / Auth e2e pack (push) Successful in 3m21s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m12s
CD / Promote to Int (push) Successful in 11s
Pond Admins can answer "what can X actually see/do here?" — load-bearing for trust in the grant system (permissions.md §UI obligations). - shared: `explainPageCapability` — the resolver's decision chain (deciding level + the single deciding grant), sharing one code path with the boolean `resolvePageCapability` (now a thin wrapper), so the trace can never diverge from real access. Unit-tested against the permissions.md worked examples. - api: `GET /ponds/:id/effective-permissions?subjectType=&subjectId=&pageId=` (Pond-Admin-gated, one pond only) resolves as the chosen subject (a user with their real Site-Admin flag, all signed-in users, or the public), optionally against a page, and returns the read + write outcome with the deciding rule enriched with subject/scope names. - web: `EffectivePermissionsInspector` in Pond Settings — pick a subject and optionally a page → see the resolved read/edit verdict, the level that decided it, and the deciding rule spelled out as a de/en sentence (reusing the #55 sentence renderer). Hidden from non-admins. - tests: explain-mode unit tests (worked examples + trace-matches-boolean); `inspector.e2e.db.test.ts` (deciding rule on a labelled page, pond-level base capability, public default-closed, Pond-Admin gating); a browser assertion in the access-rules pack. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
95 lines
3.5 KiB
TypeScript
95 lines
3.5 KiB
TypeScript
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<typeof createGrantInputSchema>;
|
|
|
|
/** 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<typeof inspectSubjectSchema>;
|
|
|
|
/** 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,
|
|
};
|
|
}
|