@tideorg/mcp 1.4.2 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/GAP_REGISTER.md +18 -8
  3. package/PRIVACY.md +41 -0
  4. package/README.md +207 -197
  5. package/adapters/AGENTS.md +3 -2
  6. package/adapters/CLAUDE.md +1 -1
  7. package/canon/anti-patterns.md +55 -62
  8. package/canon/concepts.md +46 -34
  9. package/canon/custom-contracts.md +12 -8
  10. package/canon/feature-mapping.md +27 -75
  11. package/canon/framework-matrix.md +69 -22
  12. package/canon/hosting-options.md +111 -0
  13. package/canon/iga-change-requests-api.md +211 -0
  14. package/canon/invariants.md +33 -35
  15. package/canon/security-gap-mapping.md +506 -0
  16. package/canon/security-runtime-probes.md +177 -0
  17. package/canon/tidecloak-bootstrap.md +21 -16
  18. package/canon/tidecloak-endpoints.md +60 -98
  19. package/canon/troubleshooting.md +115 -53
  20. package/canon/version-policy.md +8 -12
  21. package/mcp-server/dist/server.js +221 -19
  22. package/package.json +48 -45
  23. package/playbooks/add-auth-nextjs-existing.md +33 -17
  24. package/playbooks/add-auth-nextjs-fresh.md +1 -1
  25. package/playbooks/add-rbac-nextjs.md +7 -2
  26. package/playbooks/bootstrap-realm-from-template.md +33 -18
  27. package/playbooks/deploy-tidecloak-docker.md +31 -28
  28. package/playbooks/diagnose-missing-roles-or-claims.md +2 -2
  29. package/playbooks/initialize-admin-and-link-account.md +22 -19
  30. package/playbooks/protect-api-nextjs.md +40 -1
  31. package/playbooks/protect-aspnet-core-asgard.md +313 -0
  32. package/playbooks/protect-routes-nextjs.md +24 -10
  33. package/playbooks/provision-tidecloak-skycloak.md +213 -0
  34. package/playbooks/setup-forseti-e2ee.md +32 -50
  35. package/playbooks/setup-iga-admin-panel.md +113 -171
  36. package/playbooks/verify-jwt-server-side.md +20 -1
  37. package/prompts/build-private-customer-portal.md +1 -1
  38. package/prompts/security-gap-analysis.md +55 -0
  39. package/reference-apps/encrypted-communication/anti-patterns.md +3 -3
  40. package/reference-apps/encrypted-communication/bootstrap-sequence.md +2 -2
  41. package/reference-apps/encrypted-communication/scenario.md +2 -2
  42. package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +8 -8
  43. package/reference-apps/iga-admin-governance/anti-patterns.md +18 -16
  44. package/reference-apps/iga-admin-governance/bootstrap-sequence.md +10 -5
  45. package/reference-apps/iga-admin-governance/role-policy-matrix.md +12 -13
  46. package/reference-apps/iga-admin-governance/scenario.md +15 -13
  47. package/reference-apps/organisation-password-manager/bootstrap-sequence.md +5 -6
  48. package/reference-apps/policy-governed-signing/bootstrap-sequence.md +9 -9
  49. package/skills/tide-diagnostics/SKILL.md +1 -1
  50. package/skills/tide-integration/SKILL.md +9 -18
  51. package/skills/tide-mcp-qa/SKILL.md +139 -0
  52. package/skills/tide-rbac-and-e2ee/SKILL.md +5 -5
  53. package/skills/tide-reviewer/SKILL.md +1 -1
  54. package/skills/tide-security-analyst/SKILL.md +182 -0
  55. package/skills/tide-setup/SKILL.md +1 -1
  56. package/canon/delegation.md +0 -195
  57. package/playbooks/setup-server-delegation.md +0 -142
