@tideorg/mcp 1.4.1 → 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 (91) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/GAP_REGISTER.md +19 -9
  3. package/PRIVACY.md +41 -0
  4. package/README.md +204 -120
  5. package/adapters/AGENTS.md +3 -2
  6. package/adapters/CLAUDE.md +1 -1
  7. package/adapters/replit.md +0 -0
  8. package/canon/anti-patterns.md +55 -62
  9. package/canon/concepts.md +46 -34
  10. package/canon/custom-contracts.md +12 -8
  11. package/canon/feature-mapping.md +27 -75
  12. package/canon/framework-matrix.md +69 -22
  13. package/canon/hosting-options.md +111 -0
  14. package/canon/iga-change-requests-api.md +211 -0
  15. package/canon/invariants.md +33 -35
  16. package/canon/redirect-handler.md +0 -0
  17. package/canon/security-gap-mapping.md +506 -0
  18. package/canon/security-runtime-probes.md +177 -0
  19. package/canon/tidecloak-bootstrap.md +84 -15
  20. package/canon/tidecloak-endpoints.md +60 -98
  21. package/canon/troubleshooting.md +201 -53
  22. package/canon/version-policy.md +8 -12
  23. package/mcp-server/dist/http.d.ts +2 -0
  24. package/mcp-server/dist/http.js +46 -0
  25. package/mcp-server/dist/index.d.ts +0 -0
  26. package/mcp-server/dist/index.js +2 -601
  27. package/mcp-server/dist/server.d.ts +2 -0
  28. package/mcp-server/dist/server.js +617 -0
  29. package/package.json +48 -45
  30. package/playbooks/add-auth-nextjs-existing.md +33 -17
  31. package/playbooks/add-auth-nextjs-fresh.md +1 -1
  32. package/playbooks/add-rbac-nextjs.md +7 -2
  33. package/playbooks/bootstrap-realm-from-template.md +33 -18
  34. package/playbooks/configure-e2ee-roles-and-policies.md +0 -0
  35. package/playbooks/deploy-tidecloak-docker.md +31 -28
  36. package/playbooks/diagnose-broken-login.md +0 -0
  37. package/playbooks/diagnose-missing-roles-or-claims.md +2 -2
  38. package/playbooks/initialize-admin-and-link-account.md +22 -19
  39. package/playbooks/migrate-from-existing-auth.md +0 -0
  40. package/playbooks/protect-api-nextjs.md +40 -1
  41. package/playbooks/protect-aspnet-core-asgard.md +313 -0
  42. package/playbooks/protect-routes-nextjs.md +24 -10
  43. package/playbooks/provision-tidecloak-skycloak.md +213 -0
  44. package/playbooks/setup-forseti-e2ee.md +32 -50
  45. package/playbooks/setup-iga-admin-panel.md +113 -171
  46. package/playbooks/start-tidecloak-dev.md +0 -0
  47. package/playbooks/verify-jwt-server-side.md +20 -1
  48. package/prompts/add-admin-approval-flow.md +0 -0
  49. package/prompts/build-private-customer-portal.md +1 -1
  50. package/prompts/migrate-generic-auth-to-tide.md +0 -0
  51. package/prompts/secure-existing-app.md +0 -0
  52. package/prompts/security-gap-analysis.md +55 -0
  53. package/reference-apps/INDEX.md +0 -0
  54. package/reference-apps/encrypted-communication/anti-patterns.md +3 -3
  55. package/reference-apps/encrypted-communication/bootstrap-sequence.md +2 -2
  56. package/reference-apps/encrypted-communication/manifest.yaml +0 -0
  57. package/reference-apps/encrypted-communication/role-policy-matrix.md +0 -0
  58. package/reference-apps/encrypted-communication/scenario.md +2 -2
  59. package/reference-apps/git-pr-signing-service/anti-patterns.md +0 -0
  60. package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +8 -8
  61. package/reference-apps/git-pr-signing-service/manifest.yaml +0 -0
  62. package/reference-apps/git-pr-signing-service/role-policy-matrix.md +0 -0
  63. package/reference-apps/git-pr-signing-service/scenario.md +0 -0
  64. package/reference-apps/iga-admin-governance/anti-patterns.md +18 -16
  65. package/reference-apps/iga-admin-governance/bootstrap-sequence.md +10 -5
  66. package/reference-apps/iga-admin-governance/manifest.yaml +0 -0
  67. package/reference-apps/iga-admin-governance/role-policy-matrix.md +12 -13
  68. package/reference-apps/iga-admin-governance/scenario.md +15 -13
  69. package/reference-apps/organisation-password-manager/anti-patterns.md +0 -0
  70. package/reference-apps/organisation-password-manager/bootstrap-sequence.md +5 -6
  71. package/reference-apps/organisation-password-manager/manifest.yaml +0 -0
  72. package/reference-apps/organisation-password-manager/role-policy-matrix.md +0 -0
  73. package/reference-apps/organisation-password-manager/scenario.md +0 -0
  74. package/reference-apps/policy-governed-signing/anti-patterns.md +0 -0
  75. package/reference-apps/policy-governed-signing/bootstrap-sequence.md +9 -9
  76. package/reference-apps/policy-governed-signing/manifest.yaml +0 -0
  77. package/reference-apps/policy-governed-signing/role-policy-matrix.md +0 -0
  78. package/reference-apps/policy-governed-signing/scenario.md +0 -0
  79. package/skills/tide-diagnostics/SKILL.md +1 -1
  80. package/skills/tide-integration/SKILL.md +9 -18
  81. package/skills/tide-learning-capture/SKILL.md +0 -0
  82. package/skills/tide-mcp-qa/SKILL.md +139 -0
  83. package/skills/tide-rbac-and-e2ee/SKILL.md +5 -5
  84. package/skills/tide-reviewer/SKILL.md +1 -1
  85. package/skills/tide-route-and-api-protection/SKILL.md +0 -0
  86. package/skills/tide-scenario-resolver/SKILL.md +0 -0
  87. package/skills/tide-security-analyst/SKILL.md +182 -0
  88. package/skills/tide-setup/SKILL.md +1 -1
  89. package/skills/tide-solutions-architect/SKILL.md +0 -0
  90. package/canon/delegation.md +0 -195
  91. package/playbooks/setup-server-delegation.md +0 -142
