@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
package/package.json CHANGED
@@ -1,45 +1,48 @@
1
- {
2
- "name": "@tideorg/mcp",
3
- "version": "1.4.1",
4
- "description": "MCP server exposing Tide operational guidance — canon, playbooks, skills, prompts, adapters, and scenarios for AI coding agents",
5
- "type": "module",
6
- "bin": {
7
- "tide-agent-pack": "mcp-server/dist/index.js"
8
- },
9
- "scripts": {
10
- "build": "cd mcp-server && npm run build",
11
- "prepublishOnly": "npm run build"
12
- },
13
- "files": [
14
- "mcp-server/dist/",
15
- "canon/",
16
- "playbooks/",
17
- "skills/",
18
- "prompts/",
19
- "adapters/",
20
- "reference-apps/",
21
- "GAP_REGISTER.md"
22
- ],
23
- "dependencies": {
24
- "@modelcontextprotocol/sdk": "^1.12.1",
25
- "zod": "^3.24.0"
26
- },
27
- "keywords": [
28
- "mcp",
29
- "tidecloak",
30
- "tide",
31
- "auth",
32
- "security",
33
- "threshold-cryptography",
34
- "ai-agent",
35
- "coding-agent"
36
- ],
37
- "license": "MIT",
38
- "repository": {
39
- "type": "git",
40
- "url": "https://github.com/tide-foundation/tide-agent-pack"
41
- },
42
- "engines": {
43
- "node": ">=18"
44
- }
45
- }
1
+ {
2
+ "name": "@tideorg/mcp",
3
+ "version": "1.9.0",
4
+ "description": "MCP server exposing Tide operational guidance — canon, playbooks, skills, prompts, adapters, and scenarios for AI coding agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "tide-agent-pack": "mcp-server/dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "cd mcp-server && npm run build",
11
+ "test": "cd mcp-server && npm test",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "files": [
15
+ "mcp-server/dist/",
16
+ "canon/",
17
+ "playbooks/",
18
+ "skills/",
19
+ "prompts/",
20
+ "adapters/",
21
+ "reference-apps/",
22
+ "GAP_REGISTER.md",
23
+ "PRIVACY.md",
24
+ "CHANGELOG.md"
25
+ ],
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.12.1",
28
+ "zod": "^3.24.0"
29
+ },
30
+ "keywords": [
31
+ "mcp",
32
+ "tidecloak",
33
+ "tide",
34
+ "auth",
35
+ "security",
36
+ "threshold-cryptography",
37
+ "ai-agent",
38
+ "coding-agent"
39
+ ],
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/tide-foundation/tide-agent-pack"
44
+ },
45
+ "engines": {
46
+ "node": ">=18"
47
+ }
48
+ }
@@ -298,7 +298,9 @@ export function ProfileButton() {
298
298
  import { useTideCloak } from '@tidecloak/nextjs';
299
299
 
300
300
  export function ProfileButton() {
301
- const { authenticated, user, login, logout } = useTideCloak();
301
+ // useTideCloak() has no `user` object. Read claims via getValueFromIdToken /
302
+ // getValueFromToken (e.g. getValueFromIdToken('preferred_username')).
303
+ const { authenticated, login, logout } = useTideCloak();
302
304
 
303
305
  if (!authenticated) {
304
306
  return <button onClick={login}>Login</button>;
@@ -334,34 +336,37 @@ export async function GET(req) {
334
336
  }
335
337
  ```
336
338
 
337
- **After** (Tide - manual JWT verification):
339
+ **After** (Tide - server-side JWT verification):
338
340
 
339
- You must implement server-side JWT verification. Tide SDK does not provide automatic API protection.
341
+ Tide ships a server-side verification helper. `@tidecloak/nextjs/server` exports `verifyTideCloakToken(config, token, allowedRoles?)` (re-exported from `@tidecloak/verify`). Use it instead of hand-rolling JWKS verification.
340
342
 
341
- See [verify-jwt-server-side.md](verify-jwt-server-side.md) for full implementation.
342
-
343
- **Quick pattern** (based on keylessh):
343
+ **Quick pattern** (supported helper):
344
344
  ```typescript
345
345
  // app/api/users/route.ts
346
346
  import { NextRequest } from 'next/server';
347
- import { verifyTideJWT } from '@/lib/auth/tideJWT'; // See verify-jwt-server-side.md
347
+ import { verifyTideCloakToken } from '@tidecloak/nextjs/server';
348
+ import tcConfig from '../../../data/tidecloak.json';
348
349
 
349
350
  export async function GET(req: NextRequest) {
350
351
  const authHeader = req.headers.get('authorization');
351
352
  if (!authHeader) {
352
353
  return Response.json({ error: 'Unauthorized' }, { status: 401 });
353
354
  }
355
+ // Accepts "Bearer <jwt>" or "DPoP <jwt>"
356
+ const token = authHeader.replace(/^(Bearer|DPoP) /, '');
354
357
 
355
- try {
356
- const jwt = await verifyTideJWT(authHeader.replace('Bearer ', ''));
357
- return Response.json({ users: [...] });
358
- } catch (err) {
358
+ // Arg order is (config, token, allowedRoles) — config FIRST.
359
+ const payload = await verifyTideCloakToken(tcConfig, token, []);
360
+ if (!payload) {
359
361
  return Response.json({ error: 'Invalid token' }, { status: 401 });
360
362
  }
363
+ return Response.json({ users: [...] });
361
364
  }
362
365
  ```
363
366
 
364
- **Critical**: You must create `lib/auth/tideJWT.ts` with local JWKS verification. See [verify-jwt-server-side.md](verify-jwt-server-side.md) for exact implementation.
367
+ `verifyTideCloakToken` verifies the signature against the adapter's embedded `jwk` (local JWKS), checks the issuer/`azp`, and optionally enforces `allowedRoles`. It returns the decoded payload on success and `false` on failure.
368
+
369
+ **If you need the manual version** (or DPoP proof re-verification): see [verify-jwt-server-side.md](verify-jwt-server-side.md), which shows the same checks hand-rolled with `jose` in `lib/auth/tideJWT.ts`.
365
370
 
366
371
  ---
367
372
 
@@ -390,13 +395,24 @@ export const config = {
390
395
  };
391
396
  ```
392
397
 
393
- **After** (Tide - client-side route guard):
398
+ **After** (Tide options):
399
+
400
+ Option A — **use the Tide-provided proxy/middleware helpers.** `@tidecloak/nextjs/server` ships `createTideCloakProxy` (Next.js 16+ `proxy.ts`, Node runtime) and `createTideCloakMiddleware` (Edge `middleware.ts`, still supported). These replace a NextAuth-style middleware export and perform real token verification via `verifyTideCloakToken`:
401
+ ```typescript
402
+ // proxy.ts (Next.js 16+) — or middleware.ts on 15 and earlier
403
+ import { createTideCloakProxy } from '@tidecloak/nextjs/server';
404
+ import tcConfig from './data/tidecloak.json';
394
405
 
395
- **Remove proxy.ts / middleware.ts** if it only checks auth. Tide provider handles client-side auth state.
406
+ export default createTideCloakProxy(tcConfig);
407
+
408
+ export const config = {
409
+ matcher: ['/dashboard/:path*', '/admin/:path*'],
410
+ };
411
+ ```
396
412
 
397
- For API protection, use server-side JWT verification (Step 8), not proxy/middleware.
413
+ Option B **remove proxy.ts / middleware.ts** and rely on the client-side provider for UX gating plus server-side JWT verification in each API route (Step 8). Do this if you do not want request-interception at all.
398
414
 
399
- **Warning**: `proxy.ts` (and legacy `middleware.ts`) route protection is UI gating only, NOT real authorization. See [canon/feature-mapping.md](../canon/feature-mapping.md#protected-routes-vs-protected-apis).
415
+ **Warning**: A plain, hand-written `proxy.ts` / `middleware.ts` that only checks cookie presence is UI gating only, NOT real authorization. The `createTideCloakProxy` / `createTideCloakMiddleware` helpers above DO verify the token, but you should still verify JWTs inside API routes (Step 8) as the authoritative enforcement point. See [canon/feature-mapping.md](../canon/feature-mapping.md#protected-routes-vs-protected-apis).
400
416
 
401
417
  If you want client-side route guards, implement in component:
402
418
  ```typescript
@@ -632,7 +648,7 @@ grep -r "from 'next-auth\|from \"next-auth" . --include="*.tsx" --include="*.ts"
632
648
 
633
649
  **Cause**: Old auth proxy or middleware still active.
634
650
 
635
- **Fix**: Remove or update `proxy.ts` (or legacy `middleware.ts`). Tide has no built-in Next.js request interception. Implement client-side route guards (Step 9) or server-side JWT verification.
651
+ **Fix**: Remove or update `proxy.ts` (or legacy `middleware.ts`). Either switch it to Tide's `createTideCloakProxy` / `createTideCloakMiddleware` from `@tidecloak/nextjs/server` (Step 9, Option A), or remove it and rely on client-side route guards plus server-side JWT verification (Step 8).
636
652
 
637
653
  ---
638
654
 
@@ -83,7 +83,7 @@ ls -la app/ 2>/dev/null && echo "App Router" || echo "Pages Router"
83
83
  npm install @tidecloak/nextjs
84
84
  ```
85
85
 
86
- **Verify version**: All `@tidecloak/*` packages are currently at `0.13.x`. Run `npm view @tidecloak/nextjs version` to confirm. Do not assume `1.0.0`.
86
+ **Verify version**: `@tidecloak/*` packages are currently at `0.13.33` (run `npm view @tidecloak/nextjs version` to confirm/pin). Do not assume `1.0.0`.
87
87
 
88
88
  **Next.js 16+ bundler**: Next.js 16 defaults to Turbopack. The Tide SDK requires webpack for `strictExportPresence = false` and `@tidecloak/react` ESM alias. Update `package.json` scripts:
89
89
  ```json
@@ -137,13 +137,18 @@ Tide E2EE uses tag-based roles for **self-encryption** (user-bound, private data
137
137
  // Client-side E2EE with role enforcement (self-encryption)
138
138
  const { doEncrypt, doDecrypt } = useTideCloak();
139
139
 
140
+ // Both take a single argument: an ARRAY of items. Each encrypt item is
141
+ // { data, tags }; the tags drive the _tide_<tag>.self* role check.
140
142
  // Requires _tide_ssn.selfencrypt role
141
- const ciphertext = await doEncrypt('ssn', '123-45-6789');
143
+ const [ciphertext] = await doEncrypt([{ data: '123-45-6789', tags: ['ssn'] }]);
142
144
 
145
+ // doDecrypt takes the array of encrypted items ({ encrypted, tags }).
143
146
  // Requires _tide_ssn.selfdecrypt role
144
- const plaintext = await doDecrypt('ssn', ciphertext);
147
+ const [plaintext] = await doDecrypt([{ encrypted: ciphertext, tags: ['ssn'] }]);
145
148
  ```
146
149
 
150
+ **Signature note**: The `useTideCloak()` `doEncrypt`/`doDecrypt` are single-argument (`data`) wrappers. `data` is an array so you can encrypt/decrypt multiple fields in one call. This matches the shapes in [setup-forseti-e2ee.md](setup-forseti-e2ee.md) and [configure-e2ee-roles-and-policies.md](configure-e2ee-roles-and-policies.md). Do NOT call them with positional `(tag, value)` arguments — that form does not exist.
151
+
147
152
  Fabric enforces roles cryptographically. See [canon/concepts.md#tag-based-e2ee-roles](../canon/concepts.md#tag-based-e2ee-roles).
148
153
 
149
154
  **Self-encryption is user-bound**: only the encrypting user can decrypt. Giving another user the `selfdecrypt` role does NOT let them decrypt your data. For shared data between users, use policy-governed VVK encryption with a Forseti contract instead. See [setup-forseti-e2ee.md](setup-forseti-e2ee.md) and [canon/anti-patterns.md AP-24](../canon/anti-patterns.md#ap-24-using-self-encryption-for-shared-data-between-users).
@@ -79,6 +79,14 @@ This activates the Tide license and generates the VRK. No manual "Manage License
79
79
 
80
80
  **Critical:** Content-Type must be `application/x-www-form-urlencoded`. JSON causes failure.
81
81
 
82
+ **Form parameters** (`setUpTideRealm` on `main`):
83
+
84
+ | Param | Required | Default | Notes |
85
+ |-------|----------|---------|-------|
86
+ | `email` | Yes (for licensing) | — | Used to request the free-tier license. Ignored only when `skipLicense=true`. |
87
+ | `isRagnarokEnabled` | No | `true` | Enables realm offboarding (Ragnarok). The endpoint already defaults it to `true`; passing `isRagnarokEnabled=true` explicitly is harmless and matches the default. |
88
+ | `skipLicense` | No | `false` | When `true`, skips license activation (`email` unused) and returns early. Leave at the default for normal bootstrap. |
89
+
82
90
  ### Step 5: Enable IGA
83
91
 
84
92
  ```bash
@@ -94,28 +102,35 @@ curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/tide-admin/toggle-iga"
94
102
  Realm creation + Tide setup generates change requests that must be approved before proceeding.
95
103
 
96
104
  ```bash
97
- approve_and_commit() {
98
- local TYPE=$1
99
- TOKEN="$(get_token)"
100
- local requests=$(curl -s "$TIDECLOAK_URL/admin/realms/$REALM_NAME/tide-admin/change-set/$TYPE/requests" \
101
- -H "Authorization: Bearer $TOKEN" 2>/dev/null || echo "[]")
105
+ # Current /iga/change-requests/... surface (replaces legacy /tide-admin/change-set/...).
106
+ # Bootstrap runs in FirstAdmin/Tideless mode, so authorize signs server-side (no enclave).
107
+ # Full spec: canon/iga-change-requests-api.md.
108
+ approve_all_pending() {
109
+ local TOKEN ready id
102
110
 
103
- echo "$requests" | jq -c '.[]' | while read -r req; do
104
- local id=$(echo "$req" | jq -r .draftRecordId)
105
- local cst=$(echo "$req" | jq -r .changeSetType)
106
- local at=$(echo "$req" | jq -r .actionType)
107
- local payload="{\"changeSetId\":\"$id\",\"changeSetType\":\"$cst\",\"actionType\":\"$at\"}"
111
+ # 1. Authorize all pending CREATE/DELETE change requests in one call.
112
+ TOKEN="$(get_token)"
113
+ curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests/bulk-authorize" \
114
+ -H "Authorization: Bearer $TOKEN" \
115
+ -H "Content-Type: application/json" \
116
+ -d '{"actionTypeIn":["CREATE","DELETE"],"limit":100}' > /dev/null 2>&1
108
117
 
118
+ # 2. Commit ready CRs; loop passes so dependent CRs become ready.
119
+ for pass in 1 2 3 4 5; do
109
120
  TOKEN="$(get_token)"
110
- curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/tide-admin/change-set/sign" \
111
- -H "Authorization: Bearer $TOKEN" \
112
- -H "Content-Type: application/json" -d "$payload"
113
-
114
- curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/tide-admin/change-set/commit" \
115
- -H "Authorization: Bearer $TOKEN" \
116
- -H "Content-Type: application/json" -d "$payload"
121
+ ready=$(curl -s "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests?status=PENDING" \
122
+ -H "Authorization: Bearer $TOKEN" 2>/dev/null \
123
+ | jq -r '.[] | select(.readyToCommit==true) | .id' 2>/dev/null)
124
+ [ -z "$ready" ] && break
125
+ for id in $ready; do
126
+ TOKEN="$(get_token)"
127
+ curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests/$id/commit" \
128
+ -H "Authorization: Bearer $TOKEN" > /dev/null 2>&1
129
+ done
117
130
  done
118
131
  }
132
+ # Legacy call sites pass a type arg the new list-all surface ignores.
133
+ approve_and_commit() { approve_all_pending; }
119
134
 
120
135
  approve_and_commit clients
121
136
  ```
@@ -135,7 +150,7 @@ approve_and_commit clients
135
150
 
136
151
  | Symptom | Cause | Fix |
137
152
  |---------|-------|-----|
138
- | "Could not create tide realm" | JSON sent to `setUpTideRealm` instead of form data | Use `application/x-www-form-urlencoded` |
153
+ | "Tide realm setup failed" / "Could not set up the Tide realm" | JSON sent to `setUpTideRealm` instead of form data | Use `application/x-www-form-urlencoded` |
139
154
  | "email is null or empty" | Missing email parameter | Add `--data-urlencode "email=..."` |
140
155
  | 404 on setUpTideRealm | Wrong path (`/tide-admin/setUpTideRealm`) | Correct path: `/vendorResources/setUpTideRealm` |
141
156
  | Realm import fails | Placeholders not replaced in template | Verify `sed` replaced all three: `REALM_NAME`, `CLIENT_NAME`, `CLIENT_APP_URL` |
File without changes
@@ -372,34 +372,38 @@ curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/tide-admin/toggle-iga"
372
372
  -H "Content-Type: application/x-www-form-urlencoded" \
373
373
  --data-urlencode "isIGAEnabled=true"
374
374
 
375
- # 5. Auto-approve client change requests (created during realm setup)
376
- approve_and_commit() {
377
- local TYPE=$1
375
+ # 5. Auto-approve change requests created during realm setup.
376
+ # Uses the current /iga/change-requests/... surface (replaces the legacy
377
+ # /tide-admin/change-set/... surface). During bootstrap the realm is in
378
+ # FirstAdmin/Tideless mode, so authorize signs server-side (no enclave).
379
+ # See canon/iga-change-requests-api.md.
380
+ approve_all_pending() {
381
+ local TOKEN ready id
382
+
383
+ # 1. Authorize every pending CREATE/DELETE change request in one call.
378
384
  TOKEN="$(get_token)"
379
- local requests=$(curl -s -X GET \
380
- "$TIDECLOAK_URL/admin/realms/$REALM_NAME/tide-admin/change-set/$TYPE/requests" \
381
- -H "Authorization: Bearer $TOKEN" 2>/dev/null || echo "[]")
382
-
383
- echo "$requests" | jq -c '.[]' | while read -r req; do
384
- local payload=$(jq -n \
385
- --arg id "$(echo "$req" | jq -r .draftRecordId)" \
386
- --arg cst "$(echo "$req" | jq -r .changeSetType)" \
387
- --arg at "$(echo "$req" | jq -r .actionType)" \
388
- '{changeSetId:$id,changeSetType:$cst,actionType:$at}')
389
-
390
- # Sign then commit
385
+ curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests/bulk-authorize" \
386
+ -H "Authorization: Bearer $TOKEN" \
387
+ -H "Content-Type: application/json" \
388
+ -d '{"actionTypeIn":["CREATE","DELETE"],"limit":100}' > /dev/null 2>&1
389
+
390
+ # 2. Commit everything that is ready. Loop passes so dependent CRs
391
+ # (e.g. a role must exist before its assignment) become ready and commit.
392
+ for pass in 1 2 3 4 5; do
391
393
  TOKEN="$(get_token)"
392
- curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/tide-admin/change-set/sign" \
393
- -H "Authorization: Bearer $TOKEN" \
394
- -H "Content-Type: application/json" -d "$payload"
395
-
396
- curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/tide-admin/change-set/commit" \
397
- -H "Authorization: Bearer $TOKEN" \
398
- -H "Content-Type: application/json" -d "$payload"
394
+ ready=$(curl -s "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests?status=PENDING" \
395
+ -H "Authorization: Bearer $TOKEN" 2>/dev/null \
396
+ | jq -r '.[] | select(.readyToCommit==true) | .id' 2>/dev/null)
397
+ [ -z "$ready" ] && break
398
+ for id in $ready; do
399
+ TOKEN="$(get_token)"
400
+ curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests/$id/commit" \
401
+ -H "Authorization: Bearer $TOKEN" > /dev/null 2>&1
402
+ done
399
403
  done
400
404
  }
401
405
 
402
- approve_and_commit clients
406
+ approve_all_pending
403
407
 
404
408
  # 6. Create admin user
405
409
  TOKEN="$(get_token)"
@@ -410,7 +414,7 @@ curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/users" \
410
414
 
411
415
  # 6b. Approve user creation (user is not queryable until committed with IGA enabled)
412
416
  sleep 2
413
- approve_and_commit users
417
+ approve_all_pending
414
418
 
415
419
  # 6c. Assign tide-realm-admin role
416
420
  TOKEN="$(get_token)"
@@ -461,8 +465,7 @@ done
461
465
 
462
466
  # 9. Approve role assignment change requests
463
467
  sleep 2
464
- approve_and_commit users
465
- approve_and_commit roles
468
+ approve_all_pending
466
469
 
467
470
  # 10. Update CustomAdminUIDomain for enclave approval popups
468
471
  # CustomAdminUIDomain is OPTIONAL and APP-SPECIFIC: only needed when a separate
@@ -539,8 +542,8 @@ curl -s -X GET "$TIDECLOAK_URL/admin/realms/$REALM_NAME/vendorResources/get-inst
539
542
  | `POST /admin/realms` | Create realm from JSON template |
540
543
  | `POST /admin/realms/{realm}/vendorResources/setUpTideRealm` | Initialize Tide licensing + VRK |
541
544
  | `POST /admin/realms/{realm}/tide-admin/toggle-iga` | Enable IGA governance |
542
- | `POST /admin/realms/{realm}/tide-admin/change-set/sign` | Approve a change request |
543
- | `POST /admin/realms/{realm}/tide-admin/change-set/commit` | Commit an approved change |
545
+ | `POST /admin/realms/{realm}/iga/change-requests/{id}/authorize` | Approve (sign) a change request. Batch: `POST /iga/change-requests/bulk-authorize`. Replaces legacy `tide-admin/change-set/sign`. |
546
+ | `POST /admin/realms/{realm}/iga/change-requests/{id}/commit` | Commit an approved change. Replaces legacy `tide-admin/change-set/commit`. See `canon/iga-change-requests-api.md`. |
544
547
  | `POST /admin/realms/{realm}/tideAdminResources/get-required-action-link` | Generate admin invite link |
545
548
  | `GET /admin/realms/{realm}/vendorResources/get-installations-provider` | Download adapter config |
546
549
  | `POST /admin/realms/{realm}/vendorResources/sign-idp-settings` | Sign IdP settings with VRK. **ALWAYS required** after any Tide IDP config change (logo, background, backup toggle, custom admin domain, or client origin changes). Without it, the enclave rejects settings as unsigned. Requires active VRK/license. |
File without changes
@@ -93,7 +93,7 @@ function hasAnyRole(user: any, role: string): boolean {
93
93
 
94
94
  1. TideCloak Admin → Users → {user} → Role Mappings
95
95
  2. Assign role → {role-name}
96
- 3. If IGA enabled: Approve change-set (requires quorum)
96
+ 3. If IGA enabled: authorize + commit the change request (requires quorum; see `canon/iga-change-requests-api.md`)
97
97
  4. User logout/login to get new token
98
98
 
99
99
  **Fix (default roles)**:
@@ -189,7 +189,7 @@ async function forceTokenRefresh() {
189
189
 
190
190
  ## Symptom: E2EE Role Not Working
191
191
 
192
- **What you see**: `doEncrypt('tag', data)` fails even though user has role.
192
+ **What you see**: `doEncrypt([{ data, tags: ['tag'] }])` fails even though user has role.
193
193
 
194
194
  **Diagnostic**:
195
195
 
@@ -129,29 +129,32 @@ done
129
129
  ### Step 5: Approve user change requests
130
130
 
131
131
  ```bash
132
- # Reuse approve_and_commit from bootstrap-realm-from-template
133
- approve_and_commit() {
134
- local TYPE=$1
135
- TOKEN="$(get_token)"
136
- local requests=$(curl -s "$TIDECLOAK_URL/admin/realms/$REALM_NAME/tide-admin/change-set/$TYPE/requests" \
137
- -H "Authorization: Bearer $TOKEN" 2>/dev/null || echo "[]")
132
+ # Reuse approve_all_pending from bootstrap-realm-from-template.
133
+ # Current /iga/change-requests/... surface (replaces legacy /tide-admin/change-set/...).
134
+ # Bootstrap = FirstAdmin/Tideless: authorize signs server-side. Spec: canon/iga-change-requests-api.md.
135
+ approve_all_pending() {
136
+ local TOKEN ready id
138
137
 
139
- echo "$requests" | jq -c '.[]' | while read -r req; do
140
- local id=$(echo "$req" | jq -r .draftRecordId)
141
- local cst=$(echo "$req" | jq -r .changeSetType)
142
- local at=$(echo "$req" | jq -r .actionType)
143
- local payload="{\"changeSetId\":\"$id\",\"changeSetType\":\"$cst\",\"actionType\":\"$at\"}"
138
+ TOKEN="$(get_token)"
139
+ curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests/bulk-authorize" \
140
+ -H "Authorization: Bearer $TOKEN" \
141
+ -H "Content-Type: application/json" \
142
+ -d '{"actionTypeIn":["CREATE","DELETE"],"limit":100}' > /dev/null 2>&1
144
143
 
144
+ for pass in 1 2 3 4 5; do
145
145
  TOKEN="$(get_token)"
146
- curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/tide-admin/change-set/sign" \
147
- -H "Authorization: Bearer $TOKEN" \
148
- -H "Content-Type: application/json" -d "$payload"
149
-
150
- curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/tide-admin/change-set/commit" \
151
- -H "Authorization: Bearer $TOKEN" \
152
- -H "Content-Type: application/json" -d "$payload"
146
+ ready=$(curl -s "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests?status=PENDING" \
147
+ -H "Authorization: Bearer $TOKEN" 2>/dev/null \
148
+ | jq -r '.[] | select(.readyToCommit==true) | .id' 2>/dev/null)
149
+ [ -z "$ready" ] && break
150
+ for id in $ready; do
151
+ TOKEN="$(get_token)"
152
+ curl -s -X POST "$TIDECLOAK_URL/admin/realms/$REALM_NAME/iga/change-requests/$id/commit" \
153
+ -H "Authorization: Bearer $TOKEN" > /dev/null 2>&1
154
+ done
153
155
  done
154
156
  }
157
+ approve_and_commit() { approve_all_pending; }
155
158
 
156
159
  approve_and_commit users
157
160
  approve_and_commit roles
@@ -216,7 +219,7 @@ node -e "const c=require('$ADAPTER_OUTPUT'); console.log('jwk:', !!c.jwk, 'vendo
216
219
  | "Could not find configuration for Required Action" | Tide env vars missing from container | Restart container with all required env vars |
217
220
  | `tideUserKey` never appears | Admin didn't complete the linking flow | Open invite link in browser, complete the flow |
218
221
  | Enclave popups don't work from app | CustomAdminUIDomain not set | Run Step 6 |
219
- | Adapter JSON missing `jwk` | IGA not enabled | Run `toggle-iga` from `bootstrap-realm-from-template` Step 5 |
222
+ | Adapter JSON missing `jwk` | `setUpTideRealm` did not run / no `tide-vendor-key` component + EdDSA key on the realm (the `jwk` is gated on those, not on IGA) | Re-run `setUpTideRealm` from `bootstrap-realm-from-template` Step 4 and confirm it returned 200 |
220
223
 
221
224
  ---
222
225
 
File without changes
@@ -76,7 +76,46 @@ Any FAIL means the API route is not properly protected. Use the implementation b
76
76
 
77
77
  ---
78
78
 
79
- ## Quick Implementation (App Router)
79
+ ## Recommended: Use the Shipped `verifyTideCloakToken` Helper
80
+
81
+ Tide ships a server-side verification helper — you do not have to hand-roll JWKS verification. `@tidecloak/nextjs/server` re-exports `verifyTideCloakToken` from `@tidecloak/verify`:
82
+
83
+ ```typescript
84
+ // app/api/users/route.ts
85
+ import { NextRequest } from 'next/server';
86
+ import { verifyTideCloakToken } from '@tidecloak/nextjs/server';
87
+ import tcConfig from '../../../data/tidecloak.json';
88
+
89
+ export async function GET(req: NextRequest) {
90
+ const authHeader = req.headers.get('authorization');
91
+ if (!authHeader) {
92
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
93
+ }
94
+ const token = authHeader.replace(/^(Bearer|DPoP) /, '');
95
+
96
+ // Signature (arg order): verifyTideCloakToken(config, token, allowedRoles=[])
97
+ // config is FIRST. Pass roles to enforce them; [] to just verify the token.
98
+ const payload = await verifyTideCloakToken(tcConfig, token, ['admin']);
99
+ if (!payload) {
100
+ // Returns false on any failure (bad signature, issuer, azp, expiry, or role).
101
+ return Response.json({ error: 'Forbidden' }, { status: 403 });
102
+ }
103
+ return Response.json({ users: [...] });
104
+ }
105
+ ```
106
+
107
+ `verifyTideCloakToken`:
108
+ - verifies the signature against the adapter's embedded `jwk` (local JWKS — no network call),
109
+ - validates the issuer (`auth-server-url` + `/realms/<realm>`) and `azp` (against `config.resource`),
110
+ - checks time claims with a small `clockTolerance`,
111
+ - optionally enforces `allowedRoles` (matches across `realm_access.roles` and `resource_access[client].roles`),
112
+ - returns the decoded payload on success, `false` on failure.
113
+
114
+ Prefer this for standard API protection. The manual `jose` implementation below is the **under-the-hood alternative** — use it when you need custom behavior the helper does not cover (e.g. app-side DPoP proof re-verification, separate 401-vs-403 semantics, or `iat` future-validation). See [verify-jwt-server-side.md](verify-jwt-server-side.md).
115
+
116
+ ---
117
+
118
+ ## Quick Implementation — Manual (App Router)
80
119
 
81
120
  This section creates minimal versions of `lib/auth/tidecloakConfig.ts`, `lib/auth/tideJWT.ts`, and `lib/auth/protect.ts`. If you also follow [verify-jwt-server-side.md](verify-jwt-server-side.md), its richer versions **replace** these files (adds DPoP verification, client-role checking, `extractToken`, and `iat` validation).
82
121