@tideorg/mcp 1.4.2 → 1.9.0

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 +81 -0
  2. package/GAP_REGISTER.md +18 -8
  3. package/PRIVACY.md +41 -0
  4. package/README.md +204 -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
@@ -411,10 +411,12 @@ If route protection broken:
411
411
 
412
412
  1. **Verify auth works**:
413
413
  ```typescript
414
- // Add to protected page
415
- const { authenticated, user } = useTideCloak();
416
- console.log('Auth:', authenticated, 'User:', user);
417
- // Should log: Auth: true, User: {name: "...", ...}
414
+ // Add to protected page.
415
+ // Note: useTideCloak() has no `user` object read claims via
416
+ // getValueFromIdToken / getValueFromToken.
417
+ const { authenticated, getValueFromIdToken } = useTideCloak();
418
+ console.log('Auth:', authenticated, 'User:', getValueFromIdToken('preferred_username'));
419
+ // Should log: Auth: true, User: "<username>"
418
420
  ```
419
421
 
420
422
  2. **Check provider wraps app**:
@@ -459,10 +461,10 @@ export async function GET() {
459
461
 
460
462
  ---
461
463
 
462
- ### ❌ Do Not Use proxy.ts (or legacy middleware.ts) for Auth Checks
464
+ ### ❌ Do Not Hand-Roll a Cookie-Presence Check in proxy.ts / middleware.ts
463
465
 
464
466
  ```typescript
465
- // ❌ WRONG: Next.js proxy/middleware checks auth
467
+ // ❌ WRONG: hand-written proxy/middleware that only checks a cookie exists
466
468
  // proxy.ts (or middleware.ts in legacy projects)
467
469
  import { NextResponse } from 'next/server';
468
470
 
@@ -474,14 +476,26 @@ export function middleware(req) {
474
476
  }
475
477
  ```
476
478
 
477
- **Why**:
478
- 1. `proxy.ts` runs on the edge, cannot verify JWT signature (no access to adapter JSON)
479
- 2. Checks cookie presence, not JWT validity
479
+ **Why this hand-rolled version is wrong**:
480
+ 1. Checks cookie presence, not JWT validity
481
+ 2. No signature verification against the adapter's embedded `jwk`
480
482
  3. Not real authorization
481
483
 
484
+ **Use the shipped helper instead.** `@tidecloak/nextjs/server` exports `createTideCloakProxy` (Next.js 16+, Node runtime) and `createTideCloakMiddleware` (Edge, legacy), which DO verify the token via `verifyTideCloakToken`:
485
+ ```typescript
486
+ // proxy.ts (Next.js 16+) — createTideCloakMiddleware for middleware.ts on ≤15
487
+ import { createTideCloakProxy } from '@tidecloak/nextjs/server';
488
+ import tcConfig from './data/tidecloak.json';
489
+
490
+ export default createTideCloakProxy(tcConfig);
491
+ export const config = { matcher: ['/dashboard/:path*', '/admin/:path*'] };
492
+ ```
493
+
494
+ Even with a verifying proxy, keep server-side JWT verification inside API routes as the authoritative enforcement point (route interception governs page navigation, not direct API calls).
495
+
482
496
  **Version rule**: Next.js 16+ uses `proxy.ts`. Next.js 15 and earlier use `middleware.ts`. Check the installed version before creating or looking for this file. See [canon/framework-matrix.md](../canon/framework-matrix.md#request-interception--route-protection-ui-only).
483
497
 
484
- **Correct**: Client-side route guards for UX, server-side JWT verification for security.
498
+ **Correct**: Client-side route guards (or `createTideCloakProxy`) for UX, server-side JWT verification for security.
485
499
 
486
500
  ---
487
501
 
@@ -0,0 +1,213 @@
1
+ # Provision Hosted TideCloak via Skycloak
2
+
3
+ Provision a fully-managed TideCloak instance in Skycloak's cloud instead of self-hosting, then bootstrap the Tide realm on top of it. This is the hosted alternative to `deploy-tidecloak-docker`.
4
+
5
+ Read `canon/hosting-options.md` first — it covers the self-host vs hosted decision, the trust model, and the honest caveats you must surface to the operator.
6
+
7
+ ---
8
+
9
+ ## When to Use
10
+
11
+ - The team does not want to run auth infrastructure (containers, DB, upgrades, TLS, backups).
12
+ - You want a managed TideCloak reachable at a stable URL with no ops burden.
13
+ - You are prototyping and want an instance without local Docker.
14
+
15
+ **Do not use** if:
16
+ - The deployment must be air-gapped or fully self-controlled → `deploy-tidecloak-docker`.
17
+ - You already have a running TideCloak → go straight to realm bootstrap / integration.
18
+ - The operator has not confirmed a Skycloak account and API key exist (this playbook cannot create the account).
19
+
20
+ ---
21
+
22
+ ## Prerequisites
23
+
24
+ - A Skycloak account with a workspace (dashboard at `skycloak.io`).
25
+ - A Skycloak **API key** with scope `clusters:write` and `clusters:credentials:read`, created in Dashboard → Workspace → API keys. Shown once — capture it securely.
26
+ - `curl` and `jq`.
27
+ - Plan level sufficient for your region/size (non-US regions and larger sizes need Developer plan or higher; a `402 Payment Required` means the action isn't on the current plan).
28
+
29
+ **Secret handling (AP-HOST-3):** The API key and any cluster automation-client secret are operator/bootstrap secrets. Keep them in the shell/CI environment. Never write them into application code, the repo, or `tidecloak.json`. Same rule as master admin credentials (AP-41).
30
+
31
+ ---
32
+
33
+ ## Overview of the flow
34
+
35
+ ```
36
+ 1. Create a TideCloak cluster (Skycloak API) → cluster id
37
+ 2. Poll until status = available (Skycloak API) → cluster URL
38
+ 3. Fetch automation-client creds (Skycloak API) → admin token source
39
+ 4. Bootstrap the Tide realm (TideCloak admin API) → adapter JSON
40
+ 5. Wire the app (unchanged) → same as self-hosted
41
+ ```
42
+
43
+ Steps 1–3 are Skycloak-specific and covered here. Step 4 is the **same Tide-realm bootstrap** as the self-host path — reuse `bootstrap-realm-from-template` / the `deploy-tidecloak-docker` init sequence, pointed at the hosted URL with a token from the automation client instead of master admin creds. Step 5 is identical to any Tide app (`add-auth-nextjs-fresh` etc.).
44
+
45
+ ---
46
+
47
+ ## Step 1: Create a TideCloak cluster
48
+
49
+ ```bash
50
+ export SKYCLOAK_API_KEY="<your-key>"
51
+ API="https://api.skycloak.io"
52
+ VER="2026-06-01.beta"
53
+
54
+ # NOTE: exact request-body field names are INFERRED from the docs — if this 422s,
55
+ # inspect the RFC 9457 error body (it lists the offending field) and adjust.
56
+ curl -s -X POST "$API/clusters" \
57
+ -H "API-Key: $SKYCLOAK_API_KEY" \
58
+ -H "API-Version: $VER" \
59
+ -H "Content-Type: application/json" \
60
+ -d '{
61
+ "identityPlatform": "TideCloak",
62
+ "name": "myapp-auth",
63
+ "size": "Small",
64
+ "region": "us-east"
65
+ }' | tee cluster-create.json | jq .
66
+ ```
67
+
68
+ - **`identityPlatform: "TideCloak"`** is the field that makes this a Tide broker rather than plain Keycloak. Do not omit it.
69
+ - `size`: `Small` (DEV), `Medium` (STAGING), `Large` (PROD).
70
+ - Capture the returned cluster **id**: `CLUSTER_ID=$(jq -r '.id' cluster-create.json)`.
71
+ - Creation is **asynchronous** — the response comes back before the cluster is ready.
72
+
73
+ ---
74
+
75
+ ## Step 2: Poll until available
76
+
77
+ ```bash
78
+ for i in $(seq 1 40); do
79
+ STATUS=$(curl -s "$API/clusters/$CLUSTER_ID" \
80
+ -H "API-Key: $SKYCLOAK_API_KEY" -H "API-Version: $VER" | jq -r '.status')
81
+ echo "attempt $i: $STATUS"
82
+ case "$STATUS" in
83
+ available) echo "ready"; break ;;
84
+ failed) echo "provisioning FAILED — check the Skycloak dashboard"; exit 1 ;;
85
+ esac
86
+ sleep 15
87
+ done
88
+ ```
89
+
90
+ Provisioning is typically 2–4 minutes; Skycloak also emails on completion. Do not proceed to bootstrap until status is `available`. The cluster is reachable at `https://<cluster-id>.app.skycloak.io`:
91
+
92
+ ```bash
93
+ TIDECLOAK_URL="https://${CLUSTER_ID}.app.skycloak.io"
94
+ curl -s -f "$TIDECLOAK_URL" > /dev/null && echo "TideCloak reachable" || echo "not reachable yet"
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Step 3: Get an admin token (no master password exists)
100
+
101
+ Skycloak does **not** issue a Keycloak admin username/password. Two ways to reach the admin API:
102
+
103
+ **A. Admin Console SSO (interactive)** — for a human clicking through: open the cluster's "Go to Console" from the Skycloak dashboard, authenticated by your Skycloak account. Use this to eyeball the instance; it does not give a scriptable token.
104
+
105
+ **B. Automation client (scriptable)** — each cluster has a confidential OAuth2 client `skycloak-automation-<cluster-id>` in the `master` realm for programmatic admin access. Fetch its credentials, then use client-credentials to mint admin tokens:
106
+
107
+ ```bash
108
+ # Field names of the credentials response are INFERRED — inspect the actual JSON.
109
+ curl -s "$API/clusters/$CLUSTER_ID/credentials" \
110
+ -H "API-Key: $SKYCLOAK_API_KEY" -H "API-Version: $VER" | tee cluster-creds.json | jq .
111
+
112
+ CLIENT_ID=$(jq -r '.clientId // .automationClientId // empty' cluster-creds.json)
113
+ CLIENT_SECRET=$(jq -r '.clientSecret // .secret // empty' cluster-creds.json)
114
+
115
+ # Mint an admin token against the hosted instance's master realm:
116
+ get_token() {
117
+ curl -s -X POST "$TIDECLOAK_URL/realms/master/protocol/openid-connect/token" \
118
+ -H "Content-Type: application/x-www-form-urlencoded" \
119
+ -d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET" \
120
+ | jq -r '.access_token'
121
+ }
122
+ TOKEN="$(get_token)"
123
+ [ -n "$TOKEN" ] && [ "$TOKEN" != "null" ] && echo "admin token OK" || echo "token failed — check creds/scopes"
124
+ ```
125
+
126
+ This `get_token` replaces the master-admin `get_token` in the self-host init script. Everything downstream (realm create, `setUpTideRealm`, IGA toggle, change-request authorize/commit, adapter export) is the same — just pointed at `$TIDECLOAK_URL` with this token.
127
+
128
+ ---
129
+
130
+ ## Step 4: Bootstrap the Tide realm
131
+
132
+ Run the standard Tide-realm bootstrap against the hosted instance:
133
+
134
+ 1. `bootstrap-realm-from-template` — create the realm from the `realm.json` template (identical template to self-host; see `deploy-tidecloak-docker` Step 2), then `setUpTideRealm` + enable IGA.
135
+ 2. `initialize-admin-and-link-account` — create the admin user, assign `tide-realm-admin`, generate the Tide-account link, approve the change requests, export the adapter JSON.
136
+
137
+ Use the `get_token()` from Step 3 (automation client) everywhere the self-host script uses the master-admin token. Point `TIDECLOAK_URL` at `https://<cluster-id>.app.skycloak.io`.
138
+
139
+ > **VERIFY THIS ON THE LIVE INSTANCE (GAP-066).** The pack cannot yet confirm that a Skycloak-hosted TideCloak exposes the full Tide vendor surface (`setUpTideRealm`, `toggle-iga`, the change-request API, adapter export with `jwk`/`vendorId`/`homeOrkUrl`) or how Tide **licensing** is handled in the hosted context. Before promising a turnkey result:
140
+ > - Probe `POST .../vendorResources/setUpTideRealm` and check for a 2xx + licensing JSON.
141
+ > - After bootstrap, confirm the exported adapter JSON contains `jwk`, `vendorId`, `homeOrkUrl` (I-05, I-13).
142
+ > - If those endpoints 404 or the adapter lacks Tide fields, **stop** — Skycloak is hosting the broker but Tide-realm provisioning/licensing needs the partner's Tide-specific path. Report this to the operator and raise with the Tide/Skycloak teams. Do not fabricate the adapter JSON.
143
+
144
+ ---
145
+
146
+ ## Step 5: Wire the app
147
+
148
+ Identical to any Tide app — the hosting choice does not change integration. Adapter JSON goes to `data/tidecloak.json` (Next.js) or `public/tidecloak.json` (React/Vite), with `auth-server-url` pointing at the hosted URL. Continue with `add-auth-nextjs-fresh` / `add-auth-nextjs-existing`, then route/API protection.
149
+
150
+ ---
151
+
152
+ ## Verification Checklist
153
+
154
+ ```bash
155
+ # Cluster available
156
+ curl -s "$API/clusters/$CLUSTER_ID" -H "API-Key: $SKYCLOAK_API_KEY" -H "API-Version: $VER" | jq '.status'
157
+ # → "available"
158
+
159
+ # Hosted TideCloak reachable
160
+ curl -s -f "https://${CLUSTER_ID}.app.skycloak.io" > /dev/null && echo reachable
161
+
162
+ # Admin token from automation client works
163
+ [ -n "$(get_token)" ] && echo "token OK"
164
+
165
+ # After bootstrap: adapter has Tide extensions (the real turnkey test — GAP-066)
166
+ jq 'has("jwk") and has("vendorId") and has("homeOrkUrl")' data/tidecloak.json
167
+ # → true (if false: GAP-066 — hosted Tide vendor surface not confirmed)
168
+
169
+ # IGA enabled on the hosted realm
170
+ curl -s "https://${CLUSTER_ID}.app.skycloak.io/admin/realms/myapp" \
171
+ -H "Authorization: Bearer $(get_token)" | jq '.attributes.isIGAEnabled'
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Common Failures
177
+
178
+ ### `402 Payment Required` on create
179
+ The chosen region/size isn't available on the current plan (e.g. a non-US region on a trial workspace). Use a US region / `Small` size, or upgrade the plan. The RFC 9457 body's `detail` states the constraint.
180
+
181
+ ### `403` "does not have the required scope"
182
+ The API key lacks `clusters:write` (create) or `clusters:credentials:read` (Step 3). Create a key with the right scopes; note write includes read.
183
+
184
+ ### Missing `API-Version` header (AP-HOST-4)
185
+ Every Skycloak call needs `-H "API-Version: 2026-06-01.beta"`. Omitting it fails the request.
186
+
187
+ ### `422` on create with a field complaint
188
+ The request-body field names here are INFERRED from the docs. Read the `errors[]` array in the RFC 9457 response — it names the offending `field` — and adjust (`identityPlatform`/`size`/`region` spellings, enum values).
189
+
190
+ ### Cluster stuck in `provisioning` / went `failed`
191
+ Provisioning is async and occasionally fails server-side. Check the Skycloak dashboard for the cluster's error detail; recreate if `failed`. Do not bootstrap against a non-`available` cluster.
192
+
193
+ ### Adapter JSON missing Tide fields after bootstrap (GAP-066)
194
+ The hosted instance may not expose the Tide vendor surface, or licensing wasn't provisioned. Do not fall back to `createRemoteJWKSet` or hand-build the adapter (I-04). Stop and escalate — this is the open question the hosted path depends on.
195
+
196
+ ---
197
+
198
+ ## Anti-Patterns
199
+
200
+ - **AP-HOST-2** — Telling the user the hosted Tide path is fully turnkey before GAP-066 is confirmed. Cluster provisioning is verified; the Tide-realm bootstrap on top is not.
201
+ - **AP-HOST-3** — Putting `SKYCLOAK_API_KEY` or the `skycloak-automation-*` secret in app code, `tidecloak.json`, or the repo. Bootstrap secrets only.
202
+ - **AP-HOST-4** — Omitting the `API-Version` header.
203
+ - **Do not** fabricate `tidecloak.json` if the hosted vendor endpoints are absent. A missing adapter is a provisioning problem to escalate, not a file to invent (I-05, I-13).
204
+ - **Do not** present hosting as either a security downgrade or a magic upgrade — state the real trust model from `canon/hosting-options.md` (availability + metadata + Tideless-IGA caveat).
205
+
206
+ ---
207
+
208
+ ## References
209
+
210
+ - `canon/hosting-options.md` — decision, trust model, Skycloak API reference, GAP-066
211
+ - Skycloak docs: `https://skycloak.io/docs/api/` (API-Version `2026-06-01.beta`)
212
+ - `deploy-tidecloak-docker` — the self-host equivalent and the shared realm-bootstrap sequence
213
+ - `bootstrap-realm-from-template`, `initialize-admin-and-link-account` — the Tide-realm steps reused in Step 4
@@ -289,24 +289,27 @@ const executeTideRequest = async (request, waitForAll = false) => {
289
289
  WRONG: GET /admin/realms/{realm}/tide-admin/realm-policy
290
290
  -> { status: "none" } (even when the policy exists)
291
291
 
292
- CORRECT: GET /realms/{realm}/tide-policy-resources/admin-policy
293
- -> base64-encoded bytes (public, no auth required)
292
+ WRONG: GET /realms/{realm}/tide-policy-resources/admin-policy
293
+ -> does NOT exist on current main (the public policy endpoint was removed)
294
+
295
+ CORRECT: GET /admin/realms/{realm}/iga/role-policies (admin bearer required)
296
+ -> JSON; the signed admin policy bytes are in the `policy` field
294
297
  ```
295
298
 
296
- The correct endpoint does not require master admin credentials. Proxy through your backend.
299
+ The correct endpoint is an admin API route and **requires an admin bearer token**. Proxy through your backend so the browser never holds the admin credential.
297
300
 
298
- **CORS: Browser cannot fetch this endpoint directly.** TideCloak and the app are on different origins. Direct browser fetch returns an opaque/blocked response. Create a same-origin API route that proxies the request server-side. VERIFIED (session-001).
301
+ **CORS: Browser cannot fetch this endpoint directly.** TideCloak and the app are on different origins, and this is a privileged admin API. Direct browser fetch returns an opaque/blocked response (and would leak the admin bearer). Create a same-origin API route that proxies the request server-side with the admin token. VERIFIED (session-001).
299
302
 
300
303
  **If the proxy route is protected with `withAuth` (correct per I-03), the client must use authenticated fetch** (e.g., `appFetch` or `secureFetch` with Bearer header). A bare `fetch()` without Authorization returns 401. The resulting empty/error response produces undefined policy bytes, causing `PolicyAuthorizationFlowException: "Model does not have a policy passed with it"` at signing time. VERIFIED (LEARNINGS-batch-009 L-08).
301
304
 
302
305
  **There is no REST API for deploying Forseti contracts.** Do not try `PUT` or `POST` to `/tide-admin/forseti-contracts` — these endpoints do not exist on the dev image (404/405). Contract deployment happens via `PolicySignRequest.addForsetiContractToUpload(contractSource)` in the browser signing flow (Step 4). VERIFIED (session-001).
303
306
 
304
- **The response is base64-encoded, not raw binary.** You must decode before using:
307
+ **The signed bytes arrive as a base64 string in the `policy` field, not raw binary.** Read the `policy` field and decode before using:
305
308
 
306
309
  ```js
307
- // Backend proxy
308
- const b64 = await tcRes.text();
309
- const raw = Buffer.from(b64, "base64");
310
+ // Backend proxy (holds the admin bearer)
311
+ const json = await tcRes.json();
312
+ const raw = Buffer.from(json.policy, "base64");
310
313
  const bytes = Array.from(new Uint8Array(raw));
311
314
  res.json({ policyBytes: bytes });
312
315
 
@@ -315,12 +318,12 @@ const data = await res.json();
315
318
  const adminPolicyBytes = new Uint8Array(data.policyBytes);
316
319
  ```
317
320
 
318
- **Common mistake:** Treating the base64 string as raw bytes (passing each character's char code as a byte value). If admin policy bytes start with `[65, 81, 65, 65, ...]` (ASCII for "AQAA..."), you are passing base64 text as byte values instead of decoding it. The ORK will fail with `Index out of range` because the policy structure is garbage.
321
+ **Common mistake:** Treating the base64 `policy` string as raw bytes (passing each character's char code as a byte value). If admin policy bytes start with `[65, 81, 65, 65, ...]` (ASCII for "AQAA..."), you are passing base64 text as byte values instead of decoding it. The ORK will fail with `Index out of range` because the policy structure is garbage.
319
322
 
320
- **Common mistake:** Malformed URL from trailing slash in `auth-server-url`. The adapter JSON `auth-server-url` may include a trailing slash (e.g., `http://localhost:8080/`). Constructing the admin policy URL without stripping it produces `http://localhost:8080//realms/...` (double slash), which returns HTML or 404 instead of policy bytes. Always normalize:
323
+ **Common mistake:** Malformed URL from trailing slash in `auth-server-url`. The adapter JSON `auth-server-url` may include a trailing slash (e.g., `http://localhost:8080/`). Constructing the admin policy URL without stripping it produces `http://localhost:8080//admin/realms/...` (double slash), which returns HTML or 404 instead of policy bytes. Always normalize:
321
324
  ```js
322
325
  const baseUrl = config["auth-server-url"].replace(/\/+$/, "");
323
- const adminPolicyUrl = `${baseUrl}/realms/${config.realm}/tide-policy-resources/admin-policy`;
326
+ const adminPolicyUrl = `${baseUrl}/admin/realms/${config.realm}/iga/role-policies`;
324
327
  ```
325
328
 
326
329
  ---
@@ -528,50 +531,29 @@ These are prep steps. None produce VVK-signed bytes.
528
531
 
529
532
  Role assignments that produce VVK-signed UserContexts MUST be manually approved through the approval enclave in the admin's browser. You cannot automate this from a backend script.
530
533
 
531
- What happens when you try to auto-approve:
532
-
533
- ```
534
- POST /tide-admin/change-set/sign
535
- Response: { "requiresApprovalPopup": true, "changeSetDraftRequests": "base64..." }
536
- ```
537
-
538
- The `requiresApprovalPopup: true` response means the backend CANNOT complete the signing. The VVK-signed UserContext binds a specific user's `tideUserKey` to specific roles. This signing must happen through the ORK threshold network via the Tide enclave running in the admin's browser.
539
-
540
- **Always use `/batch` endpoints for approval:**
534
+ This is Tide MultiAdmin mode: the VVK-signed UserContext binds a specific user's `tideUserKey` to specific roles, and the signing must happen through the ORK threshold network via the Tide enclave in the admin's browser. The backend CANNOT complete it — a bare `authorize` on such a CR requires the two-phase `approval-model` enclave exchange. (Endpoints: current `/iga/change-requests/...` surface — see `canon/iga-change-requests-api.md`.)
541
535
 
542
536
  ```js
543
- // 1. Backend creates IGA draft (role assignment)
537
+ // 1. Backend performs the governed change (role assignment) → 202, a CR is created
544
538
  POST /admin/realms/{realm}/users/{userId}/role-mappings/realm
545
539
 
546
- // 2. Frontend fetches pending requests
547
- GET /admin/realms/{realm}/tide-admin/change-set/users/requests
540
+ // 2. Frontend fetches pending CRs
541
+ GET /admin/realms/{realm}/iga/change-requests?status=PENDING // objects keyed by `id`
548
542
 
549
- // 3. Frontend calls sign/batch (one call for all selected)
550
- POST /admin/realms/{realm}/tide-admin/change-set/sign/batch
551
- { changeSets: [{ changeSetId, changeSetType, actionType, policyRoleId? }] }
543
+ // 3. For each CR, fetch the enclave challenge (Tide MultiAdmin)
544
+ GET /admin/realms/{realm}/iga/change-requests/{id}/approval-model
545
+ // → { changeRequestId, requestModel: "base64..." }
552
546
 
553
- // 4. Collect all that need enclave, pass to ONE approval popup
554
- const enclaveRequests = results
555
- .filter(r => r.requiresApprovalPopup)
556
- .map(r => ({ id: r.changesetId, request: base64ToBytes(r.changeSetDraftRequests) }));
547
+ // 4. Collect the challenges, pass to ONE enclave approval popup
548
+ const enclaveRequests = pending.map(cr => ({ id: cr.id, request: base64ToBytes(cr.requestModel) }));
557
549
  const approvals = await tc.requestTideOperatorApproval(enclaveRequests);
558
550
 
559
- // 5. Submit each signed review via add-review (FormData, NOT JSON)
560
- POST /admin/realms/{realm}/tideAdminResources/add-review
561
-
562
- // 6. Commit all in batch
563
- POST /admin/realms/{realm}/tide-admin/change-set/commit/batch
564
- { changeSets: [{ changeSetId, changeSetType, actionType }] }
565
- ```
551
+ // 5. Submit each signed doken back to the same endpoint
552
+ POST /admin/realms/{realm}/iga/change-requests/{id}/approval-model
553
+ { "requestModel": "<base64 doken>" } // → { recorded, authCount, threshold }
566
554
 
567
- **The add-review endpoint uses FormData, not JSON:**
568
-
569
- ```js
570
- const formData = new FormData();
571
- formData.append("changeSetId", id);
572
- formData.append("actionType", actionType);
573
- formData.append("changeSetType", changeSetType);
574
- formData.append("requests", btoa(String.fromCharCode(...signedBytes)));
555
+ // 6. Commit each CR once ready (readyToCommit === true)
556
+ POST /admin/realms/{realm}/iga/change-requests/{id}/commit // 412 if still under threshold
575
557
  ```
576
558
 
577
559
  After commit: the user must refresh their token (or re-login) for the new role to appear in their doken. Use `IAMService.forceUpdateToken()` or `useTideCloak().forceRefreshToken()`.
@@ -656,8 +638,8 @@ The forseti-crypto-quickstart is the reference implementation that works against
656
638
  - [ ] `modelId` is `["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"]` (array, not string)
657
639
  - [ ] `PolicySignRequest` imported from `heimdall-tide`
658
640
  - [ ] `BaseTideRequest.decode()` called on `createTideRequest` result before approval
659
- - [ ] Admin policy fetched from `GET /realms/{realm}/tide-policy-resources/admin-policy` (not the admin API endpoint)
660
- - [ ] Admin policy bytes are base64-decoded (not raw char codes)
641
+ - [ ] Admin policy fetched (admin bearer) from `GET /admin/realms/{realm}/iga/role-policies`; signed bytes read from the `policy` field (the public `tide-policy-resources/admin-policy` endpoint does not exist on main)
642
+ - [ ] Admin policy bytes are base64-decoded from the `policy` field (not raw char codes)
661
643
  - [ ] Approval popup opens and admin approves
662
644
  - [ ] `executeSignRequest` called with `waitForAll = true`
663
645
  - [ ] `policy.signature = signatures[0]` assigned before `policy.toBytes()`
@@ -701,8 +683,8 @@ The forseti-crypto-quickstart is the reference implementation that works against
701
683
 
702
684
  ### `{ status: "none" }` when fetching admin policy
703
685
 
704
- **Cause:** Using the wrong endpoint `GET /admin/realms/{realm}/tide-admin/realm-policy`.
705
- **Fix:** Use `GET /realms/{realm}/tide-policy-resources/admin-policy`.
686
+ **Cause:** Using the wrong endpoint `GET /admin/realms/{realm}/tide-admin/realm-policy`. (The old public `GET /realms/{realm}/tide-policy-resources/admin-policy` endpoint no longer exists on main either.)
687
+ **Fix:** Use `GET /admin/realms/{realm}/iga/role-policies` with an admin bearer token and read the signed bytes from the `policy` field.
706
688
 
707
689
  ### Admin policy bytes start with `[65, 81, 65, 65, ...]`
708
690