@@ -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
 
@@ -12,6 +12,13 @@ Build an admin UI for managing IGA change requests with quorum governance.
12
12
 
13
13
  **Do not use** if you only need end-user authentication. See [add-auth-nextjs-fresh.md](add-auth-nextjs-fresh.md) instead.
14
14
 
15
+ **A custom panel is optional.** The stack already ships two working admin consoles that manage IGA change requests out of the box:
16
+
17
+ - **tide-console SPA** — a built Vite/React admin UI bundled inside the `tidecloak-key-provider` jar and served by TideCloak.
18
+ - **Legacy keycloak.v2 console** — the stock Keycloak admin UI, which on the shipped build carries a native `tide-change-requests` section.
19
+
20
+ Build a custom panel only when you need to embed CR governance inside your own product UI. Otherwise use one of the shipped consoles.
21
+
15
22
  ---
16
23
 
17
24
  ## Prerequisites
@@ -34,190 +41,136 @@ All endpoints below are relative to:
34
41
 
35
42
  Example: `http://localhost:8080/admin/realms/myrealm`
36
43
 
44
+ The consolidated IGA REST surface is rooted at `/admin/realms/{realm}/iga` (server class `IgaAdminResource`). Change requests live under `/iga/change-requests`.
45
+
46
+ > **Legacy bootstrap-compat surface.** The canonical bootstrap script (`localtest/init/init_tidecloak.sh`) and the `tidecloak-js` `AdminAPI` still call an older `/tide-admin/change-set/{type}/requests`, `/tide-admin/change-set/sign`, and `/tide-admin/change-set/commit` surface. That path still exists for backward compatibility and is fine for scripted bootstrap approval, but the `/iga/*` surface documented below is the current API for a live admin panel. Do not mix the two.
47
+
37
48
  ---
38
49
 
39
50
  ## Change Request Lifecycle
40
51
 
52
+ Current `/iga/change-requests/...` surface (replaces legacy `/tide-admin/change-set/...`, confirmed by Tide 2026-07-07). **Full spec: `canon/iga-change-requests-api.md`.**
53
+
41
54
  ```
42
- 1. Admin creates change (role assignment, group change, etc.)
43
- -> Draft created automatically (DRAFT/PENDING)
44
- 2. Admin reviews: GET /tide-admin/change-set/all/requests
45
- 3. Admin approves: POST /tide-admin/change-set/sign/batch
46
- -> If Tide multi-admin: returns signing challenge for enclave
47
- -> Submit signed result: POST /tideAdminResources/add-review
48
- 4. Once enough approvals: POST /tide-admin/change-set/commit/batch
49
- 5. Or cancel: POST /tide-admin/change-set/cancel/batch
55
+ 1. Admin performs a governed change (role assignment, group change, etc.)
56
+ -> The write returns 202 Accepted; a change request is created (PENDING)
57
+ 2. Admin reviews: GET /iga/change-requests?status=PENDING
58
+ 3. Admin approves: POST /iga/change-requests/{id}/authorize (body {} optional)
59
+ -> Tide MultiAdmin mode: sign via the enclave two-phase exchange
60
+ GET/POST /iga/change-requests/{id}/approval-model
61
+ -> FirstAdmin / Tideless mode: authorize signs server-side, no popup
62
+ 4. Once threshold met: POST /iga/change-requests/{id}/commit (412 if under threshold)
63
+ 5. Or reject: POST /iga/change-requests/{id}/deny
50
64
  ```