@@ -0,0 +1,211 @@
1
+ # IGA / QEA Change-Requests API (current surface)
2
+
3
+ The authoritative REST surface for IGA (QEA — Quorum Enforced Authorization) change requests. **VERIFIED** (source: `tide-foundation/tidecloak-iga-extensions` → `docs/qea-iga-api.md`, iga-core; confirmed by Tide 2026-07-07, GAP-065).
4
+
5
+ **This `/iga/change-requests/...` surface REPLACES the legacy `/tide-admin/change-set/...` surface.** The old surface (documented in the repo-root `CHANGE_REQUESTS_API.md`) is stale — do not use it for new work. Enabling IGA is unchanged: `POST /tide-admin/toggle-iga` (realm attribute `isIGAEnabled="true"`).
6
+
7
+ For the security/trust implications and the Tide-vs-Tideless mode split, see `canon/feature-mapping.md` (IGA Quorum Approval), `canon/invariants.md` (I-10), and `canon/security-gap-mapping.md` (IGA-model note).
8
+
9
+ ---
10
+
11
+ ## Base path & auth
12
+
13
+ ```
14
+ {keycloak-url}/admin/realms/{realm}/iga/...
15
+ {keycloak-url}/admin/realms/{realm}/iga-tve/... # TVE diagnostic bundles
16
+ {keycloak-url}/admin/realms/{realm}/tide-admin/... # toggle-iga, legacy
17
+ ```
18
+
19
+ Header: `Authorization: Bearer $TOKEN` — requires `manage-realm` (some reads are lighter, noted below).
20
+
21
+ ---
22
+
23
+ ## Change-request lifecycle
24
+
25
+ | Step | Method + path | Body | Success | Notable failures |
26
+ |------|---------------|------|---------|------------------|
27
+ | A governed admin write is captured | (the original admin call, e.g. assign role) | — | **202 Accepted** + `Location` header → the new CR | — |
28
+ | List pending | `GET /iga/change-requests?status=PENDING` | — | 200, array of CR objects | — |
29
+ | Get one | `GET /iga/change-requests/{id}` | — | 200 | 404 |
30
+ | Authorize (sign) — simple lane | `POST /iga/change-requests/{id}/authorize` | `{}` (optional) | 200, updated CR (**does not commit**) | 403 (missing approver role), 409 (not PENDING / already signed / conflicting CR / a MultiAdmin CR) |
31
+ | Approve — MultiAdmin enclave lane | `POST /iga/change-requests/{id}/approve` | enclave approval payload | 200, updated CR (**auto-commits once quorum met**) | 403, 409, 412 |
32
+ | Commit | `POST /iga/change-requests/{id}/commit` | — | 200, CR now APPROVED | 412 (under threshold or unmet dependency) |
33
+ | Edit pending CR | `PUT /iga/change-requests/{id}` | `{"rows":[...]}` | 200, updated CR | 404, 409 (not PENDING) |
34
+ | Deny | `POST /iga/change-requests/{id}/deny` | — | 204 | 404 |
35
+ | Bulk authorize | `POST /iga/change-requests/bulk-authorize` | `{"actionTypeIn":["CREATE","DELETE"],"limit":100}` | 200, `{results:[...], summary:{...}}` | 429 (a bulk run is already in progress — per-realm mutex) |
36
+
37
+ **`status`** values: `PENDING`, `APPROVED`, `DENIED`, `CANCELLED`.
38
+
39
+ ### `authorize` vs `approve` vs `commit` — do not conflate
40
+
41
+ Three distinct server operations (source: `IgaAdminResource` on origin/main). The button label in the admin console is **not** the same as the REST verb:
42
+
43
+ - **`POST .../{id}/authorize`** (`IgaAdminResource:529`) — the **simple lane** (FirstAdmin / Tideless). Records the caller's approval and **NEVER commits**: even when `authCount >= threshold` the server does *not* call `combineFinal()`; commit is a separate explicit step. **Refuses tide-MultiAdmin CRs.**
44
+ - **`POST .../{id}/approve`** (`IgaAdminResource:1096`) — the **MultiAdmin enclave lane**. Consumes the enclave approval and **approves AND auto-commits once quorum is met**. This is what the deployed admin console's **"Authorize" button** calls (button label ≠ REST `/authorize`).
45
+ - **`POST .../{id}/commit`** — explicit **apply-only** step (`412` if still sub-quorum / dependency unmet). Used after a simple-lane `authorize` reaches threshold.
46
+
47
+ **`PUT /iga/change-requests/{id}`** (`IgaAdminResource:1848`, `updateChangeRequest` → `service.updateRows()`) edits a still-`PENDING` CR's `rows` payload; body `{"rows":[...]}`.
48
+
49
+ **Bulk-authorize authorizes; it does not commit.** After bulk-authorize, commit the CRs that became `readyToCommit` (see the bootstrap pattern below). ("Bulk Commit" in the admin UI is a client-side loop, not a server endpoint.)
50
+
51
+ ### Two-phase MultiAdmin approval (Tide mode enclave)
52
+
53
+ In Tide mode (`iga.attestor=tide`) with MultiAdmin, the simple `authorize` lane refuses the CR; approval instead goes through `approve`, which requires an enclave signature obtained via the approval-model exchange:
54
+
55
+ ```
56
+ Phase 1: GET /iga/change-requests/{id}/approval-model
57
+ → 200 {"changeRequestId":"...","requestModel":"<base64>"}
58
+ (decode requestModel, present to the Tide enclave, admin signs → base64 doken)
59
+
60
+ Phase 2: POST /iga/change-requests/{id}/approve (MultiAdmin enclave lane — approves and auto-commits at quorum)
61
+ Body: the enclave approval / {"requestModel":"<base64 doken>"}
62
+ → 200 {"recorded":true,"authCount":N,"threshold":M}
63
+ ```
64
+
65
+ **This is the automation boundary.** In FirstAdmin mode (fresh realm before any `tide-realm-admin`) and in Tideless mode, the simple-lane `authorize` signs server-side with no enclave popup (then `commit` applies), so bootstrap is fully automatable. In Tide MultiAdmin mode a human must sign the enclave challenge for `approve`.
66
+
67
+ ---
68
+
69
+ ## Change-request object (response fields)
70
+
71
+ ```
72
+ id, realmId, entityType, entityId, actionType,
73
+ status, # PENDING | APPROVED | DENIED | CANCELLED
74
+ requestedBy, createdAt, resolvedAt, resolvedBy,
75
+ rows, # payload replayed on commit
76
+ authorizationCount, authorizers[], threshold,
77
+ readyToCommit, # boolean — safe to commit now
78
+ requiredApproverRoles, scopeMode, # scopeMode: "any" | "all"
79
+ dependsOn, blocked, blockedReason # dependency gating
80
+ ```
81
+
82
+ **Use `id` for all per-CR operations** (this replaces the legacy `draftRecordId`). `entityType`/`actionType` replace the legacy `changeSetType`/`changeSetType`+`actionType` fields, and there is no `{changeSets:[...]}` request envelope anymore — operations are per-`id`.
83
+
84
+ ---
85
+
86
+ ## Comments
87
+
88
+ ```
89
+ GET /iga/change-requests/{id}/comments → 200 array
90
+ POST /iga/change-requests/{id}/comments {"comment":"text"} (≤2000 chars) → 201
91
+ PUT /iga/change-requests/{id}/comments/{commentId} → 200 (author only)
92
+ DELETE /iga/change-requests/{id}/comments/{commentId} → 204 (author or realm admin)
93
+ ```
94
+
95
+ ## Manual ADOPT
96
+
97
+ ```
98
+ POST /iga/adopt {"entityType":"USER","entityId":"..."} → 201 | 409 (ALREADY_ATTESTED)
99
+ ```
100
+
101
+ ## Role policies (M0 admin policy)
102
+
103
+ ```
104
+ GET /iga/role-policies # read: authenticated, no manage-realm
105
+ GET /iga/role-policies/{id}
106
+ GET /iga/role-policies/name/tide-realm-admin
107
+ POST /iga/role-policies {"name":"...","policy":"<base64>","policySig":"..."} # 403 if name="tide-realm-admin"
108
+ DELETE /iga/role-policies/{id}
109
+ DELETE /iga/role-policies/name/{name}
110
+ ```
111
+
112
+ ## Diagnostics
113
+
114
+ ```
115
+ GET /iga/change-requests/{id}/diagnostic-bundle
116
+ → {"diag_kind":"iga_cr_bundle","schema_version":1,"cr":{...},"authorizations":[...],"threshold":N,"approver_role":"..."}
117
+ POST /iga-tve/tve-bundle {"mode":"synthesize|pasted","clientId":"...","userId":"..."} (Accept: application/json | application/cbor)
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Status codes (summary)
123
+
124
+ | Code | Meaning |
125
+ |------|---------|
126
+ | 202 | Governed action captured into a CR (`Location` → the CR) |
127
+ | 400 | Malformed body / missing field |
128
+ | 403 | Missing approver role |
129
+ | 404 | Unknown CR or the entity vanished |
130
+ | 409 | CR not PENDING, already signed, or a conflicting CR exists (four-eyes) |
131
+ | 412 | Under threshold, or a dependency is unmet |
132
+ | 429 | `bulk-authorize` already running (per-realm mutex) |
133
+
134
+ ## Threshold & approver resolution
135
+
136
+ - `ADOPT_*` CRs short-circuit to threshold 1 (no approver-role check).
137
+ - Otherwise the entity-level `iga.threshold` applies **only if that entity also sets `iga.approverRole`** (the coupling rule); else the realm `iga.threshold`; else 1 (clamped `max(1, …)` — the gate can't be disabled).
138
+ - **FirstAdmin** mode (fresh realm, before any `tide-realm-admin`): threshold 1, any `manage-realm` admin signs, server-side VRK, no enclave.
139
+ - **MultiAdmin** (Tide): dynamic floor `max(1, floor(0.7 × active tide-realm-admins))` unless overridden; enclave signature required.
140
+
141
+ ---
142
+
143
+ ## Bootstrap pattern (FirstAdmin / Tideless — fully automatable)
144
+
145
+ During bootstrap the realm is in FirstAdmin (or Tideless) mode, so `authorize` signs server-side and the whole loop is scriptable. Replace the legacy per-type `sign`+`commit` calls with: **authorize all pending → commit the ready ones, in dependency passes.**
146
+
147
+ ```bash
148
+ # $TC = base url, $REALM = realm, get_token() mints a fresh admin token.
149
+ approve_all_pending() {
150
+ local TOKEN
151
+ # 1. Authorize every pending CREATE/DELETE change request in one call.
152
+ # FirstAdmin/Tideless: server signs with VRK / records username — no enclave.
153
+ TOKEN="$(get_token)"
154
+ curl -s -X POST "$TC/admin/realms/$REALM/iga/change-requests/bulk-authorize" \
155
+ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
156
+ -d '{"actionTypeIn":["CREATE","DELETE"],"limit":100}' > /dev/null
157
+
158
+ # 2. Commit everything that is ready. Loop a few passes so dependent CRs
159
+ # (e.g. a role must exist before its assignment) become ready and commit.
160
+ for pass in 1 2 3 4 5; do
161
+ TOKEN="$(get_token)"
162
+ local ready
163
+ ready=$(curl -s "$TC/admin/realms/$REALM/iga/change-requests?status=PENDING" \
164
+ -H "Authorization: Bearer $TOKEN" | jq -r '.[] | select(.readyToCommit==true) | .id')
165
+ [ -z "$ready" ] && break
166
+ for id in $ready; do
167
+ TOKEN="$(get_token)"
168
+ curl -s -X POST "$TC/admin/realms/$REALM/iga/change-requests/$id/commit" \
169
+ -H "Authorization: Bearer $TOKEN" > /dev/null
170
+ done
171
+ done
172
+ }
173
+ ```
174
+
175
+ Call `approve_all_pending` at each stage that produces CRs (after client creation, after user creation, after role assignment) — the same stage points as the legacy script, but it now lists **all** pending CRs at once rather than per type.
176
+
177
+ **Per-id alternative** (if you prefer not to use bulk-authorize):
178
+
179
+ ```bash
180
+ TOKEN="$(get_token)"
181
+ for id in $(curl -s "$TC/admin/realms/$REALM/iga/change-requests?status=PENDING" \
182
+ -H "Authorization: Bearer $TOKEN" | jq -r '.[].id'); do
183
+ TOKEN="$(get_token)"
184
+ curl -s -X POST "$TC/admin/realms/$REALM/iga/change-requests/$id/authorize" \
185
+ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}' > /dev/null
186
+ done
187
+ # then the same commit-in-passes loop as above
188
+ ```
189
+
190
+ **Status of the bootstrap pattern: VERIFIED against the API spec; REQUIRES_RUNTIME_VALIDATION against a live iga-core instance.** The endpoint paths, payloads, and status codes are from the canonical `qea-iga-api.md`; the exact loop/ordering has not yet been run end-to-end here. Validate on a live instance and adjust the pass count / dependency handling if a CR stays `blocked`.
191
+
192
+ ---
193
+
194
+ ## Old → new migration map
195
+
196
+ | Legacy (`/tide-admin/change-set/...`) | Current (`/iga/change-requests/...`) |
197
+ |----------------------------------------|--------------------------------------|
198
+ | `GET .../change-set/{type}/requests` | `GET /iga/change-requests?status=PENDING` |
199
+ | `GET .../change-set/counts` | (derive from the list, or use diagnostics) |
200
+ | `POST .../change-set/sign/batch` `{changeSets:[{changeSetId,changeSetType,actionType}]}` | `POST /iga/change-requests/{id}/authorize` `{}` (per id); batch: `POST /iga/change-requests/bulk-authorize` |
201
+ | `POST .../change-set/commit/batch` `{changeSets:[...]}` | `POST /iga/change-requests/{id}/commit` |
202
+ | `POST .../change-set/cancel/batch` | `POST /iga/change-requests/{id}/deny` |
203
+ | `POST .../tideAdminResources/add-review` (enclave) | `POST /iga/change-requests/{id}/approval-model` |
204
+ | field `draftRecordId` | field `id` |
205
+ | fields `changeSetType` + `actionType` | fields `entityType` + `actionType` |
206
+ | toggle `POST /tide-admin/toggle-iga` | **unchanged** |
207
+
208
+ ## Status Legend
209
+
210
+ - **VERIFIED** — from the canonical `qea-iga-api.md` (iga-core)
211
+ - **REQUIRES_RUNTIME_VALIDATION** — bootstrap loop not yet exercised end-to-end against a live instance
@@ -136,7 +136,7 @@ grep -r "hasRole\|checkRole" server/routes/ | grep -v "after verifyJWT"
136
136
  **Rule**: JWT verification must use embedded JWKS from adapter JSON. Do not use `createRemoteJWKSet`. Do not fetch keys from the remote OIDC certs endpoint. If the `jwk` field is missing from `tidecloak.json`, this is a setup/bootstrap failure — stop and fix the export, do not fall back to remote key fetching.
137
137
 
138
138
  **What this means**:
139
- - Adapter JSON contains `jwk: { keys: JWK[] }` field (EdDSA public key, only when IGA enabled)
139
+ - Adapter JSON contains `jwk: { keys: JWK[] }` field (EdDSA public key). It is emitted whenever the realm has a Tide vendor key; it is NOT gated on IGA being enabled.
140
140
  - Use `createLocalJWKSet(config.jwk)` from `jose`
141
141
  - If `config.jwk` is missing, throw an error. The fix is to re-export the adapter from TideCloak with IGA enabled, not to fetch keys remotely.
142
142
  - Also validate `azp` matches `config.resource`
@@ -145,6 +145,8 @@ grep -r "hasRole\|checkRole" server/routes/ | grep -v "after verifyJWT"
145
145
 
146
146
  **Forbidden**: `createRemoteJWKSet` must not be used in Tide verification flows. Do not add it as a fallback. Do not import it. If verification fails with `ERR_JWKS_NO_MATCHING_KEY`, the adapter JSON is stale — re-export it from TideCloak.
147
147
 
148
+ **Note on the shipped `@tidecloak/verify`**: the packaged `verifyTideCloakToken` DOES fall back to `createRemoteJWKSet` when `config.jwk` is absent (`tidecloak-js: packages/tidecloak-verify/src/TideJWT.js`). That fallback is exactly the network path this invariant forbids. Therefore the operative rule for Tide apps is: **always pass `config.jwk` so verification uses the local JWKS; never let it fall through to the remote path.** A missing `jwk` is a setup failure to fix at the source, not a code path to rely on. This is intentional security doctrine — the pack is deliberately stricter than the SDK default.
149
+
148
150
  **Required pattern**:
149
151
  ```typescript
