Compare commits
4 Commits
main
..
64c498a97a
| Author | SHA1 | Date | |
|---|---|---|---|
| 64c498a97a | |||
| 5abee1b5b7 | |||
| 58ebaf1982 | |||
| 6fc03fa4c3 |
@@ -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
|
||||
@@ -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]].
|
||||
@@ -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.
|
||||
@@ -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]].
|
||||
@@ -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 P0–P8 (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]].
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
@@ -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)
|
||||
|
||||
P0–P8: kernel → messaging → inbox → support → adapters → routing → AI → calendar/meetings.
|
||||
Recent (the "P9 real-providers" era, mostly driven by the chat app):
|
||||
- **Real Supabase auth** — multi-issuer JWKS verification (`SessionVerifier`); first stubbed
|
||||
port turned real.
|
||||
- **Reactions / pins / saves** — one generic `IiosInteractionAnnotation` primitive
|
||||
(opaque type/value), `annotate` socket event, `GET /v1/threads/my-annotations`.
|
||||
- **@mentions → Inbox** — `mentions[]` on send → `MENTION` inbox item (projector).
|
||||
- **Media** — `StoragePort` (dev local disk → prod S3/Supabase), presigned upload/download,
|
||||
attachment on `MessageDto`.
|
||||
- **Notifications** — presence-gated Web Push: `NotificationProjector` (policy/presence/mute
|
||||
gates) + swappable `NotificationPort`, `focus_thread` presence signal, per-thread mute.
|
||||
- `senderId` (stable externalId) on messages for reliable "is this mine?".
|
||||
|
||||
## Docs (as-built)
|
||||
|
||||
- `docs/IIOS_API_AND_SDK_GUIDE.md` — the **as-built REST/socket/SDK reference** (endpoints,
|
||||
shapes, env, vocab). Keep it current when adding endpoints.
|
||||
- `docs/DEPLOYMENT.md` — deploy/topology/env/scaling.
|
||||
- `docs/IIOS_OVERVIEW_FOR_CEO.md` — plain-language capability tour.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+35
-1826
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user