51
65
 
66
+ `authorize` (simple lane: firstAdmin/Tideless) records an approval and **never commits** — it refuses tide-multiAdmin CRs. `approve` is the multiAdmin enclave lane: it approves AND auto-commits once quorum is met (this is the deployed console "Authorize" button). `commit` is the explicit apply-only step for a CR that already has enough approvals (412 if sub-quorum). Use `bulk-authorize` to act on many at once.
67
+
52
68
  ---
53
69
 
54
70
  ## Endpoints
55
71
 
56
- ### Counts (Lightweight)
72
+ ### List / Get
57
73
 
58
74
  ```
59
- GET /tide-admin/change-set/counts
75
+ GET /iga/change-requests?status=PENDING # PENDING|APPROVED|DENIED|CANCELLED
76
+ GET /iga/change-requests/{id} # single CR (keyed by `id`)
60
77
  ```
61
78
 
62
- Use this for dashboard badges and summary views. Avoids fetching full request payloads.
79
+ Each CR carries `id`, `entityType`, `actionType`, `status`, `readyToCommit`, `threshold`, `authorizers[]`, `dependsOn`/`blocked`. Derive dashboard counts from the list (there is no separate counts endpoint on the new surface; no per-type request endpoints and no `all/requests` path either).
63
80
 
64
- ### List Requests
81
+ ### Authorize (Sign)
65
82
 
66
83
  ```
67
- GET /tide-admin/change-set/all/requests # All change requests (single call)
68
- GET /tide-admin/change-set/users/requests # User changes only
69
- GET /tide-admin/change-set/roles/requests # Role changes only
70
- GET /tide-admin/change-set/clients/requests # Client changes only
71
- GET /tide-admin/change-set/groups/requests # Group changes only
84
+ POST /iga/change-requests/{id}/authorize # body {} optional → 200/403/409 (409 = four-eyes)
85
+ POST /iga/change-requests/bulk-authorize # {"actionTypeIn":["CREATE","DELETE"],"limit":100} 429 if a bulk run is active
72
86
  ```
73
87
 
74
- ### Sign / Approve
88
+ ### Commit / Deny
75
89
 
76
90
  ```
77
- POST /tide-admin/change-set/sign/batch # Approve (sign) changes
91
+ POST /iga/change-requests/{id}/commit # 200 (APPROVED) / 412 (under threshold or unmet dependency)
92
+ POST /iga/change-requests/{id}/deny # → 204
78
93
  ```
79
94
 
80
- ### Commit / Cancel
95
+ ### Enclave Review (Tide MultiAdmin)
81
96
 
82
97
  ```
83
- POST /tide-admin/change-set/commit/batch # Commit approved changes
84
- POST /tide-admin/change-set/cancel/batch # Cancel pending changes
98
+ GET /iga/change-requests/{id}/approval-model # { changeRequestId, requestModel(base64) }
99
+ POST /iga/change-requests/{id}/approval-model # { "requestModel": "<base64 doken>" } → { recorded, authCount, threshold }
85
100
  ```
86
101
 
87
- ### Enclave Review and Rejection
102
+ The two-phase `approval-model` exchange replaces the legacy `tideAdminResources/add-review`. In FirstAdmin / Tideless mode, `authorize` signs server-side and no enclave step is needed.
103
+
104
+ ### Deprecated: add-review / add-rejection
88
105
 
89
106
  ```
90
- POST /tideAdminResources/add-review # Submit Tide enclave signed review (multipart/form-data)
91
- POST /tideAdminResources/add-rejection # Reject a change (multipart/form-data)
107
+ POST /tideAdminResources/add-review # 410 Gone (deprecated)
108
+ POST /tideAdminResources/add-rejection # 410 Gone (deprecated)
92
109
  ```
93
110
 
94
- **Content-type warning**: Both `add-review` and `add-rejection` use `multipart/form-data`, NOT JSON. All other mutation endpoints (`sign/batch`, `commit/batch`, `cancel/batch`) use `application/json`. If you send JSON to `add-rejection`, the server silently ignores the payload.
111
+ Both endpoints were removed as part of the IGA decoupling and now return **`410 Gone`**. Approval goes through `/iga/change-requests/{id}/authorize` (plus the `approval-model` enclave exchange in Tide MultiAdmin mode) and rejection through `/iga/change-requests/{id}/deny`.
95
112
 