150
152
  import { createLocalJWKSet, jwtVerify } from 'jose';
@@ -186,7 +188,7 @@ grep -r "createRemoteJWKSet" lib/auth/ src/lib/auth/
186
188
 
187
189
  **Related invariants**: I-03 (Server-Side Verification), I-05 (Adapter JSON Required)
188
190
 
189
- **Note**: The `jwk` field is only present when IGA is enabled on the realm. Validate at startup. If missing, the fix is TideCloak bootstrap (enable IGA, re-export adapter), not remote JWKS.
191
+ **Note**: The `jwk` field is present whenever the realm has a Tide vendor key (it is not gated on IGA). Validate at startup. If missing, the fix is TideCloak bootstrap / re-exporting the adapter, not remote JWKS.
190
192
 
191
193
  ---
192
194
 
@@ -197,7 +199,7 @@ grep -r "createRemoteJWKSet" lib/auth/ src/lib/auth/
197
199
  **Canonical filename**: `tidecloak.json`. Do not use `adapter.json`, `keycloak.json`, or other names.
198
200
 
199
201
  **What this means**:
200
- - Export adapter JSON from TideCloak admin console via `vendorResources/get-installations-provider?providerId=keycloak-oidc-keycloak-json` (this is the only valid provider ID `tidecloak-oidc-keycloak-json` does not exist in the codebase)
202
+ - Export adapter JSON from TideCloak via the realm-level endpoint `GET /admin/realms/{realm}/vendorResources/get-installations-provider?clientId={clientId}&providerId=keycloak-oidc-keycloak-json`. The client is a **query parameter** (`clientId=`), NOT a `/clients/{id}/` path segment. `keycloak-oidc-keycloak-json` is the only valid provider ID (`tidecloak-oidc-keycloak-json` does not exist).
201
203
  - Store at the correct location for the framework (see below)
