@tideorg/mcp 1.4.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 (76) hide show
  1. package/GAP_REGISTER.md +117 -0
  2. package/README.md +38 -0
  3. package/adapters/AGENTS.md +241 -0
  4. package/adapters/CLAUDE.md +146 -0
  5. package/adapters/replit.md +224 -0
  6. package/canon/anti-patterns.md +2133 -0
  7. package/canon/concepts.md +816 -0
  8. package/canon/custom-contracts.md +533 -0
  9. package/canon/delegation.md +195 -0
  10. package/canon/feature-mapping.md +637 -0
  11. package/canon/framework-matrix.md +1125 -0
  12. package/canon/invariants.md +851 -0
  13. package/canon/redirect-handler.md +254 -0
  14. package/canon/tidecloak-bootstrap.md +287 -0
  15. package/canon/tidecloak-endpoints.md +294 -0
  16. package/canon/troubleshooting.md +1494 -0
  17. package/canon/version-policy.md +89 -0
  18. package/mcp-server/dist/index.d.ts +2 -0
  19. package/mcp-server/dist/index.js +605 -0
  20. package/package.json +45 -0
  21. package/playbooks/add-auth-nextjs-existing.md +799 -0
  22. package/playbooks/add-auth-nextjs-fresh.md +654 -0
  23. package/playbooks/add-rbac-nextjs.md +190 -0
  24. package/playbooks/bootstrap-realm-from-template.md +170 -0
  25. package/playbooks/configure-e2ee-roles-and-policies.md +196 -0
  26. package/playbooks/deploy-tidecloak-docker.md +792 -0
  27. package/playbooks/diagnose-broken-login.md +234 -0
  28. package/playbooks/diagnose-missing-roles-or-claims.md +253 -0
  29. package/playbooks/initialize-admin-and-link-account.md +235 -0
  30. package/playbooks/migrate-from-existing-auth.md +198 -0
  31. package/playbooks/protect-api-nextjs.md +451 -0
  32. package/playbooks/protect-routes-nextjs.md +544 -0
  33. package/playbooks/setup-forseti-e2ee.md +756 -0
  34. package/playbooks/setup-iga-admin-panel.md +344 -0
  35. package/playbooks/setup-server-delegation.md +142 -0
  36. package/playbooks/start-tidecloak-dev.md +130 -0
  37. package/playbooks/verify-jwt-server-side.md +439 -0
  38. package/prompts/add-admin-approval-flow.md +50 -0
  39. package/prompts/build-private-customer-portal.md +85 -0
  40. package/prompts/migrate-generic-auth-to-tide.md +67 -0
  41. package/prompts/secure-existing-app.md +80 -0
  42. package/reference-apps/INDEX.md +87 -0
  43. package/reference-apps/encrypted-communication/anti-patterns.md +123 -0
  44. package/reference-apps/encrypted-communication/bootstrap-sequence.md +152 -0
  45. package/reference-apps/encrypted-communication/manifest.yaml +100 -0
  46. package/reference-apps/encrypted-communication/role-policy-matrix.md +80 -0
  47. package/reference-apps/encrypted-communication/scenario.md +134 -0
  48. package/reference-apps/git-pr-signing-service/anti-patterns.md +61 -0
  49. package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +157 -0
  50. package/reference-apps/git-pr-signing-service/manifest.yaml +80 -0
  51. package/reference-apps/git-pr-signing-service/role-policy-matrix.md +99 -0
  52. package/reference-apps/git-pr-signing-service/scenario.md +169 -0
  53. package/reference-apps/iga-admin-governance/anti-patterns.md +133 -0
  54. package/reference-apps/iga-admin-governance/bootstrap-sequence.md +153 -0
  55. package/reference-apps/iga-admin-governance/manifest.yaml +87 -0
  56. package/reference-apps/iga-admin-governance/role-policy-matrix.md +67 -0
  57. package/reference-apps/iga-admin-governance/scenario.md +126 -0
  58. package/reference-apps/organisation-password-manager/anti-patterns.md +71 -0
  59. package/reference-apps/organisation-password-manager/bootstrap-sequence.md +127 -0
  60. package/reference-apps/organisation-password-manager/manifest.yaml +86 -0
  61. package/reference-apps/organisation-password-manager/role-policy-matrix.md +59 -0
  62. package/reference-apps/organisation-password-manager/scenario.md +107 -0
  63. package/reference-apps/policy-governed-signing/anti-patterns.md +67 -0
  64. package/reference-apps/policy-governed-signing/bootstrap-sequence.md +128 -0
  65. package/reference-apps/policy-governed-signing/manifest.yaml +78 -0
  66. package/reference-apps/policy-governed-signing/role-policy-matrix.md +62 -0
  67. package/reference-apps/policy-governed-signing/scenario.md +113 -0
  68. package/skills/tide-diagnostics/SKILL.md +99 -0
  69. package/skills/tide-integration/SKILL.md +136 -0
  70. package/skills/tide-learning-capture/SKILL.md +174 -0
  71. package/skills/tide-rbac-and-e2ee/SKILL.md +214 -0
  72. package/skills/tide-reviewer/SKILL.md +133 -0
  73. package/skills/tide-route-and-api-protection/SKILL.md +201 -0
  74. package/skills/tide-scenario-resolver/SKILL.md +81 -0
  75. package/skills/tide-setup/SKILL.md +218 -0
  76. package/skills/tide-solutions-architect/SKILL.md +130 -0
