mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
feat(spaces): validate space metadata on Better Auth organization hooks
Moves the canonical SpaceType + SPACE_MODULE_ALLOWLIST to @mana/shared-types (framework-free) so the Bun services can consume them without pulling in Svelte. shared-branding keeps only the UI-facing labels and descriptions and re-exports the canonical types for frontend convenience. Wires two Better Auth organization hooks in mana-auth: - beforeCreateOrganization asserts metadata.type is a valid SpaceType, rejecting the create with a BAD_REQUEST otherwise. - beforeDeleteOrganization rejects deletion of the personal space. Covered by bun tests (11 assertions) for the helper module. No migration and no schema change — type lives in the existing organization.metadata jsonb column. Plan: docs/plans/spaces-foundation.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9d69e4419d
commit
166d6c6ffb
11 changed files with 683 additions and 331 deletions
|
|
@ -29,6 +29,9 @@ export * from './landing-config';
|
|||
// AI structured-output Zod schemas (shared between mana-api + web frontend)
|
||||
export * from './ai-schemas';
|
||||
|
||||
// Space types (multi-tenancy primitive — see docs/plans/spaces-foundation.md)
|
||||
export * from './spaces';
|
||||
|
||||
// API types
|
||||
export interface User {
|
||||
id: string;
|
||||
|
|
|
|||
225
packages/shared-types/src/spaces.ts
Normal file
225
packages/shared-types/src/spaces.ts
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
/**
|
||||
* Space Types & Module Allowlist
|
||||
*
|
||||
* Framework-free definition of the Space primitive — the unit of data
|
||||
* ownership in Mana. Consumed by both the SvelteKit frontend and the
|
||||
* Bun/Hono services (mana-auth, mana-api, mana-sync), which is why this
|
||||
* lives in shared-types instead of shared-branding (the latter carries
|
||||
* Svelte components and is too heavy for a server).
|
||||
*
|
||||
* UI-facing labels/descriptions for these types live in shared-branding.
|
||||
*
|
||||
* See docs/plans/spaces-foundation.md for the full RFC.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The six canonical Space types. Every Better Auth organization must have
|
||||
* exactly one of these as `metadata.type`.
|
||||
*
|
||||
* - `personal` — single-member, auto-created on signup. Holds private data
|
||||
* like mood, sleep, dreams that don't belong in a shared context.
|
||||
* - `brand` — external communication identity (e.g. Edisconet, a creator
|
||||
* persona). Hosts social-relay, mail, landing, public content.
|
||||
* - `club` — association/Verein. Member management, dues, events,
|
||||
* governance. Target for the ClubDesk-replacement roadmap.
|
||||
* - `family` — household/family/WG. Shared calendar, shopping, recipes.
|
||||
* - `team` — work team / project. Tasks, chat, docs.
|
||||
* - `practice` — freelancer/solo-business. Invoicing, clients, time tracking.
|
||||
*/
|
||||
export type SpaceType = 'personal' | 'brand' | 'club' | 'family' | 'team' | 'practice';
|
||||
|
||||
export const SPACE_TYPES: readonly SpaceType[] = [
|
||||
'personal',
|
||||
'brand',
|
||||
'club',
|
||||
'family',
|
||||
'team',
|
||||
'practice',
|
||||
] as const;
|
||||
|
||||
export function isSpaceType(value: unknown): value is SpaceType {
|
||||
return typeof value === 'string' && (SPACE_TYPES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Module IDs referenced by the allowlist. Strings (not a strict enum) because
|
||||
* the allowlist intentionally includes modules that don't exist yet — e.g.
|
||||
* `club-finance`, `social-relay` — so features can be gated before the code
|
||||
* lands.
|
||||
*/
|
||||
export type SpaceModuleId = string;
|
||||
|
||||
/**
|
||||
* Which modules are available inside each Space type.
|
||||
*
|
||||
* The personal space gets everything (sentinel `'*'`). Other types get a
|
||||
* curated subset — modules dealing with intimate personal data (mood,
|
||||
* dreams, period, body measurements, …) are intentionally excluded from
|
||||
* shared spaces.
|
||||
*
|
||||
* Rule of thumb: if a module's data would feel wrong shared with co-workers
|
||||
* or club members, keep it out.
|
||||
*/
|
||||
export const SPACE_MODULE_ALLOWLIST: Record<SpaceType, readonly SpaceModuleId[] | '*'> = {
|
||||
personal: '*',
|
||||
|
||||
brand: [
|
||||
'mana',
|
||||
'social-relay', // future — not yet built
|
||||
'mail',
|
||||
'contacts',
|
||||
'calendar',
|
||||
'storage',
|
||||
'uload',
|
||||
'landing', // future
|
||||
'presi',
|
||||
'cards',
|
||||
'picture',
|
||||
'quotes',
|
||||
'news',
|
||||
'news-research',
|
||||
'research-lab',
|
||||
'ai-agents',
|
||||
'companion',
|
||||
'times',
|
||||
'notes',
|
||||
'photos',
|
||||
'invoices',
|
||||
'activity',
|
||||
'goals',
|
||||
],
|
||||
|
||||
club: [
|
||||
'mana',
|
||||
'contacts',
|
||||
'calendar',
|
||||
'events',
|
||||
'mail',
|
||||
'storage',
|
||||
'uload',
|
||||
'news',
|
||||
'research-lab',
|
||||
'club-members', // future — ClubDesk Paket A
|
||||
'club-finance', // future — ClubDesk Paket B
|
||||
'invoices',
|
||||
'finance',
|
||||
'landing', // future — Paket C (Vereinswebsite)
|
||||
'presi',
|
||||
'cards',
|
||||
'quotes',
|
||||
'companion',
|
||||
'times',
|
||||
'notes',
|
||||
'photos',
|
||||
'activity',
|
||||
'goals',
|
||||
],
|
||||
|
||||
family: [
|
||||
'mana',
|
||||
'contacts',
|
||||
'calendar',
|
||||
'events',
|
||||
'mail',
|
||||
'storage',
|
||||
'uload',
|
||||
'recipes',
|
||||
'food',
|
||||
'places',
|
||||
'presi',
|
||||
'cards',
|
||||
'photos',
|
||||
'notes',
|
||||
'companion',
|
||||
'goals',
|
||||
'activity',
|
||||
'wetter',
|
||||
'wisekeep',
|
||||
'firsts',
|
||||
],
|
||||
|
||||
team: [
|
||||
'mana',
|
||||
'contacts',
|
||||
'calendar',
|
||||
'events',
|
||||
'storage',
|
||||
'mail',
|
||||
'uload',
|
||||
'news',
|
||||
'news-research',
|
||||
'research-lab',
|
||||
'presi',
|
||||
'cards',
|
||||
'picture',
|
||||
'notes',
|
||||
'quotes',
|
||||
'invoices',
|
||||
'companion',
|
||||
'ai-agents',
|
||||
'times',
|
||||
'activity',
|
||||
'goals',
|
||||
],
|
||||
|
||||
practice: [
|
||||
'mana',
|
||||
'contacts',
|
||||
'calendar',
|
||||
'storage',
|
||||
'mail',
|
||||
'uload',
|
||||
'invoices',
|
||||
'finance',
|
||||
'times',
|
||||
'notes',
|
||||
'presi',
|
||||
'cards',
|
||||
'quotes',
|
||||
'companion',
|
||||
'research-lab',
|
||||
'activity',
|
||||
'goals',
|
||||
],
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Check whether a module is available inside a given Space type.
|
||||
*
|
||||
* Used by:
|
||||
* - Scope wrapper (apps/mana/.../data/scope/scoped-db.ts) to block queries
|
||||
* against disallowed modules — structural guard against UI bypass.
|
||||
* - UI module launcher to hide disabled modules in the active space.
|
||||
* - Route guards that check before mounting a module page.
|
||||
*/
|
||||
export function isModuleAllowedInSpace(moduleId: SpaceModuleId, spaceType: SpaceType): boolean {
|
||||
const allow = SPACE_MODULE_ALLOWLIST[spaceType];
|
||||
if (allow === '*') return true;
|
||||
return allow.includes(moduleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape of the `metadata` JSONB column on Better Auth's `organization` table
|
||||
* for our Space extension. `type` is required; other fields accumulate as
|
||||
* features land (voiceDoc, legalEntity, uid, aiPersonaId, …).
|
||||
*/
|
||||
export interface SpaceMetadata {
|
||||
type: SpaceType;
|
||||
voiceDoc?: string;
|
||||
legalEntity?: string;
|
||||
uid?: string;
|
||||
aiPersonaId?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow a raw metadata blob (from Better Auth / DB) to a validated
|
||||
* SpaceMetadata. Returns null if no valid type field is present — callers
|
||||
* decide whether to reject or default.
|
||||
*/
|
||||
export function parseSpaceMetadata(raw: unknown): SpaceMetadata | null {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
const obj = raw as Record<string, unknown>;
|
||||
if (!isSpaceType(obj.type)) return null;
|
||||
return obj as SpaceMetadata;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue