diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..02559dc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,117 @@ +# IIOS — Claude Code Rules + +## What IIOS is + +**IIOS (Insignia Interaction OS)** is a **generic, multi-tenant "interaction OS"** — one +NestJS service (`@insignia/iios-service`) + a family of SDKs. Every interaction (a chat +message, a support ticket, a routed post, an AI suggestion, a meeting) is the **same kernel +object** inside a **tenant scope**, behind the **same fail-closed gates** (policy/consent), +emitting the **same audit trail**. Products (messaging, support, community, AI, meetings) are +thin **specializations** on top of the kernel — never the other way around. + +`chat-web` (separate repo) is the reference consumer app. + +## THE #1 LOCKED RULE — generic-safety (read this before touching the kernel) + +**The kernel must never hardcode chat/domain vocabulary.** No `'dm'` / `'group'` / +`'reaction'` / `'emoji'` / `'mention'` literals in kernel or messaging *logic*. If a reviewer +asked *"is this a chat backend now?"* the answer must stay **no**. + +Domain meaning lives in **three** places, never the kernel: +1. **OPA policy** (the policy plane — `DevOpaPort` now, real OPA later). The DM-cap, + group-admin, governed-join, media-limit, and notification-trigger rules live here. +2. **Opaque thread/interaction attributes** the kernel stores but never interprets: + `thread.metadata.membership` (`'dm'|'group'`), interaction annotations (opaque + `annotationType` + `value` → the app writes `reaction`/`pin`/`save`), `mentions[]` (an + opaque userId notify-list the kernel fans out; it never parses `@`). +3. **The app** (chat-web) — rendering + product semantics. + +Reading an opaque attribute inside a **policy/notification gate** (e.g. `membership === 'dm'` +in `DevOpaPort` or `notification.projector.ts`) is allowed — that file *is* the policy plane, +not the kernel. Everywhere else, keep it generic. Verify with a grep before committing: +`grep -rniE "'dm'|'group'|reaction|emoji" packages/iios-service/src | grep -v spec` — hits +should only be in the policy/notification/app-facing layers or comments. + +`pnpm boundary` enforces the layer dependency law (specializations import the kernel, never +the reverse). Run it; don't break it. + +## Architecture + +- **Kernel primitives** (generic): `IiosScope` (six-vector: org/app/tenant/bu/…), `IiosSourceHandle` + (externalId = userId; stays `UNVERIFIED` until MDM resolves → `canonicalEntityId`), `IiosActorRef`, + `IiosThread` (subject/metadata), `IiosThreadParticipant`, `IiosInteraction` (+ `parentInteractionId` + reply link), `IiosMessagePart` (media as `contentRef`), `IiosInteractionAnnotation` (generic). +- **Platform ports** (`IiosPlatformPorts`, DI token `PLATFORM_PORTS`; dev = `LocalDevPorts`): + session, opa, cmp (consent), mdm, sas, capability — plus a `StoragePort` (media) and + `NotificationPort` (push). **Dev stubs → real adapters with zero consumer changes.** Every + op passes `decideOrThrow(ports, {action,…})` **fail-closed**. +- **Session (auth):** `SessionVerifier` verifies (a) real OIDC tokens (Supabase/`AUTH_ISSUERS`) + against the issuer JWKS (ES256, no secret), routed by `iss` → per-issuer `appId` scope; or + (b) legacy dev HS256 app tokens (`APP_SECRETS`, keyed by `appId`). `userId = email` for OIDC. +- **Events:** transactional outbox → `OutboxBus` → **projectors** (inbox, notifications). + Projectors are **idempotent** (`claim()` on `IiosProcessedEvent` + projection cursor). + Delivery is at-least-once → clients dedupe by message `id`. +- **Scope isolation:** every row is tagged by `scopeId` (org+app+tenant). By-id ops call + `assertOwns` (tenant fence → 403). Always `select`/scope Prisma queries. + +## Tech stack + +NestJS 11 · Prisma 6 / PostgreSQL 16 (docker `iios-db` on **:5434**, db `iios`) · Redis +(socket.io adapter, multi-replica) · socket.io (`/message` namespace) · Vitest · pnpm +monorepo (`packages/*`). `iios-service` is a **modular monolith** (HTTP + WS + relay + +projectors in one process). + +## Running & testing + +```bash +docker start iios-db # Postgres :5434 (OrbStack; `open -a OrbStack` if down) +pnpm --filter @insignia/iios-service exec nest build +# run (dev auth via Supabase; media + notifications enabled): +SUPABASE_URL=https://.supabase.co REDIS_URL=redis://localhost:6379 PORT=3200 \ + APP_SECRETS='{"portal-demo":"dev-secret"}' MEDIA_DIR=/tmp/iios-media \ + VAPID_PUBLIC_KEY=… VAPID_PRIVATE_KEY=… VAPID_SUBJECT=mailto:dev@insignia \ + node packages/iios-service/dist/main.js # → :3200 ; GET /health +``` + +- **`pnpm test`** — Vitest. Runs against an **isolated `iios_test` DB** (globalSetup creates + + migrates it; `DATABASE_URL` overridden). **It never wipes the dev `iios` DB.** ~205 tests. +- **TDD**: write the failing spec first (see `*.spec.ts` next to the code). DB specs use + `resetDb()` + real Postgres. +- **Flaky `outbox/replay.spec`**: it's clock/ordering-sensitive and pre-existing. If it fails + in a full run, `docker exec iios-db psql -U iios -d postgres -c "DROP DATABASE IF EXISTS iios_test WITH (FORCE)"` + then re-run — it's stale test-DB state, not a regression. +- **`pnpm boundary`** — import-boundary check (must stay OK). +- Smokes: `packages/iios-service/scripts/smoke-*.mjs` (run against a live service). + +## Conventions + +- **Commits:** conventional (`feat:`/`fix:`/`docs:`/`chore:`), git email + **`maaz@insigniaconsultancy.com`**, and co-author every commit with Claude. Branch off `main` + before committing if asked; otherwise the session has committed directly to `main`. +- **⚠️ `IIOS_DEV_TOKENS` MUST be `0`/unset in production** — it exposes `/v1/dev/*` (unauth token + minting). Single most important prod flag. +- New kernel capability = a **generic primitive** only (see the #1 rule). Add domain meaning in + policy + app. +- Prisma: always `select` to avoid leaking `passwordHash`/PII; org/tenant scope every `where`. + +## What's built (state) + +P0–P8: kernel → messaging → inbox → support → adapters → routing → AI → calendar/meetings. +Recent (the "P9 real-providers" era, mostly driven by the chat app): +- **Real Supabase auth** — multi-issuer JWKS verification (`SessionVerifier`); first stubbed + port turned real. +- **Reactions / pins / saves** — one generic `IiosInteractionAnnotation` primitive + (opaque type/value), `annotate` socket event, `GET /v1/threads/my-annotations`. +- **@mentions → Inbox** — `mentions[]` on send → `MENTION` inbox item (projector). +- **Media** — `StoragePort` (dev local disk → prod S3/Supabase), presigned upload/download, + attachment on `MessageDto`. +- **Notifications** — presence-gated Web Push: `NotificationProjector` (policy/presence/mute + gates) + swappable `NotificationPort`, `focus_thread` presence signal, per-thread mute. +- `senderId` (stable externalId) on messages for reliable "is this mine?". + +## Docs (as-built) + +- `docs/IIOS_API_AND_SDK_GUIDE.md` — the **as-built REST/socket/SDK reference** (endpoints, + shapes, env, vocab). Keep it current when adding endpoints. +- `docs/DEPLOYMENT.md` — deploy/topology/env/scaling. +- `docs/IIOS_OVERVIEW_FOR_CEO.md` — plain-language capability tour.