96
- ```typescript
97
- // Rejection example — must use FormData, not JSON
98
- const formData = new FormData();
99
- formData.append("changeSetId", draftRecordId);
100
- formData.append("actionType", actionType);
101
- formData.append("changeSetType", changeSetType);
102
-
103
- await fetch(`${TC_URL}/admin/realms/${realm}/tideAdminResources/add-rejection`, {
104
- method: 'POST',
105
- headers: { 'Authorization': `Bearer ${adminToken}` },
106
- body: formData // NOT JSON.stringify
107
- });
108
- ```
109
-
110
- ### Activity and Comments
113
+ ### Comments
111
114
 
112
115
  ```
113
- GET /tide-admin/change-set/{id}/activity # Activity log + comments
114
- POST /tide-admin/change-set/{id}/comments # Add comment to change request
116
+ GET/POST /iga/change-requests/{id}/comments # { "comment": "..." } (≤2000 chars)
117
+ PUT/DELETE /iga/change-requests/{id}/comments/{commentId}
115
118
  ```
116
119
 
117
- ---
118
-
119
- ## Approve (Sign) Request -- Code Example
120
-
121
- ```typescript
122
- const response = await fetch(`${TC_URL}/admin/realms/${realm}/tide-admin/change-set/sign/batch`, {
123
- method: 'POST',
124
- headers: {
125
- 'Authorization': `Bearer ${adminToken}`,
126
- 'Content-Type': 'application/json'
127
- },
128
- body: JSON.stringify({
129
- changeSets: [{
130
- changeSetId: "draft-record-id",
131
- changeSetType: "USER_ROLE",
132
- actionType: "CREATE",
133
- policyRoleId: "optional-role-uuid", // omit to use tide-realm-admin policy
134
- dynamicData: ["optional-base64"] // custom Forseti contract input
135
- }]
136
- })
137
- });
138
-
139
- // If Tide multi-admin mode:
140
- // response[0].requiresApprovalPopup === true
141
- // -> decode changeSetDraftRequests (base64)
142
- // -> present to Tide enclave for admin signing
143
- // -> submit via POST /tideAdminResources/add-review
144
- ```
120
+ There is no per-request `/activity` endpoint on the current surface; comments are the audit thread.
145
121
 
146
122
  ---
147
123
 
148
- ## TypeScript Client (KcAdminClient) Usage
124
+ ## TypeScript Client Usage
125
+
126
+ There is no `@keycloak/keycloak-admin-client` `KcAdminClient.tideUsersExt` extension on `tidecloak-js` main. The change-request surface is a hand-rolled fetch client, `AdminAPI` (`packages/tidecloak-js/src/AdminAPI.js`), exposed through `@tidecloak/react`. It targets the legacy bootstrap-compat `/tide-admin/change-set/*` endpoints:
149
127
 
150
128
  ```typescript
151
- import KcAdminClient from "@keycloak/keycloak-admin-client";
129
+ import { AdminAPI } from "@tidecloak/react";
152
130
 
153
- const client = new KcAdminClient({ baseUrl: "http://localhost:8080" });
154
- await client.auth({
155
- username: "admin",
156
- password: "admin",
157
- grantType: "password",
158
- clientId: "admin-cli"
159
- });
131
+ const api = new AdminAPI({ baseUrl, realm, token });
160
132
 
161
- // Counts
162
- const counts = await client.tideUsersExt.getChangeSetCounts();
133
+ // List (combines user + role change requests)
134
+ const pending = await api.getPendingChangeSets();
135
+ const userCrs = await api.getUserChangeRequests();
136
+ const roleCrs = await api.getRoleChangeRequests();
163
137
 
164
- // All requests
165
- const all = await client.tideUsersExt.getAllChangeSetRequests();
138
+ // Approve a single change set (sent as FormData, single object — not { changeSets: [...] })
139
+ await api.approveChangeSet({ changeSetId, actionType, changeSetType });
166
140
 
167
- // Approve
168
- await client.tideUsersExt.approveDraftChangeSet({
169
- changeSets: [{ changeSetId: "id", changeSetType: "USER_ROLE", actionType: "CREATE" }]
170
- });
141
+ // Multi-admin: approve with an enclave signature
142
+ await api.approveChangeSetWithSignature(/* signed payload */);
171
143
 
172
144
  // Commit
173
- await client.tideUsersExt.commitDraftChangeSet({
174
- changeSets: [{ changeSetId: "id", changeSetType: "USER_ROLE", actionType: "CREATE" }]
175
- });
176
-
177
- // Activity and comments
178
- const activity = await client.tideUsersExt.getChangeSetActivity({ id: "draft-id" });
179
- await client.tideUsersExt.addChangeSetComment({ id: "draft-id", comment: "LGTM" });
145
+ await api.commitChangeSet({ changeSetId, actionType, changeSetType });
180
146
  ```
