@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,2133 @@
1
+ # Tide Anti-Patterns
2
+
3
+ Common mistakes that defeat Tide's security properties or cause operational failures. Each anti-pattern includes why it's wrong and the correct approach.
4
+
5
+ **Critical**: These mistakes reduce Tide to generic auth, introduce security vulnerabilities, or break core functionality.
6
+
7
+ ---
8
+
9
+ ## AP-01: Treating Tide as Generic OIDC
10
+
11
+ **What it looks like**:
12
+ - Implementing custom password verification logic
13
+ - Using standard Keycloak JWKS endpoint
14
+ - Skipping adapter JSON Tide extensions
15
+ - Treating login flow as "just OAuth2/OIDC"
16
+
17
+ **Why it's wrong**:
18
+ - Tide authentication is threshold-verified (PRISM), not server-checked
19
+ - JWT signing is threshold protocol (VVK), not single-server signing
20
+ - Adapter JSON contains embedded JWKS and Fabric endpoints
21
+ - Treating Tide as generic OIDC loses all threshold enforcement
22
+
23
+ **Consequence**: Collapse to single-point-of-trust. Threshold security properties lost.
24
+
25
+ **Correct approach**:
26
+ ```typescript
27
+ // ❌ WRONG: Fetch JWKS from remote endpoint
28
+ const JWKS = createRemoteJWKSet(new URL(`${issuer}/protocol/openid-connect/certs`));
29
+
30
+ // ❌ ALSO WRONG: Remote JWKS as fallback
31
+ try { await jwtVerify(token, localJWKS); }
32
+ catch { await jwtVerify(token, createRemoteJWKSet(certsUrl)); }
33
+
34
+ // ✅ CORRECT: Use embedded JWKS from adapter JSON, fail if missing
35
+ import { loadTideConfig } from './tidecloakConfig';
36
+ const config = loadTideConfig();
37
+ if (!config.jwk) throw new Error('Missing jwk in tidecloak.json. Re-export adapter.');
38
+ const JWKS = createLocalJWKSet(config.jwk);
39
+ ```
40
+
41
+ **Related**: [Collapse Risk Register](../notes/source-audit.md#collapse-risk-register), Invariants I-04, I-05
42
+
43
+ ---
44
+
45
+ ## AP-02: Relying on Client-Side Role Checks for Authorization
46
+
47
+ **What it looks like**:
48
+ ```typescript
49
+ // ❌ WRONG: API trusts client-side role check
50
+ if (useTideCloak().hasRealmRole('admin')) {
51
+ // Show admin button
52
+ }
53
+
54
+ app.get('/api/admin/users', (req, res) => {
55
+ // Assumes client already checked role
56
+ return res.json({ users: [...] });
57
+ });
58
+ ```
59
+
60
+ **Why it's wrong**:
61
+ - `hasRealmRole()` / `hasClientRole()` are UI gating, not authorization
62
+ - Attacker can bypass by calling API directly
63
+ - Client-side checks are easily modified
64
+ - No server-side enforcement = no security
65
+
66
+ **Note**: The SDK hook exports `hasRealmRole(role)` and `hasClientRole(role, client?)`. There is no generic `hasRole()` on the hook. Use `hasRealmRole` for realm roles, `hasClientRole` for client roles.
67
+
68
+ **Consequence**: Unauthorized access to protected APIs. Attacker can impersonate admin.
69
+
70
+ **Correct approach**:
71
+ ```typescript
72
+ // ✅ CORRECT: UI gating for UX (realm role)
73
+ if (useTideCloak().hasRealmRole('admin')) {
74
+ return <AdminButton />; // Show/hide UI element
75
+ }
76
+
77
+ // ✅ CORRECT: Server-side authorization
78
+ app.get('/api/admin/users', async (req, res) => {
79
+ const jwt = await verifyTideJWT(extractToken(req));
80
+ if (!jwt.realm_access.roles.includes('admin')) {
81
+ return res.status(403).json({ error: 'Forbidden' });
82
+ }
83
+ return res.json({ users: [...] });
84
+ });
85
+ ```
86
+
87
+ **Related**: Invariants I-03, I-08, [Feature Mapping](feature-mapping.md#protected-routes-vs-protected-apis)
88
+
89
+ ---
90
+
91
+ ## AP-03: Generating Keys Locally
92
+
93
+ **What it looks like**:
94
+ ```typescript
95
+ // ❌ WRONG: Local key generation for Tide operations
96
+ const keyPair = await crypto.subtle.generateKey(
97
+ { name: 'ECDSA', namedCurve: 'P-256' },
98
+ true,
99
+ ['sign', 'verify']
100
+ );
101
+ // Use keyPair for Tide operations
102
+ ```
103
+
104
+ **Why it's wrong**:
105
+ - Tide keys are born fragmented across ORKs (DKG)
106
+ - Local key generation defeats never-whole-key invariant
107
+ - Single point of compromise; attacker can extract complete key
108
+ - Threshold enforcement requires distributed key shards
109
+
110
+ **Consequence**: All Tide security properties lost. Attacker with local key can forge tokens, decrypt data, bypass policy.
111
+
112
+ **Correct approach**:
113
+ ```typescript
114
+ // ✅ CORRECT: Delegate to SDK/Fabric
115
+ const ciphertext = await iam.doEncrypt('tag', plaintext);
116
+ const signature = await tc.executeSignRequest(signRequest);
117
+
118
+ // Keys never exist locally; operations use Fabric threshold protocols
119
+ ```
120
+
121
+ **Related**: Invariants I-01, I-02, I-09, [Concepts](concepts.md#never-whole-key-model-ineffable-cryptography)
122
+
123
+ ---
124
+
125
+ ## AP-04: Implementing Offline E2EE Fallback
126
+
127
+ **What it looks like**:
128
+ ```typescript
129
+ // ❌ WRONG: Cache session keys for offline decrypt
130
+ let cachedSessionKey;
131
+
132
+ async function offlineDecrypt(ciphertext) {
133
+ if (!navigator.onLine && cachedSessionKey) {
134
+ return decryptLocally(ciphertext, cachedSessionKey);
135
+ }
136
+ return iam.doDecrypt('tag', ciphertext);
137
+ }
138
+ ```
139
+
140
+ **Why it's wrong**:
141
+ - E2EE session keys are threshold-decrypted by Fabric
142
+ - Caching session keys defeats threshold enforcement
143
+ - Offline decryption requires complete key locally
144
+ - No Fabric participation = no threshold security
145
+
146
+ **Consequence**: Session keys exposed client-side. Attacker can decrypt all data with cached key. Threshold security lost.
147
+
148
+ **Correct approach**:
149
+ ```typescript
150
+ // ✅ CORRECT: Require online Fabric access
151
+ async function decrypt(ciphertext) {
152
+ if (!navigator.onLine) {
153
+ throw new Error('E2EE requires online Fabric access');
154
+ }
155
+ return iam.doDecrypt('tag', ciphertext);
156
+ }
157
+
158
+ // Handle offline state at application level, not E2EE level
159
+ ```
160
+
161
+ **Related**: Invariants I-02, I-11, [Concepts](concepts.md#threshold-e2ee-hermetic-e2ee)
162
+
163
+ ---
164
+
165
+ ## AP-05: Single-Admin Bypass Paths
166
+
167
+ **What it looks like**:
168
+ ```typescript
169
+ // ❌ WRONG: Admin override that skips IGA approval
170
+ app.post('/api/admin/emergency-create-user', async (req, res) => {
171
+ const jwt = await verifyTideJWT(extractToken(req));
172
+ if (jwt.realm_access.roles.includes('superadmin')) {
173
+ // Skip IGA approval for emergency
174
+ await createUserDirectly(req.body);
175
+ return res.json({ success: true });
176
+ }
177
+ });
178
+ ```
179
+
180
+ **Why it's wrong**:
181
+ - IGA quorum enforcement prevents single-admin abuse
182
+ - Bypass paths defeat threshold governance
183
+ - "Emergency" paths are first attack vector
184
+ - Compromised superadmin can bypass all controls
185
+
186
+ **Consequence**: Single compromised admin can create backdoor accounts, grant unauthorized roles, bypass governance. IGA collapses to single-admin trust.
187
+
188
+ **Correct approach**:
189
+ ```typescript
190
+ // ✅ CORRECT: All admin changes go through TideCloak admin API (IGA-governed)
191
+ // Creating a user via the standard admin endpoint automatically generates a
192
+ // change-set when IGA is enabled. There is no explicit "createChangeRequest" API.
193
+ app.post('/api/admin/create-user', async (req, res) => {
194
+ const jwt = await verifyTideJWT(extractToken(req));
195
+ if (!jwt.realm_access.roles.includes('admin')) {
196
+ return res.status(403).json({ error: 'Forbidden' });
197
+ }
198
+
199
+ // Call TideCloak admin API — IGA creates a draft change-set automatically
200
+ await fetch(`${TIDECLOAK_URL}/admin/realms/${REALM}/users`, {
201
+ method: 'POST',
202
+ headers: {
203
+ 'Authorization': `Bearer ${adminToken}`,
204
+ 'Content-Type': 'application/json'
205
+ },
206
+ body: JSON.stringify(req.body)
207
+ });
208
+
209
+ // Admin must then approve via /tide-admin/change-set/sign/batch
210
+ // and commit via /tide-admin/change-set/commit/batch
211
+ return res.json({ status: 'pending_approval' });
212
+ });
213
+
214
+ // No bypass; even emergency changes require quorum
215
+ ```
216
+
217
+ **Related**: Invariants I-09, I-10, [Concepts](concepts.md#iga-identity-governance--administration)
218
+
219
+ ---
220
+
221
+ ## AP-06: Skipping DPoP Verification
222
+
223
+ **What it looks like**:
224
+ ```typescript
225
+ // ❌ WRONG: Accept access token without DPoP proof
226
+ app.get('/api/protected', async (req, res) => {
227
+ const jwt = await verifyTideJWT(extractToken(req));
228
+ // Missing DPoP verification
229
+ return res.json({ data: [...] });
230
+ });
231
+ ```
232
+
233
+ **Why it's wrong** (if DPoP is enabled):
234
+ - DPoP binds token to client key pair
235
+ - Without verification, stolen tokens can be replayed
236
+ - Attacker doesn't need private key to use token
237
+ - Token binding security property lost
238
+
239
+ **Consequence**: Token replay attacks succeed. Stolen access token is usable from any client.
240
+
241
+ **Correct approach**:
242
+ ```typescript
243
+ // ✅ CORRECT: Verify DPoP if header present
244
+ app.get('/api/protected', async (req, res) => {
245
+ const token = extractToken(req);
246
+
247
+ // Verify DPoP if present
248
+ if (req.headers.dpop) {
249
+ await verifyDPoP(req, token);
250
+ }
251
+
252
+ const jwt = await verifyTideJWT(token);
253
+ return res.json({ data: [...] });
254
+ });
255
+ ```
256
+
257
+ **Note**: DPoP is required for Tide's full security guarantees **VERIFIED** (vendor confirmation, GAP-032 resolved). ES256 is the default algorithm; EdDSA also supported.
258
+
259
+ **Related**: Invariants I-12, [Concepts](concepts.md#dpop-demonstration-of-proof-of-possession)
260
+
261
+ ---
262
+
263
+ ## AP-07: Missing CSP Whitelist
264
+
265
+ **What it looks like**:
266
+ ```typescript
267
+ // ❌ WRONG: No CSP or overly restrictive frame-src
268
+ // Default CSP:
269
+ // Content-Security-Policy: frame-src 'self'
270
+
271
+ // Or no CSP headers at all
272
+ ```
273
+
274
+ **Why it's wrong**:
275
+ - SWE loads via iframe from Tide domains
276
+ - Browser blocks cross-origin iframes without CSP whitelist
277
+ - SWE cannot load; all Tide operations fail silently
278
+ - Login hangs, E2EE timeouts, no visible errors
279
+
280
+ **Failure symptom**: Check browser console: `Refused to frame 'https://...' because it violates the following Content Security Policy directive: "frame-src 'self'"`.
281
+
282
+ **Consequence**: Complete Tide failure. Login broken. E2EE broken. No threshold operations work.
283
+
284
+ **Correct approach**:
285
+ ```typescript
286
+ // ✅ CORRECT: frame-src '*' required for ORK re-homing
287
+ app.use(helmet({
288
+ contentSecurityPolicy: {
289
+ directives: {
290
+ frameSrc: ["'self'", '*']
291
+ }
292
+ }
293
+ }));
294
+ ```
295
+
296
+ **Related**: Invariants I-06, [Concepts](concepts.md#swe-secure-web-enclave)
297
+
298
+ ---
299
+
300
+ ## AP-08: Hardcoding 14/20 Threshold
301
+
302
+ **What it looks like**:
303
+ ```typescript
304
+ // ❌ WRONG: Hardcoded threshold
305
+ const REQUIRED_ORKS = 14;
306
+ const TOTAL_ORKS = 20;
307
+
308
+ if (approvals.length < REQUIRED_ORKS) {
309
+ throw new Error('Insufficient ORK approvals');
310
+ }
311
+ ```
312
+
313
+ **Why it's wrong**:
314
+ - Threshold is deployment-configurable via env vars
315
+ - Mainnet: 14/20, Test: 3/5, Custom: varies
316
+ - Hardcoding breaks test environments
317
+ - Code cannot adapt to different Fabric configurations
318
+
319
+ **Consequence**: Code fails in test/dev environments with different threshold. Deployment inflexibility.
320
+
321
+ **Correct approach**:
322
+ ```typescript
323
+ // ✅ CORRECT: Read threshold from config/env
324
+ const THRESHOLD = parseInt(process.env.TIDE_VENDOR_THRESHOLD_SIGNING || '3');
325
+ const TOTAL = parseInt(process.env.TIDE_VENDOR_THRESHOLD_TOTAL || '5');
326
+
327
+ if (approvals.length < THRESHOLD) {
328
+ throw new Error(`Insufficient ORK approvals (got ${approvals.length}, need ${THRESHOLD})`);
329
+ }
330
+ ```
331
+
332
+ **Related**: Invariants I-02, [Concepts](concepts.md#fabric-cybersecurity-fabric)
333
+
334
+ ---
335
+
336
+ ## AP-09: Treating BYOiD as Federated Login
337
+
338
+ **What it looks like**:
339
+ ```markdown
340
+ <!-- ❌ WRONG: Docs that conflate BYOiD with OAuth2 federated login -->
341
+ "TideCloak supports Bring Your Own Identity, allowing users to log in with their existing accounts from Google, Microsoft, or other identity providers."
342
+ ```
343
+
344
+ **Why it's wrong**:
345
+ - BYOiD is threshold password authentication (PRISM), not federated login
346
+ - Users bring their password, not an external identity
347
+ - No integration with Google/Microsoft/OAuth2 providers (unless via Keycloak federation, which is separate)
348
+ - Confusion leads to wrong implementation expectations
349
+
350
+ **Consequence**: Developer expects to configure OAuth2 providers. Actual BYOiD setup (PRISM threshold auth) is missed. Documentation misleads.
351
+
352
+ **Correct description**:
353
+ ```markdown
354
+ <!-- ✅ CORRECT: Emphasize threshold password auth -->
355
+ "TideCloak uses threshold password authentication (BYOiD). User passwords are verified across T+ ORKs using PRISM (Threshold Oblivious Pseudorandom Function). No password hash is stored anywhere; no single party learns the password."
356
+ ```
357
+
358
+ **Related**: [Concepts](concepts.md#threshold-password-authentication-byoid), [Terminology Map](../notes/terminology-map.md#terms-often-confused-with-generic-auth)
359
+
360
+ ---
361
+
362
+ ## AP-10: Treating IGA as Procedural Approval
363
+
364
+ **What it looks like**:
365
+ ```markdown
366
+ <!-- ❌ WRONG: Docs that downplay threshold signatures -->
367
+ "IGA provides an approval workflow where admin changes require multi-admin sign-off before taking effect."
368
+ ```
369
+
370
+ **Why it's wrong**:
371
+ - IGA approval is sealed by VVK threshold signatures, not procedural tracking
372
+ - Emphasis on "workflow" obscures cryptographic enforcement
373
+ - Sounds like generic approval system, not threshold-secured governance
374
+ - Developer may implement procedural bypass ("emergency override")
375
+
376
+ **Consequence**: IGA implemented as procedural workflow with bypass paths. Cryptographic enforcement lost.
377
+
378
+ **Correct description**:
379
+ ```markdown
380
+ <!-- ✅ CORRECT: Emphasize threshold signatures -->
381
+ "IGA enforces quorum approval via VVK threshold signatures. Admin changes require max(1, floor(N*0.7)) approvals, sealed by threshold cryptography. No single admin can bypass; compromised admin cannot act unilaterally. Approval is cryptographic, not procedural."
382
+ ```
383
+
384
+ **Related**: Invariants I-10, [Concepts](concepts.md#iga-identity-governance--administration)
385
+
386
+ ---
387
+
388
+ ## AP-11: Client-Side Policy Enforcement
389
+
390
+ **What it looks like**:
391
+ ```typescript
392
+ // ❌ WRONG: Forseti policy logic implemented client-side
393
+ function checkSSHPolicy(user, destination) {
394
+ if (user.role === 'admin' || destination.startsWith('dev-')) {
395
+ return true; // Allow
396
+ }
397
+ return false;
398
+ }
399
+
400
+ if (checkSSHPolicy(currentUser, targetServer)) {
401
+ await connectSSH(targetServer);
402
+ }
403
+ ```
404
+
405
+ **Why it's wrong**:
406
+ - Forseti contracts execute in distributed ORK sandboxes, not client
407
+ - Client-side checks are easily bypassed
408
+ - No majority enforcement; single client can override
409
+ - Policy becomes suggestion, not enforcement
410
+
411
+ **Consequence**: Policy bypass. Attacker modifies client code to skip checks. No distributed enforcement.
412
+
413
+ **Correct approach**:
414
+ ```typescript
415
+ // ✅ CORRECT: Submit contract to Fabric for distributed execution
416
+ const signRequest = await tc.createTideRequest({
417
+ contract: sshPolicyContract, // C# code executed on every ORK
418
+ modelName: 'DynamicApprovedCustom<SSH>:DynamicApprovedCustom<1>',
419
+ authFlow: 'Policy:1',
420
+ authorizer: tc.doken,
421
+ challengeData: { user: currentUser, destination: targetServer }
422
+ });
423
+
424
+ const result = await tc.executeSignRequest(signRequest);
425
+ // ORKs have independently evaluated policy; majority approved or rejected
426
+ ```
427
+
428
+ **Related**: Invariants I-15, [Concepts](concepts.md#forseti-policy-engine)
429
+
430
+ ---
431
+
432
+ ## AP-12: Caching DPoP Headers
433
+
434
+ **What it looks like**:
435
+ ```typescript
436
+ // ❌ WRONG: Reuse DPoP proof across requests
437
+ let cachedDPoPHeader;
438
+
439
+ async function apiCall(endpoint) {
440
+ if (!cachedDPoPHeader) {
441
+ cachedDPoPHeader = await generateDPoPProof();
442
+ }
443
+
444
+ return fetch(endpoint, {
445
+ headers: {
446
+ 'Authorization': `DPoP ${token}`,
447
+ 'DPoP': cachedDPoPHeader // Reused across requests
448
+ }
449
+ });
450
+ }
451
+ ```
452
+
453
+ **Why it's wrong**:
454
+ - DPoP proof includes `htm` (HTTP method) and `htu` (HTTP URI)
455
+ - Each request requires fresh proof matching method and URL
456
+ - Cached proof fails verification on different endpoints
457
+ - `jti` replay protection rejects reused proofs
458
+
459
+ **Consequence**: DPoP verification fails. API calls rejected with 401.
460
+
461
+ **Correct approach**:
462
+ ```typescript
463
+ // ✅ CORRECT: Generate fresh DPoP proof per request
464
+ async function apiCall(endpoint, method = 'GET') {
465
+ const dpopProof = await generateDPoPProof(method, endpoint); // Fresh for each request
466
+
467
+ return fetch(endpoint, {
468
+ method,
469
+ headers: {
470
+ 'Authorization': `DPoP ${token}`,
471
+ 'DPoP': dpopProof
472
+ }
473
+ });
474
+ }
475
+
476
+ // Or use SDK's secureFetch — but it is NOT a drop-in fetch replacement.
477
+ // secureFetch has three requirements:
478
+ // 1. useDPoP must be in the config object (not as a JSX prop)
479
+ // 2. URLs must be absolute (new URL() throws on relative paths)
480
+ // 3. Authorization: Bearer <token> must be pre-set (SDK upgrades to DPoP scheme)
481
+ ```
482
+
483
+ **Pitfall:** `useDPoP` as a JSX prop on `TideCloakProvider`. The provider only accepts `config` and `children`. `useDPoP` must be merged into the config object: `config={{ ...tcConfig, useDPoP: { mode: 'strict', alg: 'ES256' } }}`. VERIFIED (session-002).
484
+
485
+ **Pitfall:** `secureFetch('/api/vault')` — relative URL. The SDK calls `new URL(url)` internally, which throws `TypeError: Invalid URL` on relative paths. Always use absolute URLs: `secureFetch(\`\${window.location.origin}/api/vault\`)`. VERIFIED (session-002).
486
+
487
+ **Pitfall:** `secureFetch(url)` without pre-setting `Authorization: Bearer <token>`. The SDK only upgrades to DPoP scheme when it detects an existing Bearer token. Without it, the request is sent as plain fetch with no auth headers. VERIFIED (session-002).
488
+
489
+ **Pitfall:** Using regular `fetch` with `Bearer` header when `useDPoP` IS configured. DPoP-bound tokens require DPoP proof headers. Sending a DPoP-bound token as `Bearer` causes TideCloak to reject the request.
490
+
491
+ **Recommended `appFetch` wrapper** for DPoP-enabled apps:
492
+ ```typescript
493
+ // lib/appFetch.ts
494
+ import { IAMService } from '@tidecloak/js';
495
+
496
+ export async function appFetch(path: string, init?: RequestInit): Promise<Response> {
497
+ const url = `${window.location.origin}${path}`;
498
+ const token = await IAMService.getToken();
499
+ return IAMService.secureFetch(url, {
500
+ ...init,
501
+ headers: {
502
+ ...init?.headers,
503
+ Authorization: `Bearer ${token}`,
504
+ },
505
+ });
506
+ }
507
+ ```
508
+
509
+ **Why this pattern**: `IAMService.secureFetch` detects the `Authorization: Bearer` header and upgrades it to `Authorization: DPoP <token>` while attaching the DPoP proof. Without the pre-set Bearer header, the SDK sends a plain fetch with no auth. Without the absolute URL, `new URL()` throws. This wrapper handles both requirements.
510
+
511
+ **Related**: Invariants I-12, [Concepts](concepts.md#dpop-demonstration-of-proof-of-possession)
512
+
513
+ ---
514
+
515
+ ## AP-13: Using Adapter JSON Without Tide Extensions
516
+
517
+ **What it looks like**:
518
+ ```typescript
519
+ // ❌ WRONG: Export generic Keycloak adapter
520
+ // Admin Console → Clients → myclient → Installation → Keycloak OIDC JSON
521
+
522
+ // Result: Missing jwk, vendorId, homeOrkUrl
523
+ {
524
+ "realm": "myrealm",
525
+ "auth-server-url": "https://tidecloak.example.com",
526
+ "resource": "myclient",
527
+ ...
528
+ // No Tide extensions
529
+ }
530
+ ```
531
+
532
+ **Why it's wrong**:
533
+ - JWT verification requires embedded JWKS (`jwk` field)
534
+ - Client SDK needs Fabric endpoint (`homeOrkUrl`)
535
+ - Licensing uses vendor identifier (`vendorId`)
536
+ - Generic adapter is insufficient for Tide operations
537
+
538
+ **Consequence**: JWT verification fails. Client cannot connect to Fabric. Operations fail with unclear errors.
539
+
540
+ **Correct approach**:
541
+ ```bash
542
+ # ✅ CORRECT: Export Tide adapter with extensions
543
+ curl -X GET \
544
+ "${TIDECLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_ID}/vendorResources/get-installations-provider?providerId=keycloak-oidc-keycloak-json" \
545
+ -H "Authorization: Bearer ${ADMIN_TOKEN}" \
546
+ > data/tidecloak.json
547
+
548
+ # Verify Tide extensions present
549
+ jq 'has("jwk") and has("vendorId") and has("homeOrkUrl")' data/tidecloak.json
550
+ # Should output: true
551
+ ```
552
+
553
+ **Related**: Invariants I-04, I-05, I-13, [Concepts](concepts.md#adapter-json)
554
+
555
+ ---
556
+
557
+ ## AP-14: Implementing Custom Token Refresh Logic
558
+
559
+ **What it looks like**:
560
+ ```typescript
561
+ // ❌ WRONG: Reimplement token refresh
562
+ async function refreshToken() {
563
+ const refreshToken = localStorage.getItem('refresh_token');
564
+ const response = await fetch(`${tidecloakUrl}/realms/${realm}/protocol/openid-connect/token`, {
565
+ method: 'POST',
566
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
567
+ body: new URLSearchParams({
568
+ grant_type: 'refresh_token',
569
+ refresh_token: refreshToken,
570
+ client_id: clientId
571
+ })
572
+ });
573
+ const tokens = await response.json();
574
+ localStorage.setItem('access_token', tokens.access_token);
575
+ }
576
+ ```
577
+
578
+ **Why it's wrong**:
579
+ - SDK already handles token refresh (including PRISM verification)
580
+ - Custom logic may skip silent SSO, causing visible redirects
581
+ - Manual token storage is error-prone
582
+ - May miss DPoP token binding renewal
583
+
584
+ **Consequence**: Token refresh fails. User forced to re-login. Corrupted auth state.
585
+
586
+ **Correct approach**:
587
+ ```typescript
588
+ // ✅ CORRECT: Let SDK handle refresh
589
+ // SDK automatically refreshes via silent SSO iframe
590
+ // Listen to refresh events if needed
591
+ iam.on('authRefreshSuccess', () => {
592
+ console.log('Token refreshed automatically');
593
+ });
594
+
595
+ iam.on('authRefreshError', (err) => {
596
+ console.error('Refresh failed, re-login required', err);
597
+ iam.doLogin({ redirectUri: window.location.origin });
598
+ });
599
+ ```
600
+
601
+ **Related**: [Concepts](concepts.md#silent-sso-token-refresh), [Framework Matrix](framework-matrix.md#silent-sso-setup)
602
+
603
+ ---
604
+
605
+ ## AP-15: Skipping `silent-check-sso.html`
606
+
607
+ **What it looks like**:
608
+ ```bash
609
+ # ❌ WRONG: Missing silent SSO file
610
+ ls public/
611
+ # index.html, logo.png, ...
612
+ # No silent-check-sso.html
613
+ ```
614
+
615
+ **Why it's wrong**:
616
+ - Silent token refresh requires hidden iframe
617
+ - TideCloak redirects to `/silent-check-sso.html` during refresh
618
+ - 404 on this file breaks silent refresh
619
+ - User forced to full re-login on every token expiration
620
+
621
+ **Consequence**: Silent refresh fails. Session appears to expire prematurely. Frequent forced re-logins. Poor UX.
622
+
623
+ **Correct approach**:
624
+ ```html
625
+ <!-- ✅ CORRECT: Create public/silent-check-sso.html -->
626
+ <html>
627
+ <body>
628
+ <script>
629
+ parent.postMessage(location.href, location.origin);
630
+ </script>
631
+ </body>
632
+ </html>
633
+ ```
634
+
635
+ **Verification**:
636
+ ```bash
637
+ curl https://app.example.com/silent-check-sso.html
638
+ # Should return HTML with postMessage script
639
+ ```
640
+
641
+ **Related**: Invariants I-07, [Framework Matrix](framework-matrix.md#silent-sso-setup)
642
+
643
+ ---
644
+
645
+ ## AP-16: Treating Hermetic E2EE as Client-Side Encryption
646
+
647
+ **What it looks like**:
648
+ ```markdown
649
+ <!-- ❌ WRONG: Docs that imply client-side encryption with server key storage -->
650
+ "Hermetic E2EE encrypts data on the client side before sending to the server. Encryption keys are stored securely on the TideCloak server."
651
+ ```
652
+
653
+ **Why it's wrong**:
654
+ - E2EE session keys are threshold-encrypted via CVK, not stored on server
655
+ - Decryption requires live Fabric threshold participation
656
+ - "Keys stored on server" defeats E2EE definition
657
+ - Plaintext never exists on server, Fabric, or ORKs
658
+
659
+ **Consequence**: Developer implements client-side encryption with server key storage. Threshold E2EE not used. Security property lost.
660
+
661
+ **Correct description**:
662
+ ```markdown
663
+ <!-- ✅ CORRECT: Emphasize threshold decryption and no server access -->
664
+ "Threshold E2EE (Hermetic E2EE) encrypts data client-side using session keys. Session keys are threshold-encrypted via CVK shards across ORKs. Decryption requires live Fabric threshold participation. Plaintext exists only on user's device; no server, admin, or ORK can decrypt."
665
+ ```
666
+
667
+ **Related**: Invariants I-11, [Concepts](concepts.md#threshold-e2ee-hermetic-e2ee)
668
+
669
+ ---
670
+
671
+ ## AP-17: Implementing Ragnarok Without Quorum
672
+
673
+ **What it looks like**:
674
+ ```typescript
675
+ // ❌ WRONG: Single-admin Ragnarok trigger
676
+ app.post('/api/admin/ragnarok', async (req, res) => {
677
+ const jwt = await verifyTideJWT(extractToken(req));
678
+ if (jwt.realm_access.roles.includes('superadmin')) {
679
+ await executeFabricOffboarding(); // No quorum check
680
+ return res.json({ success: true });
681
+ }
682
+ });
683
+ ```
684
+
685
+ **Why it's wrong**:
686
+ - Ragnarok is the sole deviation from never-whole-key invariant
687
+ - Reconstructs keys locally; irreversible
688
+ - Requires nested threshold quorum for this reason
689
+ - Single-admin path defeats governance
690
+
691
+ **Consequence**: Single compromised admin can trigger irreversible offboarding. Keys reconstructed without quorum approval.
692
+
693
+ **Correct approach**:
694
+ ```typescript
695
+ // ✅ CORRECT: Ragnarok via IGA quorum approval
696
+ // Admin creates change-set for Ragnarok
697
+ const changeSet = await createChangeRequest({
698
+ type: 'FABRIC_OFFBOARDING',
699
+ data: { realm: realmId }
700
+ });
701
+
702
+ // Requires quorum approval (same as other admin changes)
703
+ // Sealed by threshold signatures before execution
704
+ // Irreversible; must have maximum scrutiny
705
+ ```
706
+
707
+ **Related**: Invariants I-10, [Concepts](concepts.md#ragnarok-fabric-offboarding)
708
+
709
+ ---
710
+
711
+ ## AP-18: Mixing Tide Roles with Application Roles
712
+
713
+ **What it looks like**:
714
+ ```typescript
715
+ // ❌ WRONG: Confuse Tide system roles with app roles
716
+ const tideRoles = ['_tide_enabled', '_tide_ssn.selfencrypt'];
717
+ const appRoles = ['admin', 'editor', 'viewer'];
718
+
719
+ // Check Tide role for app authorization
720
+ if (jwt.realm_access.roles.includes('_tide_ssn.selfencrypt')) {
721
+ // Grant admin access (WRONG)
722
+ return <AdminPanel />;
723
+ }
724
+ ```
725
+
726
+ **Why it's wrong**:
727
+ - Tide roles (`_tide_*`) control cryptographic access (E2EE tags)
728
+ - Application roles (`admin`, `editor`) control feature access
729
+ - `_tide_ssn.selfencrypt` means "can encrypt SSN field", not "is admin"
730
+ - Mixing role types breaks security model
731
+
732
+ **Consequence**: User with E2EE role gains unintended app privileges. Authorization broken.
733
+
734
+ **Correct approach**:
735
+ ```typescript
736
+ // ✅ CORRECT: Separate Tide roles from app roles
737
+ // Tide E2EE role check
738
+ const canEncryptSSN = jwt.realm_access.roles.includes('_tide_ssn.selfencrypt');
739
+
740
+ // App authorization role check
741
+ const isAdmin = jwt.realm_access.roles.includes('admin');
742
+
743
+ if (canEncryptSSN) {
744
+ // Allow SSN encryption only
745
+ await iam.doEncrypt('ssn', plaintext);
746
+ }
747
+
748
+ if (isAdmin) {
749
+ // Allow admin features
750
+ return <AdminPanel />;
751
+ }
752
+ ```
753
+
754
+ **Related**: [Concepts](concepts.md#tag-based-e2ee-roles), [Feature Mapping](feature-mapping.md#e2ee-features)
755
+
756
+ ---
757
+
758
+ ## AP-19: Storing or Handling Passwords Server-Side
759
+
760
+ **What it looks like**:
761
+ ```typescript
762
+ // ❌ WRONG: Backend receives and validates password
763
+ app.post('/api/login', async (req, res) => {
764
+ const { username, password } = req.body;
765
+ const user = await db.findUser(username);
766
+ if (await bcrypt.compare(password, user.passwordHash)) {
767
+ return res.json({ token: generateToken(user) });
768
+ }
769
+ });
770
+ ```
771
+
772
+ **Why it's wrong**:
773
+ - TideCloak uses PRISM zero-knowledge verification
774
+ - Passwords never leave the browser; verification happens across ORKs
775
+ - Server receiving a password means Tide auth is bypassed entirely
776
+ - Re-creates single-point-of-trust the architecture eliminates
777
+
778
+ **Consequence**: Password exposed to server operator. Zero-knowledge property destroyed. Tide threshold authentication bypassed.
779
+
780
+ **Correct approach**:
781
+ ```typescript
782
+ // ✅ CORRECT: Let TideCloak SDK handle authentication
783
+ // Frontend triggers login via SDK
784
+ const { login } = useTideCloak();
785
+ login({ redirectUri: window.location.origin });
786
+
787
+ // Backend only verifies the resulting JWT
788
+ app.get('/api/protected', async (req, res) => {
789
+ const jwt = await verifyTideJWT(extractToken(req));
790
+ // Password never touches this server
791
+ });
792
+ ```
793
+
794
+ **Related**: Invariants I-01, I-02, [Concepts](concepts.md#threshold-password-authentication-byoid)
795
+
796
+ ---
797
+
798
+ ## AP-20: Decoding JWT Without Signature Verification
799
+
800
+ **What it looks like**:
801
+ ```typescript
802
+ // ❌ WRONG: Decode without verify
803
+ import jwt_decode from 'jwt-decode';
804
+ const claims = jwt_decode(token);
805
+ if (claims.realm_access.roles.includes('admin')) {
806
+ // Trust unverified claims
807
+ }
808
+ ```
809
+
810
+ **Why it's wrong**:
811
+ - Decoded-but-unverified JWT is meaningless; anyone can forge claims
812
+ - Tide JWTs are VVK threshold-signed; verification proves distributed consensus
813
+ - Skipping verification collapses threshold trust to zero trust
814
+
815
+ **Consequence**: Attacker crafts JWT with arbitrary claims. All authorization decisions compromised.
816
+
817
+ **Correct approach**:
818
+ ```typescript
819
+ // ✅ CORRECT: Always verify signature against embedded JWKS
820
+ import TideJWT from "@tidecloak/verify";
821
+ const _mod = (TideJWT as any)?.default ?? TideJWT;
822
+ const { verifyTideCloakToken } = _mod;
823
+
824
+ const verified = await verifyTideCloakToken(token, tidecloakConfig);
825
+ // Only trust claims from verified token
826
+ ```
827
+
828
+ **Related**: Invariants I-04, I-05, AP-01
829
+
830
+ ---
831
+
832
+ ## AP-21: Storing Dokens Server-Side
833
+
834
+ **What it looks like**:
835
+ ```typescript
836
+ // ❌ WRONG: Persist Doken on server
837
+ app.post('/api/store-session', async (req, res) => {
838
+ const { doken } = req.body;
839
+ await redis.set(`doken:${userId}`, doken);
840
+ });
841
+ ```
842
+
843
+ **Why it's wrong**:
844
+ - Dokens are session-bound to the SWE's ephemeral key
845
+ - A Doken cannot be used from a different context than the one that created it
846
+ - Storing is pointless (unusable) and a leak risk
847
+ - Compromised Doken store exposes session metadata
848
+
849
+ **Consequence**: Leaked Dokens with no operational benefit. False sense of session persistence.
850
+
851
+ **Correct approach**:
852
+ ```typescript
853
+ // ✅ CORRECT: Let SDK manage Doken lifecycle in-browser
854
+ // Dokens live only in the SWE context
855
+ // Server uses JWTs for authorization, not Dokens
856
+ const { doken } = useTideCloak(); // Browser-only, never sent to server
857
+ ```
858
+
859
+ **Related**: [Concepts](concepts.md#swe-secure-web-enclave)
860
+
861
+ ---
862
+
863
+ ## AP-22: Decrypting on the Server
864
+
865
+ **What it looks like**:
866
+ ```typescript
867
+ // ❌ WRONG: Server-side decryption
868
+ app.get('/api/records/:id', async (req, res) => {
869
+ const encrypted = await db.getRecord(req.params.id);
870
+ const plaintext = await serverDecrypt(encrypted, serverKey);
871
+ return res.json({ data: plaintext });
872
+ });
873
+ ```
874
+
875
+ **Why it's wrong**:
876
+ - Hermetic E2EE means plaintext exists only in the user's browser
877
+ - Server decryption is standard server-side encryption, not E2EE
878
+ - Server operator gains access to plaintext, defeating the security model
879
+ - Threshold decryption requires live Fabric participation from the client
880
+
881
+ **Consequence**: E2EE property destroyed. Server operator can read all data. No better than standard encryption.
882
+
883
+ **Correct approach**:
884
+ ```typescript
885
+ // ✅ CORRECT: Decrypt only in the browser via SDK
886
+ const plaintext = await iam.doDecrypt('medical', ciphertext);
887
+ // Plaintext never leaves the browser
888
+ // Server stores and serves only ciphertext
889
+ ```
890
+
891
+ **Related**: Invariants I-11, AP-04, AP-16, [Concepts](concepts.md#threshold-e2ee-hermetic-e2ee)
892
+
893
+ ---
894
+
895
+ ## AP-23: Skipping Tags or Reusing a Single Tag for All Encrypted Data
896
+
897
+ **What it looks like**:
898
+ ```typescript
899
+ // ❌ WRONG: No tag
900
+ const encrypted = await iam.doEncrypt('', plaintext);
901
+
902
+ // ❌ WRONG: Same tag for everything
903
+ const encSSN = await iam.doEncrypt('data', ssn);
904
+ const encMedical = await iam.doEncrypt('data', medicalRecord);
905
+ const encNotes = await iam.doEncrypt('data', internalNotes);
906
+ ```
907
+
908
+ **Why it's wrong**:
909
+ - Tags define who can decrypt; no tag means no access control
910
+ - One tag for all data means anyone with that role decrypts everything
911
+ - Different data sensitivity levels require different tags mapped to different roles
912
+ - Granular access control is the entire point of tag-based E2EE
913
+
914
+ **Consequence**: No field-level access control. Any user with the single decrypt role reads all encrypted data regardless of sensitivity.
915
+
916
+ **Correct approach**:
917
+ ```typescript
918
+ // ✅ CORRECT: Meaningful tags per sensitivity level
919
+ const encSSN = await iam.doEncrypt('ssn', ssn); // _tide_ssn.selfdecrypt
920
+ const encMedical = await iam.doEncrypt('medical', record); // _tide_medical.selfdecrypt
921
+ const encNotes = await iam.doEncrypt('notes', notes); // _tide_notes.selfdecrypt
922
+ // Each tag has separate encrypt/decrypt roles with different user assignments
923
+ ```
924
+
925
+ **Related**: AP-18, [Concepts](concepts.md#tag-based-e2ee-roles)
926
+
927
+ ---
928
+
929
+ ## AP-24: Using Self-Encryption for Shared Data Between Users
930
+
931
+ **What it looks like**:
932
+ ```typescript
933
+ // ❌ WRONG: Encrypt with doEncrypt, expect other users to doDecrypt
934
+ // User A encrypts
935
+ const ciphertext = await iam.doEncrypt('shared', data);
936
+ await api.saveSharedDoc(ciphertext);
937
+
938
+ // User B tries to decrypt (FAILS even with _tide_shared.selfdecrypt role)
939
+ const plaintext = await iam.doDecrypt('shared', ciphertext);
940
+ ```
941
+
942
+ **Why it's wrong**:
943
+ - Self-encryption (`doEncrypt`/`doDecrypt` without policy) binds ciphertext to the encrypting user's identity
944
+ - Another user with the same `selfdecrypt` role cannot decrypt data encrypted by someone else
945
+ - Sharing between users requires policy-governed VVK encryption with a Forseti contract
946
+
947
+ **Consequence**: Other users cannot decrypt shared data. Silent decryption failures. Data appears locked to the creator.
948
+
949
+ **Correct approach**:
950
+ ```typescript
951
+ // ✅ CORRECT: Use policy-governed encryption for shared data
952
+ const ciphertext = await iam.doEncrypt(data, signedPolicyBytes);
953
+ // VVK (organizational key) encrypts; Forseti contract controls who can decrypt
954
+ // Any user whose role passes the contract's ValidateExecutor can decrypt
955
+ ```
956
+
957
+ **Related**: AP-11, [Concepts](concepts.md#threshold-e2ee-hermetic-e2ee)
958
+
959
+ ---
960
+
961
+ ## AP-25: Using `_tide_*` Roles in Forseti ValidateExecutor
962
+
963
+ **What it looks like**:
964
+ ```csharp
965
+ // ❌ WRONG: Checking voucher gate role in contract
966
+ public override bool ValidateExecutor(UserContext user) {
967
+ return user.HasRole("_tide_medical.selfdecrypt");
968
+ }
969
+ ```
970
+
971
+ **Why it's wrong**:
972
+ - `_tide_{tag}.selfencrypt/selfdecrypt` roles are voucher gates only; they enable the ORK operation type
973
+ - Forseti contracts should check regular realm roles for access control decisions
974
+ - To restrict who can decrypt, remove the regular role from users, not the voucher gate roles
975
+ - Mixing voucher gates with policy logic conflates two separate layers
976
+
977
+ **Consequence**: Access control logic tied to wrong role type. Revoking a voucher gate disables the entire operation rather than just the policy decision.
978
+
979
+ **Correct approach**:
980
+ ```csharp
981
+ // ✅ CORRECT: Check a regular realm role
982
+ public override bool ValidateExecutor(UserContext user) {
983
+ return user.HasRole("shared-data-access");
984
+ }
985
+ // Voucher gate roles (_tide_*) are assigned separately to enable the ORK operation
986
+ ```
987
+
988
+ **Related**: AP-18, AP-11
989
+
990
+ ---
991
+
992
+ ## AP-26: Renaming Self-Encryption Roles to Enable Shared Decryption
993
+
994
+ **What it looks like**:
995
+ ```typescript
996
+ // ❌ WRONG: Replace selfencrypt/selfdecrypt with encrypt/decrypt
997
+ // expecting this to make ciphertext decryptable by other users
998
+ // Before (correct for self-encryption):
999
+ // _tide_vault.selfencrypt / _tide_vault.selfdecrypt
1000
+ // After (incorrect "fix"):
1001
+ // _tide_vault.encrypt / _tide_vault.decrypt
1002
+ const ciphertext = await doEncrypt([{ data: secret, tags: ['vault'] }]);
1003
+ // Still uses doEncrypt from useTideCloak() — still self-encryption
1004
+ // Changing role names does not change the encryption model
1005
+ ```
1006
+
1007
+ **Why it's wrong**:
1008
+ - The `doEncrypt`/`doDecrypt` calls on `useTideCloak()` always perform self-encryption regardless of the role suffix
1009
+ - Changing `selfencrypt` → `encrypt` in the role name does not switch the encryption model
1010
+ - Self-encryption binds ciphertext to the encrypting user's CVK. The role suffix is a naming convention; the SDK call path determines the encryption model.
1011
+ - To switch from self-encryption to shared encryption, you must change the SDK call to `IAMService.doEncrypt(data, signedPolicyBytes)`, deploy a Forseti contract, and sign a policy
1012
+
1013
+ **Consequence**: App appears to support shared access but does not. Other users still cannot decrypt. Data locked to creator. Silent failures.
1014
+
1015
+ **Correct approach**:
1016
+ - For self-encryption (user-private data): keep `_tide_{tag}.selfencrypt` / `_tide_{tag}.selfdecrypt` and use `doEncrypt`/`doDecrypt` from `useTideCloak()`
1017
+ - For shared encryption: use generic voucher gates (e.g., `_tide_x.selfencrypt`/`_tide_x.selfdecrypt`), add a regular realm role for the Forseti contract (e.g., `shared-data-access`), deploy a Forseti contract, sign the policy, and call `IAMService.doEncrypt(data, signedPolicyBytes)`
1018
+ - These are different SDK call paths and different bootstrap flows — not a role rename
1019
+
1020
+ **Related**: AP-24, [Concepts](concepts.md#self-encryption-vs-policy-governed-vvk-encryption)
1021
+
1022
+ ---
1023
+
1024
+ ## AP-27: Using `configFilePath` Instead of `configUrl`
1025
+
1026
+ **What it looks like**:
1027
+ ```tsx
1028
+ // ❌ WRONG: Wrong prop name
1029
+ <TideCloakContextProvider configFilePath="/tidecloak.json">
1030
+ <App />
1031
+ </TideCloakContextProvider>
1032
+ ```
1033
+
1034
+ **Why it's wrong**:
1035
+ - `TideCloakContextProvider` accepts `configUrl`, not `configFilePath`
1036
+ - Wrong prop name falls through to the default `/adapter.json`, which does not exist
1037
+ - Vite serves the HTML index page instead of JSON
1038
+ - Results in `SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON`
1039
+
1040
+ **Consequence**: SDK initialization fails. Cryptic JSON parse error. Application cannot start.
1041
+
1042
+ **Correct approach**:
1043
+ ```tsx
1044
+ // ✅ CORRECT (React/Vite): Use configUrl pointing to public/tidecloak.json
1045
+ <TideCloakContextProvider configUrl="/tidecloak.json">
1046
+ <App />
1047
+ </TideCloakContextProvider>
1048
+
1049
+ // ✅ CORRECT (Next.js): Use configUrl pointing to API route that serves data/tidecloak.json
1050
+ <TideCloakContextProvider configUrl="/api/config">
1051
+ <App />
1052
+ </TideCloakContextProvider>
1053
+ ```
1054
+
1055
+ **Note**: The SDK default `configUrl` is `/adapter.json`, not `/tidecloak.json`. Always set `configUrl` explicitly. For Next.js, config lives in `data/tidecloak.json` (server-side); serve it via an API route at `app/api/config/route.ts`.
1056
+
1057
+ **Related**: AP-13
1058
+
1059
+ ---
1060
+
1061
+ ## AP-28: Calling login() Before SDK Initialization
1062
+
1063
+ **What it looks like**:
1064
+ ```tsx
1065
+ // ❌ WRONG: Guards render during init, exposing login too early
1066
+ function App() {
1067
+ return (
1068
+ <TideCloakContextProvider configUrl="/tidecloak.json">
1069
+ <Authenticated><Dashboard /></Authenticated>
1070
+ <Unauthenticated><LoginPage /></Unauthenticated>
1071
+ </TideCloakContextProvider>
1072
+ );
1073
+ }
1074
+
1075
+ function LoginPage() {
1076
+ const { login } = useTideCloak();
1077
+ // login() called before SDK ready → "TideCloak client not initialized"
1078
+ return <button onClick={login}>Login</button>;
1079
+ }
1080
+ ```
1081
+
1082
+ **Why it's wrong**:
1083
+ - `<Unauthenticated>` renders during initialization (user is not yet authenticated)
1084
+ - Child components get exposed before SDK is ready
1085
+ - Calling `login()` before `initIAM()` completes throws an error
1086
+ - The SDK exposes `isInitializing` (not `initialized`) to gate this
1087
+
1088
+ **Consequence**: Runtime error. Login button crashes the app on click.
1089
+
1090
+ **Correct approach**:
1091
+ ```tsx
1092
+ // ✅ CORRECT: Block the tree until SDK is ready
1093
+ function AppContent() {
1094
+ const { isInitializing } = useTideCloak();
1095
+ if (isInitializing) return <p>Initializing TideCloak...</p>;
1096
+ return (
1097
+ <>
1098
+ <Authenticated><Dashboard /></Authenticated>
1099
+ <Unauthenticated><LoginPage /></Unauthenticated>
1100
+ </>
1101
+ );
1102
+ }
1103
+ ```
1104
+
1105
+ **Related**: AP-14, AP-15
1106
+
1107
+ ---
1108
+
1109
+ ## AP-29: Using hasRealmRole for tide-realm-admin
1110
+
1111
+ **What it looks like**:
1112
+ ```typescript
1113
+ // ❌ WRONG: Checking as realm role
1114
+ if (useTideCloak().hasRealmRole('tide-realm-admin')) {
1115
+ return <AdminPanel />;
1116
+ }
1117
+ // Always returns false; admin UI never shows
1118
+ ```
1119
+
1120
+ **Why it's wrong**:
1121
+ - `tide-realm-admin` is a client role on the `realm-management` client, not a realm role
1122
+ - `hasRealmRole()` checks `realm_access.roles` in the JWT
1123
+ - The role lives under `resource_access["realm-management"].roles`
1124
+ - Using the wrong check means admins never see admin UI
1125
+
1126
+ **Consequence**: Admin functionality invisible. Developers assume role assignment is broken.
1127
+
1128
+ **Correct approach**:
1129
+ ```typescript
1130
+ // ✅ CORRECT: Check as client role on realm-management
1131
+ if (useTideCloak().hasClientRole('tide-realm-admin', 'realm-management')) {
1132
+ return <AdminPanel />;
1133
+ }
1134
+ ```
1135
+
1136
+ **Related**: AP-02, AP-18
1137
+
1138
+ ---
1139
+
1140
+ ## AP-30: Using Named ESM Imports with @tidecloak/verify
1141
+
1142
+ **What it looks like**:
1143
+ ```typescript
1144
+ // ❌ WRONG: Named ESM import from CJS package
1145
+ import { verifyTideCloakToken } from "@tidecloak/verify";
1146
+ // Error: does not provide an export named 'verifyTideCloakToken'
1147
+ ```
1148
+
1149
+ **Why it's wrong**:
1150
+ - `@tidecloak/verify` is a CJS package, not ESM
1151
+ - Named imports fail because CJS modules export a single `module.exports` object
1152
+ - CJS interop varies by runtime (tsx, node, bundlers wrap `.default` differently)
1153
+
1154
+ **Consequence**: Import error at startup. Server-side JWT verification completely broken.
1155
+
1156
+ **Correct approach**:
1157
+ ```typescript
1158
+ // ✅ CORRECT: Default import with CJS interop
1159
+ import TideJWT from "@tidecloak/verify";
1160
+ const _mod = (TideJWT as any)?.default ?? TideJWT;
1161
+ const { verifyTideCloakToken } = _mod;
1162
+ ```
1163
+
1164
+ **Related**: AP-01
1165
+
1166
+ ---
1167
+
1168
+ ## AP-31: Bypassing Change Request Workflow
1169
+
1170
+ **What it looks like**:
1171
+ ```typescript
1172
+ // ❌ WRONG: Direct DB write for role assignment
1173
+ app.post('/api/admin/assign-role', async (req, res) => {
1174
+ await db.query('INSERT INTO user_role_mapping (user_id, role_id) VALUES ($1, $2)',
1175
+ [req.body.userId, req.body.roleId]);
1176
+ return res.json({ success: true });
1177
+ });
1178
+ ```
1179
+
1180
+ **Why it's wrong**:
1181
+ - All role/permission changes must go through IGA draft/approve/commit lifecycle
1182
+ - Direct DB writes do not produce VVK-signed authorization proofs
1183
+ - ORKs reject JWTs that do not match a signed proof
1184
+ - Bypasses quorum governance entirely
1185
+
1186
+ **Consequence**: Users get roles in DB but JWTs lack valid proofs. ORKs reject operations. Governance defeated.
1187
+
1188
+ **Correct approach**:
1189
+ ```typescript
1190
+ // ✅ CORRECT: Use IGA change request API
1191
+ const changeRequest = await adminClient.changeRequests.create({
1192
+ type: 'ROLE_ASSIGNMENT',
1193
+ userId: req.body.userId,
1194
+ roleId: req.body.roleId
1195
+ });
1196
+ // Change request goes through draft → approve (quorum) → commit
1197
+ ```
1198
+
1199
+ **Related**: AP-05, AP-10, Invariants I-09, I-10
1200
+
1201
+ ---
1202
+
1203
+ ## AP-32: Destructuring ApprovalType/ExecutionType from Models.Policy
1204
+
1205
+ **What it looks like**:
1206
+ ```typescript
1207
+ // ❌ WRONG: Destructure from Models.Policy (it's a class, not a namespace)
1208
+ const { ApprovalType, ExecutionType } = Models.Policy;
1209
+ // ApprovalType is undefined
1210
+ // TypeError: Cannot read properties of undefined (reading 'IMPLICIT')
1211
+ ```
1212
+
1213
+ **Why it's wrong**:
1214
+ - `Models.Policy` is the Policy class itself
1215
+ - `ApprovalType` and `ExecutionType` are direct exports from `Models`, not nested under `Policy`
1216
+ - Destructuring from the class yields `undefined` for both
1217
+
1218
+ **Consequence**: Runtime TypeError. Policy construction fails.
1219
+
1220
+ **Correct approach**:
1221
+ ```typescript
1222
+ // ✅ CORRECT: Destructure from Models directly
1223
+ const { Policy, ApprovalType, ExecutionType, BaseTideRequest } = Models;
1224
+ const policy = new Policy(
1225
+ ApprovalType.IMPLICIT,
1226
+ ExecutionType.SIGN,
1227
+ // ...
1228
+ );
1229
+ ```
1230
+
1231
+ **Related**: AP-11
1232
+
1233
+ ---
1234
+
1235
+ ## AP-33: Building PolicySignRequest Manually
1236
+
1237
+ **What it looks like**:
1238
+ ```typescript
1239
+ // ❌ WRONG: Manual construction with BaseTideRequest + TideMemory
1240
+ const request = new BaseTideRequest();
1241
+ request.setModelId("Policy:1");
1242
+ const memory = new TideMemory();
1243
+ memory.write(policy.toBytes());
1244
+ request.setPayload(memory.toBytes());
1245
+ ```
1246
+
1247
+ **Why it's wrong**:
1248
+ - The ORK's `PolicySignRequestFactory` expects a specific internal structure
1249
+ - Manual construction produces bytes the ORK cannot parse
1250
+ - `PolicySignRequest.New()` from `heimdall-tide` handles the correct serialization
1251
+
1252
+ **Consequence**: ORK rejects the request with parse errors. Policy signing fails.
1253
+
1254
+ **Correct approach**:
1255
+ ```typescript
1256
+ // ✅ CORRECT: Use the factory method
1257
+ import { PolicySignRequest } from "heimdall-tide";
1258
+ const signRequest = PolicySignRequest.New(policy);
1259
+ ```
1260
+
1261
+ **Related**: AP-11
1262
+
1263
+ ---
1264
+
1265
+ ## AP-34: Mounting Project Root as H2 Data Volume
1266
+
1267
+ **What it looks like**:
1268
+ ```yaml
1269
+ # ❌ WRONG: docker-compose.yml
1270
+ volumes:
1271
+ - .:/opt/keycloak/data/h2
1272
+ ```
1273
+
1274
+ **Why it's wrong**:
1275
+ - Docker writes H2 database files as a different user (UID 1000)
1276
+ - Mounting project root causes `AccessDeniedException`
1277
+ - Database files mixed with source code
1278
+ - Container cannot start or corrupts on write
1279
+
1280
+ **Consequence**: TideCloak container fails to start. `AccessDeniedException` on H2 database files.
1281
+
1282
+ **Correct approach**:
1283
+ ```yaml
1284
+ # ✅ CORRECT: Use a dedicated data subdirectory
1285
+ volumes:
1286
+ - ./data:/opt/keycloak/data/h2
1287
+ # Also ensure correct ownership:
1288
+ # mkdir -p ./data && sudo chown -R 1000:1000 ./data
1289
+ ```
1290
+
1291
+ **Related**: AP-07
1292
+
1293
+ ---
1294
+
1295
+ ## AP-35: Skipping or Reordering Initialization Sequence
1296
+
1297
+ **What it looks like**:
1298
+ ```bash
1299
+ # ❌ WRONG: Skip license activation, jump to IGA
1300
+ curl -X POST "$KC_URL/admin/realms" -d '{"realm":"myrealm","enabled":true}'
1301
+ curl -X POST "$KC_URL/admin/realms/myrealm/vendorResources/toggle-iga"
1302
+ # Missing: setUpTideRealm, approve/commit, user creation, invite link...
1303
+ ```
1304
+
1305
+ **Why it's wrong**:
1306
+ - The initialization sequence is strict: (1) create realm, (2) setUpTideRealm with email, (3) enable IGA, (4) approve/commit client change requests, (5) create admin user + assign role, (6) generate invite link, (7) approve/commit user change requests, (8) update CustomAdminUIDomain, (9) download adapter config
1307
+ - Skipping steps or reordering causes silent failures
1308
+ - `setUpTideRealm` activates the license and VRK; without it, nothing else works
1309
+
1310
+ **Consequence**: Silent failures. Missing VRK. IGA toggle fails. Invite links not generated. Adapter config incomplete.
1311
+
1312
+ **Correct approach**:
1313
+ ```bash
1314
+ # ✅ CORRECT: Follow exact sequence
1315
+ # 1. Create realm
1316
+ # 2. POST /vendorResources/setUpTideRealm (with email)
1317
+ # 3. POST /vendorResources/toggle-iga
1318
+ # 4. Approve + commit client change requests
1319
+ # 5. Create admin user + assign tide-realm-admin
1320
+ # 6. Generate invite link + wait for account linking
1321
+ # 7. Approve + commit user change requests
1322
+ # 8. Update CustomAdminUIDomain
1323
+ # 9. Download adapter config
1324
+ ```
1325
+
1326
+ **Related**: AP-13, AP-27
1327
+
1328
+ ---
1329
+
1330
+ ## AP-36: Omitting Tide Environment Variables from Docker
1331
+
1332
+ **What it looks like**:
1333
+ ```yaml
1334
+ # ❌ WRONG: Incomplete docker-compose.yml
1335
+ services:
1336
+ tidecloak:
1337
+ image: tideorg/tidecloak-dev:latest
1338
+ environment:
1339
+ KC_BOOTSTRAP_ADMIN_USERNAME: admin
1340
+ KC_BOOTSTRAP_ADMIN_PASSWORD: admin
1341
+ # Missing: SYSTEM_HOME_ORK, USER_HOME_ORK, THRESHOLD_T, THRESHOLD_N,
1342
+ # PAYER_PUBLIC, KC_HOSTNAME
1343
+ ```
1344
+
1345
+ **Why it's wrong**:
1346
+ - Without `SYSTEM_HOME_ORK` and `USER_HOME_ORK`, the Tide IdP provider does not initialize
1347
+ - Without `THRESHOLD_T` and `THRESHOLD_N`, threshold parameters are undefined
1348
+ - Required actions like `link-tide-account-action` never register
1349
+ - Init script fails with "Could not find configuration for Required Action"
1350
+
1351
+ **Consequence**: TideCloak starts but Tide features are non-functional. Init scripts fail. No invite links. No threshold operations.
1352
+
1353
+ **Correct approach**:
1354
+ ```yaml
1355
+ # ✅ CORRECT: All required environment variables
1356
+ services:
1357
+ tidecloak:
1358
+ image: tideorg/tidecloak-dev:latest
1359
+ environment:
1360
+ KC_BOOTSTRAP_ADMIN_USERNAME: admin
1361
+ KC_BOOTSTRAP_ADMIN_PASSWORD: admin
1362
+ KC_HOSTNAME: localhost
1363
+ SYSTEM_HOME_ORK: "https://orkeyedev01-g2.australiaeast.cloudapp.azure.com:443"
1364
+ USER_HOME_ORK: "https://orkeyedev01-g2.australiaeast.cloudapp.azure.com:443"
1365
+ THRESHOLD_T: "3"
1366
+ THRESHOLD_N: "5"
1367
+ PAYER_PUBLIC: "<your-payer-public-key>"
1368
+ ```
1369
+
1370
+ **Related**: AP-35, AP-08
1371
+
1372
+ ---
1373
+
1374
+ ## AP-37: Auto-Approving IGA Role Assignments from Backend
1375
+
1376
+ **What it looks like**:
1377
+ ```typescript
1378
+ // ❌ WRONG: Backend tries to complete approval programmatically
1379
+ app.post('/api/admin/quick-approve', async (req, res) => {
1380
+ const { changeRequestId } = req.body;
1381
+ await adminClient.changeRequests.approve(changeRequestId);
1382
+ await adminClient.changeRequests.commit(changeRequestId);
1383
+ return res.json({ success: true });
1384
+ });
1385
+ ```
1386
+
1387
+ **Why it's wrong**:
1388
+ - Role assignments that produce VVK-signed UserContexts require the approval enclave in the admin's browser
1389
+ - The backend receives `requiresApprovalPopup: true` and cannot complete the signing
1390
+ - VVK signing is a threshold operation that must involve the SWE
1391
+ - Programmatic approval bypasses the cryptographic ceremony
1392
+
1393
+ **Consequence**: Approval call returns `requiresApprovalPopup: true` but signing never completes. Change request stuck. If somehow bypassed, JWTs lack valid proofs.
1394
+
1395
+ **Correct approach**:
1396
+ ```typescript
1397
+ // ✅ CORRECT: Build frontend approval UI
1398
+ // Backend exposes pending change requests
1399
+ app.get('/api/admin/pending', async (req, res) => {
1400
+ const pending = await adminClient.changeRequests.list({ status: 'PENDING' });
1401
+ return res.json(pending);
1402
+ });
1403
+
1404
+ // Frontend presents approval UI; admin's browser enclave handles signing
1405
+ // Use batch endpoints for efficiency
1406
+ await fetch('/api/admin/sign/batch', {
1407
+ method: 'POST',
1408
+ headers: { 'Authorization': `Bearer ${adminToken}`, 'DPoP': dpopProof },
1409
+ body: JSON.stringify({ changeRequestIds })
1410
+ });
1411
+ // Approval popup appears in admin's browser → SWE performs threshold signing
1412
+ ```
1413
+
1414
+ **Related**: AP-05, AP-10, AP-31, Invariants I-09, I-10
1415
+
1416
+ ---
1417
+
1418
+ ## AP-38: Duplicating tidecloak.json Config into NEXT_PUBLIC Env Vars
1419
+
1420
+ **What it looks like**:
1421
+ ```bash
1422
+ # .env.local
1423
+ NEXT_PUBLIC_TIDECLOAK_URL=http://localhost:8080
1424
+ NEXT_PUBLIC_TIDECLOAK_REALM=my-app
1425
+ NEXT_PUBLIC_TIDECLOAK_CLIENT_ID=my-app-client
1426
+ NEXT_PUBLIC_TIDECLOAK_REDIRECT_URI=http://localhost:3000/auth/redirect
1427
+ ```
1428
+
1429
+ ```tsx
1430
+ // ❌ WRONG: Provider reads env vars instead of tidecloak.json
1431
+ <TideCloakContextProvider
1432
+ authServerUrl={process.env.NEXT_PUBLIC_TIDECLOAK_URL}
1433
+ realm={process.env.NEXT_PUBLIC_TIDECLOAK_REALM}
1434
+ clientId={process.env.NEXT_PUBLIC_TIDECLOAK_CLIENT_ID}
1435
+ redirectUri={process.env.NEXT_PUBLIC_TIDECLOAK_REDIRECT_URI}
1436
+ />
1437
+ ```
1438
+
1439
+ **Why it's wrong**:
1440
+ - `tidecloak.json` already contains `auth-server-url`, `realm`, `resource` (clientId), and all Tide-specific fields
1441
+ - Duplicating these into env vars creates two sources of truth
1442
+ - Env vars can drift from the actual adapter JSON exported from TideCloak
1443
+ - `NEXT_PUBLIC_*` env vars expose config in client-side bundles unnecessarily
1444
+
1445
+ **Consequence**: Config drift between env vars and adapter JSON. Debugging breaks because the two sources disagree. New developers add env vars instead of exporting the adapter.
1446
+
1447
+ **Correct approach**:
1448
+ ```tsx
1449
+ // ✅ CORRECT: Import tidecloak.json directly
1450
+ import tcConfig from '../../data/tidecloak.json';
1451
+
1452
+ <TideCloakProvider config={tcConfig}>
1453
+ {children}
1454
+ </TideCloakProvider>
1455
+ ```
1456
+
1457
+ For deployment without a file: use `CLIENT_ADAPTER` env var containing the full JSON. Do not split it into individual vars.
1458
+
1459
+ **Related**: I-05, I-13
1460
+
1461
+ ---
1462
+
1463
+ ## AP-39: Appending `start-dev` to TideCloak Docker Run
1464
+
1465
+ **What it looks like**:
1466
+ ```bash
1467
+ # ❌ WRONG: Appending start-dev command
1468
+ docker run -d --name tidecloak \
1469
+ -p 8080:8080 \
1470
+ -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
1471
+ -e KC_BOOTSTRAP_ADMIN_PASSWORD=password \
1472
+ tideorg/tidecloak-dev:latest start-dev
1473
+ ```
1474
+
1475
+ **Why it's wrong**:
1476
+ - TideCloak images are NOT vanilla Keycloak. The entrypoint is pre-configured.
1477
+ - Passing `start-dev` overrides the container's built-in entrypoint/command and breaks Tide-specific initialization.
1478
+ - This is a Keycloak convention (`kc.sh start-dev`) that does not apply to TideCloak Docker images.
1479
+
1480
+ **Consequence**: Container may start in wrong mode, fail to initialize Tide providers, or exit immediately.
1481
+
1482
+ **Correct approach**:
1483
+ ```bash
1484
+ # ✅ CORRECT: No command argument — image handles startup internally
1485
+ sudo docker run -d --name tidecloak \
1486
+ -v ./data:/opt/keycloak/data/h2 \
1487
+ -p 8080:8080 \
1488
+ -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
1489
+ -e KC_BOOTSTRAP_ADMIN_PASSWORD=password \
1490
+ tideorg/tidecloak-dev:latest
1491
+ ```
1492
+
1493
+ Do not append `start-dev`, `start`, or any other command to `docker run` for TideCloak images.
1494
+
1495
+ **Related**: AP-36
1496
+
1497
+ ---
1498
+
1499
+ ## AP-40: Using Wrong Docker Org for TideCloak Images
1500
+
1501
+ **What it looks like**:
1502
+ ```bash
1503
+ # ❌ WRONG: tidecloak/ org does not exist
1504
+ docker run -d tideorg/tidecloak-dev:latest # ← this is correct
1505
+ docker run -d tidecloak/tidecloak-dev:latest # ← WRONG org name
1506
+ docker run -d tidecloak/tidecloak-stg-dev:latest # ← WRONG org name
1507
+ ```
1508
+
1509
+ **Why it's wrong**:
1510
+ - TideCloak images are published under the `tideorg` Docker Hub organization, not `tidecloak`.
1511
+ - AI agents frequently hallucinate `tidecloak/tidecloak-dev` because the product is called TideCloak.
1512
+ - `docker pull tidecloak/tidecloak-dev` will fail with "repository does not exist".
1513
+
1514
+ **Consequence**: Image pull fails. Container never starts.
1515
+
1516
+ **Correct approach**:
1517
+ ```bash
1518
+ # ✅ CORRECT: tideorg/ org
1519
+ docker run -d tideorg/tidecloak-dev:latest
1520
+ docker run -d tideorg/tidecloak-stg-dev:latest
1521
+ ```
1522
+
1523
+ The org is `tideorg`, not `tidecloak`.
1524
+
1525
+ **Related**: AP-39
1526
+
1527
+ ---
1528
+
1529
+ ## Anti-Pattern Detection Checklist
1530
+
1531
+ ### Code Review
1532
+ - [ ] No local key generation for Tide operations (AP-03)
1533
+ - [ ] No client-side policy enforcement (AP-11)
1534
+ - [ ] No single-admin bypass paths (AP-05)
1535
+ - [ ] No offline E2EE fallbacks (AP-04)
1536
+ - [ ] No hardcoded 14/20 threshold (AP-08)
1537
+ - [ ] No cached DPoP headers (AP-12)
1538
+ - [ ] Server-side JWT verification on all protected APIs (AP-02)
1539
+ - [ ] Embedded JWKS used, not remote endpoint (AP-01)
1540
+ - [ ] No `createRemoteJWKSet` import or usage (AP-01)
1541
+ - [ ] No server-side password handling (AP-19)
1542
+ - [ ] JWT signature always verified, never just decoded (AP-20)
1543
+ - [ ] Dokens never stored server-side (AP-21)
1544
+ - [ ] No server-side decryption of E2EE data (AP-22)
1545
+ - [ ] Meaningful unique tags on every encrypted field (AP-23)
1546
+ - [ ] Self-encryption not used for shared data (AP-24)
1547
+ - [ ] `_tide_*` roles not used in Forseti ValidateExecutor (AP-25)
1548
+ - [ ] No non-deterministic calls in Forseti contracts (AP-26)
1549
+ - [ ] `hasClientRole` used for tide-realm-admin, not `hasRealmRole` (AP-29)
1550
+ - [ ] Default import used for @tidecloak/verify (AP-30)
1551
+ - [ ] No direct DB writes bypassing change requests (AP-31)
1552
+ - [ ] `ApprovalType`/`ExecutionType` destructured from `Models`, not `Models.Policy` (AP-32)
1553
+ - [ ] `PolicySignRequest.New()` used, not manual construction (AP-33)
1554
+ - [ ] No backend auto-approval of IGA role assignments (AP-37)
1555
+
1556
+ ### Deployment Check
1557
+ - [ ] CSP includes Tide domains (AP-07)
1558
+ - [ ] `silent-check-sso.html` exists (AP-15)
1559
+ - [ ] Adapter JSON has Tide extensions (AP-13)
1560
+ - [ ] DPoP verification implemented if enabled (AP-06)
1561
+ - [ ] `configUrl` prop used, not `configFilePath` (AP-27)
1562
+ - [ ] SDK initialization gate before login (AP-28)
1563
+ - [ ] No `NEXT_PUBLIC_TIDECLOAK_*` env vars duplicating tidecloak.json (AP-38)
1564
+ - [ ] Data volume is `./data`, not project root (AP-34)
1565
+ - [ ] Initialization sequence followed in correct order (AP-35)
1566
+ - [ ] All Tide environment variables present in Docker config (AP-36)
1567
+
1568
+ ### Documentation Review
1569
+ - [ ] Tide not described as generic OIDC (AP-01)
1570
+ - [ ] BYOiD not conflated with federated login (AP-09)
1571
+ - [ ] IGA not downplayed as procedural workflow (AP-10)
1572
+ - [ ] E2EE not described as client-side encryption (AP-16)
1573
+ - [ ] UI gating vs authorization clearly distinguished (AP-02)
1574
+
1575
+ ---
1576
+
1577
+ ## Severity Classification
1578
+
1579
+ | Anti-Pattern | Security Impact | Operational Impact | Detection Difficulty |
1580
+ |--------------|----------------|-------------------|---------------------|
1581
+ | AP-01 | **CRITICAL** | High | Medium |
1582
+ | AP-02 | **CRITICAL** | Low | Easy |
1583
+ | AP-03 | **CRITICAL** | High | Easy |
1584
+ | AP-04 | **HIGH** | Medium | Medium |
1585
+ | AP-05 | **CRITICAL** | Low | Medium |
1586
+ | AP-06 | **HIGH** | Low | Easy |
1587
+ | AP-07 | **HIGH** | **CRITICAL** | Easy |
1588
+ | AP-08 | Low | **HIGH** | Easy |
1589
+ | AP-09 | **HIGH** (misleading) | Medium | Hard |
1590
+ | AP-10 | **HIGH** (misleading) | Medium | Hard |
1591
+ | AP-11 | **CRITICAL** | Low | Medium |
1592
+ | AP-12 | Low | **HIGH** | Easy |
1593
+ | AP-13 | **HIGH** | **CRITICAL** | Easy |
1594
+ | AP-14 | Low | **HIGH** | Medium |
1595
+ | AP-15 | Low | **HIGH** | Easy |
1596
+ | AP-16 | **HIGH** (misleading) | Low | Hard |
1597
+ | AP-17 | **CRITICAL** | **CRITICAL** | Medium |
1598
+ | AP-18 | **HIGH** | Low | Medium |
1599
+ | AP-19 | **CRITICAL** | Low | Easy |
1600
+ | AP-20 | **CRITICAL** | Low | Easy |
1601
+ | AP-21 | Medium | Low | Easy |
1602
+ | AP-22 | **CRITICAL** | Low | Easy |
1603
+ | AP-23 | **HIGH** | Low | Medium |
1604
+ | AP-24 | **HIGH** | Medium | Hard |
1605
+ | AP-25 | **HIGH** | Low | Medium |
1606
+ | AP-26 | **HIGH** | **HIGH** | Medium |
1607
+ | AP-27 | Low | **CRITICAL** | Easy |
1608
+ | AP-28 | Low | **HIGH** | Easy |
1609
+ | AP-29 | Low | **HIGH** | Easy |
1610
+ | AP-30 | Low | **CRITICAL** | Easy |
1611
+ | AP-31 | **CRITICAL** | Medium | Medium |
1612
+ | AP-32 | Low | **HIGH** | Easy |
1613
+ | AP-33 | Low | **HIGH** | Medium |
1614
+ | AP-34 | Low | **CRITICAL** | Easy |
1615
+ | AP-35 | **HIGH** | **CRITICAL** | Medium |
1616
+ | AP-36 | Medium | **CRITICAL** | Easy |
1617
+ | AP-37 | **CRITICAL** | **HIGH** | Medium |
1618
+
1619
+ **Impact levels**:
1620
+ - **CRITICAL**: Defeats core Tide security properties or causes complete failure
1621
+ - **HIGH**: Creates exploitable weakness or major operational failure
1622
+ - **Medium**: Degrades security or reliability
1623
+ - **Low**: Minor issue or inconvenience
1624
+
1625
+ ---
1626
+
1627
+ ## Recovery from Anti-Patterns
1628
+
1629
+ ### If AP-01 detected (Generic OIDC treatment / remote JWKS)
1630
+ 1. Remove all `createRemoteJWKSet` imports and usage
1631
+ 2. Replace with `createLocalJWKSet(config.jwk)` using embedded JWKS from adapter JSON
1632
+ 3. Add guard: `if (!config.jwk) throw new Error('Missing jwk in tidecloak.json')`
1633
+ 4. Verify adapter has `jwk`, `vendorId`, `homeOrkUrl` fields
1634
+ 5. If `jwk` is missing, re-export adapter from TideCloak with IGA enabled — do not add remote fallback
1635
+ 6. Review all auth code for Tide-specific patterns
1636
+
1637
+ ### If AP-02 detected (Client-side authorization)
1638
+ 1. Add `verifyTideJWT()` to all protected API routes
1639
+ 2. Move role checks server-side
1640
+ 3. Keep client-side `hasRealmRole()` / `hasClientRole()` for UI gating only
1641
+
1642
+ ### If AP-03 detected (Local key generation)
1643
+ 1. Remove all local key generation code
1644
+ 2. Replace with SDK operations (`doEncrypt`, `executeSignRequest`)
1645
+ 3. Verify keys never assembled locally
1646
+
1647
+ ### If AP-05 detected (Single-admin bypass)
1648
+ 1. Remove bypass paths
1649
+ 2. Route all admin changes through IGA change-set API
1650
+ 3. Document that no emergency override exists
1651
+
1652
+ ### If AP-07 detected (Missing CSP)
1653
+ 1. Add CSP with Tide domain whitelist
1654
+ 2. Test SWE iframe loads successfully
1655
+ 3. Monitor browser console for CSP violations
1656
+
1657
+ ### If AP-13 detected (Wrong adapter format)
1658
+ 1. Re-export adapter via Tide-specific endpoint
1659
+ 2. Verify `jwk`, `vendorId`, `homeOrkUrl` present
1660
+ 3. Replace generic adapter in all environments
1661
+
1662
+ ### If AP-19 detected (Server-side password handling)
1663
+ 1. Remove all password fields from API request handlers
1664
+ 2. Ensure login is delegated entirely to TideCloak SDK
1665
+ 3. Backend must only verify JWTs, never receive credentials
1666
+
1667
+ ### If AP-22 detected (Server-side decryption)
1668
+ 1. Move all decryption to the browser via `iam.doDecrypt()`
1669
+ 2. Server stores and serves ciphertext only
1670
+ 3. Verify no plaintext exists in server logs or responses
1671
+
1672
+ ### If AP-31 detected (Direct DB writes)
1673
+ 1. Replace all direct DB role/permission writes with IGA change request API
1674
+ 2. Verify all changes go through draft/approve/commit lifecycle
1675
+ 3. Audit DB for rows not backed by VVK-signed proofs
1676
+
1677
+ ### If AP-35 detected (Wrong init sequence)
1678
+ 1. Reset the realm (delete and recreate)
1679
+ 2. Follow the exact 9-step initialization sequence
1680
+ 3. Verify each step completes before proceeding to the next
1681
+
1682
+ ### If AP-36 detected (Missing Docker env vars)
1683
+ 1. Add all required variables: `SYSTEM_HOME_ORK`, `USER_HOME_ORK`, `THRESHOLD_T`, `THRESHOLD_N`, `PAYER_PUBLIC`, `KC_HOSTNAME`
1684
+ 2. Restart the container
1685
+ 3. Verify `link-tide-account-action` is registered in Required Actions
1686
+
1687
+ ---
1688
+
1689
+ ## AP-41: Using Master Admin Credentials in Generated App Code
1690
+
1691
+ **What it looks like**:
1692
+ ```typescript
1693
+ // ❌ WRONG: API route fetches users with hardcoded master credentials
1694
+ const tokenRes = await fetch(`${tcUrl}/realms/master/protocol/openid-connect/token`, {
1695
+ method: 'POST',
1696
+ body: new URLSearchParams({
1697
+ grant_type: 'password',
1698
+ username: 'admin',
1699
+ password: 'password',
1700
+ client_id: 'admin-cli',
1701
+ }),
1702
+ });
1703
+ ```
1704
+
1705
+ **Why it's wrong**:
1706
+ - Master credentials (`admin`/`password`) are bootstrap-only. They should only appear in `scripts/init-tidecloak.sh`.
1707
+ - Hardcoding them in API routes leaks bootstrap secrets into the running app.
1708
+ - It bypasses the logged-in user's authorization scope — the app acts as a superadmin for every request.
1709
+ - It breaks when credentials change.
1710
+
1711
+ **Correct approach**: Forward the logged-in user's token to TideCloak admin APIs:
1712
+ ```typescript
1713
+ // ✅ CORRECT: Use the logged-in user's token
1714
+ const userToken = req.headers.get('authorization')?.replace('Bearer ', '');
1715
+ const res = await fetch(`${tcUrl}/admin/realms/${realm}/users`, {
1716
+ headers: { Authorization: `Bearer ${userToken}` },
1717
+ });
1718
+ ```
1719
+
1720
+ For client-side code, call same-origin API proxies instead of direct cross-origin TideCloak calls.
1721
+
1722
+ **Verification**: `grep -r "grant_type=password\|username=admin\|password=password" app/ src/ lib/ --include="*.ts" --include="*.tsx"` should return zero matches outside of `scripts/init*.sh`.
1723
+
1724
+ VERIFIED (learning-batch-003, L-03).
1725
+
1726
+ ---
1727
+
1728
+ ## AP-42: Missing @tidecloak/react ESM Webpack Alias
1729
+
1730
+ **What it looks like**: Login page shows a redirect loop. Provider fails silently. `useTideCloak()` returns undefined. No clear error message.
1731
+
1732
+ **Root cause**: `@tidecloak/react`'s CJS dist (`dist/cjs/index.js`) contains ESM `import` statements. When `@tidecloak/nextjs` does `require("@tidecloak/react")`, webpack follows the CJS path and hits ESM syntax.
1733
+
1734
+ **Correct approach**: Add webpack alias in `next.config.ts`:
1735
+ ```typescript
1736
+ import path from "path";
1737
+ // inside webpack config:
1738
+ config.resolve.alias = {
1739
+ ...config.resolve.alias,
1740
+ "@tidecloak/react": path.resolve(
1741
+ __dirname,
1742
+ "node_modules/@tidecloak/react/dist/esm/index.js"
1743
+ ),
1744
+ };
1745
+ ```
1746
+
1747
+ Use `path.resolve()`, NOT `require.resolve()` — the latter throws `ERR_PACKAGE_PATH_NOT_EXPORTED` because the package's `exports` field is misconfigured.
1748
+
1749
+ This is required in addition to the `strictExportPresence = false` fix. See `canon/framework-matrix.md` for the complete webpack config.
1750
+
1751
+ VERIFIED (learning-batch-003, L-04).
1752
+
1753
+ ---
1754
+
1755
+ ## AP-43: Re-Verifying DPoP Proofs on App API Routes When Using secureFetch
1756
+
1757
+ **What it looks like**:
1758
+ ```typescript
1759
+ // ❌ WRONG: Attempt RFC 9449 DPoP proof verification on secureFetch requests
1760
+ import { jwtVerify, importJWK } from 'jose';
1761
+
1762
+ const dpopHeader = req.headers.get('dpop');
1763
+ const { payload } = await jwtVerify(dpopHeader, publicKey); // Throws: Invalid Compact JWS
1764
+ ```
1765
+
1766
+ **Why it's wrong**:
1767
+ - `secureFetch` from `@tidecloak/js` sends DPoP proofs in a Tide-specific format, not standard RFC 9449 compact JWS
1768
+ - `jwtVerify()` rejects the non-standard format with `401: Invalid Compact JWS`
1769
+ - DPoP binding is already enforced at TideCloak during token issuance — the `cnf.jkt` claim proves binding
1770
+ - JWT signature verification via embedded JWKS (VVK) is the primary security layer
1771
+
1772
+ **Consequence**: All API requests via `secureFetch` return 401. Login works but no API calls succeed.
1773
+
1774
+ **Correct approach**:
1775
+ ```typescript
1776
+ // ✅ CORRECT: Verify JWT signature only. cnf.jkt proves DPoP binding at issuance.
1777
+ const jwt = await verifyTideJWT(token);
1778
+ // The presence of cnf.jkt confirms TideCloak bound this token with DPoP.
1779
+ // No need to re-verify the proof on the app side.
1780
+ ```
1781
+
1782
+ **When to use DPoP proof verification**: Only if your app sends manual DPoP proofs (not via `secureFetch`). If using `secureFetch`, skip DPoP proof verification entirely.
1783
+
1784
+ VERIFIED (LEARNINGS-batch-005, L-06).
1785
+
1786
+ **Related**: I-12, [verify-jwt-server-side.md](../playbooks/verify-jwt-server-side.md) Step 4
1787
+
1788
+ ---
1789
+
1790
+ ## AP-44: Using In-Memory Stores in Next.js API Routes
1791
+
1792
+ **What it looks like**:
1793
+ ```typescript
1794
+ // ❌ WRONG: Module-level Map in API route
1795
+ const vaultStore = new Map<string, any>();
1796
+
1797
+ export async function POST(req: NextRequest) {
1798
+ vaultStore.set(key, value); // Stored in memory
1799
+ }
1800
+
1801
+ export async function GET(req: NextRequest) {
1802
+ return Response.json(vaultStore.get(key)); // Empty after hot reload
1803
+ }
1804
+ ```
1805
+
1806
+ **Why it's wrong**:
1807
+ - Next.js App Router in dev mode reimports modules on every file change (hot reload)
1808
+ - Module-level variables reset on reimport — all in-memory state is lost
1809
+ - POST stores data, but the next GET returns empty because the Map was reset
1810
+ - This affects `Map`, `Array`, `Set`, or any module-level mutable state
1811
+
1812
+ **Consequence**: Data loss between requests during development. POST succeeds, GET returns 404/empty. Appears as intermittent bug.
1813
+
1814
+ **Correct approach**:
1815
+ ```typescript
1816
+ // ✅ CORRECT: Use filesystem for state that must survive between requests
1817
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
1818
+ import { join } from 'path';
1819
+
1820
+ const DATA_DIR = join(process.cwd(), 'data');
1821
+ mkdirSync(DATA_DIR, { recursive: true });
1822
+
1823
+ function readStore(): Record<string, any> {
1824
+ try {
1825
+ return JSON.parse(readFileSync(join(DATA_DIR, 'store.json'), 'utf-8'));
1826
+ } catch { return {}; }
1827
+ }
1828
+
1829
+ function writeStore(data: Record<string, any>): void {
1830
+ writeFileSync(join(DATA_DIR, 'store.json'), JSON.stringify(data, null, 2));
1831
+ }
1832
+ ```
1833
+
1834
+ For production, use a database. For demos and dev, filesystem is sufficient and survives hot reload.
1835
+
1836
+ VERIFIED (LEARNINGS-batch-005, L-08).
1837
+
1838
+ **Related**: None (general Next.js dev behavior, not Tide-specific)
1839
+
1840
+ ---
1841
+
1842
+ ## AP-45: Rejecting DPoP-Upgraded Authorization Headers on Server
1843
+
1844
+ **What it looks like**:
1845
+ ```typescript
1846
+ // ❌ WRONG: Only accepts Bearer scheme
1847
+ if (!authHeader.startsWith('Bearer ')) {
1848
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
1849
+ }
1850
+ const token = authHeader.substring(7);
1851
+ ```
1852
+
1853
+ **Why it's wrong**:
1854
+ - When `useDPoP` is enabled, `IAMService.secureFetch` upgrades `Authorization: Bearer <token>` to `Authorization: DPoP <token>`
1855
+ - Server middleware that only checks for `Bearer` silently rejects all DPoP-upgraded requests with 401
1856
+ - The client sees the request "succeed" (no thrown error) but the API returns 401, so data operations fail
1857
+
1858
+ **Consequence**: All API calls via `secureFetch` return 401 when DPoP is enabled. Data appears to save on the frontend but is silently rejected by the server.
1859
+
1860
+ **Correct approach**:
1861
+ ```typescript
1862
+ // ✅ CORRECT: Accept both Bearer and DPoP schemes
1863
+ let token: string;
1864
+ if (authHeader.startsWith('Bearer ')) {
1865
+ token = authHeader.substring(7);
1866
+ } else if (authHeader.startsWith('DPoP ')) {
1867
+ token = authHeader.substring(5);
1868
+ } else {
1869
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
1870
+ }
1871
+ ```
1872
+
1873
+ Or use `extractToken()` from the template which handles both schemes.
1874
+
1875
+ VERIFIED (LEARNINGS-session-003, L-01).
1876
+
1877
+ **Related**: I-12, AP-43
1878
+
1879
+ ---
1880
+
1881
+ ## AP-46: Eager Config Loading in Next.js Server Modules
1882
+
1883
+ **What it looks like**:
1884
+ ```typescript
1885
+ // ❌ WRONG: Module-level config loading
1886
+ const config = loadTideConfig();
1887
+ const JWKS = createLocalJWKSet(config.jwk);
1888
+ ```
1889
+
1890
+ **Why it's wrong**:
1891
+ - Next.js 16 evaluates module-level code during `next build` for static page generation
1892
+ - If `tidecloak.json` doesn't exist yet or has no `jwk` field, the build crashes
1893
+ - This happens before the app runs — the config may not be ready at build time
1894
+
1895
+ **Consequence**: `next build --webpack` fails with "Adapter JSON missing jwk field" even though the config will exist at runtime.
1896
+
1897
+ **Correct approach**:
1898
+ ```typescript
1899
+ // ✅ CORRECT: Lazy initialization on first use
1900
+ let _config: ReturnType<typeof loadTideConfig> | null = null;
1901
+ let _jwks: ReturnType<typeof createLocalJWKSet> | null = null;
1902
+
1903
+ function getConfig() {
1904
+ if (!_config) {
1905
+ _config = loadTideConfig();
1906
+ _jwks = createLocalJWKSet(_config.jwk);
1907
+ }
1908
+ return { config: _config, JWKS: _jwks! };
1909
+ }
1910
+ ```
1911
+
1912
+ VERIFIED (LEARNINGS-session-003, L-03).
1913
+
1914
+ **Related**: None (general Next.js build behavior)
1915
+
1916
+ ---
1917
+
1918
+ ## AP-47: Calling Admin API Without Required Permissions
1919
+
1920
+ TideCloak admin endpoints (`/admin/realms/{realm}/users`, etc.) require the calling user to have appropriate admin permissions — client roles on `realm-management` such as `tide-realm-admin`, `view-users`, `manage-users`, etc. If a user lacks the required permissions, the API returns 403. This is authorization working as intended. Ensure the user has the correct roles assigned before calling admin endpoints.
1921
+
1922
+ VERIFIED (LEARNINGS-batch-006, L-07).
1923
+
1924
+ **Related**: AP-41 (master admin credentials in app code)
1925
+
1926
+ ---
1927
+
1928
+ ## AP-48: Re-encoding doEncrypt Output as Bytes
1929
+
1930
+ **What it looks like**:
1931
+ ```typescript
1932
+ // ❌ WRONG: Treat doEncrypt result as bytes
1933
+ const encrypted = await IAMService.doEncrypt([{ data: plaintext, tags: [tag] }]);
1934
+ const bytes = new TextEncoder().encode(encrypted[0]);
1935
+ const stored = btoa(String.fromCharCode(...bytes));
1936
+ // Double-encoded → doDecrypt throws "RangeError: Index out of range"
1937
+ ```
1938
+
1939
+ **Why it's wrong**:
1940
+ - `doEncrypt` returns **base64 strings**, not `Uint8Array`
1941
+ - Re-encoding the string as bytes produces double-encoded ciphertext
1942
+ - `doDecrypt` cannot parse the result
1943
+
1944
+ **Consequence**: Encryption appears to succeed. Decryption fails with `RangeError: Index out of range`.
1945
+
1946
+ **Correct approach**:
1947
+ ```typescript
1948
+ // ✅ CORRECT: Store the string directly
1949
+ const encrypted = await IAMService.doEncrypt([{ data: plaintext, tags: [tag] }]);
1950
+ const ciphertext = String(encrypted[0]); // already base64
1951
+ // Store ciphertext as-is in database
1952
+
1953
+ // ✅ CORRECT: Decrypt by passing the stored string back
1954
+ const decrypted = await IAMService.doDecrypt([{ encrypted: ciphertext, tags: [tag] }]);
1955
+ ```
1956
+
1957
+ **Type check**: `typeof encrypted[0] === "string"` must be true. If it's not a string, something is wrong.
1958
+
1959
+ **`doDecrypt` return type caveat**: `IAMService.doDecrypt` may return `Uint8Array[]` instead of `string[]` for policy-governed decryption. The `useTideCloak()` convenience `doDecrypt` is typed as `string[]` but the underlying `IAMService` call may differ. Use defensive decoding:
1960
+ ```typescript
1961
+ const result = await IAMService.doDecrypt([{ encrypted: ciphertext, tags: [tag] }]);
1962
+ const plaintext = result[0] instanceof Uint8Array
1963
+ ? new TextDecoder().decode(result[0])
1964
+ : String(result[0]);
1965
+ ```
1966
+ VERIFIED (LEARNINGS-batch-006 L-08, LEARNINGS-batch-007 L-05).
1967
+
1968
+ **Related**: None
1969
+
1970
+ ---
1971
+
1972
+ ## AP-49: Replacing Existing Server Auth Instead of Layering Tide
1973
+
1974
+ **What it looks like**: Removing the existing app's authentication system (phone auth, email auth, server registration) entirely and replacing it with Tide login. All existing server APIs start returning 401 because the server expects its original credentials.
1975
+
1976
+ **Why it's wrong**: Many apps have server-side infrastructure (messaging, storage, notifications) tightly coupled to their original auth system. Removing it breaks the app's core functionality. Tide authentication gates access to the app and protects keys at rest — it doesn't replace server-side session management.
1977
+
1978
+ **Correct approach**: Layer Tide on top of the existing auth. Use a "gate" pattern:
1979
+ 1. App start → Tide login required (gate component wraps the app)
1980
+ 2. If Tide authenticated but no server registration → show original registration flow
1981
+ 3. If both authenticated → full app access
1982
+ 4. On subsequent launches → Tide auto-refreshes, server credentials persist → straight to app
1983
+
1984
+ Tide is an access gate, not a replacement for existing server auth. VERIFIED (TIDE_LEARNINGS-001 L-19).
1985
+
1986
+ **Related**: Playbook `add-auth-nextjs-existing`
1987
+
1988
+ ---
1989
+
1990
+ ## AP-50: Passing Uint8Array to doEncrypt/doDecrypt Instead of Array-of-Objects
1991
+
1992
+ **What it looks like**:
1993
+ ```typescript
1994
+ // ❌ WRONG: Raw data, not wrapped in array-of-objects
1995
+ const result = await doEncrypt(myBytes);
1996
+ // Throws: "Pass array as parameter" or "All entries must be an object"
1997
+ ```
1998
+
1999
+ **Why it's wrong**: `doEncrypt` and `doDecrypt` expect `Array<{data, tags}>` and `Array<{encrypted, tags}>` respectively. Raw data or plain arrays throw.
2000
+
2001
+ **Correct approach**:
2002
+ ```typescript
2003
+ // ✅ CORRECT: Array of objects with data and tags
2004
+ const encrypted = await doEncrypt([{ data: myString, tags: ['mytag'] }]);
2005
+ const ciphertext = String(encrypted[0]); // base64 string
2006
+
2007
+ const decrypted = await doDecrypt([{ encrypted: ciphertext, tags: ['mytag'] }]);
2008
+ ```
2009
+
2010
+ **String-in/string-out pattern**: Always pass data as strings (base64 for binary). String input → base64 encrypted string output. Avoids Uint8Array serialization issues during storage. For binary data:
2011
+ ```typescript
2012
+ const base64 = btoa(String.fromCharCode(...myBytes));
2013
+ const encrypted = await doEncrypt([{ data: base64, tags: ['keys'] }]);
2014
+ // Store encrypted[0] as string
2015
+ ```
2016
+
2017
+ The tag must match realm roles: tag `'keys'` requires `_tide_keys.selfencrypt` and `_tide_keys.selfdecrypt`.
2018
+
2019
+ VERIFIED (TIDE_LEARNINGS-001 L-15, L-23).
2020
+
2021
+ **Related**: AP-48
2022
+
2023
+ ---
2024
+
2025
+ ## AP-51: Calling IAMService.getInstance()
2026
+
2027
+ `IAMService` from `@tidecloak/js` is a pre-instantiated singleton. The import IS the instance. Do not call `IAMService.getInstance()` — it does not exist and throws `is not a function`. Use `IAMService` directly. VERIFIED (ripple-learnings L-06).
2028
+
2029
+ ---
2030
+
2031
+ ## AP-52: Passing Destructured Config to initIAM
2032
+
2033
+ Do not pass `{ url, realm, clientId }` to `initIAM()`. The SDK reads `config["auth-server-url"]`, `config.realm`, `config.resource`, `config.vendorId`, `config.homeOrkUrl`, and `config["client-origin-auth-" + origin]`. Pass the full adapter JSON object from `tidecloak.json`. VERIFIED (ripple-learnings L-07).
2034
+
2035
+ ---
2036
+
2037
+ ## AP-53: Passing JSON Object to createTideRequest
2038
+
2039
+ `createTideRequest(encoded)` takes a single `Uint8Array` — a pre-encoded `BaseTideRequest`. Do not pass `{ contract, modelName, authFlow, ... }`. Build the request with `new BaseTideRequest(name, version, authFlow, draft)`, call `request.encode()`, then pass the result. VERIFIED (ripple-learnings L-08).
2040
+
2041
+ ---
2042
+
2043
+ ## AP-54: Policy Params as Plain Object
2044
+
2045
+ `new Policy({ params: { key: value } })` throws "object is not iterable". The `PolicyParameters` constructor calls `new Map(data)` which needs `[key, value]` pairs. Use: `params: [['Role', 'myRole'], ['Resource', 'myResource']]`. VERIFIED (ripple-learnings L-11).
2046
+
2047
+ ---
2048
+
2049
+ ## AP-55: Storing Raw Signature Instead of policy.toBytes()
2050
+
2051
+ After `executeSignRequest`, the result is the VVK signature array. Do not store this directly as the "signed policy." Set `policy.signature` to the signature bytes, then call `policy.toBytes()` to get the serialized policy with signature attached. The ORK expects the full serialized Policy, not just the signature. VERIFIED (ripple-learnings L-16).
2052
+
2053
+ ---
2054
+
2055
+ ## AP-56: Wrong Forseti Contract Namespace
2056
+
2057
+ Do not use `using Tide.Ork.Classes.Forseti;` or `using Tide.Forseti;` — these namespaces do not exist. The correct namespace is `using Ork.Forseti.Sdk;`. The contract class must be named `Contract` and implement `IAccessPolicy`. Method signatures use context objects: `ValidateData(DataContext ctx)`, `ValidateApprovers(ApproversContext ctx)`, `ValidateExecutor(ExecutorContext ctx)`. Use `PolicyDecision.Allow()` not `PolicyDecision.Approve()`. VERIFIED (ripple-learnings L-13).
2058
+
2059
+ ---
2060
+
2061
+ ## AP-57: Storing request.encode() Instead of policy.toBytes() for Signing
2062
+
2063
+ Do not store the output of `PolicySignRequest.encode()` or `BaseTideRequest.encode()` as the policy bytes for later signing requests. `encode()` includes the full request envelope (authorization data, signatures, expiry) plus the policy. The ORK's `addPolicy()` expects only the serialized `Policy` object from `policy.toBytes()`.
2064
+
2065
+ **Error when wrong**: `System.ArgumentOutOfRangeException: Index out of range` at `Ork.Shared.Models.Contracts.Policy.From(ReadOnlyMemory data)` during ORK PreSign.
2066
+
2067
+ **Correct**:
2068
+ ```typescript
2069
+ // Store raw policy bytes (after VVK signature is attached)
2070
+ policy.signature = vvkSignatureBytes;
2071
+ const signedPolicyBytes = policy.toBytes(); // Store THIS
2072
+ ```
2073
+
2074
+ **Wrong**:
2075
+ ```typescript
2076
+ const encoded = initializedRequest.encode(); // Includes auth envelope — DO NOT store as policy
2077
+ ```
2078
+
2079
+ Related: AP-55 (storing raw signature instead of policy.toBytes()). AP-57 is the inverse error — storing too much instead of too little. VERIFIED (LEARNINGS-ratidefy-batch-001 L-13).
2080
+
2081
+ ---
2082
+
2083
+ ## AP-58: Using Static IAMService for secureFetch/getToken in React/Next.js Apps
2084
+
2085
+ Do not import `IAMService` from `@tidecloak/js` and call `IAMService.getToken()` or `IAMService.secureFetch()` when using `@tidecloak/nextjs`'s `TideCloakContextProvider`. The static `IAMService` class is not initialized by the React provider — its token and auth state are null.
2086
+
2087
+ **Error when wrong**: 403 Forbidden on all TideCloak admin API calls, or null/invalid tokens.
2088
+
2089
+ **Correct**:
2090
+ ```typescript
2091
+ const { secureFetch, getToken } = useTideCloak();
2092
+ // Pass to utility functions that need authenticated fetch
2093
+ initAdmin(secureFetch, getToken);
2094
+ ```
2095
+
2096
+ **Wrong**:
2097
+ ```typescript
2098
+ import { IAMService } from '@tidecloak/js';
2099
+ const token = await IAMService.getToken(); // Returns null — not connected to React provider
2100
+ ```
2101
+
2102
+ **Exception**: `(IAMService as any)._tc` works for ORK signing operations (doken, `createTideRequest`, `executeSignRequest`) because the React provider does initialize the internal `_tc` instance. But `IAMService.getToken()` and `IAMService.secureFetch()` do not route through the provider. VERIFIED (LEARNINGS-ratidefy-batch-001 L-20).
2103
+
2104
+ ---
2105
+
2106
+ ## AP-59: Assuming initializeTideRequest Mutates In Place
2107
+
2108
+ The `initializeTideRequest` function from the `useTideCloak()` React context returns a **new initialized object**. It does NOT mutate the original request. Calling `encode()` or `getUniqueId()` on the original object after initialization fails.
2109
+
2110
+ **Error when wrong**: `Must initialize request to generate unique id` or `encode()` returns uninitialized data.
2111
+
2112
+ **Correct**:
2113
+ ```typescript
2114
+ const initialized = await initializeTideRequest(policyRequest);
2115
+ const bytes = initialized.encode(); // Use returned object
2116
+ const id = initialized.getUniqueId();
2117
+ ```
2118
+
2119
+ **Wrong**:
2120
+ ```typescript
2121
+ await initializeTideRequest(policyRequest);
2122
+ const bytes = policyRequest.encode(); // FAILS — original not initialized
2123
+ ```
2124
+
2125
+ VERIFIED (LEARNINGS-ratidefy-batch-001 L-25).
2126
+
2127
+ ---
2128
+
2129
+ ## Status Legend
2130
+
2131
+ - **VERIFIED** - Directly sourced from documentation or keylessh exemplar
2132
+ - **INFERRED** - Strongly implied by source material
2133
+ - **REQUIRES_RUNTIME_VALIDATION** - Single-app evidence; needs confirmation