202
204
  - Load at startup
203
205
  - Validate presence of Tide extensions
@@ -429,12 +431,14 @@ grep -r "if.*admin.*skip.*verify" server/
429
431
 
430
432
  **Rule**: Admin changes to roles, users, clients require quorum approval sealed by VVK threshold signatures. No single admin can act unilaterally. **VERIFIED** (IGA.mdx, SetupIGA.md, keylessh `init-tidecloak.sh`)
431
433
 
432
- **Quorum formula**: `max(1, floor(TotalAdmins * 0.7))` **VERIFIED** (SetupIGA.md)
434
+ **Quorum formula**: `max(1, floor(TotalAdmins * 0.7))` **VERIFIED** (SetupIGA.md). This is the cryptographic quorum in **Tide mode** (`iga.attestor=tide`). In **Tideless mode** (`iga.attestor=simple`/unset) the same quorum count is enforced by TideCloak server logic with a username attestation and no cryptography — see `canon/security-gap-mapping.md` IGA-model note.
435
+
436
+ > **⚠️ API SURFACE MIGRATION — confirmed by Tide 2026-07-07 (GAP-065).** The change-request endpoints have moved to **`/admin/realms/{realm}/iga/change-requests/...`** (iga-core), which **replaces** the legacy `/tide-admin/change-set/...` paths below. New surface: approve `POST .../iga/change-requests/{id}/authorize` (409 = four-eyes re-sign), commit `POST .../iga/change-requests/{id}/commit` (412 = quorum unmet), reject `POST .../{id}/deny`, batch `POST /iga/change-requests/bulk-authorize`, list `GET /iga/change-requests?status=PENDING`, Tide-mode enclave step `POST .../{id}/approval-model`. Enabling IGA is unchanged (`POST /tide-admin/toggle-iga`). **Full endpoint spec, payloads, and the bootstrap approve/commit loop: `canon/iga-change-requests-api.md`.** Bootstrap scripts and the examples below now use the new surface; verify against the live instance (bootstrap loop is REQUIRES_RUNTIME_VALIDATION).
433
437
 