181
147
 
148
+ There are no `getChangeSetCounts`, `getAllChangeSetRequests`, `getChangeSetActivity`, or `addChangeSetComment` methods. For a panel built on the current `/iga/*` surface, call the `/iga/change-requests/*` endpoints (see the **Endpoints** section above and `canon/iga-change-requests-api.md`) directly with `fetch` rather than via this SDK.
149
+
182
150
  ---
183
151
 
184
152
  ## Policy Management Endpoints
185
153
 
186
154
  ```
187
- PUT /tide-admin/forseti-contracts # Create/update Forseti contract
188
- GET /tide-admin/forseti-contracts # List contracts
189
- POST /tide-admin/policy-templates # Create policy template
190
- GET /tide-admin/policy-templates # List templates
191
- PUT /tide-admin/ssh-policies # Attach policy to role
192
- GET /tide-admin/ssh-policies # List role policies
193
- POST /tide-iga-provider/role-policy/{roleId}/init-cert # Store signed policy on role (JSON: initCert required, initCertSig optional; no doken field)
194
- GET /tide-admin/role-policies # Roles with policy status
155
+ GET /iga/forseti-contracts # List contracts
156
+ GET /iga/forseti-contracts/{id} # Get one contract
157
+ POST /iga/forseti-contracts # Create a Forseti contract (POST, not PUT)
158
+ DELETE /iga/forseti-contracts/{id} # Delete a contract
159
+ GET /iga/role-policies # Roles with policy status
195
160
  ```
196
161
 
162
+ Create is `POST /iga/forseti-contracts` (there is no PUT). Policy-template, SSH-policy, and `role-policy/{roleId}/init-cert` endpoints are not part of the current surface.
163
+
197
164
  ---
198
165
 
199
- ## ChangeSetType Values
200
-
201
- **VERIFIED** (vendor confirmation, GAP-041 resolved). Full enum (14 values):
202
-
203
- | Value | Description | Draft Status |
204
- |-------|-------------|-------------|
205
- | `DEFAULT_ROLES` | Default role assignment | ACTIVE (auto) |
206
- | `USER` | User creation | DRAFT |
207
- | `USER_ROLE` | User-to-role assignment | DRAFT |
208
- | `USER_GROUP_MEMBERSHIP` | User-to-group membership | DRAFT |
209
- | `ROLE` | Role definition change | ACTIVE (auto, creation) / DRAFT (deletion if users assigned) |
210
- | `COMPOSITE_ROLE` | Composite role change | DRAFT |
211
- | `CLIENT` | Client configuration change | DRAFT (when users exist) |
212
- | `CLIENT_DEFAULT_USER_CONTEXT` | Client scope mappings | DRAFT |
213
- | `CLIENT_FULLSCOPE` | Client full-scope toggle | DRAFT |
214
- | `GROUP` | Group definition change | — |
215
- | `GROUP_ROLE` | Group-to-role assignment | DRAFT |
216
- | `GROUP_MOVE` | Group hierarchy change | DRAFT |
217
- | `REALM_LICENSING` | Realm license change | DRAFT |
218
- | `POLICY` | Policy change (derived from USER_ROLE) | DRAFT |
219
-
220
- **ACTIVE vs DRAFT**: Role creation and default-role assignment get ACTIVE status (recorded for audit but non-blocking — no sign/commit needed). All other actions get DRAFT status requiring quorum sign/commit. Master realm and IGA-disabled realms are always exempt.
166
+ ## Change Request Taxonomy
167
+
168
+ The current surface does not use a `ChangeSetType` enum. Each change request carries two free-form String columns on `IgaChangeRequestEntity`:
169
+
170
+ - **`entityType`** one of: `USER`, `REALM`, `ROLE`, `GROUP`, `CLIENT`, `CLIENT_SCOPE`, `ORGANIZATION`, `COMPOSITE_ROLE`, `CLIENT_SCOPE_CLIENT`, `CLIENT_SCOPE_ROLE`, `PROTOCOL_MAPPER`, `REALM_DEFAULT_SCOPE`, `SCOPE_MAPPING`.
171
+ - **`actionType`** — a free-form verb such as `CREATE`, `SET`, `UPDATE`, `ASSIGN`, or an `ADOPT_*` variant.
172
+
173
+ Read both values back from the `GET /iga/change-requests` response; do not hardcode a fixed enum. Some change requests (e.g. role creation and default-role adoption) commit automatically as part of the login-closure converge and never need an explicit authorize/commit; the rest require quorum authorize/commit. The master realm and IGA-disabled realms are exempt from governance entirely.
221
174
 
