@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,637 @@
1
+ # Tide Feature Mapping
2
+
3
+ Maps Tide SDK features to their actual security properties and implementation requirements.
4
+
5
+ **Critical Rule**: UI gating is NOT authorization. Protected routes are NOT protected APIs. Clearly distinguish client-side convenience from server-side enforcement.
6
+
7
+ ---
8
+
9
+ ## Authentication Features
10
+
11
+ ### Login / SSO / Logout
12
+
13
+ **SDK surface**:
14
+ - `doLogin()`, `doLogout()` (Vanilla JS)
15
+ - `useTideCloak().login()`, `useTideCloak().logout()` (React/Next.js)
16
+
17
+ **What it looks like**: Standard OIDC login flow with redirect to TideCloak.
18
+
19
+ **What is actually Tide-specific**:
20
+ - Password verification is threshold (PRISM across T+ ORKs) **VERIFIED**
21
+ - No password hash stored anywhere **VERIFIED**
22
+ - Each ORK verifies password challenge independently **VERIFIED**
23
+ - Attacker compromising TideCloak server cannot learn passwords **VERIFIED**
24
+
25
+ **What is generic OIDC**:
26
+ - Redirect flow (authorization code grant)
27
+ - Token endpoint calls
28
+ - Cookie/session management
29
+ - Silent SSO via iframe
30
+
31
+ **Agent implication**:
32
+ - Standard SDK methods work; do not implement custom password flows
33
+ - Login redirect, token exchange, refresh are OIDC standard
34
+ - Threshold verification is transparent to SDK consumer
35
+ - **Do not present login UX as the differentiator**; backend threshold verification is the actual Tide feature
36
+
37
+ **Verification**:
38
+ ```javascript
39
+ // Login succeeds
40
+ await iam.doLogin({ redirectUri: window.location.origin });
41
+ // Check token is present
42
+ const token = await iam.getToken();
43
+ assert(token !== null);
44
+ ```
45
+
46
+ **Common confusion**: Login UI looks like any OIDC provider. The difference is server-side: TideCloak delegates password verification to Fabric, not database check.
47
+
48
+ ---
49
+
50
+ ### Silent SSO Token Refresh
51
+
52
+ **SDK surface**:
53
+ - `silent-check-sso.html` in `public/`
54
+ - Automatic refresh via SDK
55
+
56
+ **What it looks like**: Standard OIDC silent refresh via hidden iframe.
57
+
58
+ **What is Tide-specific**: None. This is standard Keycloak silent SSO.
59
+
60
+ **Agent implication**:
61
+ - Required file: `public/silent-check-sso.html` **VERIFIED**
62
+ - Corrupted state detection pattern from keylessh **VERIFIED** (keylessh `AuthContext.tsx`):
63
+ ```javascript
64
+ // Check for stale auth state on startup
65
+ const storageKeys = Object.keys(localStorage);
66
+ const hasIamData = storageKeys.some(k => k.includes('iam') || k.includes('auth'));
67
+ if (hasIamData && !initSuccess) {
68
+ // Offer reset via ?reset=true
69
+ window.location.href = '/?reset=true';
70
+ }
71
+ ```
72
+
73
+ **Failure modes**:
74
+ - CSP blocks iframe: silent refresh fails, requires full re-login
75
+ - Stale localStorage: IAM init hangs, 10s timeout triggers error
76
+ - Network timeout: falls back to full redirect
77
+
78
+ ---
79
+
80
+ ### Account Linking (Admin Onboarding)
81
+
82
+ **What it is**: New admin users must link their Tide account to activate threshold features. **VERIFIED** (keylessh `init-tidecloak.sh`)
83
+
84
+ **Workflow**:
85
+ 1. Create user in TideCloak admin console
86
+ 2. Generate linking URL via `POST /admin/realms/{realm}/users/{user-id}/tideAdminResources/get-required-action-link` with `lifespan=43200` (12 hours) **VERIFIED**
87
+ 3. User opens link, completes linking in browser
88
+ 4. Poll user attributes for `tideUserKey` and `vuid`
89
+ 5. When both present, linking complete
90
+
91
+ **Agent implication**:
92
+ - Required action: `link-tide-account-action` **VERIFIED** (keylessh `realm.json`)
93
+ - Linking URL is time-limited (default 12 hours)
94
+ - Whether mandatory for non-admin users is unclear **PARTIALLY_RESOLVED** (GAP-030)
95
+
96
+ **Verification**:
97
+ ```bash
98
+ # Poll for completion
99
+ USER_KEY=$(curl -s ... /admin/realms/myrealm/users/$USER_ID | jq -r '.attributes.tideUserKey[0]')
100
+ if [ -n "$USER_KEY" ]; then echo "Linked"; fi
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Server-Side Delegation
106
+
107
+ ### Delegation Token Exchange
108
+
109
+ **SDK surface**:
110
+ - `TideDelegation` class from `@tidecloak/server`
111
+ - `createTideFetch` from `@tidecloak/js`
112
+ - `requireDelegation()` and `handleDelegation()` Express middleware
113
+ - `req.delegation.fetch()` helper for admin API calls
114
+
115
+ **What it does**: Lets your server call TideCloak admin APIs on behalf of authenticated users without using master admin credentials.
116
+
117
+ **What is actually Tide-specific**:
118
+ - Two-hop chain of trust: browser DPoP key -> delegation request -> server ephemeral key
119
+ - DelegationToken:1 signing model validated by ORK network
120
+ - DPoP approval via ORK enclave (session key signs server's JKT)
121
+ - Threshold EdDSA signing of delegation token
122
+
123
+ **What is standard OAuth2**:
124
+ - Token exchange (RFC 8693) with `act` claim
125
+ - DPoP (RFC 9449) binding for proof-of-possession
126
+
127
+ **Agent implication**:
128
+ - Use `@tidecloak/server` for server-side admin calls. Do not use master admin credentials.
129
+ - `createTideFetch` handles the 419 interrupt pattern transparently on the browser side
130
+ - Delegation tokens are short-lived (max 600s), cached per user session
131
+ - Scoped delegation via `requested_roles` can restrict roles in the delegation token
132
+
133
+ **Verification**:
134
+ ```javascript
135
+ // Server: admin route with delegation
136
+ app.get('/api/admin/users',
137
+ authenticate, requireAdmin,
138
+ getDelegation().requireDelegation(),
139
+ async (req, res) => {
140
+ const users = await req.delegation.fetch(adminUrl + '/users');
141
+ res.json(users);
142
+ }
143
+ );
144
+
145
+ // Browser: transparent delegation
146
+ const appFetch = createTideFetch(window.fetch);
147
+ const users = await appFetch('/api/admin/users');
148
+ ```
149
+
150
+ **Full reference**: See `canon/delegation.md` and `playbooks/setup-server-delegation.md`.
151
+
152
+ ---
153
+
154
+ ## Authorization Features
155
+
156
+ ### Role-Based Route Protection (Client-Side)
157
+
158
+ **SDK surface**:
159
+ - `useTideCloak().hasRealmRole(roleName)` — check realm roles
160
+ - `useTideCloak().hasClientRole(roleName, clientId?)` — check client roles
161
+ - `iam.hasRealmRole(roleName)` / `iam.hasClientRole(roleName, clientId?)`
162
+
163
+ The SDK hook does not export a generic `hasRole()`. Use `hasRealmRole` for realm roles and `hasClientRole` for client roles. `tide-realm-admin` is a client role on `realm-management` — use `hasClientRole('tide-realm-admin', 'realm-management')`, not `hasRealmRole`.
164
+
165
+ **What it looks like**: Protected routes that check user roles before rendering components.
166
+
167
+ **What is actually happening**: UI gating only. This is client-side convenience, NOT server-side authorization.
168
+
169
+ **Security property**: **NONE**. An attacker can bypass by modifying client code or replaying tokens.
170
+
171
+ **Agent implication**:
172
+ - **DO NOT** rely on `hasRealmRole()` / `hasClientRole()` for API authorization
173
+ - **DO** use them for UI/UX decisions (show/hide buttons, render different components)
174
+ - **DO** implement server-side role verification for protected APIs (see JWT Verification below)
175
+
176
+ **Example**:
177
+ ```javascript
178
+ // CLIENT-SIDE (UI gating only)
179
+ if (useTideCloak().hasRealmRole('admin')) {
180
+ return <AdminPanel />; // Attacker can bypass
181
+ }
182
+
183
+ // SERVER-SIDE (real authorization)
184
+ app.get('/api/admin/users', (req, res) => {
185
+ const jwt = verifyJWT(req.headers.authorization); // Verify signature
186
+ if (!jwt.realm_access.roles.includes('admin')) {
187
+ return res.status(403).json({ error: 'Forbidden' });
188
+ }
189
+ // Proceed with admin operation
190
+ });
191
+ ```
192
+
193
+ **Common confusion**: SDK docs show role checks for route protection without emphasizing this is UI-only. Do not treat UI gating as authorization.
194
+
195
+ ---
196
+
197
+ ### JWT Verification (Server-Side Authorization)
198
+
199
+ **What it is**: Real authorization. Verifies threshold-signed JWT on server-side before granting API access.
200
+
201
+ **Tide-specific properties**:
202
+ - JWT signed by VVK threshold protocol (T+ ORKs) **VERIFIED**
203
+ - Signature verified against embedded JWKS from adapter JSON (local-only, no remote fallback) **VERIFIED** (keylessh `tideJWT.ts`). If `jwk` is missing, this is a setup failure — re-export adapter with IGA enabled.
204
+ - Each VVK ORK independently verified claims before partial-signing **VERIFIED**
205
+
206
+ **Server-side verification pattern** **VERIFIED** (keylessh `server/lib/auth/tideJWT.ts`):
207
+ ```typescript
208
+ import { createLocalJWKSet, jwtVerify } from 'jose';
209
+ import { loadTideConfig } from './tidecloakConfig';
210
+
211
+ const config = loadTideConfig();
212
+ // Use embedded JWKS only (I-04). Do not use createRemoteJWKSet.
213
+ const JWKS = createLocalJWKSet(config.jwk);
214
+
215
+ async function verifyTideJWT(token: string) {
216
+ const { payload } = await jwtVerify(token, JWKS, {
217
+ issuer: config['auth-server-url'].replace(/\/+$/, '') + '/realms/' + config.realm,
218
+ });
219
+
220
+ // TideCloak uses azp (not aud) for client ID
221
+ if (payload.azp !== config.resource) throw new Error('Token azp mismatch');
222
+
223
+ const now = Math.floor(Date.now() / 1000);
224
+ if (payload.exp < now) throw new Error('Token expired');
225
+ if (payload.iat > now + 60) throw new Error('Token issued in future');
226
+
227
+ return payload;
228
+ }
229
+ ```
230
+
231
+ **Agent implication**:
232
+ - **DO NOT** fetch JWKS from `{realm}/protocol/openid-connect/certs`; use adapter JSON
233
+ - **DO** verify `iss` (issuer), `azp` (authorized party), `exp`, `iat`
234
+ - **DO** check roles in `realm_access.roles` or `resource_access` after signature verification
235
+ - Adapter JSON field: `jwk: { keys: JWK[] }` (only present when IGA enabled)
236
+
237
+ **Verification**:
238
+ ```bash
239
+ # Extract public key from adapter JSON
240
+ jq '.jwk.keys[0]' data/tidecloak.json
241
+ # Verify JWT signature against this key
242
+ ```
243
+
244
+ **Common confusion**: SDK docs mention `@tidecloak/verify` package but do not emphasize embedded JWKS usage. Authentication API docs omit this entirely.
245
+
246
+ ---
247
+
248
+ ### DPoP Token Binding (API Security)
249
+
250
+ **What it is**: RFC 9449 standard. Binds access tokens to client key pair. Server must verify proof on every request. **VERIFIED** (keylessh `server/auth.ts`)
251
+
252
+ **Tide-specific configuration** **VERIFIED** (vendor confirmation, GAP-032 resolved):
253
+ ```typescript
254
+ // ES256 is the default and recommended for new projects
255
+ useDPoP: { mode: 'strict', alg: 'ES256' }
256
+ // EdDSA also supported (with automatic browser fallback)
257
+ useDPoP: { mode: 'strict', alg: 'EdDSA' }
258
+ ```
259
+
260
+ **Client-side** (automatic via SDK):
261
+ - SDK generates ephemeral key pair
262
+ - Each request includes `DPoP` header with fresh proof
263
+ - Proof signs HTTP method + URL + timestamp + nonce
264
+ - Algorithm negotiated via server's `dpop_signing_alg_values_supported` OIDC metadata
265
+
266
+ **Server-side verification pattern** **VERIFIED** (keylessh `server/auth.ts`):
267
+ 1. Extract `DPoP` header
268
+ 2. Verify JWT: `typ: "dpop+jwt"`, `alg` matches expected (ES256 or EdDSA)
269
+ 3. Verify signature against `jwk` in proof
270
+ 4. Check `htm` matches HTTP method
271
+ 5. Check `htu` matches request URL (ignore query string)
272
+ 6. Check `iat` freshness (120s window)
273
+ 7. Check `jti` not replayed (2-min TTL in-memory cache)
274
+ 8. Extract access token, verify `cnf.jkt` matches proof thumbprint
275
+
276
+ **Agent implication**:
277
+ - DPoP is per-request; do not cache headers
278
+ - Server must maintain `jti` replay cache with TTL
279
+ - DPoP is required for Tide's full security guarantees **VERIFIED** (vendor confirmation, GAP-032 resolved)
280
+
281
+ **Verification**:
282
+ ```bash
283
+ # Request with DPoP
284
+ curl -H "Authorization: DPoP $TOKEN" -H "DPoP: $PROOF" https://api.example.com/protected
285
+ # Server verifies proof before granting access
286
+ ```
287
+
288
+ **Common confusion**: DPoP is NOT bearer token. Replaying `Authorization` header without fresh `DPoP` proof fails.
289
+
290
+ ---
291
+
292
+ ## E2EE Features
293
+
294
+ ### Choosing the Encryption Model
295
+
296
+ **Decision tree** — answer before writing any encryption code:
297
+
298
+ | Question | If yes | If no |
299
+ |----------|--------|-------|
300
+ | Does only the encrypting user need to decrypt? | **Self-encryption**. Use `doEncrypt`/`doDecrypt` from `useTideCloak()`. Roles: `_tide_{tag}.selfencrypt`/`.selfdecrypt`. | Continue below. |
301
+ | Do multiple users need to decrypt the same ciphertext? | **Policy-governed VVK encryption**. Use `IAMService.doEncrypt(data, signedPolicyBytes)`. Requires Forseti contract + signed policy + voucher gates + contract role. See playbook `setup-forseti-e2ee`. | Self-encryption is sufficient. |
302
+
303
+ **These are different SDK call paths, different bootstrap flows, and different security models.** Switching from self-encryption to policy-governed encryption is not a role rename. It requires:
304
+ 1. A Forseti contract deployed to the ORK network
305
+ 2. A Policy object constructed and signed via the 5-step flow
306
+ 3. Admin policy attached before commit
307
+ 4. Signed policy bytes stored server-side
308
+ 5. `IAMService.doEncrypt(data, policyBytes)` instead of `doEncrypt(data, tags)`
309
+
310
+ **If self-encryption fails** (e.g., "User has not been given any access"), fix the self-encryption setup:
311
+ - Check `_tide_{tag}.selfencrypt`/`.selfdecrypt` roles exist and are assigned
312
+ - Check user has re-logged in after role assignment
313
+ - Check IGA change requests are approved and committed
314
+ - Do NOT rename roles to `_tide_{tag}.encrypt`/`.decrypt` — this does not fix anything (AP-26)
315
+
316
+ ### Encrypt / Decrypt (Self-Encryption)
317
+
318
+ **SDK surface**:
319
+ - `doEncrypt(tag, plaintext)` → ciphertext
320
+ - `doDecrypt(tag, ciphertext)` → plaintext
321
+
322
+ **What it looks like**: Standard E2EE with application-side key management.
323
+
324
+ **What is actually Tide-specific**:
325
+ - Session key encrypted via CVK threshold across ORKs **VERIFIED**
326
+ - Decryption requires live Fabric threshold participation **VERIFIED**
327
+ - Plaintext never exists on server, admin console, or any ORK **VERIFIED**
328
+ - Enforcement is cryptographic: Fabric won't decrypt without role proof in JWT **VERIFIED**
329
+
330
+ **Tag-based role enforcement** **VERIFIED** (e2ee.md):
331
+ - Role pattern: `_tide_<tag>.selfencrypt`, `_tide_<tag>.selfdecrypt`
332
+ - Admin creates roles in TideCloak console
333
+ - Tag names are fully application-defined, no reserved prefixes or values. Case-sensitive, passed to ORK as-is (UTF-8, no normalization), max ~237 chars for self-encryption. **VERIFIED** (vendor confirmation, batch-02 Q-10, A-26 resolved). For policy-based encryption, tags are opaque data passed to the Forseti contract — no `_tide_` roles needed.
334
+ - User must have matching role for encrypt/decrypt to succeed
335
+ - Role check is embedded in JWT; Fabric verifies before threshold-decrypting session key
336
+
337
+ **Agent implication**:
338
+ - Both `Uint8Array` (binary) and string inputs supported **VERIFIED** (vendor confirmation, GAP-013 resolved)
339
+ - No application-level max payload size. Payloads <32 bytes use ElGamal; >=32 bytes use hybrid AES-256-GCM + ElGamal. Practical ceiling ~28.6 MB (Kestrel default). **VERIFIED** (vendor confirmation, GAP-013 resolved)
340
+ - Ciphertext is a TideMemory-serialized envelope. Base64 string when input was string, raw `Uint8Array` when input was `Uint8Array`. Overhead ~157B / ~217B before base64. Versioned (v1), stable. **VERIFIED** (vendor confirmation, batch-02 Q-05, A-28 resolved)
341
+ - Requires online Fabric access; no offline decryption **VERIFIED**
342
+ - Both `selfencrypt` AND `selfdecrypt` roles needed to round-trip **INFERRED** (A-27)
343
+
344
+ **Workflow**:
345
+ 1. Admin creates roles: `_tide_ssn.selfencrypt`, `_tide_ssn.selfdecrypt`
346
+ 2. Admin assigns roles to users via IGA approval
347
+ 3. User calls `doEncrypt('ssn', '123-45-6789')` → ciphertext stored in app DB
348
+ 4. User calls `doDecrypt('ssn', ciphertext)` → Fabric checks JWT for `_tide_ssn.selfdecrypt` role → threshold-decrypts session key → plaintext returned
349
+
350
+ **Verification**:
351
+ ```javascript
352
+ const ciphertext = await iam.doEncrypt('test', 'secret data');
353
+ assert(ciphertext !== 'secret data');
354
+ const plaintext = await iam.doDecrypt('test', ciphertext);
355
+ assert(plaintext === 'secret data');
356
+ ```
357
+
358
+ **Common confusion**: E2EE is NOT client-side encryption with server key storage. Decryption requires live Fabric threshold. Do not implement offline decrypt fallback.
359
+
360
+ **Common confusion**: Renaming `_tide_{tag}.selfencrypt` to `_tide_{tag}.encrypt` does NOT switch from self-encryption to shared encryption. The role suffix is a naming convention. The SDK call path (`doEncrypt` vs `IAMService.doEncrypt(data, policyBytes)`) determines the encryption model. See AP-26 in `canon/anti-patterns.md`.
361
+
362
+ ---
363
+
364
+ ## Governance Features
365
+
366
+ ### IGA Quorum Approval
367
+
368
+ **What it is**: Admin changes require multi-admin approval sealed by VVK threshold signatures. **VERIFIED** (IGA.mdx, SetupIGA.md, keylessh `init-tidecloak.sh`)
369
+
370
+ **Change-set workflow** **VERIFIED** (CHANGE_REQUEST_API.md, all exemplar init scripts):
371
+ 1. **Create**: Admin action triggers draft creation automatically (DRAFT/PENDING)
372
+ 2. **Approve**: `POST /admin/realms/{realm}/tide-admin/change-set/sign/batch`
373
+ 3. **Enclave** (MultiAdmin mode): decode `changeSetDraftRequests`, present to enclave, submit via `POST .../tideAdminResources/add-review`
374
+ 4. **Commit**: `POST /admin/realms/{realm}/tide-admin/change-set/commit/batch`
375
+
376
+ **Quorum formula**: `max(1, floor(TotalAdmins * 0.7))` **VERIFIED** (SetupIGA.md). Note: this is the IGA admin quorum, distinct from the SSH policy `threshold` (per-role signing approval count).
377
+
378
+ **What is Tide-specific**:
379
+ - Approval is cryptographic, not procedural **VERIFIED**
380
+ - VVK ORKs seal approved changes with threshold signatures (Authorization Proofing) **VERIFIED**
381
+ - VVK ORKs verify future JWT claims against these proofs **VERIFIED**
382
+ - No single admin can bypass **VERIFIED**
383
+ - Two signing modes: FirstAdmin (VRK, no popup) vs MultiAdmin (enclave challenge) **VERIFIED** (CHANGE_REQUEST_API.md)
384
+
385
+ **What is generic workflow**:
386
+ - Change request tracking
387
+ - Approval UI
388
+ - Audit trail
389
+
390
+ **Agent implication**:
391
+ - Enable IGA via `POST /admin/realms/{realm}/tide-admin/toggle-iga` **VERIFIED** (all exemplar init scripts)
392
+ - Canonical ordering: license → IGA → E2EE (only valid sequence) **VERIFIED** (vendor confirmation, batch-02 Q-04, A-21/A-22 resolved)
393
+ - After commit, token refresh required for roles to appear in doken (up to 120s delay) **VERIFIED** (test-cases F3)
394
+ - Change requests have a 1-month (2628000s) cryptographic expiry enforced by the Tide enclave. No automatic DB cleanup — stale drafts persist until cancelled. Not configurable. **VERIFIED** (vendor confirmation, batch-02 Q-08, A-25 resolved)
395
+ - **Draft-triggering scope** **VERIFIED** (vendor confirmation, GAP-041 resolved): When IGA is enabled on a non-master realm, 18 specific admin API actions create change-set drafts. Not all mutating endpoints create drafts. Operations fall into two categories: DRAFT (requires quorum sign/commit) and ACTIVE (auto-approved, audit-only). Role creation gets ACTIVE status — no sign/commit needed. Role assignment to users gets DRAFT status — sign/commit required.
396
+
397
+ **Verification**:
398
+ ```bash
399
+ # List pending user change-sets
400
+ curl "${TIDECLOAK_URL}/admin/realms/${REALM}/tide-admin/change-set/users/requests" \
401
+ -H "Authorization: Bearer ${ADMIN_TOKEN}" | jq '.[].draftRecordId'
402
+ # Sign (approve)
403
+ curl -X POST "${TIDECLOAK_URL}/admin/realms/${REALM}/tide-admin/change-set/sign/batch" \
404
+ -H "Authorization: Bearer ${ADMIN_TOKEN}" \
405
+ -H "Content-Type: application/json" \
406
+ -d '{"changeSets":[{"changeSetId":"...","changeSetType":"USER_ROLE","actionType":"CREATE"}]}'
407
+ # Commit
408
+ curl -X POST "${TIDECLOAK_URL}/admin/realms/${REALM}/tide-admin/change-set/commit/batch" \
409
+ -H "Authorization: Bearer ${ADMIN_TOKEN}" \
410
+ -H "Content-Type: application/json" \
411
+ -d '{"changeSets":[{"changeSetId":"...","changeSetType":"USER_ROLE","actionType":"CREATE"}]}'
412
+ ```
413
+
414
+ **Common confusion**: IGA docs read as "approval workflow" without emphasizing threshold signatures. This is NOT procedural approval; it is cryptographic enforcement.
415
+
416
+ ---
417
+
418
+ ### Forseti Policy Enforcement
419
+
420
+ **What it is**: Programmable C# contracts executed in every ORK sandbox. Majority of ORKs must approve for operation to proceed. **VERIFIED** (Forseti.mdx, tier2-protocol-forseti.mdx, keylessh `sshPolicy.ts`)
421
+
422
+ **SDK surface**:
423
+ ```typescript
424
+ const signRequest = await tc.createTideRequest({
425
+ contract: "using System; ...", // C# code
426
+ modelName: "BasicCustom<APP>:BasicCustom<1>",
427
+ authFlow: "Policy:1",
428
+ authorizer: tc.doken,
429
+ challengeData: { ... }
430
+ });
431
+ const result = await tc.executeSignRequest(signRequest);
432
+ ```
433
+
434
+ **What is Tide-specific**:
435
+ - Real C# contract execution in every ORK **VERIFIED**
436
+ - Sandbox enforces namespace restrictions **VERIFIED** (Forseti.mdx)
437
+ - No single ORK can bypass; majority enforcement **VERIFIED**
438
+ - Compromised ORK cannot override policy **VERIFIED**
439
+
440
+ **What is generic**:
441
+ - Contract compilation and packaging (application-side)
442
+
443
+ **Agent implication**:
444
+ - Sandbox: five-layer security model with namespace block-list, gas metering (default 50,000 gas) **VERIFIED** (vendor confirmation, GAP-008 resolved)
445
+ - Contract debugging: `PolicyDecision.Deny("message")` propagates to client. Internal ORK errors still opaque (GAP-018).
446
+ - Doken accessed via `tc.doken` **VERIFIED** (keylessh `tideSsh.ts`)
447
+ - Doken `exp` copied from SSO access token; auto-refreshed with token refresh **VERIFIED** (vendor confirmation, GAP-017 resolved)
448
+
449
+ **Verification**:
450
+ ```javascript
451
+ const result = await tc.executeSignRequest(signRequest);
452
+ assert(result.signature !== null); // ORKs approved and signed
453
+ ```
454
+
455
+ **Common confusion**: Forseti is NOT authorization middleware config. Real compiled C# executes in distributed ORK sandboxes.
456
+
457
+ ---
458
+
459
+ ## Infrastructure Features
460
+
461
+ ### Adapter JSON Configuration
462
+
463
+ **What it is**: Config file with Tide extensions for embedded JWKS, vendor ID, home ORK endpoint. **VERIFIED** (keylessh `tidecloakConfig.ts`)
464
+
465
+ **Tide-specific fields**:
466
+ - `jwk: { keys: JWK[] }` - Local JWT verification **VERIFIED**
467
+ - `vendorId: string` - Tide vendor identifier **VERIFIED**
468
+ - `homeOrkUrl: string` - Home ORK endpoint **VERIFIED**
469
+
470
+ **Generic Keycloak fields**:
471
+ - `realm`, `auth-server-url`, `ssl-required`, `resource`, `public-client`, `confidential-port`
472
+
473
+ **Export endpoint** **VERIFIED** (vendor confirmation, GAP-044 resolved):
474
+ ```
475
+ GET /admin/realms/{realm}/clients/{client-id}/vendorResources/get-installations-provider?providerId=keycloak-oidc-keycloak-json
476
+ ```
477
+
478
+ There is a single provider ID: `keycloak-oidc-keycloak-json`. The string `tidecloak-oidc-keycloak-json` does not exist in the codebase. TideCloak enriches the adapter JSON via this vendor endpoint by post-processing the standard provider output to inject Tide-specific fields.
479
+
480
+ **Loading locations** **VERIFIED** (keylessh configs):
481
+ - File: `data/tidecloak.json`
482
+ - Env var: `CLIENT_ADAPTER` (JSON string)
483
+ - Env var: `TIDECLOAK_CONFIG_B64` (base64)
484
+
485
+ **Agent implication**:
486
+ - `jwk` field is only present when IGA is enabled on the realm. Validate at startup.
487
+ - Do not use generic Keycloak adapter parsers; Tide extensions required
488
+
489
+ **Verification**:
490
+ ```bash
491
+ jq '.jwk, .vendorId, .homeOrkUrl' data/tidecloak.json
492
+ # All three fields must be present
493
+ ```
494
+
495
+ ---
496
+
497
+ ### CSP for SWE Iframe
498
+
499
+ **What it is**: Content Security Policy whitelist for SWE iframe to function. **VERIFIED** (keylessh `server/index.ts`)
500
+
501
+ **Required CSP directive** **VERIFIED** (vendor confirmation, GAP-028 resolved):
502
+ ```
503
+ frame-src 'self' *
504
+ ```
505
+
506
+ `frame-src '*'` is required because users can re-home their SWE session to any ORK. No fixed domain list.
507
+
508
+ **Failure symptom**: SWE iframe silently fails; login hangs, E2EE operations timeout, no visible errors (check browser console for CSP violations).
509
+
510
+ **Verification**:
511
+ ```bash
512
+ # Check response headers
513
+ curl -I https://app.example.com | grep -i content-security-policy
514
+ # Verify frame-src includes Tide domains
515
+ ```
516
+
517
+ ---
518
+
519
+ ### Realm Initialization
520
+
521
+ **What it is**: Automated realm setup with Tide defaults. **VERIFIED** (keylessh `init-tidecloak.sh`, `realm.json`)
522
+
523
+ **Setup endpoint** **VERIFIED** (vendor confirmation, GAP-029 strengthened):
524
+ ```
525
+ POST /admin/realms/{realm}/vendorResources/setUpTideRealm
526
+ Content-Type: application/x-www-form-urlencoded
527
+ Params: email (string, required), isRagnarokEnabled (boolean, optional, defaults true)
528
+ Response: 200 with raw licensing JSON as text/plain
529
+ ```
530
+ The endpoint creates the Tide IDP, `tide-vendor-key` component, provisions a free-tier license via the ORK network, and generates the initial VVK/VRK keys. Exactly two parameters — no hidden or undocumented ones. Sending JSON body causes `"Could not create tide realm (email is null)"`.
531
+
532
+ **Tide realm defaults** **VERIFIED** (keylessh `realm.json`):
533
+ - Default roles: `_tide_enabled`, `offline_access`, `uma_authorization`
534
+ - Browser flow: `tidebrowser` (replaces standard Keycloak flow)
535
+ - Token lifetimes: `accessTokenLifespan: 600`, `ssoSessionIdleTimeout: 1800`, `ssoSessionMaxLifespan: 36000`
536
+ - Protocol mappers: `tideUserKey`→`tideuserkey`, `vuid`→`vuid`, Tide IGA Role Mapper
537
+ - Required action: `link-tide-account-action`
538
+
539
+ **Agent implication**:
540
+ - Protocol mappers are auto-created on client creation **VERIFIED** (vendor confirmation, GAP-036 resolved)
541
+ - `tidebrowser` flow: confirmed across 4 exemplar realm.json files **VERIFIED**
542
+ - Token lifetimes: `accessTokenLifespan: 600` confirmed across all exemplar realm.json files **VERIFIED**
543
+ - Declare `_tide_enabled` in realm.json template — `setUpTideRealm` does not create it **VERIFIED** (vendor confirmation, GAP-031 resolved)
544
+
545
+ **Verification**:
546
+ ```bash
547
+ # Check realm configuration
548
+ curl .../admin/realms/myrealm | jq '.browserFlow, .accessTokenLifespan'
549
+ ```
550
+
551
+ ---
552
+
553
+ ## Feature Comparison: Generic vs Tide-Specific
554
+
555
+ | Feature | Generic OIDC | Tide-Specific | Enforcement Location |
556
+ |---------|-------------|---------------|---------------------|
557
+ | Login redirect flow | ✓ | - | Client + TideCloak |
558
+ | Password verification | ✓ (DB hash) | ✓ (threshold PRISM) | Fabric ORKs |
559
+ | JWT signing | ✓ (server key) | ✓ (threshold VVK) | Fabric ORKs |
560
+ | JWT verification | ✓ (remote JWKS) | ✓ (embedded JWKS, local-only) | Application server |
561
+ | Token refresh | ✓ | - | Client + TideCloak |
562
+ | Role-based UI gating | ✓ | - | Client (NOT real authorization) |
563
+ | Role-based API authorization | ✓ | ✓ (threshold-verified roles) | Application server + Fabric |
564
+ | E2EE | - | ✓ (threshold decrypt) | Fabric ORKs |
565
+ | Admin approval workflow | ✓ (procedural) | ✓ (threshold signatures) | Fabric ORKs |
566
+ | Policy enforcement | - | ✓ (Forseti contracts) | Fabric ORKs |
567
+ | DPoP token binding | ✓ (RFC 9449) | ✓ (ES256 default, EdDSA supported) | Application server |
568
+
569
+ ---
570
+
571
+ ## Protected Routes vs Protected APIs
572
+
573
+ ### Protected Routes (Client-Side)
574
+
575
+ **What they are**: UI components that check auth state before rendering.
576
+
577
+ **Security property**: UI convenience only. Attacker can bypass by modifying client code.
578
+
579
+ **Example**:
580
+ ```javascript
581
+ // Next.js middleware (runs client-side or edge)
582
+ if (!session || !session.roles.includes('admin')) {
583
+ return NextResponse.redirect('/login');
584
+ }
585
+ ```
586
+
587
+ **Agent implication**: Use for UX decisions, NOT security enforcement.
588
+
589
+ ---
590
+
591
+ ### Protected APIs (Server-Side)
592
+
593
+ **What they are**: API endpoints that verify JWT signature and claims before granting access.
594
+
595
+ **Security property**: Real authorization. Threshold-signed JWT cannot be forged.
596
+
597
+ **Example**:
598
+ ```javascript
599
+ app.get('/api/admin/users', async (req, res) => {
600
+ try {
601
+ const jwt = await verifyTideJWT(extractToken(req));
602
+ if (!jwt.realm_access.roles.includes('admin')) {
603
+ return res.status(403).json({ error: 'Forbidden' });
604
+ }
605
+ // Proceed with admin operation
606
+ } catch (err) {
607
+ return res.status(401).json({ error: 'Unauthorized' });
608
+ }
609
+ });
610
+ ```
611
+
612
+ **Agent implication**: Always verify JWT on server-side for protected operations.
613
+
614
+ ---
615
+
616
+ ## UI Gating vs Real Authorization
617
+
618
+ | Aspect | UI Gating | Real Authorization |
619
+ |--------|-----------|-------------------|
620
+ | **Location** | Client-side | Server-side |
621
+ | **Purpose** | UX decisions (show/hide buttons) | Security enforcement |
622
+ | **Bypass risk** | High (modify client code) | Low (threshold signatures) |
623
+ | **Tide involvement** | None (standard React patterns) | Yes (threshold JWT verification) |
624
+ | **Example** | `if (hasRealmRole('admin')) <AdminButton />` | `verifyJWT(token) && hasRole(jwt, 'admin')` |
625
+
626
+ **Critical rule**: Never rely on UI gating for security. Always verify on server-side.
627
+
628
+ ---
629
+
630
+ ## Status Legend
631
+
632
+ - **VERIFIED** - Directly sourced from documentation or keylessh exemplar
633
+ - **INFERRED** - Strongly implied by source material
634
+ - **ASSUMED** - Operator guidance where sources are silent
635
+ - **REQUIRES_RUNTIME_VALIDATION** - Single-app evidence; needs confirmation
636
+ - **STILL_UNRESOLVED** - Open gap
637
+ - **PARTIALLY_RESOLVED** - Partial evidence; gaps remain