diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index fe377a5..b71255c 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -46,7 +46,14 @@ Every knob is an environment variable — see [`packages/iios-service/.env.examp for the full, commented list. Highlights: - **Secrets** (inject from a vault, never bake into the image): `DATABASE_URL`, - `APP_SECRETS` (per-app JWT signing keys), `ADAPTER_SECRETS` (webhook HMAC keys). + `APP_SECRETS` (per-app HS256 JWT keys), `ADAPTER_SECRETS` (webhook HMAC keys), + `MEDIA_SECRET` (signs presigned media upload/download URLs). +- **Auth (real IdP):** set `SUPABASE_URL` or `AUTH_ISSUERS` to trust real OIDC access + tokens, verified against the issuer's public **JWKS** (ES256) — **no secret to store**. + `AUTH_ISSUERS` is a JSON registry `[{url, appId, orgId?}]` routing many issuers → isolated + app scopes. The dev HS256 path (`APP_SECRETS`) stays for local/tests. +- **Media storage:** `MEDIA_DIR` + `PUBLIC_URL` configure the **dev** local-disk store; for + prod, bind the `StoragePort` to object storage (see topology) — the API/SDK don't change. - **⚠️ `IIOS_DEV_TOKENS` MUST be `0`/unset in production.** It exposes `/v1/dev/*` (unauthenticated token minting, webhook injection, chaos, retention sweep). This is the single most important prod-hardening flag. @@ -76,7 +83,12 @@ for the full, commented list. Highlights: > by message `id`** (the payload always carries one). - **Platform ports** (OPA policy, CMP consent, MDM, CRRE, SAS) — today in-process permissive stubs (`LocalDevPorts`). For production, point these at real external services; the service - already calls them **fail-closed**. + already calls them **fail-closed**. The **session** port already verifies real OIDC tokens + (Supabase/JWKS) when configured. +- **Media storage** (`StoragePort`) — dev = **local disk** (`MEDIA_DIR`). ⚠️ Local disk is + **single-instance and non-durable**; with N>1 replicas or for persistence, bind it to shared + **object storage** (S3/R2/Supabase Storage). Bytes always flow **client ↔ storage directly** + via presigned URLs — they never transit the service — so this scales independently. ## Scaling & release strategy diff --git a/docs/IIOS_API_AND_SDK_GUIDE.md b/docs/IIOS_API_AND_SDK_GUIDE.md index ebd9115..59a47ad 100644 --- a/docs/IIOS_API_AND_SDK_GUIDE.md +++ b/docs/IIOS_API_AND_SDK_GUIDE.md @@ -103,12 +103,24 @@ Grouped by domain. All paths are relative to the base URL. **Auth = Bearer token ### 5.3 Threads & Messaging (native chat) | Method | Path | Body / Headers | Returns | |---|---|---|---| +| GET | `/v1/threads` | — | `ThreadSummary[]` — your threads (members, unread, last message, `membership`) | +| POST | `/v1/threads` | `{membership?, creatorRole?, subject?}` | `{threadId, status, history}` (201) — `membership`/`creatorRole`/`subject` are **opaque app attributes** the kernel stores but never interprets | +| POST | `/v1/threads/:id/participants` | `{userId, role?}` | `{threadId, participantCount}` (201) — **governed** (DM cap / group-admin via OPA) | | GET | `/v1/threads/:id/messages` | — | `{threadId, messages[]}` | -| POST | `/v1/threads/:id/messages` | `{content, contentRef?}` + `idempotency-key?` header | `Message` (201) | +| POST | `/v1/threads/:id/messages` | `{content, contentRef?, mimeType?, sizeBytes?, checksumSha256?, parentInteractionId?}` + `idempotency-key?` header | `Message` (201) | +| GET | `/v1/threads/my-annotations` | `?type=save` | `{message, threadId, threadSubject}[]` — messages you annotated (e.g. saved) | -**Realtime (Socket.IO, namespace `/message`):** connect, then emit client→server events: -- `open_thread` `{threadId?|otherUserId}`, `send_message` `{threadId, content, idempotencyKey?}`, `read` `{threadId}`, `delivered` `{threadId}`. -- Server emits to the thread room: `message` (a Message) and `receipt` `{interactionId, actorId, kind: READ|DELIVERED}`. +A **`Message`** carries `{id, threadId, senderId, senderName, content, attachment?, parentInteractionId?, annotations[], traceId, createdAt}`. `senderId` is the sender's stable externalId (email/username) — the reliable "is this mine?" check. `attachment` = `{contentRef, mimeType, sizeBytes, kind}` (§5.10). `annotations` = `[{type, value, users[]}]` — the reaction/pin/save aggregate. + +**Realtime (Socket.IO, namespace `/message`).** Token verified on connect (`auth.token`), then emit client→server: +- `open_thread` `{threadId?, membership?, creatorRole?, subject?}` — opens, or **creates** when no id; a **governed join** for membership threads. Returns `{threadId, status, history}` or `{error}` (acked, never a throw — clients don't hang). +- `send_message` `{threadId, content, contentRef?, mimeType?, sizeBytes?, parentInteractionId?, mentions?}` — `mentions` is an **opaque userId notify-list** (the app parses `@`; the kernel never does). +- `add_participant` `{threadId, userId, role?}` · `read` `{threadId, interactionId}` · `delivered` `{…}` · `typing` `{threadId}`. +- `annotate` `{threadId, interactionId, type, value}` — toggle a **generic annotation** (chat app uses `type: reaction|pin|save`; `value` = emoji, or empty). + +Server → thread room: `message` (a Message), `receipt` `{interactionId, actorId, kind: READ|DELIVERED}`, `typing` `{threadId, userId}`, `annotation` `{interactionId, type, value, op: add|remove, users[], userId}`. + +**Governance & primitives (all fail-closed via OPA).** Membership (`iios.thread.participant.add`, `iios.thread.join`), annotating (`iios.interaction.annotate` — participant-only), and send all gate on policy. **Reactions/pins/saves are one generic primitive** — an *interaction annotation* the kernel stores + aggregates as opaque `(type, value)` but never interprets (the same primitive backs pins/saves/flags/tags). **Mentions → Inbox:** `mentions[]` flows into the message event; the inbox projector fans out a `MENTION` inbox item to each mentioned participant (never the sender); reading the thread resolves it. ### 5.4 Inbox (work queue) | Method | Path | Query / Body | Returns | @@ -116,6 +128,8 @@ Grouped by domain. All paths are relative to the base URL. **Auth = Bearer token | GET | `/v1/inbox/items` | `?state=OPEN|SNOOZED|DONE|ARCHIVED|CANCELLED|STALE` | `InboxItem[]` | | PATCH | `/v1/inbox/items/:id` | `{state, reason?}` | `InboxItem` | +Item **kinds** include `NEEDS_REPLY` (unreplied thread activity, one per owner+thread) and `MENTION` (someone @-mentioned you; `priority: HIGH`, one per source message). Reading the thread resolves both to `DONE`. + ### 5.5 Support (tickets, escalation, callbacks, queues, agents) | Method | Path | Body / Query | Returns | |---|---|---|---| @@ -239,7 +253,8 @@ import { RestClient } from '@insignia/iios-kernel-client'; const client = new RestClient({ serviceUrl: 'http://localhost:3200', token }); ``` Methods (all return typed promises): -- **Messaging:** `getThreadMessages(threadId)`, `sendMessage(threadId, content, {attachment?, parentInteractionId?, mentions?, idempotencyKey?})` +- **Messaging:** `listThreads()`, `createThread({membership?, creatorRole?, subject?})`, `addParticipant(threadId, userId, role?)`, `getThreadMessages(threadId)`, `sendMessage(threadId, content, {attachment?, parentInteractionId?, mentions?, idempotencyKey?})` +- **Reactions / pins / saves (annotations):** `MessageSocket.react(threadId, interactionId, emoji)` · `pin(...)` · `save(...)` (generic `annotate` under the hood); `listMyAnnotated('save')` for a cross-thread saved list. Subscribe to the `annotation` event for live updates. - **Media:** `uploadMedia(file, {onProgress?}) → {contentRef, mimeType, sizeBytes, checksumSha256, kind}` (presign → PUT-with-progress → normalized ref); `mediaUrl(contentRef) → signed view URL` (cached, short-lived). *This plumbing is identical for every app, so it lives in the SDK; the app only renders by `kind`.* - **Inbox:** `listInboxItems(state?)`, `patchInboxItem(id, {state, reason?})` - **Support:** `createTicket({subject, priority?, threadId?})`, `escalate(threadId, subject?)`, `listTickets('mine'|'assigned')`, `patchTicket(id, state)`, `requestCallback({...})`, `createQueue(name)`, `joinQueue(id)`, `joinDefaultQueue()`, `setAvailability(state)` @@ -358,7 +373,9 @@ Automated unit/integration suite: `pnpm test` (192 tests). Import-boundary check - **Interaction kind:** MESSAGE, EMAIL, SYSTEM_NOTICE, INBOX_WORK, SUPPORT_CASE, MEETING_REQUEST, DIGEST, SUMMARY, NOTIFICATION - **Channel types:** WEBHOOK, EMAIL, WHATSAPP, PORTAL -- **Inbox state:** OPEN, SNOOZED, DONE, ARCHIVED, CANCELLED, STALE · **Inbox kind:** NEEDS_REPLY, NEEDS_REVIEW, NEEDS_APPROVAL, SUPPORT_UPDATE, MEETING_FOLLOWUP, DIGEST, SYSTEM_ALERT, CRM_OWNER_INTEREST +- **Inbox state:** OPEN, SNOOZED, DONE, ARCHIVED, CANCELLED, STALE · **Inbox kind:** NEEDS_REPLY, NEEDS_REVIEW, NEEDS_APPROVAL, **MENTION**, SUPPORT_UPDATE, MEETING_FOLLOWUP, DIGEST, SYSTEM_ALERT, CRM_OWNER_INTEREST +- **Message part kind:** TEXT, HTML, MARKDOWN, MEDIA_REF, FILE_REF, VOICE_REF, LOCATION, STRUCTURED_JSON · **Attachment kind (DTO):** image, video, audio, file +- **Interaction annotation (app-level, opaque to the kernel):** `type` = reaction | pin | save … ; `value` = emoji (reactions) or empty - **Ticket state:** NEW, OPEN, PENDING, RESOLVED, CLOSED · **priority:** LOW, NORMAL, HIGH, URGENT - **Route mode:** MANUAL, AUTOMATIC, HYBRID, SIMULATION_ONLY · **output format:** FORWARD, THREADED, DIGEST, SUMMARY, TRANSCRIPT · **decision:** ALLOW, DENY, REVIEW, SUPPRESS, SIMULATED - **AI job:** CLASSIFY, SUMMARIZE, EXTRACT · **artifact:** CLASSIFICATION, SUMMARY, TRANSCRIPT, DIGEST, EXTRACTION · **artifact status:** PROPOSED, ACCEPTED, REJECTED, SUPERSEDED diff --git a/docs/IIOS_OVERVIEW_FOR_CEO.md b/docs/IIOS_OVERVIEW_FOR_CEO.md index fd69ac9..a114ac1 100644 --- a/docs/IIOS_OVERVIEW_FOR_CEO.md +++ b/docs/IIOS_OVERVIEW_FOR_CEO.md @@ -40,7 +40,7 @@ Today the external world (real WhatsApp, real AI models, real calendars, the rea | **P8** | **Calendar & meetings** — schedule, attendee consent, transcript, summary, action items | `iios-meeting-web` | Support/scheduling; callback→meeting | meeting-studio (5178) | | **P9** | *(next)* **Production hardening** — real providers, real policy/consent, scale, retention | — | Ops / platform | — | -All of it runs on **one NestJS service** (`iios-service`) with **one Postgres database** (46 tables across 9 migrations), fronted by **one low-level client** (`iios-kernel-client`) that every React SDK is built on. +All of it runs on **one NestJS service** (`iios-service`) with **one Postgres database** (53 tables across 19 migrations), fronted by **one low-level client** (`iios-kernel-client`) that every React SDK is built on. --- @@ -159,7 +159,9 @@ Each product SDK is a handful of React hooks — a front-end dev wires the UI, t | Calendar/Zoom providers | Simulated sync | P9 | | Multi-tenant scale, retention, SLOs | Not yet | P9 | -**Proof it works:** 95 automated tests pass; every capability has a runnable demo and an end-to-end smoke script; the layer-boundary check enforces the architecture. +**Proof it works:** 192 automated tests pass; every capability has a runnable demo and an end-to-end smoke script; the layer-boundary check enforces the architecture. + +**P9 has begun (real providers).** A production chat app (`chat-web`) now runs on IIOS with **real Supabase login** (the session port verifies real OIDC tokens via JWKS — the first stubbed port turned real), plus richer chat built generically on the kernel: **emoji reactions, pinned & saved messages, @mentions → inbox, and media sharing** (images/video/audio/docs on a swappable storage port). Each was a thin, generic addition — no chat-specific logic in the kernel — which is the reuse thesis paying off. --- @@ -175,4 +177,4 @@ Narrative arc for the CEO: **one engine → chat → inbox → support → chann --- -*Appendix — repo facts: 11 packages, 6 demo apps, one NestJS service, 46 Postgres tables across 9 migrations (kernel → messaging → inbox → support → adapters → routing → ai → calendar). Boundary-enforced dependency law; 95 passing tests; 7 end-to-end smoke scripts.* +*Appendix — repo facts: 11 packages, 6 demo apps, one NestJS service, 53 Postgres tables across 19 migrations (kernel → messaging → inbox → support → adapters → routing → ai → calendar → annotations/mentions → media). Boundary-enforced dependency law; 192 passing tests; 8+ end-to-end smoke scripts. First real-provider swap live: Supabase auth (JWKS-verified) + a media storage port.*