From 269bdab589f7f6953f613af5e12680e8595ce9ac Mon Sep 17 00:00:00 2001 From: maaz519 Date: Thu, 2 Jul 2026 22:04:33 +0530 Subject: [PATCH] docs: API & SDK guide for QA and developers Full as-built reference: run/auth (dev tokens), conventions (idempotency, tenancy, error codes), every REST endpoint across health/dev/interactions/threads/inbox/ support/adapters/routes/ai/calendar with request/response shapes, the kernel-client + 6 React hook SDKs, a curl QA quickstart + the 8 smoke scripts, env var reference, and enum vocabularies. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/IIOS_API_AND_SDK_GUIDE.md | 316 +++++++++++++++++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 docs/IIOS_API_AND_SDK_GUIDE.md diff --git a/docs/IIOS_API_AND_SDK_GUIDE.md b/docs/IIOS_API_AND_SDK_GUIDE.md new file mode 100644 index 0000000..5f33815 --- /dev/null +++ b/docs/IIOS_API_AND_SDK_GUIDE.md @@ -0,0 +1,316 @@ +# IIOS — API & SDK Guide (for QA and Developers) + +> Everything you need to run, test, and build against the IIOS service and SDKs. One NestJS API (`iios-service`) + a set of browser SDKs. This doc is generated from the current code — endpoints, request/response shapes, SDK methods, auth, and env are all as-built. + +--- + +## 1. Quick facts + +| Thing | Value | +|---|---| +| Base URL (dev) | `http://localhost:3200` (env `PORT`, default `3200`) | +| Auth | `Authorization: Bearer ` — HS256, per-app secret | +| Content type | `application/json` | +| Validation | Global — **unknown body fields are rejected (400)**; types are coerced | +| CORS | Enabled (`origin: true`) | +| Realtime | Socket.IO namespace `/message` | +| DB | PostgreSQL (dev: `localhost:5434`, db `iios`) | + +**Everything is scoped to a tenant.** A caller can only see/act on data in its own scope (`orgId` + `appId` + optional `tenantId`, derived from the token). Cross-tenant access returns **403**. + +--- + +## 2. Getting started (run it locally) + +```bash +# 1. Postgres (docker-compose in the repo root brings up 'iios-db' on :5434) +docker compose up -d + +# 2. Install + generate + migrate +pnpm install +pnpm -F @insignia/iios-service prisma:generate +pnpm -F @insignia/iios-service prisma:migrate + +# 3. Run the API (dev tokens ON so you can mint test tokens) +cd packages/iios-service && IIOS_DEV_TOKENS=1 pnpm start +# → iios-service listening on :3200 +``` + +Health check: `GET http://localhost:3200/health` → `{ "status": "ok", "db": true, "egressProviders": { ... } }` + +--- + +## 3. Auth model (how to get a token) + +Tokens are per-app JWTs signed with a secret from the `APP_SECRETS` env map (`{"portal-demo":"dev-secret"}` by default). The token carries `sub` (userId), `appId`, `orgId`, optional `tenantId`. + +**Get a dev token** (only when `IIOS_DEV_TOKENS=1`): + +```bash +curl -s -X POST http://localhost:3200/v1/dev/token \ + -H 'content-type: application/json' \ + -d '{"appId":"portal-demo","userId":"alice","orgId":"org_A"}' +# → { "token": "eyJhbGc..." } +``` + +Use it on every call: `-H "authorization: Bearer "`. + +- **Different `orgId` = different tenant.** Mint two tokens with `org_A` / `org_B` to test isolation. +- Token TTL: 2h. + +### Error responses (what QA will see) +| Status | When | +|---|---| +| `400` | Missing `Authorization` header, unknown `appId`, or invalid/unknown body fields | +| `401` | Invalid/expired token, or bad webhook signature | +| `403` | **Cross-tenant access** (resource not in your scope) | +| `404` | Resource id not found (within your scope) | +| `201` | Created (POST) · `200` others | + +--- + +## 4. Conventions + +- **Idempotency** — native message send accepts an `idempotency-key` header; adapter send takes `idempotencyKey` in the body; AI/route/meeting derive their own keys. Re-sending the same key returns the same record (no duplicate). +- **Scopes/tenancy** — inferred from the token; you never pass `scopeId` in requests (except the raw `/v1/interactions/ingest` and adapter ingest which carry a full scope object). +- **Fail-closed** — reads/writes/sends pass through policy (OPA) + consent (CMP) gates; a denial blocks the action rather than silently allowing it. +- **Preview / propose first** — routing simulates before sending; AI proposes (never auto-decides); meetings gate recording on consent. + +--- + +## 5. REST API reference + +Grouped by domain. All paths are relative to the base URL. **Auth = Bearer token required** unless noted. + +### 5.1 Health & Dev +| Method | Path | Auth | Body / Query | Returns | +|---|---|---|---|---| +| GET | `/health` | none | — | `{status, db, egressProviders}` | +| POST | `/v1/dev/token` | none* | `{appId, userId, name?, orgId?}` | `{token}` | +| POST | `/v1/dev/webhook/:channelType` | none* | `{from?, text?}` | raw event (simulated signed inbound) | + +*Dev endpoints require `IIOS_DEV_TOKENS=1`; never enabled in prod. + +### 5.2 Interactions (kernel ingest) +| Method | Path | Body | Returns | +|---|---|---|---| +| POST | `/v1/interactions/ingest` | `IngestRequestDto` | `{interactionId, ...}` | + +`IngestRequestDto`: `{ scope:{orgId,appId,buId?,tenantId?,workspaceId?,projectId?,userScopeId?}, channel:{type,externalChannelId?,capabilityContract?}, source:{handleKind,externalId,displayName?}, kind?, thread?:{externalThreadId?,subject?}, parts:[{kind,bodyText?,contentRef?,mimeType?}], occurredAt (ISO8601), providerEventId?, metadata? }`. Send `providerEventId` twice → deduped. + +### 5.3 Threads & Messaging (native chat) +| Method | Path | Body / Headers | Returns | +|---|---|---|---| +| GET | `/v1/threads/:id/messages` | — | `{threadId, messages[]}` | +| POST | `/v1/threads/:id/messages` | `{content, contentRef?}` + `idempotency-key?` header | `Message` (201) | + +**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}`. + +### 5.4 Inbox (work queue) +| Method | Path | Query / Body | Returns | +|---|---|---|---| +| GET | `/v1/inbox/items` | `?state=OPEN|SNOOZED|DONE|ARCHIVED|CANCELLED|STALE` | `InboxItem[]` | +| PATCH | `/v1/inbox/items/:id` | `{state, reason?}` | `InboxItem` | + +### 5.5 Support (tickets, escalation, callbacks, queues, agents) +| Method | Path | Body / Query | Returns | +|---|---|---|---| +| POST | `/v1/support/tickets` | `{subject, priority?, threadId?}` | `Ticket` | +| POST | `/v1/support/escalate` | `{threadId, subject?}` | `Ticket` (opens customer↔agent thread) | +| GET | `/v1/support/tickets` | `?scope=mine|assigned` | `Ticket[]` | +| PATCH | `/v1/support/tickets/:id` | `{state, reason?}` | `Ticket` | +| POST | `/v1/support/callbacks` | `{preferChannel, preferredTime?, notes?, ticketId?}` | `CallbackRequest` | +| POST | `/v1/support/queues` | `{name}` | `{id}` | +| POST | `/v1/support/queues/:id/members` | — | membership | +| POST | `/v1/support/agents/me/join` | — | joins default queue (go online) | +| PATCH | `/v1/support/members/me/availability` | `{state}` | membership | + +`priority ∈ {LOW, NORMAL, HIGH, URGENT}`; ticket `state ∈ {NEW, OPEN, PENDING, RESOLVED, CLOSED}` (transitions validated). + +### 5.6 Adapters (channels — inbound/outbound) +| Method | Path | Auth | Body / Headers | Returns | +|---|---|---|---|---| +| POST | `/v1/adapters/:channelType/webhook` | signature | raw provider body + `x-iios-signature` (HMAC) | `202` (async normalize) | +| POST | `/v1/adapters/:channelType/send` | Bearer | `{target, payload?, idempotencyKey?}` | `OutboundCommand` | +| GET | `/v1/adapters/inbound` | Bearer | — | raw inbound events (+ `interactionId` once normalized) | +| GET | `/v1/adapters/outbound` | Bearer | — | outbound commands (+ delivery attempts) | + +`channelType ∈ {WEBHOOK, EMAIL, WHATSAPP, PORTAL}`. Outbound goes through the **Capability Broker** (policy + consent + provider); a blocked send → command status `BLOCKED`, a tenant over quota → `RATE_LIMITED`. + +### 5.7 Routing / rules (community) +| Method | Path | Body / Query | Returns | +|---|---|---|---| +| POST | `/v1/routes/bindings` | `CreateBindingDto` | `RouteBinding` | +| GET | `/v1/routes/bindings` | — | `RouteBinding[]` (your scope) | +| POST | `/v1/routes/simulate` | `{interactionId, originChannelType, originRef?}` | `{simulationId, decisions[]}` (no send) | +| GET | `/v1/routes/decisions` | `?state=ALLOW|DENY|REVIEW|SUPPRESS|SIMULATED` | `RouteDecision[]` (your scope) | +| POST | `/v1/routes/decisions/:id/approve` | — | `RouteDecision` (executes to sandbox) | +| POST | `/v1/routes/decisions/:id/deny` | — | `RouteDecision` | +| POST | `/v1/routes/bindings/:id/flush-digest` | — | `{forwarded}` | + +`CreateBindingDto`: `{originChannelType, originRef?, destinationChannelType, destinationRef?, restrictionProfile?, mode?, outputFormat?, frequency?, includeAttachments?, requiresReview?, enabled?}`. `mode ∈ {MANUAL, AUTOMATIC, HYBRID, SIMULATION_ONLY}`; `outputFormat ∈ {FORWARD, THREADED, DIGEST, SUMMARY, TRANSCRIPT}`. Restricted destinations are **deny-by-default** for flagged content. + +### 5.8 AI (proposal layer) +| Method | Path | Body / Query | Returns | +|---|---|---|---| +| POST | `/v1/ai/jobs` | `{interactionId, jobType}` | `{job, artifact}` | +| GET | `/v1/ai/artifacts` | `?interactionId=&status=` | `AiArtifact[]` (your scope) | +| GET | `/v1/ai/artifacts/:id` | — | `AiArtifact` (claims + evidence + modelRun) | +| POST | `/v1/ai/artifacts/:id/accept` | — | `AiArtifact` (ACCEPTED) | +| POST | `/v1/ai/artifacts/:id/reject` | — | `AiArtifact` (REJECTED) | + +`jobType ∈ {CLASSIFY, SUMMARIZE, EXTRACT}`. Artifacts carry `confidence`, `evidence` (citations), and `abstentionReason` when the model declines. **AI proposes; a human accepts.** + +### 5.9 Calendar & Meetings +| Method | Path | Body / Query | Returns | +|---|---|---|---| +| POST | `/v1/calendar/meetings` | `ScheduleMeetingDto` | `Meeting` (SCHEDULED) | +| POST | `/v1/calendar/requests` | `RequestMeetingDto` + `?fromCallback=&fromClaim=` | meeting request | +| GET | `/v1/calendar/meetings` | — | `Meeting[]` (your scope) | +| GET | `/v1/calendar/meetings/:id` | — | `Meeting` (participants, transcripts, summaries, action items) | +| POST | `/v1/calendar/meetings/:id/consent` | `{actorRefId, status}` | participant | +| POST | `/v1/calendar/meetings/:id/transcript` | — | transcript (`READY` or `BLOCKED`) | +| POST | `/v1/calendar/meetings/:id/summarize` | — | `Meeting` (summary + action items) | +| GET | `/v1/calendar/meetings/:id/action-items` | — | `MeetingActionItem[]` | +| POST | `/v1/calendar/providers` | — | provider account (simulated) | +| POST | `/v1/calendar/providers/:id/sync` | — | `{pulled, cursor}` | + +`ScheduleMeetingDto`: `{meetingType, title, startAt, endAt?, timezone?, attendees?:[{userId, displayName?, role?, visibility?}], requestId?}`. `meetingType ∈ {ZOOM, PHONE, IN_PERSON, CALLBACK, INTERNAL}`; consent `status ∈ {UNKNOWN, GRANTED, DENIED, REVOKED}`. **Transcript/summary is `BLOCKED` until every attendee has `GRANTED` consent.** + +--- + +## 6. SDK reference + +Two layers: a low-level `RestClient` (+ socket) and per-domain React hook packages. + +### 6.1 Low-level client — `@insignia/iios-kernel-client` +```ts +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, {contentRef?, idempotencyKey?})` +- **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)` +- **Routing:** `createBinding(input)`, `listBindings()`, `simulateRoute({interactionId, originChannelType, originRef?})`, `listRouteDecisions(state?)`, `approveDecision(id)`, `denyDecision(id)` +- **AI:** `runAiJob({interactionId, jobType})`, `listArtifacts({interactionId?, status?})`, `getArtifact(id)`, `acceptArtifact(id)`, `rejectArtifact(id)` +- **Calendar:** `scheduleMeeting(input)`, `requestMeeting(input, {fromCallback?, fromClaim?})`, `listMeetings()`, `getMeeting(id)`, `setConsent(meetingId, actorRefId, status)`, `generateTranscript(meetingId)`, `summarizeMeeting(meetingId)`, `listActionItems(meetingId)`, `connectCalendarProvider()`, `syncCalendarProvider(id)` +- **Ingest:** `ingest(body, idempotencyKey)` +- Realtime: `MessageSocket` (Socket.IO facade for `/message`). + +### 6.2 React hook packages (front-end teams) +Each package exports a `Provider` (wrap your app with `serviceUrl` + `token`) plus hooks: + +| Package | Provider | Hooks | +|---|---|---| +| `@insignia/iios-message-web` | `MessageProvider` | `useThread`, `useMessages` | +| `@insignia/iios-inbox-web` | `InboxProvider` | `useInbox` | +| `@insignia/iios-support-web` | `SupportProvider` | `useEscalate`, `useTickets`, `useAssignedTickets`, `useCallbackRequest`, `useAvailability`, `useGoOnline` (+ re-exports `useThread`/`useMessages`) | +| `@insignia/iios-community-web` | `CommunityProvider` | `useBindings`, `useRoutePreview`, `useRouteDecisions` | +| `@insignia/iios-ai-web` | `AiProvider` | `useRunJob`, `useAiProposals`, `useArtifact` | +| `@insignia/iios-meeting-web` | `MeetingProvider` | `useMeetings`, `useMeeting`, `useSchedule`, `useConsent`, `useSummarize`, `useActionItems` | + +```tsx +// Example: an AI-assist panel +import { AiProvider, useRunJob, useAiProposals } from '@insignia/iios-ai-web'; + + + +; + +function Panel({ interactionId }: { interactionId: string }) { + const run = useRunJob(); + const { artifacts, accept, reject } = useAiProposals({ interactionId, pollMs: 2500 }); + return <> + + {artifacts.map(a => accept(a.id)} onReject={() => reject(a.id)} />)} + ; +} +``` + +--- + +## 7. QA quickstart (curl walkthrough) + +```bash +BASE=http://localhost:3200 +TOKEN=$(curl -s -X POST $BASE/v1/dev/token -H 'content-type: application/json' \ + -d '{"appId":"portal-demo","userId":"alice","orgId":"org_A"}' | jq -r .token) +AUTH="authorization: Bearer $TOKEN" + +# schedule a meeting +MID=$(curl -s -X POST $BASE/v1/calendar/meetings -H "$AUTH" -H 'content-type: application/json' \ + -d '{"meetingType":"INTERNAL","title":"QA test","startAt":"2026-07-02T10:00:00.000Z"}' | jq -r .id) + +# transcript is BLOCKED (no consent yet) +curl -s -X POST $BASE/v1/calendar/meetings/$MID/transcript -H "$AUTH" | jq .status # "BLOCKED" + +# tenant isolation: a second tenant cannot read it → 403 +TOKEN_B=$(curl -s -X POST $BASE/v1/dev/token -H 'content-type: application/json' \ + -d '{"appId":"portal-demo","userId":"bob","orgId":"org_B"}' | jq -r .token) +curl -s -o /dev/null -w "%{http_code}\n" $BASE/v1/calendar/meetings/$MID \ + -H "authorization: Bearer $TOKEN_B" # 403 +``` + +### Ready-made end-to-end tests (smoke scripts) +Run against a live service (from `packages/iios-service`, `node scripts/`): + +| Script | Proves | +|---|---| +| `smoke-realtime.mjs` | native chat + realtime + unread (P2) | +| `smoke-inbox.mjs` | needs-reply queue + auto-resolve (P3) | +| `smoke-support.mjs` (+ `seed-support.mjs`) | ticket assign + escalate + live reply (P4) | +| `smoke-adapter.mjs` | signed webhook ingest + sandbox send (P5) | +| `smoke-route.mjs` | preview-first routing, deny-by-default, approve→send (P6) | +| `smoke-ai.mjs` | AI classify/summarize/extract, abstain, KG-05 (P7) | +| `smoke-calendar.mjs` | consent-gated transcript, summary, action items (P8) | +| `smoke-capability.mjs` | governed egress + real HTTP provider (needs `IIOS_PROVIDER_URL_EMAIL`) | +| `smoke-tenant.mjs` | cross-tenant 403 + list isolation | + +Automated unit/integration suite: `pnpm test` (114 tests). Import-boundary check: `pnpm boundary`. + +--- + +## 8. Tenancy & security notes for QA + +- **Cross-tenant** — any by-id read/mutate of another tenant's resource → **403**; lists only ever return your own scope's data. Test with two `orgId`s. +- **Consent** — a meeting transcript/summary is `BLOCKED` unless every attendee consented; an outbound send to an opted-out recipient is `BLOCKED` (consent gate). +- **Per-tenant quota** — one tenant flooding outbound gets `RATE_LIMITED` without affecting others. +- **AI safety** — AI outputs are proposals (`PROPOSED`) with confidence/evidence; they never auto-send or auto-approve; low-confidence → abstains. +- **Idempotency** — replaying a webhook/message/job with the same key yields one record. + +--- + +## 9. Environment variables + +| Var | Default | Purpose | +|---|---|---| +| `PORT` | `3200` | HTTP port | +| `DATABASE_URL` | `postgresql://iios:iios@localhost:5434/iios?schema=public` | Postgres | +| `APP_SECRETS` | `{"portal-demo":"dev-secret"}` | per-app JWT secrets (`{appId: secret}`) | +| `IIOS_DEV_TOKENS` | `0` | set `1` to enable `/v1/dev/*` | +| `ADAPTER_SECRETS` | `{}` | per-channel HMAC secrets (default `dev-adapter-secret`) | +| `IIOS_OUTBOUND_LIMIT` / `_WINDOW_MS` | `5` / `60000` | per-(channel,target) rate limit | +| `IIOS_TENANT_OUTBOUND_LIMIT` / `_WINDOW_MS` | `10000` / `60000` | per-tenant egress quota | +| `IIOS_AI_BUDGET_UNITS` | `1000000` | per-tenant AI cost budget | +| `IIOS_CELL_ID` | `cell-default` | cell assignment for new scopes | +| `IIOS_PROVIDER_URL_` | — | set to route that channel's egress to a real HTTP provider | +| `IIOS_RELAY_INTERVAL_MS` | `500` | outbox → event relay poll interval | + +--- + +## 10. Vocabularies (enums) + +- **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 +- **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 +- **Meeting type:** ZOOM, PHONE, IN_PERSON, CALLBACK, INTERNAL · **status:** REQUESTED, SCHEDULED, CONFIRMED, CANCELLED, COMPLETED, NO_SHOW · **consent:** UNKNOWN, GRANTED, DENIED, REVOKED · **participant visibility:** FULL, LIMITED, NONE + +--- + +*Base URL, ports, and demos: the service is `http://localhost:3200`; runnable demo UIs live under `apps/` (message-demo 5173, agent-demo 5174, adapter-inspector 5175, route-admin 5176, ai-studio 5177, meeting-studio 5178). For architecture/what-each-SDK-is-for, see `docs/IIOS_OVERVIEW_FOR_CEO.md`.*