222
175
  ## Status Values
223
176
 
@@ -235,13 +188,13 @@ GET /tide-admin/role-policies # Roles with policy status
235
188
 
236
189
  A working IGA admin panel must include:
237
190
 
238
- 1. **Pending list** -- Display all change requests in `DRAFT` or `PENDING` status. Use `GET /tide-admin/change-set/all/requests` and filter by status.
191
+ 1. **Pending list** -- Display change requests via `GET /iga/change-requests?status=PENDING` (keyed by `id`).
239
192
 
240
- 2. **Approve button** -- Triggers `POST /tide-admin/change-set/sign/batch` with selected change set IDs.
193
+ 2. **Approve button** -- Triggers `POST /iga/change-requests/{id}/authorize` per selected CR (or `POST /iga/change-requests/bulk-authorize` for a batch).
241
194
 
242
- 3. **Enclave popup** -- When `requiresApprovalPopup === true` in the sign response, decode `changeSetDraftRequests` (base64) and present the Tide enclave iframe for the admin to complete threshold signing. Submit the signed result via `POST /tideAdminResources/add-review`.
195
+ 3. **Enclave popup** -- In Tide MultiAdmin mode, complete the two-phase enclave exchange: `GET /iga/change-requests/{id}/approval-model` sign in the enclave `POST /iga/change-requests/{id}/approval-model` with the base64 doken. In FirstAdmin/Tideless mode, `authorize` signs server-side and no popup is needed.
243
196
 
244
- 4. **Batch support** -- The sign, commit, and cancel endpoints accept arrays. The UI should support selecting multiple change requests and acting on them in a single call.
197
+ 4. **Batch support** -- Use `POST /iga/change-requests/bulk-authorize` (`{"actionTypeIn":[...],"limit":N}`) to approve many CRs at once; commit each ready CR (`readyToCommit === true`).
245
198
 
246
199
  5. **Role check** -- Only users with admin roles should see the panel. Verify the admin token includes the required realm-admin or delegated role before rendering the UI. This is UI gating only; the server enforces authorization on every API call.
247
200
 
@@ -251,22 +204,21 @@ A working IGA admin panel must include:
251
204
 
252
205
  ### Lifecycle
253
206
 
254
- - [ ] Creating a role assignment produces a DRAFT change request visible via `GET .../all/requests`
255
- - [ ] Signing a change request transitions status from DRAFT/PENDING toward APPROVED
256
- - [ ] In multi-admin mode, signing returns `requiresApprovalPopup === true` and enclave flow completes
257
- - [ ] Committing an APPROVED change request applies the change and sets status to ACTIVE
258
- - [ ] Cancelling a PENDING change request removes it from the pending list
207
+ - [ ] A governed write returns 202 and a change request appears via `GET /iga/change-requests?status=PENDING`
208
+ - [ ] `POST .../{id}/authorize` records an approval (409 if the same admin re-signs)
209
+ - [ ] In Tide MultiAdmin mode, the `approval-model` enclave exchange completes and increments `authCount`
210
+ - [ ] Committing a ready CR (`readyToCommit`) applies the change (APPROVED); 412 while under threshold
211
+ - [ ] `POST .../{id}/deny` removes it from the pending list (DENIED)
259
212
 
260
213
  ### Endpoints
261
214
 
262
- - [ ] `GET .../counts` returns correct totals matching the full request list
263
- - [ ] `GET .../all/requests` returns all change types (user, role, client, group)
264
- - [ ] `POST .../sign/batch` accepts an array and processes all items
265
- - [ ] `POST .../commit/batch` and `POST .../cancel/batch` work with arrays
266
- - [ ] `POST /tideAdminResources/add-review` accepts the enclave-signed payload
267
- - [ ] `POST /tideAdminResources/add-rejection` rejects and sets status to DENIED
268
- - [ ] `GET .../{id}/activity` returns audit trail with timestamps
269
- - [ ] `POST .../{id}/comments` persists and appears in activity
215
+ - [ ] `GET /iga/change-requests?status=PENDING` returns all pending CRs across types
216
+ - [ ] `POST /iga/change-requests/{id}/authorize` records the approval
217
+ - [ ] `POST /iga/change-requests/bulk-authorize` authorizes a batch (429 if a bulk run is active)
218
+ - [ ] `POST /iga/change-requests/{id}/commit` applies the change; 412 under threshold
219
+ - [ ] `POST /iga/change-requests/{id}/deny` sets status to DENIED
220
+ - [ ] `GET/POST /iga/change-requests/{id}/comments` persist and read back
221
+ - [ ] `POST /tideAdminResources/add-review` and `/add-rejection` return `410 Gone` (deprecated — do not use)
270
222
 
