2 Commits

Author SHA1 Message Date
Claude eb385707fa feat(observability): OpenTelemetry tracing + log/trace correlation
CI / build (push) Successful in 3m49s
Deploy iios-service / build-deploy (push) Successful in 4m19s
Adds OTel auto-instrumentation (http, express, prisma/pg, redis, ...) exporting
OTLP to Tempo, so iios-service requests appear as distributed traces in Grafana.

observability/tracing.ts is imported FIRST in main.ts — auto-instrumentation
patches modules as they are require()d, so anything loaded earlier is never
traced. Env-driven (OTEL_SERVICE_NAME / OTEL_EXPORTER_OTLP_ENDPOINT); with the
endpoint unset the SDK never starts, so dev and tests carry zero overhead.
Health/metrics probes are excluded — they would otherwise swamp Tempo and bury
real request traces.

The existing P9 trace middleware now adopts the ACTIVE OTel trace id, so an
http.request log line and its distributed trace share one id (Loki -> Tempo
pivot in Grafana), falling back to x-trace-id / traceparent / generated.

pnpm-lock.yaml regenerated — CI and the Dockerfile both use
'pnpm install --frozen-lockfile', which would hard-fail on an unchanged lock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 13:14:53 +00:00
mcp-bot 0bdae453fc ci(iios): rm -rf /tmp/kp before clone (persistent runner leaves stale dir)
Deploy iios-service / build-deploy (push) Successful in 4m8s
CI / build (push) Successful in 5m7s
The dind-builder runner is host-mode/persistent, so /tmp/kp from a prior run
survives and 'git clone ... /tmp/kp' fails with 'destination path already exists
and is not an empty directory'. Clear it first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 18:14:42 +00:00
31 changed files with 1928 additions and 687 deletions
-12
View File
@@ -1,12 +0,0 @@
# 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
@@ -1,28 +0,0 @@
---
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
@@ -1,19 +0,0 @@
---
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
@@ -1,23 +0,0 @@
---
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
@@ -1,31 +0,0 @@
---
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]].
@@ -1,20 +0,0 @@
---
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
@@ -1,30 +0,0 @@
---
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
@@ -1,27 +0,0 @@
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,6 +42,7 @@ 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
@@ -1,6 +0,0 @@
# @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
@@ -1,122 +0,0 @@
# 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
@@ -1,53 +0,0 @@
# 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,10 +54,6 @@ 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.
+1 -51
View File
@@ -243,54 +243,6 @@ 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.
@@ -304,7 +256,6 @@ 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)`
@@ -382,7 +333,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` (205 tests). Import-boundary check: `pnpm boundary`.
Automated unit/integration suite: `pnpm test` (192 tests). Import-boundary check: `pnpm boundary`.
---
@@ -407,7 +358,6 @@ Automated unit/integration suite: `pnpm test` (205 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:** 205 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, 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.
**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.
---
@@ -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, 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.*
*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.*
-113
View File
@@ -1,113 +0,0 @@
# 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.*
+1 -3
View File
@@ -7,9 +7,7 @@
"build": "pnpm -r build",
"typecheck": "pnpm -r typecheck",
"test": "vitest run",
"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"
"boundary": "node scripts/check-import-boundary.mjs"
},
"devDependencies": {
"@types/node": "^26.0.1",
+3 -7
View File
@@ -1,19 +1,15 @@
{
"name": "@insignia/iios-adapter-sdk",
"version": "0.1.0",
"version": "0.0.0",
"private": true,
"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/"
}
}
+4 -13
View File
@@ -1,19 +1,13 @@
{
"name": "@insignia/iios-ai-web",
"version": "0.1.0",
"version": "0.0.0",
"private": true,
"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"
@@ -29,8 +23,5 @@
"react": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+4 -13
View File
@@ -1,19 +1,13 @@
{
"name": "@insignia/iios-community-web",
"version": "0.1.0",
"version": "0.0.0",
"private": true,
"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"
@@ -29,8 +23,5 @@
"react": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+3 -7
View File
@@ -1,16 +1,12 @@
{
"name": "@insignia/iios-contracts",
"version": "0.1.0",
"version": "0.0.0",
"private": true,
"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/"
}
}
+4 -13
View File
@@ -1,19 +1,13 @@
{
"name": "@insignia/iios-inbox-web",
"version": "0.1.0",
"version": "0.0.0",
"private": true,
"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"
@@ -29,8 +23,5 @@
"react": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+4 -13
View File
@@ -1,19 +1,13 @@
{
"name": "@insignia/iios-kernel-client",
"version": "0.1.0",
"version": "0.0.0",
"private": true,
"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"
@@ -25,8 +19,5 @@
"devDependencies": {
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+4 -13
View File
@@ -1,19 +1,13 @@
{
"name": "@insignia/iios-meeting-web",
"version": "0.1.0",
"version": "0.0.0",
"private": true,
"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"
@@ -29,8 +23,5 @@
"react": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+4 -13
View File
@@ -1,19 +1,13 @@
{
"name": "@insignia/iios-message-web",
"version": "0.1.0",
"version": "0.0.0",
"private": true,
"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"
@@ -29,8 +23,5 @@
"react": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+4 -1
View File
@@ -30,7 +30,10 @@
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"socket.io": "^4.8.3",
"web-push": "^3.6.7"
"web-push": "^3.6.7",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/sdk-node": "^0.220.0",
"@opentelemetry/auto-instrumentations-node": "^0.78.0"
},
"devDependencies": {
"@insignia/iios-testkit": "workspace:*",
+1
View File
@@ -1,3 +1,4 @@
import './observability/tracing'; // MUST be first — auto-instrumentation patches modules on require
import 'dotenv/config';
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
@@ -1,4 +1,5 @@
import type { Request, Response, NextFunction } from 'express';
import { trace } from '@opentelemetry/api';
import { runWithTrace, newTraceId, traceIdFromTraceparent } from './trace-context';
import { logJson } from './logger';
@@ -9,8 +10,13 @@ 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 = headerTrace || traceIdFromTraceparent(req.headers['traceparent'] as string | undefined) || newTraceId();
const traceId = otelTraceId || headerTrace || traceIdFromTraceparent(req.headers['traceparent'] as string | undefined) || newTraceId();
res.setHeader('x-trace-id', traceId);
const startedAt = Date.now();
runWithTrace(traceId, () => {
@@ -0,0 +1,50 @@
/**
* 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);
}
+4 -13
View File
@@ -1,19 +1,13 @@
{
"name": "@insignia/iios-support-web",
"version": "0.1.0",
"version": "0.0.0",
"private": true,
"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"
@@ -30,8 +24,5 @@
"react": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
}
}
+1826 -35
View File
File diff suppressed because it is too large Load Diff