434
- **Change-set workflow** **VERIFIED** (CHANGE_REQUEST_API.md):
438
+ **Change-request workflow** (legacy surface — see migration banner) **VERIFIED for legacy** (CHANGE_REQUEST_API.md):
435
439
  1. Create: Admin action triggers draft creation automatically
436
- 2. Approve: `POST /admin/realms/{realm}/tide-admin/change-set/sign/batch` (MultiAdmin mode returns enclave challenge)
437
- 3. Commit: `POST /admin/realms/{realm}/tide-admin/change-set/commit/batch`
440
+ 2. Approve: legacy `POST .../tide-admin/change-set/sign/batch` → new `POST .../iga/change-requests/{id}/authorize` (MultiAdmin/Tide mode returns enclave challenge)
441
+ 3. Commit: legacy `POST .../tide-admin/change-set/commit/batch` → new `POST .../iga/change-requests/{id}/commit`
438
442
 
439
443
  **What this means**:
440
444
  - Single admin cannot create, modify, or delete users
@@ -459,19 +463,19 @@ curl "${TIDECLOAK_URL}/admin/realms/${REALM}" | jq '.igaEnabled'
459
463
 
460
464
  # Attempt admin change without approval
461
465
  curl -X POST "${TIDECLOAK_URL}/admin/realms/${REALM}/users" -d '{...}'
