docs: publish governance, governed boosts, receipts, OIDC phase 2, files SSO

Engineering guide: session/refresh/logout architecture, publish-governance
and amplify gating sections, file map to migrations 001-034. Admin guide:
governance administration, files-SSO operations, verification harnesses,
migration list + deploy order + 034 session note. User manual: calendar
receipt badges, boost gating, connections governance note (replaces the
stale 'stub' statuses). Design doc 2026-10-13: marked implemented with the
as-shipped phase 2. Wiki (EN/ES): Drive sign-in + boosts/approvals.
README: governance + Files SSO bullets.
This commit is contained in:
glass beadian 2026-10-14 11:52:48 +00:00
parent 643fbb94bc
commit b8671113f5
7 changed files with 113 additions and 21 deletions

View file

@ -60,8 +60,10 @@ Cross-tenant functions take an explicit `p_workspace_id` and call `fn_assert_ten
### 3.4 Continuous authorization (audit finding #2)
Any membership insert/update/delete fires `bump_authz_epoch()` — `COALESCE(NEW.workspace_id, OLD.workspace_id)` (OLD is NULL on INSERT; the original bug silently no-oped). Sessions embed the epoch at issue; `RbacGuard` compares against the live `workspaces.authz_epoch` and fails closed (`401, epoch stale`) on mismatch. Revocation propagates to live sessions without waiting for re-login.
### 3.5 Sessions & OIDC
HMAC-SHA256 signed tokens (`apps/api/src/auth/session.ts`), 12h TTL, constant-time verify. OIDC: generic code flow + PKCE S256, discovery-document driven (`auth/oidc.ts`) — Authentik/Keycloak/any compliant issuer via `OIDC_*` env.
### 3.5 Sessions, logout & the Hub as OIDC issuer
HMAC-SHA256 signed tokens (`apps/api/src/auth/session.ts`), 12h TTL, constant-time verify. Since migration `034`, every session also carries a `sid` backed by an `auth_sessions` row: login/reissue write the row, `POST /auth/logout` revokes it, and `reissue` refuses both revoked rows and pre-`034` tokens (no `sid`) so revocation applies from the next epoch bump. Request validation (`RbacGuard`) stays stateless — rows are consulted only by reissue and the OIDC refresh path.
The Hub is also its own OpenID Connect **provider** for oCIS (`apps/api/src/oidc/`, issuer `https://api.occupymedia.cc/oidc`): discovery, JWKS (ES256, keys sealed via the vault pattern), `/oidc/authorize` → SPA consent → single-use code (60 s, PKCE S256 enforced) → tokens. Only the first-party client `web` (the oCIS web app) is registered; anything else fails closed. Phase 2 added **session-bound rotating refresh tokens** (`oidc_refresh_tokens`, SHA-256 at rest, `family_id` chains): a chain lives and dies with the consenting Hub session, reuse of a consumed token revokes the whole family (OAuth BCP), and every accept re-checks that the session row is live and the membership active — so Hub logout propagates to oCIS at the next refresh instead of at the next silent authorize. The unauthenticated token endpoint resolves tokens through the definer-rights `oidc_refresh_lookup()` (the `audit_append` pattern). Matrix SSO (`apps/api/src/matrix-sso/`) is a separate 2-minute `m.login.token` broker and intentionally out of propagation scope: Element holds its own Matrix-native session after mint.
### 3.6 Credential vault
Envelope encryption (`vault/vault.ts`): per-workspace DEK (AES-256-GCM) wrapped by master KEK (`SUPERPOSTER_KEK`, 32B base64; production-required). Rotation re-wraps; old DEK dropped → pre-rotation ciphertext unreadable (tested). Connectors receive opaque `CredentialRef` handles via `CredentialAccessor` — never ciphertext, never raw tokens.
@ -105,6 +107,23 @@ Hash-chained per branch (`audit_append` for tenant rows, `audit_append_system` f
- **State machine** (`publishing.service.ts`): `editing → in_review → approved → scheduled → published` (+ `rejected`, `cancelled`). Every transition permission-checked (`approval.request` / `approval.grant` / `publication.schedule`) and audited.
- **Durability** (`006_publishing.sql`): `scheduled_publications` carries a `UNIQUE idempotency_key`; the DB row commits first; if Temporal is unreachable the row stays `pending` and the worker's startup sweep picks it up.
- **Temporal** (`scheduler/src/workflows/publish.workflow.ts`): `scheduledPublish(id)` activity (5-min timeout) claims pending-only, resolves the connector via registry, publishes, records the publication, idempotent on re-run.
- **Receipts**: `GET /publishing/drafts/:draftId/publications` returns per-attempt outcomes (`ready/in_progress/completed/failed/unknown/abandoned`, `remoteId/remoteUrl`); the calendar page renders the latest outcome per scheduled draft as a badge with an open-remote link.
### 5.1 Per-connection publish governance (migration 033)
`connections.fields.publishGovernance` decides what happens when an approved draft targets that connection:
| Setting | Behavior on approval |
|---|---|
| unset | today's manual flow: a `publication.publish` holder calls `POST :draftId/publish` |
| `auto` | the approval itself schedules the publish (console approve or passing proposal poll) |
| `admin1` | a second chatgov poll (`subject_kind='draft_publish'`) opens; one admin yes releases |
| `admin_majority` | same, `quorum [{admins, x: floor(n/2)+1}]` snapshotted at open |
| `admin_consensus` | same, `[{admins, x: n}] join all`, veto allowed |
Route: `PATCH /connections/:id/publish-governance` (requires `chatgov.manage`). The publish route answers `202 {state:"awaiting_publish_approval", pollId, closesAt, rule}` while held; a passing poll schedules the publish as the original requester (re-checked at claim). The scheduler's **E3 pre-dispatch check** refuses any dispatch to a gated destination without a passed publish poll for the current approved revision — including schedules created before the gate was switched on. One open publish poll per (draft, destination), index-enforced, never colliding with review polls.
### 5.2 Governed boost (amplify)
`POST /publishing/drafts/:draftId/amplify {connectionId, kind: "repost"|"quote"}` executes the connector's `amplify` against `providerMetadata.sourceRemoteRef` recorded at draft creation — never a caller-supplied id. Target gates: draft state `approved` + the connection's publish governance. A gated boost holds with `202 awaiting_publish_approval` on the same publish-phase poll and is released by re-invoking amplify after the pass (hold-and-retry: no durable amplify queue exists, and the poll-close transaction must not do network I/O). Unset/`auto` connections behave exactly as before (`200` + execute).
## 6. Frontend (apps/web)
@ -140,12 +159,14 @@ Live Source Monitor (adaptive polling, anti-flapping, per-host budgets); media e
| Concern | Path |
|---|---|
| Tenant context / pools | `apps/api/src/db/client.ts` |
| Migrations 001–006 | `apps/api/migrations/` |
| Migrations 001–034 | `apps/api/migrations/` |
| Vault / storage crypto | `apps/api/src/vault/vault.ts`, `apps/api/src/storage/storage.ts` |
| Sessions / OIDC | `apps/api/src/auth/` |
| Sessions / OIDC provider (issuer + refresh) | `apps/api/src/auth/`, `apps/api/src/oidc/` |
| RBAC guard | `apps/api/src/rbac/rbac.guard.ts` |
| Audit chain | `migrations/001_tenants.sql` (SQL) + `apps/api/src/audit/` |
| Publishing | `apps/api/src/publishing/`, `apps/scheduler/src/` |
| Publishing + publish governance + amplify | `apps/api/src/publishing/`, `apps/scheduler/src/activities/dispatch-gate.ts` |
| Matrix SSO broker | `apps/api/src/matrix-sso/` |
| Connector SDK | `packages/connector-sdk/src/` |
| Frontend mock-registry | `apps/web/src/lib/` |
| Lint: no-provider-branching | `eslint.config.mjs` |
| E2E: governance sim (S1–S9), brand palette guard | `scripts/e2e/governance-sim.mjs`, `scripts/e2e/brand-palette-check.mjs` |