build: publish @insignia/iios-* SDKs to the Gitea package registry

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 17:13:55 +05:30
parent 5abee1b5b7
commit 64c498a97a
14 changed files with 307 additions and 35 deletions
+27
View File
@@ -0,0 +1,27 @@
name: publish-sdks
# Publish the @insignia/iios-* SDK packages to the Gitea npm registry on a version tag.
# Requires: Gitea Actions enabled + a runner, and a repo secret GITEA_PUBLISH_TOKEN
# (a token with `write:package` scope for the `insignia` org).
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
- run: pnpm install --frozen-lockfile
- run: pnpm -r build
- run: pnpm -r publish --no-git-checks
env:
# maps to ${GITEA_TOKEN} in .npmrc; private packages (service, testkit) are skipped
GITEA_TOKEN: ${{ secrets.GITEA_PUBLISH_TOKEN }}
+6
View File
@@ -0,0 +1,6 @@
# @insignia SDK packages publish to / install from the Gitea package registry.
# Auth comes from the GITEA_TOKEN env var (never commit the token itself).
# publish → token needs the `write:package` scope
# install → token needs `read:package`
@insignia:registry=https://git.lynkedup.cloud/api/packages/insignia/npm/
//git.lynkedup.cloud/api/packages/insignia/npm/:_authToken=${GITEA_TOKEN}
+53
View File
@@ -0,0 +1,53 @@
# Publishing the IIOS SDKs (Gitea package registry)
The `@insignia/iios-*` **frontend SDKs** publish to our self-hosted Gitea npm registry:
```
https://git.lynkedup.cloud/api/packages/insignia/npm/
```
**Published (public in the registry):** `iios-contracts`, `iios-kernel-client`, `iios-adapter-sdk`,
and the React hook packages `iios-message-web`, `iios-inbox-web`, `iios-support-web`,
`iios-ai-web`, `iios-community-web`, `iios-meeting-web`.
**Kept private (never published):** `iios-service` (the deployed backend) and `iios-testkit` (dev fakes).
> be-crm does **not** consume these — it talks to IIOS over REST. The SDKs are a **frontend** concern
> (chat-web, the CRM support UI, mobile).
## One-time: get a token
Gitea → **Settings → Applications → Generate Token**:
- to **publish**: scope `write:package`
- to **install** (private packages): scope `read:package`
Export it (never commit it):
```bash
export GITEA_TOKEN=<your-gitea-token>
```
The repo `.npmrc` already routes the `@insignia` scope to Gitea and reads `${GITEA_TOKEN}`.
## Publish
```bash
pnpm release:dry # build all + pack (no upload) — verify the 9 packages pack cleanly
pnpm release # build all + publish the non-private packages to Gitea
```
`pnpm -r publish` automatically **skips** `private` packages, so only the 9 SDKs go out.
Versions are **immutable** — bump before re-publishing (edit `version`, or adopt `changesets`).
Or push a tag and let CI do it (see `.gitea/workflows/publish-sdks.yml`; needs Gitea Actions +
a runner + the `GITEA_PUBLISH_TOKEN` secret):
```bash
git tag v0.1.0 && git push origin v0.1.0
```
## Consume (in chat-web / the CRM front-end)
Add an `.npmrc` to the consuming repo:
```ini
@insignia:registry=https://git.lynkedup.cloud/api/packages/insignia/npm/
//git.lynkedup.cloud/api/packages/insignia/npm/:_authToken=${GITEA_TOKEN}
```
Then:
```bash
export GITEA_TOKEN=<read-token>
pnpm add @insignia/iios-kernel-client @insignia/iios-contracts
```
This replaces the **vendored** client that chat-web copies today — one source of truth for all frontends.
+113
View File
@@ -0,0 +1,113 @@
# Insignia Platform — Live State (from the mesh-verify probe, 2026-07-10)
> Distilled from the authenticated `mesh-verify.lynkedup.cloud` dashboard (`/api/results` +
> `/api/journey`). This is the **real** platform IIOS is meant to plug into — service inventory,
> the identity/session/governance flow, and the exact contracts to wire IIOS's platform ports.
> Tokens redacted (the raw JSON dumps contain live SAT/PAT/refresh tokens — do not commit them).
## Cluster / mesh
- **Cluster:** `lynkedup-tech` (NYC2 / DigitalOcean). **Istio** (istio-envoy) + **SPIFFE/SPIRE**,
trust domain **`spiffe://insignia.tech`** (SVIDs like `spiffe://insignia.tech/ns/sre/sa/default`,
`.../sa/realmdm-sas`). mTLS **PERMISSIVE**. Reached by ClusterIP DNS.
- **Namespaces:** `sre` (MDM, OPA, misc), `cmp` (consent platform), `insignia` (identity/session/
app-facing services), `istio-system`.
- Public edges: `*.lynkedup.cloud` (behind oauth2-proxy → Keycloak).
## The identity/session/governance flow (7 steps — the doctrine)
> **Separate authorities:** Session Broker *authenticates*, OPA *authorizes*, CMP decides *purpose*,
> RealMDM *resolves identity*. Purpose-proof ≠ authorization. The PAT carries the external `sub`
> only as a **SHA-256 hash**, never raw; `canonical_person_id` is null until MDM VERIFIES (MDM never
> blocks login).
1. **Anonymous consent (CMP Edge)** — browser CMP SDK → `POST /edge/v1/cache-policy` → EdDSA-signed
cache-category manifest; a ConsentReceipt goes to CMP over gRPC. Purpose proof only.
2. **External login (Supabase)****SAT** (ES256 JWT). Claims: `iss` (project `/auth/v1`), opaque
UUID `sub`, `aud=authenticated`, `email`, `user_metadata` (full_name, avatar_url), `aal`, `amr`.
Proves the *session*, not the person/permission. Verified via Supabase **JWKS** (kid-selected).
3. **Session Broker exchange SAT → PAT**`POST /v1/sessions/exchange` (Bearer SAT +
`X-Client-Authorization` = BFF Keycloak client-creds, aud=session-broker). Broker verifies the
SAT, calls the MDM bridge, resolves scope from **memberships**, mints the **PAT** (~5 min).
3b. **Workload identity (SPIFFE/mTLS)** — each meshed pod gets an X.509-SVID; OPA receives **both**
the user (`principal`) and the caller (`caller.spiffe_id`) — different layers, never merged.
4. **MDM Auth-Subject Bridge**`POST /v1/auth-subjects/resolve {issuer, subject}` → `{platform_
principal_id, canonical_person_id (null until VERIFIED), link_state (PENDING/VERIFIED),
link_version, match_method}`. Keyed on **iss+sub** (never email). Idempotent.
5. **OPA decision** — `POST /v1/decisions` → `{decision_id, allow, reason_codes, obligations,
policy_version}`. Obligations = masks / row-filters / denied fields / audit level / ttl. A
decision, not a 50-line entitlement JWT — the PEP MUST enforce every obligation.
6. **AppShell assembles the ACE** — combines PAT + OPA obligations + CMP consent into an App
Context Envelope (HttpOnly cookie): `capabilities[]` (policy-derived), `ui_obligations`
(hide/mask/step_up), `consent`. **No tokens, no raw PII in the browser.**
7. **CRM renders** — applies OPA `row_filter` (SQL WHERE) + `mask_fields` + `deny_fields`
server-side; rows reference `canonical_person_id`, not email.
## Service inventory + real endpoints
**Identity / session (`insignia` ns):**
- **Session Broker** `session-broker.insignia:80` — `POST /v1/sessions/exchange` (SAT→PAT).
- **Memberships** `memberships.insignia` (public `insignia-memberships.lynkedup.cloud`) —
`POST /v1/internal/resolve`, `GET /v1/memberships` (Bearer PAT) → scope tuple + `allowed[]`.
- **AppShell BFF** `appshell-bff.insignia:80` (public `insignia-appshell.lynkedup.cloud`) —
`GET /apps/crm-web/bootstrap` (Bearer PAT) → ACE.
- **Profile** `profile.insignia:80` — `GET /v1/profile`, `GET /v1/stats`. Backed by **sqlite3**
(`/data/profiles.db`, PVC `insignia-profile-data`, WAL, persistent, single-replica RWO).
- **CRM** `crm.insignia:80` — `GET /v1/leads?view=list` (applies obligations).
- **Policy Gateway** `policy-gateway.insignia:80` — `POST /v1/decisions`, `POST /v1/decisions/batch`.
**MDM (`sre` ns):** `mdm-kernel.sre:80` (`/v1/auth-subjects/resolve`, `/healthz`, `/readyz`; auth =
Keycloak service token **aud=realmdm**) · `mdm-ai.sre:80` · `mdm-sas.sre:9090` (**gRPC** — tokenization/SAS).
**OPA / policy (`sre` ns):** `opa.sre:8181` (`/health`, `GET /v1/data` = live policy tree;
default-deny) · `realmdm-opa.sre:8181` · `opal-server.sre:7002` (OPAL policy distribution).
**CMP (`cmp` ns), backed by Postgres + NATS + Redis:** `cmp-core:8080` (real `CheckConsent` gRPC;
service token aud=cmp) · `cmp-admin:8086` · `cmp-evidence:8081` · `cmp-sync:8085` ·
`cmp-tollgate:8082` (NATS JetStream gating) · `cmp-media:8083` · `cmp-worker:8084`. Plus
`insignia-consent-edge.insignia` (`POST /edge/v1/cache-policy`) and `insignia-consent-adapter.insignia`
(`POST /v1/consent/evaluate {purpose}` → `{permitted, legal_basis, state, consent_epoch}`).
**Other (`sre` ns):** `poi-api:5000` · `roof:8002` (YOLO segmentation) · `commit:8081` ·
`presign:8080` · `egs:8090` · `artifact-retrieval:8082` (was 503 on probe day) · `cmp-docs`.
## The two contracts IIOS must match
### PAT (Platform Access Token) — what IIOS should verify
`iss = https://identity.insignia.internal` · **ES256** (EC P-256), verify via the broker **JWKS**
(`.../.well-known/jwks.json`, kid-selected — **no shared secret**) · `aud` includes the app (e.g.
`crm-web`) · `lifetime ~300s`. Claims:
```
sub = platform_principal_id app_id tenant_id org_id bu_id
region environment role aal amr auth_source
external_subject_hash = sha256:… (NOT the raw external sub)
canonical_person_id (null until VERIFIED)
session_epoch · policy_epoch · consent_epoch (stale-detection)
sid (platform_session_id) · typ = platform-access+jwt
```
### OPA decision — `POST http://policy-gateway.insignia.svc.cluster.local/v1/decisions`
Request `{ input: { principal{platform_principal_id, canonical_person_id, auth_source, aal, roles,
memberships[]}, caller{spiffe_id}, resource{type, id, tenant_id, classification[]}, action,
context{purpose, device_trust, consent_receipt_ids[], network_zone, time} } }`
Response `{ decision_id, allow, reason_codes[], obligations{ row_filter, allow_fields[], mask_fields{},
deny_fields[], audit, decision_ttl_seconds }, policy_version }`.
## What this means for wiring IIOS's ports
1. **Auth — verify the PAT, not the Supabase SAT.** IIOS today verifies the Supabase SAT directly
(a dev shortcut). In the real platform the **Session Broker** does SAT→PAT; a platform workload
verifies the **PAT**. The PAT already carries the full scope tuple + `platform_principal_id`, so
`MessagePrincipal` maps ~1:1: `userId = platform_principal_id` (→ `canonical_person_id` once
VERIFIED), `appId = app_id`, `orgId = org_id`, `tenantId = tenant_id`, `+ buId`. Wire it by adding
the broker as an `AUTH_ISSUERS` entry (iss `https://identity.insignia.internal`, ES256, its JWKS).
2. **OPA — point `OpaPort` at the Policy Gateway** (`POST /v1/decisions`). Build `{principal, caller.
spiffe_id, resource, action, context}` from the PAT + the op; `decideOrThrow` maps `allow` → proceed
and **must enforce the obligations** (masks/row-filter/deny). The gateway's input is richer than
IIOS's current `{action,…}` — that's the adapter's job to assemble.
3. **MDM — usually don't call it.** The PAT already carries `platform_principal_id`/`canonical_person_id`
(the broker resolved at login). Only call `/v1/auth-subjects/resolve` if IIOS is the identity-exchange
edge (it isn't — the Broker is).
4. **CMP — consent gate** via `insignia-consent-adapter /v1/consent/evaluate {purpose}` when processing
content for AI/analytics/marketing.
5. **SAS — tokenization/masking** is `mdm-sas` (gRPC) — the "tokenize sensitive parts before storage"
requirement.
*Source: two API payloads captured 2026-07-10 (`result.json` = probes, `journey.json` = the CRM
first-vertical-slice journey). Re-capture from the authenticated dashboard to refresh.*
+3 -1
View File
@@ -7,7 +7,9 @@
"build": "pnpm -r build", "build": "pnpm -r build",
"typecheck": "pnpm -r typecheck", "typecheck": "pnpm -r typecheck",
"test": "vitest run", "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": { "devDependencies": {
"@types/node": "^26.0.1", "@types/node": "^26.0.1",
+7 -3
View File
@@ -1,15 +1,19 @@
{ {
"name": "@insignia/iios-adapter-sdk", "name": "@insignia/iios-adapter-sdk",
"version": "0.0.0", "version": "0.1.0",
"private": true,
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"files": ["dist"], "files": [
"dist"
],
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit" "typecheck": "tsc -p tsconfig.json --noEmit"
}, },
"dependencies": { "dependencies": {
"@insignia/iios-contracts": "workspace:*" "@insignia/iios-contracts": "workspace:*"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
} }
} }
+13 -4
View File
@@ -1,13 +1,19 @@
{ {
"name": "@insignia/iios-ai-web", "name": "@insignia/iios-ai-web",
"version": "0.0.0", "version": "0.1.0",
"private": true,
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.js", "module": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, "exports": {
"files": ["dist"], ".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": { "scripts": {
"build": "tsup", "build": "tsup",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
@@ -23,5 +29,8 @@
"react": "^19.0.0", "react": "^19.0.0",
"tsup": "^8.3.5", "tsup": "^8.3.5",
"typescript": "^5.7.3" "typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
} }
} }
+13 -4
View File
@@ -1,13 +1,19 @@
{ {
"name": "@insignia/iios-community-web", "name": "@insignia/iios-community-web",
"version": "0.0.0", "version": "0.1.0",
"private": true,
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.js", "module": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, "exports": {
"files": ["dist"], ".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": { "scripts": {
"build": "tsup", "build": "tsup",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
@@ -23,5 +29,8 @@
"react": "^19.0.0", "react": "^19.0.0",
"tsup": "^8.3.5", "tsup": "^8.3.5",
"typescript": "^5.7.3" "typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
} }
} }
+7 -3
View File
@@ -1,12 +1,16 @@
{ {
"name": "@insignia/iios-contracts", "name": "@insignia/iios-contracts",
"version": "0.0.0", "version": "0.1.0",
"private": true,
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"files": ["dist"], "files": [
"dist"
],
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit" "typecheck": "tsc -p tsconfig.json --noEmit"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
} }
} }
+13 -4
View File
@@ -1,13 +1,19 @@
{ {
"name": "@insignia/iios-inbox-web", "name": "@insignia/iios-inbox-web",
"version": "0.0.0", "version": "0.1.0",
"private": true,
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.js", "module": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, "exports": {
"files": ["dist"], ".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": { "scripts": {
"build": "tsup", "build": "tsup",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
@@ -23,5 +29,8 @@
"react": "^19.0.0", "react": "^19.0.0",
"tsup": "^8.3.5", "tsup": "^8.3.5",
"typescript": "^5.7.3" "typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
} }
} }
+13 -4
View File
@@ -1,13 +1,19 @@
{ {
"name": "@insignia/iios-kernel-client", "name": "@insignia/iios-kernel-client",
"version": "0.0.0", "version": "0.1.0",
"private": true,
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.js", "module": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, "exports": {
"files": ["dist"], ".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": { "scripts": {
"build": "tsup", "build": "tsup",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
@@ -19,5 +25,8 @@
"devDependencies": { "devDependencies": {
"tsup": "^8.3.5", "tsup": "^8.3.5",
"typescript": "^5.7.3" "typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
} }
} }
+13 -4
View File
@@ -1,13 +1,19 @@
{ {
"name": "@insignia/iios-meeting-web", "name": "@insignia/iios-meeting-web",
"version": "0.0.0", "version": "0.1.0",
"private": true,
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.js", "module": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, "exports": {
"files": ["dist"], ".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": { "scripts": {
"build": "tsup", "build": "tsup",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
@@ -23,5 +29,8 @@
"react": "^19.0.0", "react": "^19.0.0",
"tsup": "^8.3.5", "tsup": "^8.3.5",
"typescript": "^5.7.3" "typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
} }
} }
+13 -4
View File
@@ -1,13 +1,19 @@
{ {
"name": "@insignia/iios-message-web", "name": "@insignia/iios-message-web",
"version": "0.0.0", "version": "0.1.0",
"private": true,
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.js", "module": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, "exports": {
"files": ["dist"], ".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": { "scripts": {
"build": "tsup", "build": "tsup",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
@@ -23,5 +29,8 @@
"react": "^19.0.0", "react": "^19.0.0",
"tsup": "^8.3.5", "tsup": "^8.3.5",
"typescript": "^5.7.3" "typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
} }
} }
+13 -4
View File
@@ -1,13 +1,19 @@
{ {
"name": "@insignia/iios-support-web", "name": "@insignia/iios-support-web",
"version": "0.0.0", "version": "0.1.0",
"private": true,
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.js", "module": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, "exports": {
"files": ["dist"], ".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": { "scripts": {
"build": "tsup", "build": "tsup",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
@@ -24,5 +30,8 @@
"react": "^19.0.0", "react": "^19.0.0",
"tsup": "^8.3.5", "tsup": "^8.3.5",
"typescript": "^5.7.3" "typescript": "^5.7.3"
},
"publishConfig": {
"registry": "https://git.lynkedup.cloud/api/packages/insignia/npm/"
} }
} }