4 Commits

Author SHA1 Message Date
maaz519 64c498a97a build: publish @insignia/iios-* SDKs to the Gitea package registry
- .npmrc routes the @insignia scope to https://git.lynkedup.cloud/api/packages/insignia/npm/
  (auth via ${GITEA_TOKEN} env — no secret committed).
- The 9 frontend SDK packages (contracts, kernel-client, adapter-sdk, *-web) are now
  publishable: private dropped, version 0.1.0, publishConfig pinned to Gitea. iios-service
  and iios-testkit stay private (pnpm publish skips them).
- Root `release` / `release:dry` scripts; a Gitea Actions workflow publishes on a v* tag.
- PUBLISHING.md documents publish + consumer (.npmrc) setup.

Verified: dry-run packs cleanly and workspace:* deps resolve to 0.1.0 in the tarball.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 17:13:55 +05:30
maaz519 5abee1b5b7 docs: add repo-local project memory under .claude/memory/
Versioned, per-topic project memory (index + entries): IIOS overview, the generic-safety
rule, run/test + the replay.spec flake workaround, recent features (Supabase auth,
reactions/pins/saves, mentions, media, notifications), the chat-web consumer, and workflow
conventions. CLAUDE.md points to it as the accumulating-notes companion to the canonical rules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 19:55:38 +05:30
maaz519 58ebaf1982 docs: add CLAUDE.md — project rules for future Claude Code sessions
Captures what IIOS is, the #1 generic-safety guardrail (no chat vocabulary in the
kernel; meaning lives in OPA policy + opaque attributes + the app), the architecture
(kernel + platform ports + fail-closed + outbox/projectors), tech stack, run/test
commands (isolated iios_test DB, the replay.spec flake note), conventions (commit
email, IIOS_DEV_TOKENS off in prod), and current build state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 19:48:10 +05:30
maaz519 6fc03fa4c3 docs(iios): notifications — API guide §5.11 (endpoints + engine diagram), env, counts
- API guide §5.11 Notifications: vapid-public-key / subscribe / unsubscribe / thread
  mute endpoints, the focus_thread presence signal, the three gates, and the engine
  flow diagram; SDK reference (registerPush/muteThread/focus); VAPID env; tests 205.
- DEPLOYMENT: VAPID env + the in-memory-presence → Redis (multi-replica) caveat.
- CEO overview: counts (205 tests, 54 tables / 20 migrations) + notifications in the
  "P9 has begun" note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 19:45:11 +05:30
