mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 15:59:40 +02:00
Scaffold the unified visibility/privacy layer introduced by docs/plans/
visibility-system.md. No module adopts it yet — this is the foundation
PR (M1). Module rollout lands in follow-ups starting with Library (M2).
What ships:
- @mana/shared-privacy package
- VisibilityLevel enum ('private' | 'space' | 'unlisted' | 'public')
- VisibilityLevelSchema + UnlistedTokenSchema (zod)
- defaultVisibilityFor(spaceType): personal → private, else → space
- predicates: canEmbedOnWebsite, isReachableByLink,
isVisibleToSpaceMember, canAiAccessCrossUser (always false in P1)
- generateUnlistedToken() — 32-char base64url, CSPRNG, ~192 bits
- VISIBILITY_METADATA: German labels + descriptions + phosphor icon
names so non-UI surfaces (audit logs, CLI) label levels consistently
- <VisibilityPicker> svelte component: compact lock/globe trigger with
4-option menu, full descriptions, optional compact + disabledLevels
- VisibilityChangedPayload type for the domain-event catalog (consumer
registers it when the first module adopts the system)
- .claude/guidelines/visibility.md — step-by-step for module authors
(schema migrations + store wiring + picker placement + embed resolver +
legacy isPublic migration), with a pre-PR checklist
- Plan-doc "Offene Fragen" section rewritten as "Designentscheidungen"
with the seven resolutions the user approved
- CLAUDE.md: shared-privacy listed in the packages table; visibility.md
listed in the guidelines table
- 15 unit tests covering predicates (one-and-only-one 'public' for
embed; phase-1 AI always-deny), defaults (personal vs multi-member,
null fallback), token uniqueness + schema round-trip
Key constraints honored:
- `visibility` stays plaintext (NOT added to the encryption registry)
so RLS predicates and publish resolvers can read it without the user's
master key
- Publish flow remains "decrypt client-side, inline plaintext into
snapshot" — the pattern picture.board already uses in embeds.ts
- Deny-by-default everywhere (personal default = private; unknown space
type defaults to private; cross-user AI always false)
Not in this PR (per plan):
- No schema migrations in any module (M2–M6)
- No RLS predicate updates (arrives with M2)
- No /settings/privacy overview (M7)
- No unlisted share routes (M8)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
43 lines
1.5 KiB
TypeScript
43 lines
1.5 KiB
TypeScript
import type { VisibilityLevel } from './types';
|
|
|
|
/**
|
|
* Can this record be embedded on a published website? This is the
|
|
* strictest exposure — a public website snapshot is readable by any
|
|
* anonymous visitor, so gate hard on `public` only. Unlisted is
|
|
* link-sharing, not website-embedding.
|
|
*
|
|
* Every embed resolver in `apps/mana/apps/web/src/lib/modules/website/
|
|
* embeds.ts` must call this before inlining records into the snapshot.
|
|
*/
|
|
export function canEmbedOnWebsite(level: VisibilityLevel): boolean {
|
|
return level === 'public';
|
|
}
|
|
|
|
/**
|
|
* Can this record be fetched via a direct unlisted-token link? Includes
|
|
* `public` because a public record is reachable by link too (it just
|
|
* also appears in embeds).
|
|
*/
|
|
export function isReachableByLink(level: VisibilityLevel): boolean {
|
|
return level === 'public' || level === 'unlisted';
|
|
}
|
|
|
|
/**
|
|
* Is this record visible to other members of the owner's space, under
|
|
* the normal `spaceModulePermissions` matrix? All non-private levels
|
|
* are. Private records are owner-only, even inside multi-member spaces.
|
|
*/
|
|
export function isVisibleToSpaceMember(level: VisibilityLevel): boolean {
|
|
return level !== 'private';
|
|
}
|
|
|
|
/**
|
|
* Placeholder for a future cross-user AI-agent feature (see
|
|
* docs/plans/visibility-system.md §3). Always returns false in Phase 1
|
|
* so no current AI code path accidentally leaks data. When we're ready
|
|
* to let agents read public cross-user records, flip this per-module
|
|
* with an explicit opt-in.
|
|
*/
|
|
export function canAiAccessCrossUser(_level: VisibilityLevel): boolean {
|
|
return false;
|
|
}
|