271
223
  ### Authorization
272
224
 
@@ -278,33 +230,33 @@ A working IGA admin panel must include:
278
230
 
279
231
  ## Common Failures
280
232
 
281
- ### Sign Response Ignored in Multi-Admin Mode
233
+ ### Enclave Step Skipped in Tide MultiAdmin Mode
282
234
 
283
- **Symptom**: Approve button appears to succeed but change request stays in PENDING.
235
+ **Symptom**: Approve appears to succeed but the change request stays in PENDING and `authCount` does not increase.
284
236
 
285
- **Cause**: The `requiresApprovalPopup` flag was not checked. In multi-admin mode, signing returns a challenge that must be completed through the Tide enclave.
237
+ **Cause**: In Tide MultiAdmin mode, a bare `authorize` is not enough the admin must sign the enclave challenge via the two-phase `approval-model` exchange.
286
238
 
287
- **Fix**: After calling `sign/batch`, check `response[0].requiresApprovalPopup`. If `true`, decode `changeSetDraftRequests`, present the enclave iframe, and submit the signed result via `POST /tideAdminResources/add-review`.
239
+ **Fix**: `GET /iga/change-requests/{id}/approval-model`, decode `requestModel`, sign it in the Tide enclave, then `POST /iga/change-requests/{id}/approval-model` with `{ "requestModel": "<base64 doken>" }`. (FirstAdmin/Tideless mode signs server-side on `authorize` — no enclave step.)
288
240
 
289
241
  ---
290
242
 
291
- ### Committing Before Enough Approvals
243
+ ### Committing Before Enough Approvals (412)
292
244
 
293
- **Symptom**: `POST .../commit/batch` returns an error or silently fails.
245
+ **Symptom**: `POST /iga/change-requests/{id}/commit` returns **412 Precondition Failed**.
294
246
 
295
- **Cause**: The change request has not reached APPROVED status. Quorum threshold not met.
247
+ **Cause**: The CR is under threshold, or a dependency has not committed yet.
296
248
 
297
- **Fix**: Check the status before committing. Only commit change requests with status `APPROVED`. If still `PENDING`, more admin approvals are needed.
249
+ **Fix**: Only commit CRs where `readyToCommit === true`. If still short, collect more approvals; if `blocked`, commit the CR named in `dependsOn` first (see the commit-in-passes loop in `canon/iga-change-requests-api.md`).
298
250
 
299
251
  ---
300
252
 
301
- ### Wrong ChangeSetType in Payload
253
+ ### Re-signing the Same CR (409)
302
254
 
303
- **Symptom**: 400 Bad Request or unexpected behavior when signing/committing.
255
+ **Symptom**: `POST /iga/change-requests/{id}/authorize` returns **409 Conflict**.
304
256
 
305
- **Cause**: The `changeSetType` in the request body does not match the actual type of the draft record.
257
+ **Cause**: Four-eyes enforcement the same admin cannot sign a CR twice, or the CR is no longer PENDING.
306
258
 
307
- **Fix**: Read the `changeSetType` from the `GET .../all/requests` response and pass it back exactly. Do not hardcode a type.
259
+ **Fix**: Have a *different* admin authorize, or refresh the list; do not retry the same admin's signature.
308
260
 
309
261
  ---
310
262
 
@@ -314,31 +266,21 @@ A working IGA admin panel must include:
314
266
 
315
267
  **Cause**: Admin token not included or expired.
316
268
 
317
- **Fix**: Obtain a fresh admin token before each batch of calls. If using `KcAdminClient`, call `client.auth(...)` and handle token refresh.
318
-
319
- ---
320
-
321
- ### Rejection Silently Fails
322
-
323
- **Symptom**: Clicking "Reject" appears to succeed, but the change request stays in PENDING. No error returned.
324
-
325
- **Cause**: The rejection endpoint (`POST /tideAdminResources/add-rejection`) expects `multipart/form-data`. If you send `Content-Type: application/json`, the server accepts the request but ignores the payload fields.
326
-
327
- **Fix**: Use `FormData`, not `JSON.stringify`. Do not set `Content-Type` manually — the browser sets the correct multipart boundary automatically. See the code example in the Endpoints section above.
269
+ **Fix**: Obtain a fresh admin token before each batch of calls and handle token refresh.
328
270
 
329
271
  ---
330
272
 
331
- ### Counts Out of Sync with List
273
+ ### Calling a Deprecated add-review / add-rejection Endpoint
332
274
 
333
- **Symptom**: Dashboard badge shows a different number than the actual list.
275
+ **Symptom**: `POST /tideAdminResources/add-review` or `/add-rejection` returns `410 Gone`.
334
276
 
