Commit graph

2 commits

Author SHA1 Message Date
Till JS
5501f472ae feat(shared-privacy): M8.2 — unlisted-client + SharedLinkControls
Second milestone of the unlisted-share rollout. Backend endpoints
from M8.1 are now callable from the client, and a reusable
SharedLinkControls component is available for the detail views that
wire up in M8.3/M8.4.

Scope: shared primitives only. No module store integrates them yet —
that's the next step per module.

Changes:
- @mana/shared-privacy/unlisted-client.ts:
    publishUnlistedSnapshot(opts) → { token, url }
      Idempotent per (collection, recordId) — server reuses token on
      re-publish, so store code can call on every edit without caring
      whether it's first publish or refresh.
    revokeUnlistedSnapshot(opts)
      Idempotent — resolves silently even on { revoked: 0 }.
    buildShareUrl(origin, token)
      Convenience for UIs that already know the token.
    UnlistedApiError
      Thrown on non-2xx. Carries { status, code } so callers can
      distinguish 400 COLLECTION_NOT_ALLOWED vs 410 REVOKED vs
      500 UNKNOWN.
- @mana/shared-privacy/SharedLinkControls.svelte:
    Dumb presentational component. Props: token, url, expiresAt,
    onRegenerate, onRevoke, onExpiryChange (optional), disabled.
    Renders URL + copy, regenerate with confirm dialog, revoke,
    optional datetime-local expiry picker, debug token fingerprint.
    Clipboard-API fallback to prompt() for unsecure origins.
    QR-code button deferred to M8.5 polish.
- Exports added to index.ts: functions, error class, both types,
  SharedLinkControls component.
- 10 new unit tests (25 total): publish URL shape, headers, body,
  expiresAt serialisation, 4xx/5xx handling, trailing-slash
  trimming on apiUrl, revoke idempotence, buildShareUrl join.

Verified:
- pnpm --filter @mana/shared-privacy test: 25/25 green
- pnpm --filter @mana/shared-privacy check: 0 errors
- pnpm --filter @mana/web check: 7531 files, 0 errors

Next: M8.3 — wire Calendar through the new client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:18:56 +02:00
Till JS
49935c9628 feat(shared-privacy): M1 — visibility foundation package
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>
2026-04-24 01:59:11 +02:00