462
- # Should create change-set, not immediate user creation
466
+ # Should return 202 (captured as a change request), not immediate user creation
463
467
 
464
- # Check pending change-sets
465
- curl "${TIDECLOAK_URL}/admin/realms/${REALM}/tide-admin/change-set/users/requests" \
466
- -H "Authorization: Bearer ${ADMIN_TOKEN}"
467
- # Should list pending change
468
+ # Check pending change requests (current /iga/ surface — see canon/iga-change-requests-api.md)
469
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}/iga/change-requests?status=PENDING" \
470
+ -H "Authorization: Bearer ${ADMIN_TOKEN}" | jq '.[].id'
471
+ # Should list pending change request ids
468
472
  ```
469
473
 
470
474
  **Consequence of violation**: Single compromised admin can create backdoor accounts, grant unauthorized roles, bypass all governance. IGA enforcement collapses to single-admin trust.
471
475
 
472
476
  **Related invariants**: I-09 (No Single Bypass)
473
477
 
474
- Change requests have a hardcoded 1-month (2628000s) cryptographic expiry enforced by the Tide enclave. After this window, signing is rejected. No automatic DB cleanup stale drafts persist until cancelled. Not configurable. **VERIFIED** (vendor confirmation, batch-02 Q-08, A-25 resolved).
478
+ Change requests carry a cryptographic expiry enforced by the Tide enclave; after the window, signing is rejected, and there is no automatic DB cleanup (stale drafts persist until cancelled). Note: `2628000s` (~1 month) is the VRK rotation period (`vrkRotationPeriodSeconds`, `ork` TaskConstants), NOT a per-change-request expiry constant — do not conflate the two. The exact CR expiry is enclave-side and was not confirmed as a hardcoded value in the extension repos on main.
475
479
 
476
480
  ---
477
481
 
@@ -509,15 +513,13 @@ No application-level max payload size **VERIFIED** (vendor confirmation, GAP-013
509
513
 
510
514
  ## I-12: DPoP Token Binding (Bidirectional Lockstep)
511
515
 
512
- **Rule**: DPoP requires simultaneous configuration on both server and client. If either side is enabled without the other, authentication fails. **VERIFIED** (session-002)
516
+ **Rule**: DPoP is **ENABLED and ENFORCED by default** in the TideCloak SDK. `useDPoP` defaults to `{ mode: 'strict' }` in `IAMService` — it is opt-OUT, not opt-in. Disabling it requires setting `useDPoP: false` (or `{ mode: 'auto' }`) explicitly, and in strict mode init fails if the realm does not advertise DPoP support. **VERIFIED** (`tidecloak-js: packages/tidecloak-js/src/IAMService.js` — "DPoP is enabled and ENFORCED by default across all TideCloak SDKs").
513
517
 
514
- **Bidirectional lockstep**:
515
- - **Server-side** (realm template): `"dpop.bound.access.tokens": "true"` on the OIDC client. This tells TideCloak to bind tokens with DPoP.
516
- - **Client-side** (provider config): `useDPoP: { mode: 'strict', alg: 'ES256' }` inside the config object. This tells the SDK to generate DPoP proofs.
517
- - **Both must be set simultaneously.**
518
- - Server on, client off → token endpoint returns 400 "DPoP proof is missing" (no hint the fix is client-side). VERIFIED (session-002).
519
- - Client on, server off → proofs generated but ignored by TideCloak.
520
- - `useDPoP` goes inside the config object, NOT as a JSX prop on `TideCloakProvider`. VERIFIED (session-002).
518
+ **Server/client must agree (lockstep)**:
519
+ - **Server-side** (realm template): `"dpop.bound.access.tokens": "true"` on the OIDC client, and the realm must advertise `dpop_signing_alg_values_supported`. Because the client defaults to strict, a realm that does not advertise DPoP makes SDK init fail.
520
+ - **Client-side** (provider config): the SDK default is `useDPoP: { mode: 'strict', alg: 'ES256' }`. To weaken/disable, set `useDPoP` explicitly. `useDPoP` goes inside the config object, NOT as a JSX prop on the provider. VERIFIED (session-002).
521
+ - Server off, client left at default strict → init fails ("server does not advertise DPoP support"). Set `useDPoP: false` or enable DPoP on the realm.
522
+ - Server on, client set to `false` proofs not generated; token endpoint returns 400 "DPoP proof is missing".
521
523
 
522
524
  **`tide_dpop_auth.html` required** (when DPoP is enabled):
523
525
  - Copy `tide_dpop_auth.html` to `public/`. This file is loaded by the Tide enclave during login to prove DPoP key possession to the ORKs. **Do not modify it** — its content is integrity-checked.
@@ -572,10 +574,10 @@ DPoP is required for Tide's full security guarantees **VERIFIED** (vendor confir
572
574
  **Rule**: Adapter JSON must include Tide extensions (`jwk`, `vendorId`, `homeOrkUrl`). Generic Keycloak adapter is insufficient. **VERIFIED** (keylessh `tidecloakConfig.ts`)
573
575
 
574
576
  **What this means**:
575
- - Export adapter from TideCloak via `vendorResources/get-installations-provider?providerId=keycloak-oidc-keycloak-json` (the only valid provider ID)
577
+ - Export adapter from TideCloak via the realm-level `GET /admin/realms/{realm}/vendorResources/get-installations-provider?clientId={clientId}&providerId=keycloak-oidc-keycloak-json` (client is a query param; `keycloak-oidc-keycloak-json` is the only valid provider ID)
576
578
  - Do not use generic Keycloak adapter export
577
579
  - Do not manually construct adapter JSON without Tide extensions
578
- - `jwk` field is only present when IGA is enabled on the realm
580
+ - `jwk` field is emitted whenever the realm has a Tide vendor key (NOT gated on IGA)
579
581
 
580
582
  **Required Tide extensions**:
581
583
  - `jwk: { keys: JWK[] }` - Embedded JWKS for local JWT verification
@@ -597,7 +599,7 @@ jq '.jwk.keys | length' data/tidecloak.json
597
599
 
598
600
  **Related invariants**: I-04 (Embedded JWKS), I-05 (Adapter JSON Required)
599
601
 
600
- **Note**: The `jwk` field is only present when IGA is enabled. Validate at startup.
602
+ **Note**: The `jwk` field is present whenever the realm has a Tide vendor key (not gated on IGA). Validate at startup.
601
603
 
602
604
  ---
603
605
 
@@ -651,17 +653,12 @@ Vendor guidance: universally required. Declare in realm.json template.
651
653
  - Compromised ORK cannot override policy
652
654
  - No client-side or server-side policy bypass
653
655
 
654
- **Contract submission** **VERIFIED** (keylessh `tideSsh.ts`):
656
+ **Contract submission**: `tc.createTideRequest` takes a single **pre-encoded `Uint8Array`** (a `BaseTideRequest` you build and `.encode()`), NOT a `{ contract, modelName, authFlow, ... }` object. Build the request first, then submit its encoding (see AP-53). The C# contract source is carried inside the request's draft.
655
657
  ```typescript