31 changed files with 687 additions and 1928 deletions
+12
View File
@@ -0,0 +1,12 @@
# IIOS Project Memory — Index
Repo-local, versioned project memory. The full canonical rules are in the repo root
**`CLAUDE.md`**; these files are granular, per-topic memories that accumulate across sessions.
Read the relevant ones before working in that area; add/update files as the project evolves.
- [project_iios_overview.md](project_iios_overview.md) — what IIOS is (generic interaction OS; kernel + specializations + platform ports)
- [feedback_generic_safety.md](feedback_generic_safety.md) — THE #1 rule: no chat/domain vocabulary in the kernel
- [reference_run_and_test.md](reference_run_and_test.md) — run command, isolated `iios_test` DB, and the `replay.spec` flake workaround
- [project_recent_features.md](project_recent_features.md) — the "real-providers" era: Supabase auth, reactions/pins/saves, mentions→inbox, media, notifications
- [reference_chat_web_consumer.md](reference_chat_web_consumer.md) — chat-web is the reference app driving IIOS feature work
- [feedback_workflow.md](feedback_workflow.md) — commits (email + co-author), IIOS_DEV_TOKENS off in prod, TDD, build-before-restart
+28
View File
@@ -0,0 +1,28 @@
---
name: feedback_generic_safety
description: THE #1 locked rule — the kernel must never hardcode chat/domain vocabulary
metadata:
type: feedback
---
**The kernel must stay generic.** 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**.
**Why:** IIOS is a generic interaction OS; chat is one consumer. Baking chat meaning into the
kernel destroys reuse (support/community/meetings share the same core).
**How to apply:** domain meaning lives in exactly three places, never the kernel —
1. **OPA policy** (`DevOpaPort` → real OPA): DM-cap, group-admin, governed-join, media limits,
notification triggers.
2. **Opaque attributes** the kernel stores but never interprets: `thread.metadata.membership`
(`dm`/`group`), interaction annotations (opaque `annotationType`+`value` → app writes
`reaction`/`pin`/`save`), `mentions[]` (opaque userId notify-list; kernel never parses `@`).
3. **The app** (chat-web): rendering + product semantics.
Reading an opaque attr inside a **policy/notification gate** (`membership === 'dm'` in
`DevOpaPort` or `notification.projector.ts`) is OK — that file IS the policy plane. Everywhere
else, keep generic. Verify before committing:
`grep -rniE "'dm'|'group'|reaction|emoji" packages/iios-service/src | grep -v spec` — hits only
in policy/notification/app-facing layers or comments. Also run `pnpm boundary`.
Related: [[project_iios_overview]].
+19
View File
@@ -0,0 +1,19 @@
---
name: feedback_workflow
description: Working conventions — commits, prod flags, TDD, rebuild-before-restart
metadata:
type: feedback
---
- **Commits:** conventional (`feat:`/`fix:`/`docs:`/`chore:`), git email
**`maaz@insigniaconsultancy.com`**, co-author every commit with Claude. The user commits
directly to `main` in this project (fine); branch only if asked.
- **⚠ `IIOS_DEV_TOKENS` MUST be `0`/unset in production** — it exposes `/v1/dev/*` (unauth token
minting, chaos, retention sweep). The single most important prod-hardening flag.
- **TDD** — write the failing spec first; verify it fails; implement; verify it passes.
- **Rebuild before restart** — after backend changes, `nest build` then restart from `dist`;
`lsof -ti :3200 | xargs kill -9` first or the old build keeps serving (stale-dist bites).
- **New kernel capability = a generic primitive only** — add domain meaning in OPA policy + the
app, never the kernel. See [[feedback_generic_safety]].
- The user prefers **direct, fast iteration** (implement → verify end-to-end → commit), not
heavyweight multi-agent/spec ceremony. Include "how to test" in summaries; report failures honestly.
+23
View File
@@ -0,0 +1,23 @@
---
name: project_iios_overview
description: What IIOS is — a generic multi-tenant interaction OS; kernel + specializations + swappable platform ports
metadata:
type: project
---
IIOS (Insignia Interaction OS) = one NestJS service (`@insignia/iios-service`) + SDKs, in a
pnpm monorepo (`packages/*`). Every interaction (chat message, ticket, routed post, AI
suggestion, meeting) is the **same kernel object** inside a **tenant scope** (`IiosScope`:
org/app/tenant/…), behind the **same fail-closed gates**, emitting the **same audit trail**.
Products (messaging, inbox, support, routing, AI, calendar, media, notifications) are thin
**specializations** on top of a tiny kernel — never the reverse (`pnpm boundary` enforces it).
Platform seams are **ports** (`IiosPlatformPorts`, DI token `PLATFORM_PORTS`; dev = permissive
`LocalDevPorts`): session, opa, cmp (consent), mdm, sas, capability, plus `StoragePort` (media)
and `NotificationPort` (push). **Dev stubs swap to real adapters with zero consumer changes.**
Every op passes `decideOrThrow(ports, {action,…})` fail-closed. Events go through a
transactional outbox → `OutboxBus` → idempotent projectors (inbox, notifications).
See the repo `CLAUDE.md` and `docs/IIOS_API_AND_SDK_GUIDE.md` (as-built reference) for detail.
Related: [[feedback_generic_safety]].
+31
View File
@@ -0,0 +1,31 @@
---
name: project_recent_features
description: The "real-providers" era — Supabase auth, reactions/pins/saves, mentions→inbox, media, notifications
metadata:
type: project
---
Beyond P0P8 (kernel → messaging → inbox → support → adapters → routing → AI →
calendar/meetings), recent work (mostly driven by the chat app, the start of P9 "real
providers"):
- **Real Supabase auth** — `SessionVerifier` verifies real OIDC tokens against issuer JWKS
(ES256, no secret), a **multi-issuer registry** routed by the `iss` claim → per-issuer `appId`
scope (`AUTH_ISSUERS` JSON, or `SUPABASE_URL` single-issuer shorthand). `userId = email`.
Legacy HS256 app-token path (`APP_SECRETS`) stays for dev/tests.
- **Reactions / pins / saves** — one generic `IiosInteractionAnnotation` primitive (opaque
`annotationType`+`value`); socket `annotate` event → `annotation` broadcast;
`GET /v1/threads/my-annotations?type=save`.
- **@mentions → Inbox** — `send(... mentions[])` (opaque userId list) → `InboxProjector` fans out
a `MENTION` inbox item to mentioned participants; reading resolves it.
- **Media** — `StoragePort` (dev = local disk `MEDIA_DIR`; prod swap to S3/Supabase), presigned
upload/download (signed HS256 tokens, OPA-gated size/type, tenant-fenced), `attachment` on
`MessageDto`; parts use generic `MEDIA_REF/VOICE_REF/FILE_REF`.
- **Notifications** — presence-gated Web Push: `NotificationProjector` runs 3 gates
(policy=DM/mention/reply-to-you · presence=`focus_thread` signal · per-thread `muted`) →
swappable `NotificationPort` (Web Push/VAPID); dead sub (410) pruned. Presence is in-memory
(single-instance) → Redis for multi-replica.
- `senderId` (stable externalId) on `MessageDto` for reliable "is this mine?".
~205 tests. Keep `docs/IIOS_API_AND_SDK_GUIDE.md` current when adding endpoints.
Related: [[reference_chat_web_consumer]], [[feedback_generic_safety]].
@@ -0,0 +1,20 @@
---
name: reference_chat_web_consumer
description: chat-web is the reference consumer app that drives IIOS feature work
metadata:
type: reference
---
**chat-web** (separate repo, `~/Documents/insignia-work/chat-web`) is the reference app on IIOS —
a 1:1 + group chat UI (Vite + React + TanStack Router/Query + socket.io-client + supabase-js).
It's frontend-only; IIOS provides identity, threads, messages, realtime, reactions, mentions,
media, notifications.
Feature work usually spans **both repos**: a generic primitive/port in iios + the app UI in
chat-web. The layer split we follow: **service** owns storage/auth/governance, the **SDK layer**
(`chat-web/src/lib/*`, mirrors `@insignia/iios-kernel-client`) owns client plumbing
(e.g. `uploadMedia`/`mediaUrl`, `registerPush`), the **app** owns rendering.
chat-web uses real Supabase login (`VITE_SUPABASE_URL` + anon key in its `.env`); identity =
email. Run it with `pnpm dev`. Commit both repos with git email `maaz@insigniaconsultancy.com`.
Related: [[project_recent_features]].
+30
View File
@@ -0,0 +1,30 @@
---
name: reference_run_and_test
description: How to run the service + the test-DB isolation and the replay.spec flake workaround
metadata:
type: reference
---
**Infra:** Postgres in docker `iios-db` on **:5434** (db `iios`), Redis on :6379. If Docker is
down: `open -a OrbStack` then `docker start iios-db`.
**Run** (dev auth via Supabase; media + push enabled):
```
pnpm --filter @insignia/iios-service exec nest build # rebuild after backend changes
SUPABASE_URL=https://<ref>.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
```
Restarting: `lsof -ti :3200 | xargs kill -9` first (stale instance → EADDRINUSE / old build served).
**Tests:** `pnpm test` (Vitest) runs against an **isolated `iios_test` DB** (globalSetup creates
+ migrates it; `DATABASE_URL` overridden) — it **never wipes the dev `iios` DB**. TDD: spec next
to code; DB specs use `resetDb()`.
**⚠ Known flake — `outbox/replay.spec`:** clock/ordering-sensitive, pre-existing. If it fails in a
full run, it's stale `iios_test` state, NOT a regression. Fix:
`docker exec iios-db psql -U iios -d postgres -c "DROP DATABASE IF EXISTS iios_test WITH (FORCE)"`
then re-run. Prove it's not yours by stashing changes and re-running.
`pnpm boundary` = import-boundary check (must stay OK). Smokes: `packages/iios-service/scripts/smoke-*.mjs`.
+27
View File
@@ -0,0 +1,27 @@
name: publish-sdks
# Publish the @insignia/iios-* SDK packages to the Gitea npm registry on a version tag.
# Requires: Gitea Actions enabled + a runner, and a repo secret GITEA_PUBLISH_TOKEN
# (a token with `write:package` scope for the `insignia` org).
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
- run: pnpm install --frozen-lockfile
- run: pnpm -r build
- run: pnpm -r publish --no-git-checks
env:
# maps to ${GITEA_TOKEN} in .npmrc; private packages (service, testkit) are skipped
GITEA_TOKEN: ${{ secrets.GITEA_PUBLISH_TOKEN }}
@@ -42,7 +42,6 @@ jobs:
- name: Bump k8s-pods image tag (ArgoCD deploys)
run: |
rm -rf /tmp/kp # dind-builder is a persistent host: clear any stale checkout from a prior run
git clone --depth 1 -b main \
"https://mcp-bot:${{ secrets.K8S_PODS_TOKEN }}@git.lynkedup.cloud/platform-engineering/k8s-pods.git" /tmp/kp
cd /tmp/kp
+6
View File
@@ -0,0 +1,6 @@
# @insignia SDK packages publish to / install from the Gitea package registry.
# Auth comes from the GITEA_TOKEN env var (never commit the token itself).
# publish → token needs the `write:package` scope
# install → token needs `read:package`
@insignia:registry=https://git.lynkedup.cloud/api/packages/insignia/npm/
//git.lynkedup.cloud/api/packages/insignia/npm/:_authToken=${GITEA_TOKEN}
+122
View File
@@ -0,0 +1,122 @@
# IIOS — Claude Code Rules
> **Project memory:** granular, versioned per-topic notes live in **`.claude/memory/`** — read
> `.claude/memory/MEMORY.md` (the index) and the relevant entries before working in an area, and
> add/update memories as the project evolves. This file is the canonical rules; those are the
> accumulating notes.
## 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://<ref>.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)
P0P8: 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.
+53
View File
@@ -0,0 +1,53 @@
# Publishing the IIOS SDKs (Gitea package registry)
The `@insignia/iios-*` **frontend SDKs** publish to our self-hosted Gitea npm registry:
```
https://git.lynkedup.cloud/api/packages/insignia/npm/
```
**Published (public in the registry):** `iios-contracts`, `iios-kernel-client`, `iios-adapter-sdk`,
and the React hook packages `iios-message-web`, `iios-inbox-web`, `iios-support-web`,
`iios-ai-web`, `iios-community-web`, `iios-meeting-web`.
**Kept private (never published):** `iios-service` (the deployed backend) and `iios-testkit` (dev fakes).
> be-crm does **not** consume these — it talks to IIOS over REST. The SDKs are a **frontend** concern
> (chat-web, the CRM support UI, mobile).
## One-time: get a token
Gitea → **Settings → Applications → Generate Token**:
- to **publish**: scope `write:package`
- to **install** (private packages): scope `read:package`
Export it (never commit it):
```bash
export GITEA_TOKEN=<your-gitea-token>
```
The repo `.npmrc` already routes the `@insignia` scope to Gitea and reads `${GITEA_TOKEN}`.
## Publish
```bash
pnpm release:dry # build all + pack (no upload) — verify the 9 packages pack cleanly
pnpm release # build all + publish the non-private packages to Gitea
```
`pnpm -r publish` automatically **skips** `private` packages, so only the 9 SDKs go out.
Versions are **immutable** — bump before re-publishing (edit `version`, or adopt `changesets`).
Or push a tag and let CI do it (see `.gitea/workflows/publish-sdks.yml`; needs Gitea Actions +
a runner + the `GITEA_PUBLISH_TOKEN` secret):
```bash
git tag v0.1.0 && git push origin v0.1.0
```
## Consume (in chat-web / the CRM front-end)
Add an `.npmrc` to the consuming repo:
```ini
@insignia:registry=https://git.lynkedup.cloud/api/packages/insignia/npm/
//git.lynkedup.cloud/api/packages/insignia/npm/:_authToken=${GITEA_TOKEN}
```
Then:
```bash
export GITEA_TOKEN=<read-token>
pnpm add @insignia/iios-kernel-client @insignia/iios-contracts
```
This replaces the **vendored** client that chat-web copies today — one source of truth for all frontends.
+4
View File
@@ -54,6 +54,10 @@ for the full, commented list. Highlights:
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.
- **Notifications (Web Push):** set `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` / `VAPID_SUBJECT`
to enable push (unset → the engine no-ops). ⚠️ Presence (the "are you viewing this thread?"
gate) is **in-memory / single-instance** today — with N>1 replicas, back `PresenceService`
with Redis so the projector on one replica sees focus from another.
- **⚠️ `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.
+51 -1
View File
@@ -243,6 +243,54 @@ sequenceDiagram
---
### 5.11 Notifications (push, presence-gated)
The engine reacts to `message.sent` and pushes to **absent** recipients through a swappable
**NotificationPort** (Web Push/VAPID now; email/FCM later). The DB holds only push
subscriptions + a per-thread mute flag; the reusable "who has an unread" feed stays `IiosInboxItem`.
| Method | Path | Body | Returns |
|---|---|---|---|
| GET | `/v1/notifications/vapid-public-key` | — | `{key}` — the public VAPID key the client needs to subscribe |
| POST | `/v1/notifications/subscribe` | `{kind?, endpoint, keys:{p256dh, auth}, userAgent?}` | `{ok:true}` — store/refresh the caller's push subscription |
| DELETE | `/v1/notifications/subscribe` | `{endpoint}` | `{ok:true}` |
| POST | `/v1/threads/:id/mute` · `/unmute` | — | `{threadId, muted}` — per-thread notification mute for the caller |
**Presence (socket):** the app emits `focus_thread` `{threadId | null}` on the `/message`
namespace whenever the foreground conversation changes (or the tab blurs). This is how the
engine knows you're *viewing* a thread — **room membership ≠ viewing**, because the sidebar
joins every thread room for live updates. `GET /v1/threads` returns each thread's `muted`.
**The three gates (fail-closed, in the notification policy — not the kernel):**
1. **Policy** — DM always; a group message only if you were `@`-mentioned **or** it's a reply to you.
2. **Presence** — skip if you're currently focused on that thread (you saw it live).
3. **Mute** — skip if you muted the thread.
A dead subscription (Web Push `404/410`) is pruned. DM-vs-group is read from the opaque
`membership` thread attribute here in the *notification policy*; the kernel never branches on it.
```mermaid
flowchart TB
SENT["message.sent (outbox → bus)"] --> PROJ["NotificationProjector"]
PROJ --> LOOP{{"for each recipient (never the sender)"}}
LOOP --> G1{"POLICY: DM? · @mention? · reply-to-you?"}
G1 -->|no| X1["skip"]
G1 -->|yes| G2{"PRESENCE: focused on this thread?"}
G2 -->|yes| X2["skip — seen live"]
G2 -->|no| G3{"MUTE: thread muted?"}
G3 -->|yes| X3["skip"]
G3 -->|no| SUBS["load subscriptions"] --> PORT["NotificationPort.deliver()"]
PORT --> WP["Web Push (VAPID)"]
WP -->|sent| OUT["→ browser push service → service worker → OS notification"]
WP -->|"gone (404/410)"| PRUNE["prune dead subscription"]
```
**Client (SDK layer, `lib/notifications.ts` → belongs in `@insignia/iios-kernel-client`):**
`registerPush()` (permission → register service worker → `PushManager.subscribe` with the
VAPID key → POST the subscription), `muteThread(threadId, muted)`, and `MessageSocket.focus(threadId)`.
A service worker renders the OS notification on `push` and deep-links to the thread on click.
---
## 6. SDK reference
Two layers: a low-level `RestClient` (+ socket) and per-domain React hook packages.
@@ -256,6 +304,7 @@ Methods (all return typed promises):
- **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`.*
- **Notifications:** `registerPush()` (service worker + `PushManager.subscribe` + POST subscription), `muteThread(threadId, muted)`, `MessageSocket.focus(threadId)` (presence signal). The engine (projector + Web Push port) is server-side; the SDK/app own registration + rendering.
- **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)`
@@ -333,7 +382,7 @@ Run against a live service (from `packages/iios-service`, `node scripts/<name>`)
| `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` (192 tests). Import-boundary check: `pnpm boundary`.
Automated unit/integration suite: `pnpm test` (205 tests). Import-boundary check: `pnpm boundary`.
---
@@ -358,6 +407,7 @@ Automated unit/integration suite: `pnpm test` (192 tests). Import-boundary check
| `MEDIA_DIR` | `<tmp>/iios-media` | local media storage dir (dev `StoragePort`) |
| `MEDIA_SECRET` | `dev-media-secret` | signs media upload/download URLs |
| `PUBLIC_URL` | `http://localhost:$PORT` | base used to build presigned media URLs |
| `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` / `VAPID_SUBJECT` | — | Web Push (notifications). Unset → push disabled (engine no-ops). Generate once: `node -e "console.log(require('web-push').generateVAPIDKeys())"` |
| `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 |
+3 -3
View File
@@ -159,9 +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:** 192 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:** 205 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.
**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, media sharing** (images/video/audio/docs on a swappable storage port), and **presence-gated push notifications** (Web Push via a swappable notification port). Each was a thin, generic addition — no chat-specific logic in the kernel — which is the reuse thesis paying off.
---
@@ -177,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, 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.*
*Appendix — repo facts: 11 packages, 6 demo apps, one NestJS service, 54 Postgres tables across 20 migrations (kernel → messaging → inbox → support → adapters → routing → ai → calendar → annotations/mentions → media → notifications). Boundary-enforced dependency law; 205 passing tests; 8+ end-to-end smoke scripts. First real-provider swaps live: Supabase auth (JWKS-verified), a media storage port, and Web Push notifications.*
+113
View File
@@ -0,0 +1,113 @@
# Insignia Platform — Live State (from the mesh-verify probe, 2026-07-10)
> Distilled from the authenticated `mesh-verify.lynkedup.cloud` dashboard (`/api/results` +
> `/api/journey`). This is the **real** platform IIOS is meant to plug into — service inventory,
> the identity/session/governance flow, and the exact contracts to wire IIOS's platform ports.
> Tokens redacted (the raw JSON dumps contain live SAT/PAT/refresh tokens — do not commit them).
## Cluster / mesh
- **Cluster:** `lynkedup-tech` (NYC2 / DigitalOcean). **Istio** (istio-envoy) + **SPIFFE/SPIRE**,
trust domain **`spiffe://insignia.tech`** (SVIDs like `spiffe://insignia.tech/ns/sre/sa/default`,
`.../sa/realmdm-sas`). mTLS **PERMISSIVE**. Reached by ClusterIP DNS.
- **Namespaces:** `sre` (MDM, OPA, misc), `cmp` (consent platform), `insignia` (identity/session/
app-facing services), `istio-system`.
- Public edges: `*.lynkedup.cloud` (behind oauth2-proxy → Keycloak).
## The identity/session/governance flow (7 steps — the doctrine)
> **Separate authorities:** Session Broker *authenticates*, OPA *authorizes*, CMP decides *purpose*,
> RealMDM *resolves identity*. Purpose-proof ≠ authorization. The PAT carries the external `sub`
> only as a **SHA-256 hash**, never raw; `canonical_person_id` is null until MDM VERIFIES (MDM never
> blocks login).
1. **Anonymous consent (CMP Edge)** — browser CMP SDK → `POST /edge/v1/cache-policy` → EdDSA-signed
cache-category manifest; a ConsentReceipt goes to CMP over gRPC. Purpose proof only.
2. **External login (Supabase)****SAT** (ES256 JWT). Claims: `iss` (project `/auth/v1`), opaque
UUID `sub`, `aud=authenticated`, `email`, `user_metadata` (full_name, avatar_url), `aal`, `amr`.
Proves the *session*, not the person/permission. Verified via Supabase **JWKS** (kid-selected).
3. **Session Broker exchange SAT → PAT**`POST /v1/sessions/exchange` (Bearer SAT +
`X-Client-Authorization` = BFF Keycloak client-creds, aud=session-broker). Broker verifies the
SAT, calls the MDM bridge, resolves scope from **memberships**, mints the **PAT** (~5 min).
3b. **Workload identity (SPIFFE/mTLS)** — each meshed pod gets an X.509-SVID; OPA receives **both**
the user (`principal`) and the caller (`caller.spiffe_id`) — different layers, never merged.
4. **MDM Auth-Subject Bridge**`POST /v1/auth-subjects/resolve {issuer, subject}` → `{platform_
principal_id, canonical_person_id (null until VERIFIED), link_state (PENDING/VERIFIED),
link_version, match_method}`. Keyed on **iss+sub** (never email). Idempotent.
5. **OPA decision**`POST /v1/decisions` → `{decision_id, allow, reason_codes, obligations,
policy_version}`. Obligations = masks / row-filters / denied fields / audit level / ttl. A
decision, not a 50-line entitlement JWT — the PEP MUST enforce every obligation.
6. **AppShell assembles the ACE** — combines PAT + OPA obligations + CMP consent into an App
Context Envelope (HttpOnly cookie): `capabilities[]` (policy-derived), `ui_obligations`
(hide/mask/step_up), `consent`. **No tokens, no raw PII in the browser.**
7. **CRM renders** — applies OPA `row_filter` (SQL WHERE) + `mask_fields` + `deny_fields`
server-side; rows reference `canonical_person_id`, not email.
## Service inventory + real endpoints
**Identity / session (`insignia` ns):**
- **Session Broker** `session-broker.insignia:80``POST /v1/sessions/exchange` (SAT→PAT).
- **Memberships** `memberships.insignia` (public `insignia-memberships.lynkedup.cloud`) —
`POST /v1/internal/resolve`, `GET /v1/memberships` (Bearer PAT) → scope tuple + `allowed[]`.
- **AppShell BFF** `appshell-bff.insignia:80` (public `insignia-appshell.lynkedup.cloud`) —
`GET /apps/crm-web/bootstrap` (Bearer PAT) → ACE.
- **Profile** `profile.insignia:80``GET /v1/profile`, `GET /v1/stats`. Backed by **sqlite3**
(`/data/profiles.db`, PVC `insignia-profile-data`, WAL, persistent, single-replica RWO).
- **CRM** `crm.insignia:80``GET /v1/leads?view=list` (applies obligations).
- **Policy Gateway** `policy-gateway.insignia:80``POST /v1/decisions`, `POST /v1/decisions/batch`.
**MDM (`sre` ns):** `mdm-kernel.sre:80` (`/v1/auth-subjects/resolve`, `/healthz`, `/readyz`; auth =
Keycloak service token **aud=realmdm**) · `mdm-ai.sre:80` · `mdm-sas.sre:9090` (**gRPC** — tokenization/SAS).
**OPA / policy (`sre` ns):** `opa.sre:8181` (`/health`, `GET /v1/data` = live policy tree;
default-deny) · `realmdm-opa.sre:8181` · `opal-server.sre:7002` (OPAL policy distribution).
**CMP (`cmp` ns), backed by Postgres + NATS + Redis:** `cmp-core:8080` (real `CheckConsent` gRPC;
service token aud=cmp) · `cmp-admin:8086` · `cmp-evidence:8081` · `cmp-sync:8085` ·
`cmp-tollgate:8082` (NATS JetStream gating) · `cmp-media:8083` · `cmp-worker:8084`. Plus
`insignia-consent-edge.insignia` (`POST /edge/v1/cache-policy`) and `insignia-consent-adapter.insignia`
(`POST /v1/consent/evaluate {purpose}``{permitted, legal_basis, state, consent_epoch}`).
**Other (`sre` ns):** `poi-api:5000` · `roof:8002` (YOLO segmentation) · `commit:8081` ·
`presign:8080` · `egs:8090` · `artifact-retrieval:8082` (was 503 on probe day) · `cmp-docs`.
## The two contracts IIOS must match
### PAT (Platform Access Token) — what IIOS should verify
`iss = https://identity.insignia.internal` · **ES256** (EC P-256), verify via the broker **JWKS**
(`.../.well-known/jwks.json`, kid-selected — **no shared secret**) · `aud` includes the app (e.g.
`crm-web`) · `lifetime ~300s`. Claims:
```
sub = platform_principal_id app_id tenant_id org_id bu_id
region environment role aal amr auth_source
external_subject_hash = sha256:… (NOT the raw external sub)
canonical_person_id (null until VERIFIED)
session_epoch · policy_epoch · consent_epoch (stale-detection)
sid (platform_session_id) · typ = platform-access+jwt
```
### OPA decision — `POST http://policy-gateway.insignia.svc.cluster.local/v1/decisions`
Request `{ input: { principal{platform_principal_id, canonical_person_id, auth_source, aal, roles,
memberships[]}, caller{spiffe_id}, resource{type, id, tenant_id, classification[]}, action,
context{purpose, device_trust, consent_receipt_ids[], network_zone, time} } }`
Response `{ decision_id, allow, reason_codes[], obligations{ row_filter, allow_fields[], mask_fields{},
deny_fields[], audit, decision_ttl_seconds }, policy_version }`.
## What this means for wiring IIOS's ports
1. **Auth — verify the PAT, not the Supabase SAT.** IIOS today verifies the Supabase SAT directly
(a dev shortcut). In the real platform the **Session Broker** does SAT→PAT; a platform workload
verifies the **PAT**. The PAT already carries the full scope tuple + `platform_principal_id`, so
`MessagePrincipal` maps ~1:1: `userId = platform_principal_id` (→ `canonical_person_id` once
VERIFIED), `appId = app_id`, `orgId = org_id`, `tenantId = tenant_id`, `+ buId`. Wire it by adding
the broker as an `AUTH_ISSUERS` entry (iss `https://identity.insignia.internal`, ES256, its JWKS).
2. **OPA — point `OpaPort` at the Policy Gateway** (`POST /v1/decisions`). Build `{principal, caller.
spiffe_id, resource, action, context}` from the PAT + the op; `decideOrThrow` maps `allow` → proceed
and **must enforce the obligations** (masks/row-filter/deny). The gateway's input is richer than
IIOS's current `{action,…}` — that's the adapter's job to assemble.
3. **MDM — usually don't call it.** The PAT already carries `platform_principal_id`/`canonical_person_id`
(the broker resolved at login). Only call `/v1/auth-subjects/resolve` if IIOS is the identity-exchange
edge (it isn't — the Broker is).
4. **CMP — consent gate** via `insignia-consent-adapter /v1/consent/evaluate {purpose}` when processing
content for AI/analytics/marketing.
5. **SAS — tokenization/masking** is `mdm-sas` (gRPC) — the "tokenize sensitive parts before storage"
requirement.
*Source: two API payloads captured 2026-07-10 (`result.json` = probes, `journey.json` = the CRM
first-vertical-slice journey). Re-capture from the authenticated dashboard to refresh.*
+3 -1
View File
@@ -7,7 +7,9 @@
"build": "pnpm -r build",
"typecheck": "pnpm -r typecheck",
"test": "vitest run",
"boundary": "node scripts/check-import-boundary.mjs"
"boundary": "node scripts/check-import-boundary.mjs",
"release:dry": "pnpm -r build && pnpm -r publish --dry-run --no-git-checks",
"release": "pnpm -r build && pnpm -r publish --no-git-checks"
},
"devDependencies": {
"@types/node": "^26.0.1",
+7 -3
View File
@@ -1,15 +1,19 @@
{
"name": "@insignia/iios-adapter-sdk",
"version": "0.0.0",
"private": true,
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@insignia/iios-contracts": "workspace:*"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+13 -4
View File
@@ -1,13 +1,19 @@
{
"name": "@insignia/iios-ai-web",
"version": "0.0.0",
"private": true,
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit"
@@ -23,5 +29,8 @@
"react": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+13 -4
View File
@@ -1,13 +1,19 @@
{
"name": "@insignia/iios-community-web",
"version": "0.0.0",
"private": true,
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit"
@@ -23,5 +29,8 @@
"react": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+7 -3
View File
@@ -1,12 +1,16 @@
{
"name": "@insignia/iios-contracts",
"version": "0.0.0",
"private": true,
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+13 -4
View File
@@ -1,13 +1,19 @@
{
"name": "@insignia/iios-inbox-web",
"version": "0.0.0",
"private": true,
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit"
@@ -23,5 +29,8 @@
"react": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+13 -4
View File
@@ -1,13 +1,19 @@
{
"name": "@insignia/iios-kernel-client",
"version": "0.0.0",
"private": true,
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit"
@@ -19,5 +25,8 @@
"devDependencies": {
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+13 -4
View File
@@ -1,13 +1,19 @@
{
"name": "@insignia/iios-meeting-web",
"version": "0.0.0",
"private": true,
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit"
@@ -23,5 +29,8 @@
"react": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+13 -4
View File
@@ -1,13 +1,19 @@
{
"name": "@insignia/iios-message-web",
"version": "0.0.0",
"private": true,
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit"
@@ -23,5 +29,8 @@
"react": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+1 -4
View File
@@ -30,10 +30,7 @@
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"socket.io": "^4.8.3",
"web-push": "^3.6.7",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/sdk-node": "^0.220.0",
"@opentelemetry/auto-instrumentations-node": "^0.78.0"
"web-push": "^3.6.7"
},
"devDependencies": {
"@insignia/iios-testkit": "workspace:*",
-1
View File
@@ -1,4 +1,3 @@
import './observability/tracing'; // MUST be first — auto-instrumentation patches modules on require
import 'dotenv/config';
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
@@ -1,5 +1,4 @@
import type { Request, Response, NextFunction } from 'express';
import { trace } from '@opentelemetry/api';
import { runWithTrace, newTraceId, traceIdFromTraceparent } from './trace-context';
import { logJson } from './logger';
@@ -10,13 +9,8 @@ import { logJson } from './logger';
* the response finishes (method, path, status, ms). Wired via app.use() in main.ts.
*/
export function traceMiddleware(req: Request, res: Response, next: NextFunction): void {
// Prefer the ACTIVE OpenTelemetry trace id: that is what makes this log line
// joinable to its distributed trace in Grafana (Loki -> Tempo). Falls back to
// the inbound header, then traceparent, then a generated id, so a correlation
// id is always present even when tracing is disabled.
const otelTraceId = trace.getActiveSpan()?.spanContext().traceId;
const headerTrace = (req.headers['x-trace-id'] as string | undefined)?.trim();
const traceId = otelTraceId || headerTrace || traceIdFromTraceparent(req.headers['traceparent'] as string | undefined) || newTraceId();
const traceId = headerTrace || traceIdFromTraceparent(req.headers['traceparent'] as string | undefined) || newTraceId();
res.setHeader('x-trace-id', traceId);
const startedAt = Date.now();
runWithTrace(traceId, () => {
@@ -1,50 +0,0 @@
/**
* OpenTelemetry bootstrap. MUST be imported before anything else in main.ts
* auto-instrumentation works by patching modules (http, express, pg, redis, )
* as they are require()d, so any module loaded before this runs is never traced.
*
* Env-driven on purpose: the OTel SDK reads OTEL_SERVICE_NAME,
* OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_PROTOCOL itself, so the
* collector target is a deployment concern rather than a code change. When
* OTEL_EXPORTER_OTLP_ENDPOINT is unset the SDK never starts, so local dev and
* tests run with zero tracing overhead and no exporter errors.
*
* Traces land in Tempo and are viewable in Grafana. The existing P9 trace
* middleware adopts the active OTel trace id, so an `http.request` log line and
* its distributed trace share one id (Loki -> Tempo pivot).
*/
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
if (endpoint) {
const sdk = new NodeSDK({
instrumentations: [
getNodeAutoInstrumentations({
// Noisy and low value: every file read becomes a span.
'@opentelemetry/instrumentation-fs': { enabled: false },
// k8s probes hit /health constantly; tracing them would swamp Tempo
// and bury the real request traces.
'@opentelemetry/instrumentation-http': {
ignoreIncomingRequestHook: (req) => {
const url = req.url ?? '';
return ['/health', '/ready', '/healthz', '/readyz', '/metrics'].some((p) =>
url.startsWith(p),
);
},
},
}),
],
});
sdk.start();
const shutdown = (): void => {
// Flush buffered spans before exit, else the last requests before a
// rollout are lost.
void sdk.shutdown().finally(() => process.exit(0));
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
}
+13 -4
View File
@@ -1,13 +1,19 @@
{
"name": "@insignia/iios-support-web",
"version": "0.0.0",
"private": true,
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit"
@@ -24,5 +30,8 @@
"react": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+35 -1826
View File
File diff suppressed because it is too large Load Diff