@@ -0,0 +1,1494 @@
1
+ # Tide Troubleshooting Guide
2
+
3
+ Symptom-led debugging for common Tide implementation failures. Each entry includes diagnostic steps, fixes, and verification.
4
+
5
+ **Format**: Symptom → Possible Causes → Diagnostics → Fix → Verification
6
+
7
+ ---
8
+
9
+ ## T-01: Login Hangs Indefinitely
10
+
11
+ **Symptom**: User clicks login, redirects to TideCloak, then hangs with spinner. No error visible. Browser tab never completes loading.
12
+
13
+ **Possible Causes**:
14
+ 1. CSP blocks SWE iframe (most common)
15
+ 2. SWE domain unreachable
16
+ 3. Corrupted localStorage auth state
17
+ 4. Missing `silent-check-sso.html`
18
+
19
+ **Diagnostics**:
20
+
21
+ ```bash
22
+ # 1. Check browser console for CSP violations
23
+ # Open DevTools → Console
24
+ # Look for: "Refused to frame 'https://...' because it violates the following Content Security Policy directive"
25
+
26
+ # 2. Check CSP headers
27
+ curl -I https://app.example.com | grep -i content-security-policy
28
+
29
+ # 3. Check silent-check-sso.html exists
30
+ curl https://app.example.com/silent-check-sso.html
31
+ # Should return HTML with postMessage script
32
+
33
+ # 4. Check localStorage for stale state
34
+ # DevTools → Application → Local Storage
35
+ # Look for keys containing 'iam', 'auth', 'kc-', 'tc-'
36
+ ```
37
+
38
+ **Fix**:
39
+
40
+ For CSP violation (Cause 1):
41
+ ```typescript
42
+ // Next.js: next.config.js
43
+ module.exports = {
44
+ async headers() {
45
+ return [{
46
+ source: '/:path*',
47
+ headers: [{
48
+ key: 'Content-Security-Policy',
49
+ value: "frame-src 'self' *"
50
+ }]
51
+ }]
52
+ }
53
+ }
54
+
55
+ // Express: server/index.ts
56
+ import helmet from 'helmet';
57
+ app.use(helmet({
58
+ contentSecurityPolicy: {
59
+ directives: {
60
+ frameSrc: ["'self'", '*']
61
+ }
62
+ }
63
+ }));
64
+ ```
65
+
66
+ For corrupted state (Cause 3):
67
+ ```typescript
68
+ // Add reset handler in app initialization
69
+ const params = new URLSearchParams(window.location.search);
70
+ if (params.get('reset') === 'true') {
71
+ Object.keys(localStorage).forEach(key => {
72
+ if (key.includes('iam') || key.includes('auth') || key.startsWith('kc-') || key.startsWith('tc-')) {
73
+ localStorage.removeItem(key);
74
+ }
75
+ });
76
+ window.location.href = '/';
77
+ }
78
+ ```
79
+
80
+ For missing silent SSO file (Cause 4):
81
+ ```html
82
+ <!-- Create public/silent-check-sso.html -->
83
+ <html>
84
+ <body>
85
+ <script>
86
+ parent.postMessage(location.href, location.origin);
87
+ </script>
88
+ </body>
89
+ </html>
90
+ ```
91
+
92
+ **Verification**:
93
+ ```bash
94
+ # Check CSP includes Tide domains
95
+ curl -I https://app.example.com | grep frame-src
96
+ # Should include frame-src '*' (required for ORK re-homing)
97
+
98
+ # Test login flow completes
99
+ # Login should redirect to TideCloak and back within 5-10 seconds
100
+ ```
101
+
102
+ **Related**: Anti-Pattern AP-07, Invariants I-06, I-07
103
+
104
+ ---
105
+
106
+ ## T-02: JWT Verification Fails (401 Unauthorized)
107
+
108
+ **Symptom**: API calls return 401 Unauthorized despite successful login. Token appears valid in DevTools but server rejects it.
109
+
110
+ **Possible Causes**:
111
+ 1. Adapter JSON missing `jwk` field (IGA not enabled or wrong export)
112
+ 2. Using `createRemoteJWKSet` instead of embedded JWKS
113
+ 3. Wrong `issuer` or `audience` in verification
114
+ 4. Token expired (clock skew)
115
+ 5. Generic Keycloak adapter instead of Tide adapter
116
+
117
+ **Diagnostics**:
118
+
119
+ ```bash
120
+ # 1. Check adapter JSON has Tide extensions
121
+ jq 'has("jwk") and has("vendorId") and has("homeOrkUrl")' data/tidecloak.json
122
+ # Should output: true
123
+
124
+ jq '.jwk.keys | length' data/tidecloak.json
125
+ # Should output: 1 or more
126
+
127
+ # 2. Check JWT verification uses local JWKS only
128
+ grep -r "createLocalJWKSet\|config.jwk" lib/auth/ src/lib/auth/
129
+ # Should find local JWKS usage
130
+
131
+ # 2b. Check code does NOT use remote JWKS (forbidden for this pack)
132
+ grep -r "createRemoteJWKSet" lib/auth/ src/lib/auth/
133
+ # Should find ZERO matches. Any match is AP-01.
134
+
135
+ # 3. Decode token and check claims
136
+ # Copy token from browser DevTools → Network → request headers
137
+ echo "YOUR_TOKEN_HERE" | base64 -d | jq .
138
+ # Check iss, azp, exp, iat
139
+
140
+ # 4. Check server issuer/audience config
141
+ grep -r "issuer.*audience" server/lib/auth/
142
+ ```
143
+
144
+ **Fix**:
145
+
146
+ For missing Tide adapter (Cause 5):
147
+ ```bash
148
+ # Re-export adapter with Tide extensions
149
+ curl -X GET \
150
+ "${TIDECLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_ID}/vendorResources/get-installations-provider?providerId=keycloak-oidc-keycloak-json" \
151
+ -H "Authorization: Bearer ${ADMIN_TOKEN}" \
152
+ > data/tidecloak.json
153
+
154
+ # Verify Tide fields present
155
+ jq '.jwk, .vendorId, .homeOrkUrl' data/tidecloak.json
156
+ ```
157
+
158
+ For wrong JWKS source (Cause 2):
159
+ ```typescript
160
+ // ❌ WRONG: remote JWKS (forbidden in this pack)
161
+ const JWKS = createRemoteJWKSet(new URL(`${issuer}/protocol/openid-connect/certs`));
162
+
163
+ // ❌ ALSO WRONG: remote as fallback
164
+ // Do not add createRemoteJWKSet as a fallback. Fix the adapter export instead.
165
+
166
+ // ✅ CORRECT: local JWKS only, fail if missing
167
+ import { loadTideConfig } from './tidecloakConfig';
168
+ const config = loadTideConfig();
169
+ if (!config.jwk) {
170
+ throw new Error('Missing jwk in tidecloak.json. Re-export adapter with IGA enabled.');
171
+ }
172
+ const JWKS = createLocalJWKSet(config.jwk);
173
+ ```
174
+
175
+ For missing `jwk` field (Cause 1):
176
+ ```bash
177
+ # Re-export adapter with Tide extensions (IGA must be enabled)
178
+ curl -X GET \
179
+ "${TIDECLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_ID}/vendorResources/get-installations-provider?providerId=keycloak-oidc-keycloak-json" \
180
+ -H "Authorization: Bearer ${ADMIN_TOKEN}" \
181
+ > data/tidecloak.json
182
+
183
+ # Verify jwk is present
184
+ jq '.jwk.keys | length' data/tidecloak.json
185
+ # If 0 or error: IGA is not enabled on the realm. Enable it first.
186
+ ```
187
+
188
+ For wrong issuer/audience (Cause 3):
189
+ ```typescript
190
+ const { payload } = await jwtVerify(token, JWKS, {
191
+ issuer: `${config['auth-server-url'].replace(/\/+$/, '')}/realms/${config.realm}`, // Must match token iss
192
+ });
193
+ // TideCloak uses azp (not aud) for client ID. aud is typically "account".
194
+ if (payload.azp !== config.resource) throw new Error('Token azp mismatch');
195
+ ```
196
+
197
+ **Verification**:
198
+ ```bash
199
+ # Test API call with valid token
200
+ TOKEN=$(curl -s ... | jq -r '.access_token')
201
+ curl -H "Authorization: Bearer $TOKEN" https://app.example.com/api/protected
202
+ # Should return 200, not 401
203
+ ```
204
+
205
+ **Related**: Anti-Pattern AP-01, AP-13, Invariants I-03, I-04
206
+
207
+ ---
208
+
209
+ ## T-03: E2EE Operations Timeout
210
+
211
+ **Symptom**: `doEncrypt()` or `doDecrypt()` calls hang and eventually timeout. No error message or generic timeout error.
212
+
213
+ **Possible Causes**:
214
+ 1. User missing E2EE role (`_tide_<tag>.selfencrypt` or `_tide_<tag>.selfdecrypt`)
215
+ 2. Fabric connectivity issues (homeOrkUrl unreachable)
216
+ 3. SWE iframe blocked (CSP issue)
217
+ 4. Tag mismatch (encrypt with 'ssn', decrypt with 'SSN')
218
+ 5. Offline (E2EE requires online Fabric access)
219
+
220
+ **Diagnostics**:
221
+
222
+ ```bash
223
+ # 1. Check user has E2EE roles
224
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}/users/${USER_ID}/role-mappings/realm" | \
225
+ jq '.[] | select(.name | startswith("_tide_"))'
226
+ # Should include _tide_<tag>.selfencrypt and _tide_<tag>.selfdecrypt
227
+
228
+ # 2. Check Fabric connectivity
229
+ jq -r '.homeOrkUrl' data/tidecloak.json
230
+ # Test ORK endpoint
231
+ curl https://sork1.tideprotocol.com/health # or homeOrkUrl from adapter
232
+
233
+ # 3. Check browser console for E2EE errors
234
+ # DevTools → Console
235
+ # Look for: "Failed to load resource", "CORS error", "Timeout"
236
+
237
+ # 4. Check browser network tab for ORK requests
238
+ # DevTools → Network → Filter by tideprotocol.com
239
+ # Should see requests to ORK endpoints during encrypt/decrypt
240
+ ```
241
+
242
+ **Fix**:
243
+
244
+ For missing roles (Cause 1):
245
+ ```bash
246
+ # Create E2EE roles in TideCloak admin console
247
+ # Roles → Create Role
248
+ # Name: _tide_ssn.selfencrypt, _tide_ssn.selfdecrypt
249
+
250
+ # Assign to user via IGA
251
+ # Users → {user} → Role Mappings → Assign role
252
+ # (Creates change-set requiring quorum approval if IGA enabled)
253
+ ```
254
+
255
+ For Fabric connectivity (Cause 2):
256
+ ```bash
257
+ # Check firewall allows outbound to Tide ORKs
258
+ # Check homeOrkUrl is correct
259
+ jq -r '.homeOrkUrl' data/tidecloak.json
260
+
261
+ # Test from client environment
262
+ curl -I https://sork1.tideprotocol.com
263
+ ```
264
+
265
+ For CSP issue (Cause 3):
266
+ ```typescript
267
+ // Add Tide domains to CSP (see T-01)
268
+ ```
269
+
270
+ For offline (Cause 5):
271
+ ```typescript
272
+ // E2EE requires online access; no offline fallback
273
+ if (!navigator.onLine) {
274
+ throw new Error('E2EE requires online connection to Fabric');
275
+ }
276
+ ```
277
+
278
+ **Verification**:
279
+ ```javascript
280
+ // Test encrypt/decrypt round-trip
281
+ const plaintext = 'test data';
282
+ const ciphertext = await iam.doEncrypt('test', plaintext);
283
+ console.log('Encrypted:', ciphertext); // Should complete within 2-5 seconds
284
+
285
+ const decrypted = await iam.doDecrypt('test', ciphertext);
286
+ console.log('Decrypted:', decrypted); // Should match plaintext
287
+ assert(decrypted === plaintext);
288
+ ```
289
+
290
+ **Related**: Anti-Pattern AP-04, Invariants I-11, [Concepts](concepts.md#threshold-e2ee-hermetic-e2ee)
291
+
292
+ ---
293
+
294
+ ## T-04: Silent Token Refresh Fails
295
+
296
+ **Symptom**: User forced to re-login every 10 minutes (or token expiration). No silent refresh. Session appears to expire prematurely.
297
+
298
+ **Possible Causes**:
299
+ 1. Missing `silent-check-sso.html`
300
+ 2. CSP blocks silent SSO iframe
301
+ 3. Corrupted localStorage auth state
302
+ 4. Silent SSO URL misconfigured in SDK init
303
+
304
+ **Diagnostics**:
305
+
306
+ ```bash
307
+ # 1. Check silent-check-sso.html exists and is accessible
308
+ curl https://app.example.com/silent-check-sso.html
309
+ # Should return HTML with postMessage script
310
+
311
+ # 2. Check browser console during refresh
312
+ # Look for: 404 on silent-check-sso.html, CSP violation, postMessage errors
313
+
314
+ # 3. Check SDK init includes silentCheckSsoRedirectUri
315
+ grep -r "silentCheckSsoRedirectUri" src/ client/
316
+ # Should be set to {origin}/silent-check-sso.html
317
+
318
+ # 4. Check browser network tab around token expiration
319
+ # DevTools → Network → around 10-min mark
320
+ # Should see iframe request to TideCloak → redirect to silent-check-sso.html
321
+ ```
322
+
323
+ **Fix**:
324
+
325
+ For missing file (Cause 1):
326
+ ```html
327
+ <!-- Create public/silent-check-sso.html -->
328
+ <html>
329
+ <body>
330
+ <script>
331
+ parent.postMessage(location.href, location.origin);
332
+ </script>
333
+ </body>
334
+ </html>
335
+ ```
336
+
337
+ For SDK config (Cause 4) — pass the full adapter JSON from `tidecloak.json`, not destructured fields (AP-52):
338
+ ```typescript
339
+ const config = await fetch('/api/config').then(r => r.json()); // full tidecloak.json
340
+ await iam.initIAM({
341
+ ...config,
342
+ silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
343
+ });
344
+ ```
345
+
346
+ For corrupted state (Cause 3):
347
+ ```typescript
348
+ // Implement reset handler (see T-01)
349
+ // Or: localStorage.clear() and reload
350
+ ```
351
+
352
+ **Verification**:
353
+ ```javascript
354
+ // Test silent refresh
355
+ // 1. Login successfully
356
+ // 2. Wait for token to approach expiration (check DevTools → Application → Local Storage for token exp)
357
+ // 3. Silent refresh should happen automatically without visible redirect
358
+ // 4. Check console for 'authRefreshSuccess' event
359
+
360
+ iam.on('authRefreshSuccess', () => {
361
+ console.log('Silent refresh succeeded');
362
+ });
363
+ ```
364
+
365
+ **Related**: Anti-Pattern AP-14, AP-15, Invariants I-07
366
+
367
+ ---
368
+
369
+ ## T-05: DPoP Verification Fails (401 or 403)
370
+
371
+ **Symptom**: API calls return 401 or 403 despite valid JWT. Error message mentions DPoP or token binding.
372
+
373
+ **Possible Causes**:
374
+ 1. DPoP header missing or malformed
375
+ 2. DPoP proof signature verification fails
376
+ 3. `htm` or `htu` mismatch (method or URL wrong)
377
+ 4. `iat` timestamp out of freshness window (>120s)
378
+ 5. `jti` replayed (duplicate nonce)
379
+ 6. `cnf.jkt` mismatch (token not bound to proof)
380
+
381
+ **Diagnostics**:
382
+
383
+ ```bash
384
+ # 1. Check request includes DPoP header
385
+ # DevTools → Network → request → Headers
386
+ # Should have: DPoP: eyJ...
387
+
388
+ # 2. Decode DPoP proof
389
+ echo "DPOP_HEADER_VALUE" | base64 -d | jq .
390
+ # Check: typ="dpop+jwt", alg="ES256" or "EdDSA", htm, htu, iat, jti, jwk
391
+
392
+ # 3. Check server DPoP verification logic
393
+ grep -r "verifyDPoP\|DPoP.*htm\|DPoP.*htu" server/
394
+
395
+ # 4. Check server logs for DPoP error details
396
+ ```
397
+
398
+ **Fix**:
399
+
400
+ For missing header (Cause 1):
401
+ ```typescript
402
+ // Use SDK's secureFetch (automatically adds DPoP)
403
+ // PREREQUISITE: useDPoP must be configured on TideCloakProvider.
404
+ // Without useDPoP, secureFetch does not attach valid DPoP headers and requests fail with 401.
405
+ const response = await iam.secureFetch('/api/protected');
406
+
407
+ // Or manually add DPoP if not using SDK
408
+ const dpopProof = await generateDPoPProof('GET', '/api/protected');
409
+ const response = await fetch('/api/protected', {
410
+ headers: {
411
+ 'Authorization': `DPoP ${token}`,
412
+ 'DPoP': dpopProof
413
+ }
414
+ });
415
+ ```
416
+
417
+ For htm/htu mismatch (Cause 3):
418
+ ```typescript
419
+ // Ensure DPoP proof matches request
420
+ const method = 'POST'; // Must match actual request method
421
+ const url = 'https://app.example.com/api/users'; // Must match actual URL (no query string)
422
+
423
+ const dpopProof = await generateDPoPProof(method, url);
424
+ ```
425
+
426
+ For iat freshness (Cause 4):
427
+ ```typescript
428
+ // Generate fresh proof per request; do not cache
429
+ // Check server and client clocks are synchronized (NTP)
430
+ ```
431
+
432
+ For jti replay (Cause 5):
433
+ ```typescript
434
+ // Do not reuse DPoP proofs across requests
435
+ // Each request needs fresh proof with unique jti
436
+ ```
437
+
438
+ For cnf.jkt mismatch (Cause 6):
439
+ ```typescript
440
+ // Token and DPoP proof must use same key pair
441
+ // If token renewed, generate new DPoP proofs
442
+ ```
443
+
444
+ **Verification**:
445
+ ```bash
446
+ # Test DPoP request
447
+ TOKEN="..."
448
+ DPOP="..."
449
+ curl -H "Authorization: DPoP $TOKEN" -H "DPoP: $DPOP" https://app.example.com/api/protected
450
+ # Should return 200, not 401/403
451
+ ```
452
+
453
+ **Related**: Anti-Pattern AP-06, AP-12, Invariants I-12
454
+
455
+ ---
456
+
457
+ ## T-06: IGA Changes Don't Apply
458
+
459
+ **Symptom**: Admin creates user or modifies role, but change doesn't take effect. No error visible.
460
+
461
+ **Possible Causes**:
462
+ 1. IGA not enabled on realm
463
+ 2. Change-set created but not approved
464
+ 3. Change-set approved but not committed
465
+ 4. Quorum not reached
466
+
467
+ **Possible Causes** (added):
468
+ 5. Token not refreshed after commit — roles take up to 120s to appear in doken
469
+
470
+ **Diagnostics**:
471
+
472
+ ```bash
473
+ # 1. Check IGA is enabled
474
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}" | jq '.igaEnabled // "not enabled"'
475
+ # Should output: true
476
+
477
+ # 2. Check pending change-sets
478
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}/tide-admin/change-set/all/requests" \
479
+ -H "Authorization: Bearer ${ADMIN_TOKEN}" | jq '.'
480
+ # Look for change-sets in 'PENDING' or 'APPROVED' status
481
+
482
+ # 3. Check counts (lightweight)
483
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}/tide-admin/change-set/counts" \
484
+ -H "Authorization: Bearer ${ADMIN_TOKEN}"
485
+ # Returns: {"users":N,"roles":N,"clients":N,"groups":N,"total":N}
486
+ ```
487
+
488
+ **Fix**:
489
+
490
+ For IGA not enabled (Cause 1):
491
+ ```bash
492
+ # Enable IGA
493
+ curl -X POST "${TIDECLOAK_URL}/admin/realms/${REALM}/tide-admin/toggle-iga" \
494
+ -H "Authorization: Bearer ${ADMIN_TOKEN}"
495
+ ```
496
+
497
+ For change-set not approved (Cause 2):
498
+ ```bash
499
+ # Sign (approve) the change-set
500
+ curl -X POST "${TIDECLOAK_URL}/admin/realms/${REALM}/tide-admin/change-set/sign/batch" \
501
+ -H "Authorization: Bearer ${ADMIN_TOKEN}" \
502
+ -H "Content-Type: application/json" \
503
+ -d '{"changeSets":[{"changeSetId":"DRAFT_RECORD_ID","changeSetType":"USER_ROLE","actionType":"CREATE"}]}'
504
+
505
+ # If MultiAdmin: response has requiresApprovalPopup=true — complete enclave signing
506
+ # Repeat with different admin tokens until quorum reached
507
+ ```
508
+
509
+ For change-set not committed (Cause 3):
510
+ ```bash
511
+ # Commit after quorum approval
512
+ curl -X POST "${TIDECLOAK_URL}/admin/realms/${REALM}/tide-admin/change-set/commit/batch" \
513
+ -H "Authorization: Bearer ${ADMIN_TOKEN}" \
514
+ -H "Content-Type: application/json" \
515
+ -d '{"changeSets":[{"changeSetId":"DRAFT_RECORD_ID","changeSetType":"USER_ROLE","actionType":"CREATE"}]}'
516
+ ```
517
+
518
+ For token not refreshed (Cause 5):
519
+ ```
520
+ User must refresh token (logout/login or forceUpdateToken()) after commit.
521
+ New roles may take up to 120 seconds to appear in doken.
522
+ ```
523
+
524
+ **Verification**:
525
+ ```bash
526
+ # After commit, check change applied
527
+ # For user creation:
528
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}/users?username=${NEW_USERNAME}" | jq '.'
529
+ # User should exist
530
+
531
+ # For role assignment:
532
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}/users/${USER_ID}/role-mappings/realm" | jq '.[] | .name'
533
+ # Role should be in list
534
+ ```
535
+
536
+ **Related**: Anti-Pattern AP-05, AP-10, Invariants I-10
537
+
538
+ ---
539
+
540
+ ## T-07: Account Linking Stuck
541
+
542
+ **Symptom**: New admin user clicks account linking URL, completes linking in browser, but backend poll never detects `tideUserKey` or `vuid`.
543
+
544
+ **Possible Causes**:
545
+ 1. Linking URL expired (default 12h lifespan)
546
+ 2. Poll timeout too short
547
+ 3. Wrong user ID in poll
548
+ 4. TideCloak/Fabric connectivity issue during linking
549
+
550
+ **Diagnostics**:
551
+
552
+ ```bash
553
+ # 1. Check linking URL was generated correctly
554
+ # Look for lifespan parameter
555
+ echo $LINK_URL | grep -o "lifespan=[0-9]*"
556
+ # Default: 43200 (12 hours)
557
+
558
+ # 2. Check user attributes
559
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}/users/${USER_ID}" | \
560
+ jq '.attributes.tideUserKey, .attributes.vuid'
561
+ # Should show values after linking completes
562
+
563
+ # 3. Check poll logic
564
+ grep -A 10 "poll.*tideUserKey" script/
565
+ # Verify poll interval and timeout
566
+ ```
567
+
568
+ **Fix**:
569
+
570
+ For expired URL (Cause 1):
571
+ ```bash
572
+ # Generate new linking URL with longer lifespan
573
+ curl -X POST \
574
+ "${TIDECLOAK_URL}/admin/realms/${REALM}/tideAdminResources/get-required-action-link?userId=${USER_ID}&lifespan=86400" \
575
+ -H "Authorization: Bearer ${ADMIN_TOKEN}" \
576
+ -H "Content-Type: application/json" \
577
+ -d '["link-tide-account-action"]'
578
+ ```
579
+
580
+ For poll timeout (Cause 2):
581
+ ```bash
582
+ # Increase poll timeout in init script
583
+ # Example from keylessh:
584
+ for i in {1..60}; do # Poll for up to 5 minutes (60 * 5s = 300s)
585
+ USER_KEY=$(curl -s ... | jq -r '.attributes.tideUserKey[0]')
586
+ if [ -n "$USER_KEY" ]; then
587
+ echo "Linked successfully"
588
+ break
589
+ fi
590
+ sleep 5
591
+ done
592
+ ```
593
+
594
+ **Verification**:
595
+ ```bash
596
+ # After linking, verify attributes
597
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}/users/${USER_ID}" | \
598
+ jq '.attributes | {tideUserKey, vuid}'
599
+ # Both should be non-null
600
+ ```
601
+
602
+ **Related**: [Concepts](concepts.md#account-linking-onboarding), GAP-030
603
+
604
+ ---
605
+
606
+ ## T-08: Forseti Signing Fails
607
+
608
+ **Symptom**: `executeSignRequest()` throws error or rejects. Contract evaluation fails.
609
+
610
+ **Possible Causes**:
611
+ 1. Contract compilation error (C# syntax)
612
+ 2. Doken missing or expired
613
+ 3. Challenge data format mismatch
614
+ 4. ORK majority rejected policy
615
+ 5. Gas limit exceeded (undocumented)
616
+
617
+ **Diagnostics**:
618
+
619
+ ```bash
620
+ # 1. Check contract syntax
621
+ # Copy C# contract code and validate locally or via online compiler
622
+
623
+ # 2. Check doken is present
624
+ console.log(tc.doken);
625
+ # Should output JWT-shaped string, not null/undefined
626
+
627
+ # 3. Check browser console for Forseti errors
628
+ # DevTools → Console
629
+ # Look for: "Contract compilation failed", "Policy rejected", "Timeout"
630
+
631
+ # 4. Check challengeData structure matches contract expectations
632
+ console.log(signRequest.challengeData);
633
+ ```
634
+
635
+ **Fix**:
636
+
637
+ For contract error (Cause 1):
638
+ ```typescript
639
+ // Validate C# syntax
640
+ const contract = `
641
+ using System;
642
+ public class Policy {
643
+ public static bool Evaluate(dynamic challenge) {
644
+ // Ensure valid C# syntax
645
+ return challenge.user == "admin";
646
+ }
647
+ }
648
+ `;
649
+ ```
650
+
651
+ For missing doken (Cause 2):
652
+ ```typescript
653
+ // Ensure user is authenticated before creating request
654
+ if (!tc.doken) {
655
+ await iam.doLogin({ redirectUri: window.location.origin });
656
+ }
657
+
658
+ const signRequest = await tc.createTideRequest({
659
+ authorizer: tc.doken, // Should be present after login
660
+ ...
661
+ });
662
+ ```
663
+
664
+ For policy rejection (Cause 4):
665
+ ```typescript
666
+ // Review contract logic; ORKs may be correctly rejecting based on policy
667
+ // Example: User lacks required role, destination is blocked, etc.
668
+ // Check challengeData matches policy expectations
669
+ ```
670
+
671
+ **Verification**:
672
+ ```typescript
673
+ // Test successful signing
674
+ const signRequest = await tc.createTideRequest({
675
+ contract: simplePolicyContract,
676
+ modelName: 'BasicCustom<TEST>:BasicCustom<1>',
677
+ authFlow: 'Policy:1',
678
+ authorizer: tc.doken,
679
+ challengeData: { test: true }
680
+ });
681
+
682
+ const result = await tc.executeSignRequest(signRequest);
683
+ console.log('Signature:', result.signature); // Should be present
684
+ ```
685
+
686
+ **Related**: [Concepts](concepts.md#forseti-policy-engine), Invariants I-15, GAP-007, GAP-008, GAP-018
687
+
688
+ ---
689
+
690
+ ## T-09: Token Expired Errors Despite Recent Login
691
+
692
+ **Symptom**: User logs in successfully, but API calls return "Token expired" within minutes. Token lifetime appears much shorter than expected.
693
+
694
+ **Possible Causes**:
695
+ 1. Server and client clock skew
696
+ 2. Token lifetime misconfigured in realm
697
+ 3. Refresh logic not working (see T-04)
698
+ 4. Token verification using wrong timezone
699
+
700
+ **Diagnostics**:
701
+
702
+ ```bash
703
+ # 1. Check token claims
704
+ # Extract token from browser, decode
705
+ echo "TOKEN" | base64 -d | jq '.iat, .exp'
706
+ # iat = issued-at timestamp, exp = expiration timestamp
707
+
708
+ # Compare to current time
709
+ date +%s
710
+ # Should be between iat and exp
711
+
712
+ # 2. Check realm token lifetime
713
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}" | jq '.accessTokenLifespan'
714
+ # keylessh default: 600 (10 minutes)
715
+
716
+ # 3. Check server clock
717
+ date -u
718
+ # Should match client clock within ~60 seconds
719
+ ```
720
+
721
+ **Fix**:
722
+
723
+ For clock skew (Cause 1):
724
+ ```bash
725
+ # Sync server clock with NTP
726
+ sudo ntpdate -s time.nist.gov
727
+
728
+ # Or use systemd-timesyncd
729
+ sudo timedatectl set-ntp true
730
+ ```
731
+
732
+ For token lifetime (Cause 2):
733
+ ```bash
734
+ # Adjust realm token lifetime if needed
735
+ curl -X PUT "${TIDECLOAK_URL}/admin/realms/${REALM}" \
736
+ -H "Content-Type: application/json" \
737
+ -d '{"accessTokenLifespan": 1800}' # 30 minutes
738
+
739
+ # Note: keylessh uses 600s; adjust based on app needs
740
+ ```
741
+
742
+ For refresh logic (Cause 3):
743
+ ```typescript
744
+ // See T-04 (Silent Token Refresh Fails)
745
+ ```
746
+
747
+ **Verification**:
748
+ ```bash
749
+ # Login and check token lifespan
750
+ TOKEN=$(curl -s ... | jq -r '.access_token')
751
+ echo $TOKEN | base64 -d | jq '.exp - .iat'
752
+ # Should output token lifetime in seconds (e.g., 600 for 10 min)
753
+ ```
754
+
755
+ **Related**: T-04, [Concepts](concepts.md#required-roles-and-configuration)
756
+
757
+ ---
758
+
759
+ ## T-10: Realm Setup Fails
760
+
761
+ **Symptom**: Realm initialization via `setUpTideRealm` endpoint fails or realm missing Tide configuration.
762
+
763
+ **Possible Causes**:
764
+ 1. Endpoint not available (wrong TideCloak version)
765
+ 2. Missing request body fields (email, terms)
766
+ 3. IGA toggle fails
767
+ 4. Protocol mappers not created
768
+ 5. `tidebrowser` flow not created
769
+
770
+ **Diagnostics**:
771
+
772
+ ```bash
773
+ # 1. Check TideCloak version supports Tide endpoints
774
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}/vendorResources/setUpTideRealm" \
775
+ -X POST -I
776
+ # Should return 200 or 400, not 404
777
+
778
+ # 2. Check request body (must be form-urlencoded, NOT JSON)
779
+ curl -X POST "${TIDECLOAK_URL}/admin/realms/${REALM}/vendorResources/setUpTideRealm" \
780
+ -H "Authorization: Bearer ${ADMIN_TOKEN}" \
781
+ -H "Content-Type: application/x-www-form-urlencoded" \
782
+ -d "email=admin@example.com&isRagnarokEnabled=true"
783
+
784
+ # 3. Check realm configuration after setup
785
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}" | jq '.browserFlow, .defaultRoles'
786
+ # browserFlow should be 'tidebrowser'
787
+ # defaultRoles should include '_tide_enabled'
788
+
789
+ # 4. Check protocol mappers
790
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}/client-scopes" | \
791
+ jq '.[] | select(.name=="roles") | .protocolMappers[] | select(.name | contains("tide"))'
792
+ ```
793
+
794
+ **Fix**:
795
+
796
+ For endpoint unavailable (Cause 1):
797
+ ```bash
798
+ # Verify TideCloak version
799
+ curl "${TIDECLOAK_URL}/admin/serverinfo" | jq '.systemInfo.version'
800
+ # Should be TideCloak version, not vanilla Keycloak
801
+ ```
802
+
803
+ For manual realm configuration:
804
+ ```json
805
+ // Import realm JSON with Tide defaults (based on keylessh realm.json)
806
+ {
807
+ "realm": "myrealm",
808
+ "browserFlow": "tidebrowser",
809
+ "defaultRoles": ["_tide_enabled", "offline_access", "uma_authorization"],
810
+ "accessTokenLifespan": 600,
811
+ "ssoSessionIdleTimeout": 1800,
812
+ "ssoSessionMaxLifespan": 36000,
813
+ "roles": {
814
+ "realm": [
815
+ {"name": "_tide_enabled"}
816
+ ]
817
+ },
818
+ "protocolMappers": [
819
+ {
820
+ "name": "Tide User Key Mapper",
821
+ "protocol": "openid-connect",
822
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
823
+ "config": {
824
+ "user.attribute": "tideUserKey",
825
+ "claim.name": "tideuserkey",
826
+ "jsonType.label": "String",
827
+ "id.token.claim": "true",
828
+ "access.token.claim": "true"
829
+ }
830
+ },
831
+ {
832
+ "name": "VUID Mapper",
833
+ "protocol": "openid-connect",
834
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
835
+ "config": {
836
+ "user.attribute": "vuid",
837
+ "claim.name": "vuid",
838
+ "jsonType.label": "String",
839
+ "id.token.claim": "true",
840
+ "access.token.claim": "true"
841
+ }
842
+ }
843
+ ]
844
+ }
845
+ ```
846
+
847
+ **Verification**:
848
+ ```bash
849
+ # Check realm has Tide defaults
850
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}" | \
851
+ jq '{browserFlow, defaultRoles, accessTokenLifespan}'
852
+ # Should match Tide configuration
853
+ ```
854
+
855
+ **Related**: [Concepts](concepts.md#realm-initialization), Invariants I-14
856
+
857
+ ---
858
+
859
+ ## T-10b: Init Script Fails with "Invalid json representation for RoleRepresentation"
860
+
861
+ **Symptom**: `npm run init` (or `bootstrap-tidecloak.sh`) fails with:
862
+ ```
863
+ {"error":"Invalid json representation for RoleRepresentation. Unrecognized field \"error\" at line 1 column 12."}
864
+ ```
865
+
866
+ **Cause**: The script tries to assign `tide-realm-admin` to the admin user before the user creation change request is approved. With IGA enabled, the user is not queryable until committed. The role lookup returns `{"error":"..."}`, which is then passed as the body of the role mapping POST.
867
+
868
+ **Diagnostics**:
869
+ ```bash
870
+ # Check if user exists
871
+ TOKEN="$(get_token)"
872
+ curl -s "$TIDECLOAK_URL/admin/realms/$REALM_NAME/users?username=admin" \
873
+ -H "Authorization: Bearer $TOKEN" | jq '.[0].id'
874
+ # If null: user change request was not approved
875
+
876
+ # Check pending user change requests
877
+ curl -s "$TIDECLOAK_URL/admin/realms/$REALM_NAME/tide-admin/change-set/users/requests" \
878
+ -H "Authorization: Bearer $TOKEN" | jq length
879
+ # If > 0: approve them first
880
+ ```
881
+
882
+ **Fix**: Approve user creation change request between user creation and role assignment:
883
+ ```bash
884
+ # After creating user, BEFORE looking up user ID:
885
+ sleep 2
886
+ approve_and_commit users
887
+
888
+ # Then proceed with user ID lookup and role assignment
889
+ ```
890
+
891
+ Also validate the role lookup result before using it:
892
+ ```bash
893
+ ROLE_JSON=$(curl -s "$TIDECLOAK_URL/admin/realms/$REALM_NAME/clients/$CLIENT_UUID/roles/tide-realm-admin" \
894
+ -H "Authorization: Bearer $TOKEN")
895
+ if echo "$ROLE_JSON" | jq -e '.error' > /dev/null 2>&1; then
896
+ echo "ERROR: tide-realm-admin role not found: $ROLE_JSON"
897
+ exit 1
898
+ fi
899
+ ```
900
+
901
+ **Verification**: Re-run `npm run init` after fixing. The role assignment step should succeed without errors.
902
+
903
+ **Related**: [initialize-admin-and-link-account](../playbooks/initialize-admin-and-link-account.md) Step 1b
904
+
905
+ ---
906
+
907
+ ## T-11: Adapter JSON Export Fails
908
+
909
+ **Symptom**: Adapter JSON downloaded from TideCloak missing `jwk`, `vendorId`, or `homeOrkUrl` fields.
910
+
911
+ **Possible Causes**:
912
+ 1. Using generic Keycloak adapter endpoint instead of Tide endpoint
913
+ 2. Wrong `providerId` parameter
914
+ 3. Client not properly configured in TideCloak
915
+
916
+ **Diagnostics**:
917
+
918
+ ```bash
919
+ # 1. Check adapter JSON contents
920
+ jq 'keys' data/tidecloak.json
921
+ # Should include: jwk, vendorId, homeOrkUrl
922
+
923
+ jq 'has("jwk") and has("vendorId") and has("homeOrkUrl")' data/tidecloak.json
924
+ # Should output: true
925
+
926
+ # 2. Check export endpoint used
927
+ # Correct endpoint:
928
+ # GET /admin/realms/{realm}/clients/{client-id}/vendorResources/get-installations-provider?providerId=keycloak-oidc-keycloak-json
929
+ ```
930
+
931
+ **Fix**:
932
+
933
+ ```bash
934
+ # Use correct Tide adapter export endpoint
935
+ curl -X GET \
936
+ "${TIDECLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_ID}/vendorResources/get-installations-provider?providerId=keycloak-oidc-keycloak-json" \
937
+ -H "Authorization: Bearer ${ADMIN_TOKEN}" \
938
+ -o data/tidecloak.json
939
+
940
+ # Verify Tide extensions present
941
+ jq '{jwk: (.jwk.keys | length), vendorId, homeOrkUrl}' data/tidecloak.json
942
+ # Should show all three fields
943
+ ```
944
+
945
+ **Verification**:
946
+ ```bash
947
+ # Validate adapter JSON structure
948
+ jq '
949
+ if (has("jwk") and has("vendorId") and has("homeOrkUrl")) then
950
+ "Valid Tide adapter"
951
+ else
952
+ "Invalid: missing Tide extensions"
953
+ end
954
+ ' data/tidecloak.json
955
+ ```
956
+
957
+ **Related**: Anti-Pattern AP-13, Invariants I-05, I-13
958
+
959
+ ---
960
+
961
+ ## T-12: Policy Signing / Forseti Errors
962
+
963
+ **Symptom**: ORK rejects with "Index out of range", "Policy supplied has not been signed", "Policy refers to wrong contract", or similar errors during policy operations.
964
+
965
+ **Possible Causes**:
966
+ 1. Destructuring `{ ApprovalType, ExecutionType }` from `Models.Policy` instead of `Models` (`Models.Policy` is the Policy class, not a namespace)
967
+ 2. Building `PolicySignRequest` manually with `BaseTideRequest` + `TideMemory` instead of using `PolicySignRequest.New(policy)` from `heimdall-tide`
968
+ 3. Not decoding `createTideRequest` result via `BaseTideRequest.decode()` before passing to approval
969
+ 4. Calling `addPolicy(policy.toBytes())` during `BaseTideRequest` construction (attaching unsigned policy)
970
+ 5. Using `modelId: "any"` instead of specific IDs like `["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"]`
971
+ 6. Admin policy bytes are base64 text passed as raw bytes instead of decoded
972
+ 7. Contract hash mismatch (`contractId` doesn't match deployed contract source)
973
+ 8. Contract hash is lowercase but ORK stores uppercase (case-sensitive comparison)
974
+
975
+ **Diagnostics**:
976
+
977
+ ```bash
978
+ # 1. Check imports are correct
979
+ grep -n "Models.Policy" src/
980
+ # If you see: const { ApprovalType } = Models.Policy → WRONG
981
+ # Should be: const { Policy, ApprovalType, ExecutionType, BaseTideRequest } = Models;
982
+
983
+ # 2. Check PolicySignRequest source
984
+ grep -n "PolicySignRequest" src/
985
+ # Should use: PolicySignRequest.New(policy) from 'heimdall-tide'
986
+ # NOT: manual BaseTideRequest + TideMemory construction
987
+
988
+ # 3. Check BaseTideRequest.decode is called after createTideRequest
989
+ grep -A 2 "createTideRequest" src/
990
+ # Should see: BaseTideRequest.decode(await tc.createTideRequest(...))
991
+
992
+ # 4. Check addPolicy is NOT called before approval
993
+ grep -B 5 "addPolicy" src/
994
+ # addPolicy should appear AFTER requestTideOperatorApproval, not before
995
+
996
+ # 5. Check modelId in Policy constructor
997
+ grep -A 5 "new Policy" src/
998
+ # Should see: modelId: ["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"]
999
+ # NOT: modelId: "any"
1000
+
1001
+ # 6. Check admin policy bytes are properly decoded
1002
+ grep -A 3 "adminPolicy\|policyBytes" src/
1003
+ # If bytes start with [65, 81, 65, 65, ...] (ASCII for "AQAA..."),
1004
+ # you are passing base64 text as byte values instead of decoding
1005
+ ```
1006
+
1007
+ **Fix**:
1008
+
1009
+ For wrong imports (Cause 1):
1010
+ ```typescript
1011
+ // ❌ WRONG — Models.Policy is the class itself, not a namespace
1012
+ const { ApprovalType, ExecutionType } = Models.Policy;
1013
+ // Results in: Cannot read properties of undefined (reading 'IMPLICIT')
1014
+
1015
+ // ✅ CORRECT
1016
+ const { Models } = await import("@tideorg/js");
1017
+ const { Policy, ApprovalType, ExecutionType, BaseTideRequest } = Models;
1018
+ ```
1019
+
1020
+ For manual request construction (Cause 2):
1021
+ ```typescript
1022
+ // ❌ WRONG — manual construction produces bytes ORK cannot parse
1023
+ const request = new BaseTideRequest();
1024
+ request.setDraft(TideMemory.CreateFromArray([policy.toBytes(), contractBytes]));
1025
+
1026
+ // ✅ CORRECT — use PolicySignRequest.New from heimdall-tide
1027
+ const { PolicySignRequest } = await import("heimdall-tide");
1028
+ const policyRequest = PolicySignRequest.New(policy);
1029
+ policyRequest.addForsetiContractToUpload(CONTRACT_SOURCE);
1030
+ policyRequest.setCustomExpiry(604800);
1031
+ ```
1032
+
1033
+ For missing decode (Cause 3):
1034
+ ```typescript
1035
+ // ❌ WRONG — passing raw bytes without decode
1036
+ const raw = await tc.createTideRequest(policyRequest.encode());
1037
+ const approvals = await tc.requestTideOperatorApproval([
1038
+ { id: "policy", request: raw } // raw bytes cause serialization errors
1039
+ ]);
1040
+
1041
+ // ✅ CORRECT — decode then re-encode
1042
+ const initialized = BaseTideRequest.decode(
1043
+ await tc.createTideRequest(policyRequest.encode())
1044
+ );
1045
+ const approvals = await tc.requestTideOperatorApproval([
1046
+ { id: "policy", request: initialized.encode() }
1047
+ ]);
1048
+ ```
1049
+
1050
+ For premature addPolicy (Cause 4):
1051
+ ```typescript
1052
+ // ❌ WRONG — attaching unsigned policy before approval
1053
+ tideRequest.addPolicy(policy.toBytes());
1054
+ const result = await tc.createTideRequest(tideRequest.encode());
1055
+
1056
+ // ✅ CORRECT — attach signed admin policy AFTER approval
1057
+ const approved = BaseTideRequest.decode(approvals[0].request);
1058
+ const adminPolicyBytes = await fetchAdminPolicyFromTideCloak(token);
1059
+ approved.addPolicy(adminPolicyBytes);
1060
+ const signatures = await tc.executeSignRequest(approved.encode(), true);
1061
+ ```
1062
+
1063
+ For wrong modelId (Cause 5):
1064
+ ```typescript
1065
+ // ❌ WRONG
1066
+ const policy = new Policy({
1067
+ modelId: "any", // ORK fails with "Index out of range" in Policy.From
1068
+ // ...
1069
+ });
1070
+
1071
+ // ✅ CORRECT
1072
+ const policy = new Policy({
1073
+ modelId: ["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"],
1074
+ // ...
1075
+ });
1076
+ ```
1077
+
1078
+ For base64 admin policy bytes (Cause 6):
1079
+ ```typescript
1080
+ // ❌ WRONG — passing base64 string characters as byte values
1081
+ const policyBytes = new TextEncoder().encode(base64String);
1082
+ // Results in bytes like [65, 81, 65, 65, ...] (ASCII for "AQAA...")
1083
+
1084
+ // ✅ CORRECT — decode base64 to actual bytes
1085
+ const policyBytes = Uint8Array.from(atob(base64String), c => c.charCodeAt(0));
1086
+ ```
1087
+
1088
+ **Verification**:
1089
+ ```typescript
1090
+ // Test the full policy signing flow
1091
+ const { Models } = await import("@tideorg/js");
1092
+ const { Policy, ApprovalType, ExecutionType, BaseTideRequest } = Models;
1093
+ const { PolicySignRequest } = await import("heimdall-tide");
1094
+
1095
+ const policy = new Policy({
1096
+ version: "3",
1097
+ contractId: contractHash,
1098
+ modelId: ["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"],
1099
+ keyId: vendorId,
1100
+ approvalType: ApprovalType.IMPLICIT,
1101
+ executionType: ExecutionType.EXECUTE_ON_APPROVE,
1102
+ params: new Map([["Role", roleName]]),
1103
+ });
1104
+
1105
+ const policyRequest = PolicySignRequest.New(policy);
1106
+ policyRequest.addForsetiContractToUpload(CONTRACT_SOURCE);
1107
+ const initialized = BaseTideRequest.decode(
1108
+ await tc.createTideRequest(policyRequest.encode())
1109
+ );
1110
+ // Should not throw "Index out of range" or other serialization errors
1111
+
1112
+ const approvals = await tc.requestTideOperatorApproval([
1113
+ { id: "enc-policy", request: initialized.encode() }
1114
+ ]);
1115
+ console.log('Approval status:', approvals[0].status);
1116
+ // Should be "approved", not an error
1117
+ ```
1118
+
1119
+ **Anti-pattern**: Do NOT try to debug ORK byte serialization errors by inspecting raw bytes. Instead, copy the exact working pattern from the forseti-crypto-quickstart reference implementation. If the flow deviates from the 5-step pattern (create policy, create request, approve via popup, attach admin policy, execute), start over from the reference.
1120
+
1121
+ **Related**: T-08, Anti-Pattern AP-04, Invariants I-15, [Concepts](concepts.md#forseti-policy-engine)
1122
+
1123
+ ---
1124
+
1125
+ ## T-15: Package-Boundary Import Failure (Models/PolicySignRequest from Wrong Package)
1126
+
1127
+ **Symptom**: `Cannot destructure property 'Policy' of 'Models' as it is undefined` or similar destructuring failures when setting up policy signing.
1128
+
1129
+ **Root Cause**: `Models`, `PolicySignRequest`, `BaseTideRequest`, `ApprovalType`, and `ExecutionType` were imported from `@tidecloak/nextjs`. That package is the Next.js hooks/provider layer. It does NOT export these symbols. They are `undefined` at runtime.
1130
+
1131
+ **Diagnostics**:
1132
+
1133
+ ```bash
1134
+ # Check for wrong import source
1135
+ grep -n "@tidecloak/nextjs" src/ | grep -i "Models\|PolicySignRequest\|BaseTideRequest\|ApprovalType\|ExecutionType"
1136
+ # ANY match = wrong package
1137
+
1138
+ # Check dynamic imports
1139
+ grep -n 'import("@tidecloak/nextjs")' src/
1140
+ # If this appears in policy-signing code, it's the problem
1141
+ ```
1142
+
1143
+ **Fix**: Import `Models` from `@tideorg/js` and `PolicySignRequest` from `heimdall-tide`:
1144
+
1145
+ ```typescript
1146
+ // ❌ WRONG — @tidecloak/nextjs does not export these
1147
+ const { Models, PolicySignRequest } = await import("@tidecloak/nextjs");
1148
+
1149
+ // ✅ CORRECT — use the source packages
1150
+ const { Models } = await import("@tideorg/js");
1151
+ const { PolicySignRequest } = await import("heimdall-tide");
1152
+ const { Policy, ApprovalType, ExecutionType, BaseTideRequest } = Models;
1153
+ ```
1154
+
1155
+ **Package-boundary rule**: `@tidecloak/nextjs` exports `TideCloakProvider`, `useTideCloak`, `useAuthCallback`, guard components. It does NOT export `Models`, `PolicySignRequest`, or any policy/model helpers. Those come from `@tideorg/js` and `heimdall-tide`.
1156
+
1157
+ **Related**: [Concepts: SDK Internals](concepts.md#sdk-internals)
1158
+
1159
+ ---
1160
+
1161
+ ## T-16: `IAMService.createTideRequest is not a function`
1162
+
1163
+ **Symptom**: `IAMService.createTideRequest is not a function` (or same for `requestTideOperatorApproval`, `executeSignRequest`) during policy signing.
1164
+
1165
+ **Root Cause**: `createTideRequest`, `requestTideOperatorApproval`, and `executeSignRequest` are methods on the underlying **TideCloak** instance, not on `IAMService` directly. The TideCloak instance is accessible at `IAMService._tc`.
1166
+
1167
+ **Diagnostics**:
1168
+
1169
+ ```bash
1170
+ # Check for direct IAMService calls that should go through _tc
1171
+ grep -n "IAMService\.createTideRequest\|IAMService\.requestTideOperatorApproval\|IAMService\.executeSignRequest" src/
1172
+ # ANY match = wrong call target
1173
+ ```
1174
+
1175
+ **Fix**:
1176
+
1177
+ ```typescript
1178
+ // ❌ WRONG — these methods do not exist on IAMService
1179
+ await IAMService.createTideRequest(request);
1180
+ await IAMService.requestTideOperatorApproval(requests);
1181
+ await IAMService.executeSignRequest(request, true);
1182
+
1183
+ // ✅ CORRECT — access the underlying TideCloak instance via _tc
1184
+ const tc = IAMService._tc;
1185
+ await tc.createTideRequest(request);
1186
+ await tc.requestTideOperatorApproval(requests);
1187
+ await tc.executeSignRequest(request, true);
1188
+ ```
1189
+
1190
+ **Method locations**:
1191
+
1192
+ | Method | Lives on | NOT on |
1193
+ |--------|----------|--------|
1194
+ | `createTideRequest()` | `IAMService._tc` (TideCloak) | `IAMService` |
1195
+ | `requestTideOperatorApproval()` | `IAMService._tc` (TideCloak) | `IAMService` |
1196
+ | `executeSignRequest()` | `IAMService._tc` (TideCloak) | `IAMService` |
1197
+ | `doEncrypt()` / `doDecrypt()` | `IAMService` | — |
1198
+ | `getToken()` / `getUserInfo()` | `IAMService` | — |
1199
+
1200
+ **Related**: [Playbook: setup-forseti-e2ee](../playbooks/setup-forseti-e2ee.md), [Concepts: SDK Internals](concepts.md#sdk-internals)
1201
+
1202
+ ---
1203
+
1204
+ ## Common Error Messages
1205
+
1206
+ | Error | Cause |
1207
+ |-------|-------|
1208
+ | "Policy supplied has not been signed" | Admin policy not attached in step 3, or policy bytes are unsigned |
1209
+ | "Policy refers to wrong contract" | contractId hash doesn't match deployed contract source. Hash comparison is case-sensitive — ORK stores uppercase hex. Use `.toUpperCase()` if computing locally, or fetch `contractHash` from the API. |
1210
+ | "Model does not have a policy passed with it" | `addPolicy()` was skipped or admin policy bytes are null |
1211
+ | "Index out of range" in `Policy.From` | Admin policy bytes are base64 text passed as raw bytes instead of decoded |
1212
+ | "IAccessPolicy could not be found" | Contract missing `using Ork.Forseti.Sdk;` |
1213
+ | "vendordecrypt voucher not allowed" | User missing `_tide_{tag}.selfdecrypt` role |
1214
+ | "Missing role 'group-member:...'" | Doken doesn't have the role, need logout/login or `forceUpdateToken()` after IGA approval |
1215
+ | "Unexpected token '<'" on config load | `configFilePath` used instead of `configUrl`, or `tidecloak.json` not in `public/` |
1216
+ | "TideCloak client not initialized" | `login()` called before SDK finished initializing, check `isInitializing` before guards |
1217
+ | `requiresApprovalPopup: true` | Backend cannot auto-approve, must open approval enclave in admin's browser |
1218
+ | `Cannot destructure property 'Policy' of 'Models' as it is undefined` | `Models` imported from `@tidecloak/nextjs` (wrong package). Import from `@tideorg/js` instead. See T-15. |
1219
+ | `IAMService.createTideRequest is not a function` | `createTideRequest` (and `requestTideOperatorApproval`, `executeSignRequest`) live on `IAMService._tc` (TideCloak), not `IAMService`. See T-16. |
1220
+ | `Cannot destructure 'verifyTideCloakToken'` | `@tidecloak/verify` is CJS, use defensive interop pattern |
1221
+ | "Could not create tide realm" (email is null) | `setUpTideRealm` expects `Content-Type: application/x-www-form-urlencoded` with `email`, not JSON |
1222
+ | "Could not find configuration for Required Action" | Missing Tide env vars (`SYSTEM_HOME_ORK`, `USER_HOME_ORK`, `THRESHOLD_T`, `THRESHOLD_N`, `PAYER_PUBLIC`) from Docker |
1223
+ | AccessDeniedException on H2 | Data directory mounted as project root or wrong permissions; mount `./data` subdirectory and `chown 1000:1000` |
1224
+ | `"User has not been given any access to '${tag}'"` | User lacks `_tide_{tag}.selfencrypt` or `.selfdecrypt` role for self-encryption. Fatal — assign role, then re-login. |
1225
+ | `"[TIDECLOAK] No doken found"` | Missing delegated token for encryption. Retryable — re-auth may fix. |
1226
+ | `"enclave.networkFailure"` | ORK unreachable. Retryable — transient network issue. |
1227
+ | `"enclave.throttled"` | Rate limited by ORK. Retryable — backoff and retry. |
1228
+ | `"enclave.thresholdTimeoutFailure"` | Not enough ORK responses before timeout. Retryable — transient. |
1229
+ | `"Mismatch between session key private and Doken session key public"` | Session key mismatch. Fatal — re-login required. |
1230
+ | `"DPoP is set to strict mode but the server does not advertise DPoP support"` | DPoP strict mode enabled but server lacks `dpop_signing_alg_values_supported`. Fatal — fix server config or DPoP mode. |
1231
+ | `"DPoP requires IndexedDB for secure key storage"` | IndexedDB unavailable in browser environment. Fatal — environment limitation. |
1232
+ | `NetworkError` (with `.response`) | Server responded with invalid HTTP status. Retryable — transient. Only custom error class in SDK. |
1233
+
1234
+ ---
1235
+
1236
+ ## T-13: Self-Encryption Fails — Do Not Rename Roles
1237
+
1238
+ **Symptom**: `doEncrypt` or `doDecrypt` fails with "User has not been given any access" or similar. Agent considers renaming `_tide_{tag}.selfencrypt` to `_tide_{tag}.encrypt`.
1239
+
1240
+ **Root cause**: This is a self-encryption setup problem, not a signal to switch encryption models.
1241
+
1242
+ **Diagnostics**:
1243
+ ```bash
1244
+ # 1. Check self-encryption roles exist
1245
+ curl -s "$TIDECLOAK_URL/admin/realms/$REALM/roles" \
1246
+ -H "Authorization: Bearer $TOKEN" | jq '.[].name' | grep '_tide_'
1247
+
1248
+ # 2. Check roles are in default composite
1249
+ curl -s "$TIDECLOAK_URL/admin/realms/$REALM/roles/default-roles-$REALM" \
1250
+ -H "Authorization: Bearer $TOKEN" | jq '.composites.realm'
1251
+
1252
+ # 3. Check IGA change requests are approved
1253
+ curl -s "$TIDECLOAK_URL/admin/realms/$REALM/tide-admin/change-set/roles/requests" \
1254
+ -H "Authorization: Bearer $TOKEN" | jq length
1255
+ # Should be 0. If > 0, approve and commit.
1256
+
1257
+ # 4. Check user's JWT has the roles
1258
+ # Decode token and check realm_access.roles
1259
+ ```
1260
+
1261
+ **Fix**: Fix the self-encryption setup. Do NOT rename roles.
1262
+
1263
+ 1. Create missing roles: `_tide_{tag}.selfencrypt`, `_tide_{tag}.selfdecrypt`
1264
+ 2. Add to default composite
1265
+ 3. Approve IGA change requests
1266
+ 4. User must re-login (roles propagate on next token refresh, up to 120s)
1267
+
1268
+ **Forbidden**: Renaming `selfencrypt` to `encrypt` or `selfdecrypt` to `decrypt`. The role suffix does not change the encryption model. The SDK call path (`doEncrypt` from `useTideCloak()` vs `IAMService.doEncrypt(data, policyBytes)`) determines self vs shared. See AP-26.
1269
+
1270
+ **When shared encryption IS needed**: If multiple users must decrypt the same ciphertext, this is a different encryption model entirely. Route to playbook `setup-forseti-e2ee`. It requires a Forseti contract, policy signing, and `IAMService.doEncrypt(data, signedPolicyBytes)`.
1271
+
1272
+ ---
1273
+
1274
+ ## T-14: Policy Commit Fails Due to Missing Admin Policy
1275
+
1276
+ **Symptom**: `executeSignRequest` or policy commit fails with "Policy supplied has not been signed" or "Model does not have a policy passed with it".
1277
+
1278
+ **Root cause**: The admin authorizer policy was not attached to the request before commit.
1279
+
1280
+ **Diagnostics**:
1281
+ ```bash
1282
+ # 1. Check admin-policy endpoint is reachable
1283
+ curl -s "${TIDECLOAK_URL}/realms/${REALM}/tide-policy-resources/admin-policy"
1284
+ # Should return base64-encoded bytes, not HTML or error JSON
1285
+
1286
+ # 2. Check URL construction (common: double slash from trailing slash in auth-server-url)
1287
+ echo "${TIDECLOAK_URL}/realms/${REALM}/tide-policy-resources/admin-policy"
1288
+ # Must not contain // between host and path (except after protocol)
1289
+
1290
+ # 3. Check if addPolicy was called AFTER approval, not before
1291
+ grep -B 5 "addPolicy" src/
1292
+ # addPolicy must appear after requestTideOperatorApproval, not during request construction
1293
+ ```
1294
+
1295
+ **Fix**:
1296
+
1297
+ 1. **Fetch admin policy from the correct endpoint** (no auth required):
1298
+ ```
1299
+ GET /realms/{realm}/tide-policy-resources/admin-policy
1300
+ ```
1301
+ Not: `GET /admin/realms/{realm}/tide-admin/realm-policy` (returns `{ status: "none" }`).
1302
+
1303
+ 2. **Decode base64 response**:
1304
+ ```js
1305
+ const b64 = await res.text();
1306
+ const raw = Buffer.from(b64, "base64");
1307
+ const bytes = new Uint8Array(raw);
1308
+ ```
1309
+ If bytes start with `[65, 81, 65, 65, ...]` (ASCII for "AQAA"), you are passing base64 text as raw bytes.
1310
+
1311
+ 3. **Attach after approval, not during construction**:
1312
+ ```js
1313
+ // Step 3 of 5-step flow: AFTER requestTideOperatorApproval
1314
+ const approved = BaseTideRequest.decode(approvalResults[0].request);
1315
+ approved.addPolicy(adminPolicyBytes); // HERE — after approval
1316
+ ```
1317
+
1318
+ 4. **URL normalization**: Strip trailing slashes from `auth-server-url` before constructing endpoint URLs:
1319
+ ```js
1320
+ const baseUrl = config["auth-server-url"].replace(/\/+$/, "");
1321
+ const adminPolicyUrl = `${baseUrl}/realms/${config.realm}/tide-policy-resources/admin-policy`;
1322
+ ```
1323
+
1324
+ **This is a bootstrap/policy-flow failure, not a role issue.** Do not attempt to fix by renaming roles or switching encryption models.
1325
+
1326
+ ---
1327
+
1328
+ ## Common Diagnostic Commands
1329
+
1330
+ ### Check Auth State
1331
+ ```javascript
1332
+ // Browser console
1333
+ console.log('Authenticated:', iam.authenticated);
1334
+ console.log('User:', await iam.getUserInfo());
1335
+ console.log('Token:', await iam.getToken());
1336
+ console.log('Doken:', tc?.doken);
1337
+ ```
1338
+
1339
+ ### Check Roles
1340
+ ```bash
1341
+ # Server-side
1342
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}/users/${USER_ID}/role-mappings/realm" | \
1343
+ jq '.[] | .name'
1344
+ ```
1345
+
1346
+ ### Check Fabric Connectivity
1347
+ ```bash
1348
+ # From adapter JSON
1349
+ HOME_ORK=$(jq -r '.homeOrkUrl' data/tidecloak.json)
1350
+ curl -I $HOME_ORK
1351
+ # Should return 200 or 30x
1352
+ ```
1353
+
1354
+ ### Check CSP Headers
1355
+ ```bash
1356
+ curl -I https://app.example.com | grep -i content-security-policy
1357
+ ```
1358
+
1359
+ ### Check Browser Network Tab
1360
+ ```text
1361
+ DevTools → Network → Filter:
1362
+ - "tidecloak" - Auth requests to TideCloak
1363
+ - "tideprotocol.com" - ORK/Fabric requests
1364
+ - "dauth.me" - SWE iframe requests
1365
+
1366
+ Look for: 404s, CORS errors, timeouts
1367
+ ```
1368
+
1369
+ ### Check Browser Console
1370
+ ```text
1371
+ DevTools → Console → Filter errors
1372
+ Common Tide errors:
1373
+ - "CSP violation" → T-01
1374
+ - "Failed to verify JWT" → T-02
1375
+ - "E2EE timeout" → T-03
1376
+ - "DPoP verification failed" → T-05
1377
+ - "Contract compilation failed" → T-08
1378
+ ```
1379
+
1380
+ ---
1381
+
1382
+ ## Symptom Index
1383
+
1384
+ | Symptom | Troubleshooting Entry |
1385
+ |---------|----------------------|
1386
+ | Login hangs indefinitely | T-01 |
1387
+ | 401 Unauthorized on API calls | T-02, T-05 |
1388
+ | E2EE encrypt/decrypt timeouts | T-03 |
1389
+ | Silent refresh fails, frequent re-login | T-04 |
1390
+ | DPoP verification errors | T-05 |
1391
+ | Admin changes don't apply | T-06 |
1392
+ | Account linking stuck | T-07 |
1393
+ | Forseti signing fails | T-08 |
1394
+ | Token expired errors | T-09 |
1395
+ | Realm setup fails | T-10 |
1396
+ | Adapter JSON missing fields | T-11 |
1397
+ | Policy signing / Forseti ORK rejections | T-12 |
1398
+
1399
+ ---
1400
+
1401
+ ## When to Check Source Material
1402
+
1403
+ If troubleshooting entries don't resolve the issue:
1404
+
1405
+ 1. Check [Gap Register](../GAP_REGISTER.md) for known unresolved questions
1406
+ 2. Review [Anti-Patterns](anti-patterns.md) for common mistakes
1407
+ 3. Verify [Invariants](invariants.md) are not violated
1408
+ 4. Consult keylessh exemplar for implementation patterns
1409
+ 5. Check TideCloak Admin Console audit trail for event errors
1410
+
1411
+ ---
1412
+
1413
+ ## Escalation Path
1414
+
1415
+ If issue persists after troubleshooting:
1416
+
1417
+ 1. Collect diagnostics:
1418
+ - Browser console errors
1419
+ - Network tab filtered by "tidecloak", "tideprotocol"
1420
+ - Server logs around failure time
1421
+ - Adapter JSON contents (redact sensitive fields)
1422
+ - Realm configuration (defaultRoles, browserFlow, token lifetimes)
1423
+
1424
+ 2. Check if gap is documented:
1425
+ - Search [Gap Register](../GAP_REGISTER.md) for similar issue
1426
+ - Review [Assumptions](../notes/assumptions.md) for related uncertainties
1427
+
1428
+ 3. Create minimal reproduction:
1429
+ - Simplest code that triggers issue
1430
+ - Environment details (framework, TideCloak version, browser)
1431
+
1432
+ 4. Document as new gap if unresolved
1433
+
1434
+ ---
1435
+
1436
+ ## T-17: pnpm `ERR_PNPM_NO_MATCHING_VERSION` for Tide Packages
1437
+
1438
+ **Symptom**: `pnpm add @tidecloak/react` or `pnpm install` fails with `ERR_PNPM_NO_MATCHING_VERSION`.
1439
+
1440
+ **Root cause**: pnpm's `minimum-release-age` setting in `.npmrc` blocks recently-published packages. Tide's entire dependency tree (`@tidecloak/*`, `heimdall-tide`, `@tideorg/*`) is published on the same cadence and all hit this gate.
1441
+
1442
+ **Fix**: Add exclusions to `.npmrc`:
1443
+ ```ini
1444
+ minimum-release-age-exclude[]=@tidecloak/*
1445
+ minimum-release-age-exclude[]=heimdall-tide
1446
+ minimum-release-age-exclude[]=@tideorg/*
1447
+ minimum-release-age-exclude[]=asgard-tide
1448
+ ```
1449
+
1450
+ **Verification**: `pnpm add @tidecloak/react` resolves without version errors.
1451
+
1452
+ VERIFIED (TIDE_LEARNINGS-001 L-05).
1453
+
1454
+ ---
1455
+
1456
+ ## T-18: IGA Change-Set Approval May Require Admin Console UI
1457
+
1458
+ **Symptom**: REST-based `POST /tide-admin/change-set/sign` succeeds but `POST /tide-admin/change-set/commit` fails with "Could not find authorization signature."
1459
+
1460
+ **Root cause**: IGA approval is a cryptographic operation requiring the admin's Tide doken and ORK network interaction. In some configurations, the REST-based sign endpoint auto-signs but the commit requires the browser-based SDK (`approveTideRequests()` via the admin console UI).
1461
+
1462
+ **When this happens**: Typically when the admin has a linked Tide identity and IGA requires the cryptographic round-trip. The bootstrap script's REST-based `approve_and_commit` may work for the initial bootstrap (master admin token) but fail for subsequent operations.
1463
+
1464
+ **Fix**: If REST commit fails, approve and commit change requests from the TideCloak admin console UI: `http://localhost:8080/admin/master/console/#/{realm}`. Navigate to the change request section and approve from the UI.
1465
+
1466
+ **Verification**: After approval, `GET /tide-admin/change-set/counts` returns `total: 0`.
1467
+
1468
+ VERIFIED (TIDE_LEARNINGS-001 L-11).
1469
+
1470
+ ---
1471
+
1472
+ ## T-19: TideCloak 500 Error Masked as CORS Error in Browser
1473
+
1474
+ **Symptom**: Browser console shows "CORS header 'Access-Control-Allow-Origin' missing" on a TideCloak request (typically the token endpoint). The app appears to have a CORS misconfiguration.
1475
+
1476
+ **Root cause**: TideCloak (Keycloak-based) does not include CORS headers on error responses (especially 500 Internal Server Error). The browser blocks the response due to missing CORS headers and reports it as a CORS error, hiding the real server-side failure.
1477
+
1478
+ **Diagnostics**:
1479
+ 1. Open DevTools → Network tab
1480
+ 2. Find the failing request to TideCloak
1481
+ 3. Check the actual HTTP status code (ignore the console error message)
1482
+ 4. If status is 500: the issue is server-side, not CORS
1483
+ 5. Check TideCloak container logs: `docker logs <container>` for the real error
1484
+
1485
+ **Common 500 causes**:
1486
+ - ORK PreSign failure (`Provided TideAuthData OR PreviousTokenAuthorization`) — stale realm, recreate it
1487
+ - Database connection failure
1488
+ - Realm misconfiguration after incomplete bootstrap
1489
+
1490
+ **Fix**: Resolve the underlying server-side error. CORS headers will appear on successful responses. Do not add CORS workarounds for what is actually a 500 error.
1491
+
1492
+ **Verification**: After fixing the server error, the request returns 200 with proper CORS headers.
1493
+
1494
+ VERIFIED (LEARNINGS-ratidefy-batch-001 L-15).