656
- const signRequest = await tc.createTideRequest({
657
- contract: "using System; ...", // C# code
658
- modelName: "BasicCustom<APP>:BasicCustom<1>",
659
- authFlow: "Policy:1",
660
- authorizer: tc.doken,
661
- challengeData: { ... }
662
- });
663
-
664
- const result = await tc.executeSignRequest(signRequest);
658
+ // Build a BaseTideRequest (name, version, authFlow, draft), then encode it.
659
+ const request = new BaseTideRequest(name, version, "Policy:1", draft);
660
+ const initializedBytes = await tc.createTideRequest(request.encode());
661
+ const result = await tc.executeSignRequest(initializedBytes);
665
662
  ```
666
663
 
667
664
  **Verification**:
@@ -679,7 +676,7 @@ grep -r "bypassPolicy\|skipPolicy" src/ server/
679
676
 
680
677
  **Related invariants**: I-02 (Threshold), I-09 (No Single Bypass)
681
678
 
682
- **Sandbox model** **VERIFIED** (vendor confirmation, GAP-008 resolved): Five-layer security (Roslyn compilation, IL vetting with namespace block-list, AssemblyLoadContext isolation, VmHost process isolation, gas metering at 50,000 gas default). Block-list: `System.IO`, `System.Net`, `System.Diagnostics`, `System.Threading`, `System.Reflection`, `System.Runtime.InteropServices`, `System.Reflection.Emit`, `Microsoft.Win32`, plus `System.Console` and `System.Runtime.CompilerServices.Unsafe`. Non-deterministic calls blocked by default. Gas metering: `Claim(key)` = 5 gas, `Log(message)` = 25-30 gas, `OutOfGasException` when exhausted.
679
+ **Sandbox model** **VERIFIED** (vendor confirmation, GAP-008 resolved): Five-layer security (Roslyn compilation, IL vetting with namespace block-list, AssemblyLoadContext isolation, VmHost process isolation, gas metering at 50,000 gas default). Block-list: `System.IO`, `System.Net`, `System.Diagnostics`, `System.Threading`, `System.Reflection`, `System.Runtime.InteropServices`, `System.Reflection.Emit`, `Microsoft.Win32`, plus `System.Console` and `System.Runtime.CompilerServices.Unsafe`. Non-deterministic calls blocked by default. Gas metering: `Claim(key)` = 5 gas, `Log(message)` = 25 gas, `OutOfGasException` when exhausted.
683
680
 
684
681
  **Remaining open questions**: Contract debugging opaque **PARTIALLY_RESOLVED** (GAP-018).
685
682
 
@@ -785,6 +782,7 @@ curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/auth/redirect
785
782
  |--------|----------|----------|------------------------|
786
783
  | App state | Fresh app (no auth) | Existing app (has auth) | Does the project have an existing auth system? |
787
784
  | Infrastructure | Generated app (has init script) | Manual / BYO TideCloak | Does `package.json` have an `"init"` script? Is TideCloak already running? |
785
+ | TideCloak hosting | Self-hosted (Docker/K8s) | Partner-hosted (Skycloak, managed) | Does the team want to run auth infra, or use a managed instance? See `canon/hosting-options.md`. Does not change app wiring; changes bootstrap. |
788
786
  | Encryption model | Self-encryption (private, user-bound) | Policy-governed VVK (shared, cross-user) | Do other users need to decrypt the same ciphertext? |
789
787
  | App scope | Single-user private app | Organisation / team / shared app | Are there multiple users who share data or resources? |
790
788
  | Task type | Diagnosis (something is broken) | Setup (something needs building) | Is the app partially working, or not started? |