335
- **Cause**: `GET .../counts` and `GET .../all/requests` called at different times; a change was created or committed between calls.
277
+ **Cause**: Both endpoints were removed in the IGA decoupling. They are no longer functional.
336
278
 
337
- **Fix**: Refresh counts after any mutation (sign, commit, cancel). Do not cache counts across operations.
279
+ **Fix**: Approve via `POST /iga/change-requests/{id}/authorize` (passing the enclave-signed approval in the body) and reject via `POST /iga/change-requests/{id}/deny`.
338
280
 
339
281
  ---
340
282
 
341
283
  ## References
342
284
 
343
- - Source: `sources/docs/CHANGE_REQUEST_API.md` (normative), operational exemplars (keylessh, forseti-crypto-quickstart, tidecloak-test-cases)
344
- - Content-type asymmetry confirmed by: CHANGE_REQUEST_API.md, forseti-crypto-quickstart `tidecloakApi.ts` (uses FormData for `add-review`)
285
+ - Current surface: `IgaAdminResource` (`tidecloak-iga-extensions` iga-core) `/iga/change-requests/*`, `/iga/forseti-contracts`, `/iga/role-policies`.
286
+ - Legacy bootstrap-compat surface: `/tide-admin/change-set/*`, still used by `localtest/init/init_tidecloak.sh` and the `tidecloak-js` `AdminAPI` client.
File without changes
@@ -21,7 +21,26 @@ Complete guide to implementing server-side JWT verification for Tide.
21
21
 
22
22
  ---
23
23
 
24
- ## Complete Implementation
24
+ ## Recommended First: the Shipped `verifyTideCloakToken` Helper
25
+
26
+ Before hand-rolling anything, know that Tide ships a one-call verifier. `@tidecloak/verify` exports `verifyTideCloakToken`, re-exported via `@tidecloak/nextjs/server`:
27
+
28
+ ```typescript
29
+ import { verifyTideCloakToken } from '@tidecloak/nextjs/server'; // or '@tidecloak/verify'
30
+ import tcConfig from '../data/tidecloak.json';
31
+
32
+ // Arg order: (config, token, allowedRoles=[]) — config FIRST.
33
+ const payload = await verifyTideCloakToken(tcConfig, token, ['admin']);
34
+ // payload = decoded claims on success, `false` on any failure.
35
+ ```
36
+
37
+ It does local-JWKS signature verification (embedded `jwk`), issuer + `azp` validation, time-claim checks with `clockTolerance`, and optional role enforcement across realm and client roles. For the majority of API-protection needs this is all you require — see [protect-api-nextjs.md](protect-api-nextjs.md).
38
+
39
+ The complete manual implementation below is the **"under the hood" alternative**. Reach for it when you need something `verifyTideCloakToken` does not do: app-side DPoP proof re-verification, distinct 401 vs 403 responses, `iat` future-dating rejection, or an Express integration.
40
+
41
+ ---
42
+
43
+ ## Complete Implementation (Manual)
25
44
 
26
45
  If you already created `lib/auth/tidecloakConfig.ts`, `lib/auth/tideJWT.ts`, and `lib/auth/protect.ts` from [protect-api-nextjs.md](protect-api-nextjs.md), the versions below **replace** them. This playbook adds DPoP verification, client-role checking in `hasRole`, `extractToken` (supports both Bearer and DPoP schemes), and `iat` future-validation.
27
46
 
File without changes
@@ -48,7 +48,7 @@ Copy-paste this prompt to an AI coding agent to scaffold a customer portal with
48
48
  > **If TideCloak is running but checks 2-4 fail**, follow the `tide-setup` skill to complete setup. Use playbook `add-auth-nextjs-fresh`.
49
49
  >
50
50
  > **Package versions** (before writing package.json):
51
- > 1. Run `npm view @tidecloak/nextjs version` to get the current stable version. Pin it exactly (e.g., `"0.13.26"`). Do not use `"latest"` or `"^"` for `@tidecloak/*` packages.
51
+ > 1. Run `npm view @tidecloak/nextjs version` to get the current stable version. Pin it exactly (e.g., `"0.13.33"`). Do not use `"latest"` or `"^"` for `@tidecloak/*` packages.
52
52
  > 2. If the resolved version is 0.99.x, skip it. Use the highest non-0.99.x version instead. If unsure, fall back to the versions in `canon/version-policy.md`.
53
53
  > 3. Run `npm view next version` and `npm view react version` to get current stable framework versions. Use caret ranges (e.g., `"^15.0.0"`). Align React and React DOM to the same major.
54
54
  > 4. Do not use `--force` or `--legacy-peer-deps` as the default install strategy. If peer dependency conflicts arise, resolve them by aligning versions.
File without changes
File without changes