@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,1125 @@
1
+ # Tide Framework Implementation Matrix
2
+
3
+ Framework-specific implementation guidance. Organized by priority: Next.js → React → Vanilla JS.
4
+
5
+ **Rule**: Focus on reusable patterns from keylessh exemplar (LIKELY_REUSABLE_PATTERN), not app-specific SSH logic.
6
+
7
+ ---
8
+
9
+ ## Next.js Implementation
10
+
11
+ ### Prerequisites
12
+
13
+ **Verified requirements** (tidecloak-nextjs-tutorial-quickstart.md):
14
+ - Next.js 13+ (App Router or Pages Router)
15
+ - Node.js 18+
16
+ - TideCloak instance running
17
+ - IGA enabled on realm **VERIFIED** (vendor confirmation, batch-02 Q-04). Canonical ordering: license → IGA → E2EE.
18
+
19
+ ### Package Installation
20
+
21
+ ```bash
22
+ npm install @tidecloak/nextjs
23
+ ```
24
+
25
+ For Forseti policy-governed encryption or threshold signing, also install:
26
+ ```bash
27
+ npm install @tideorg/js heimdall-tide asgard-tide
28
+ ```
29
+
30
+ ### SDK Package Layer Map
31
+
32
+ The Tide SDK is split across multiple packages. Each exports different classes. Importing from the wrong package causes undefined exports or runtime errors.
33
+
34
+ | Package | Key exports | Used for |
35
+ |---------|------------|----------|
36
+ | `@tidecloak/js` | `IAMService`, `TideCloak`, `Tools` (re-exports `TideMemory`), `Models` (re-exports `BaseTideRequest`, `Policy`, `ApprovalType`, `ExecutionType`, `Doken`) | Auth, admin API, context, core primitives |
37
+ | `@tidecloak/nextjs` | `TideCloakContextProvider`, `useTideCloak` | Next.js provider and hooks. Does NOT export `Models`, `PolicySignRequest`, or signing classes |
38
+ | `@tidecloak/react` | `TideCloakProvider`, `useTideCloak` | React provider (prefer `@tidecloak/nextjs` for Next.js) |
39
+ | `@tideorg/js` | `BaseTideRequest`, `Policy`, `TideMemory`, `Doken` | Core primitives (prefer accessing via `@tidecloak/js` re-exports to avoid ESM issues) |
40
+ | `asgard-tide` | `BasicCustomRequest`, `DynamicPayloadCustomRequest` | Building ORK signing requests. NOT in `@tidecloak/js` or `@tideorg/js` |
41
+ | `heimdall-tide` | `PolicySignRequest`, `RequestEnclave`, `ApprovalEnclave` | Policy deployment, enclave UI |
42
+
43
+ **Critical import rules**:
44
+ - `BasicCustomRequest` comes from `asgard-tide` only. Not from `@tideorg/js` or `@tidecloak/js`. VERIFIED (LEARNINGS-ratidefy-batch-001 L-11).
45
+ - `PolicySignRequest` comes from `heimdall-tide`. VERIFIED (LEARNINGS-ratidefy-batch-001 L-12).
46
+ - `Models` from `@tidecloak/nextjs` returns `undefined` at runtime. Import from `@tidecloak/js` or `@tideorg/js`.
47
+ - `secureFetch` and `getToken` must come from the `useTideCloak()` hook, NOT from static `IAMService` import. The static class is not connected to the React provider's auth state. Exception: `(IAMService as any)._tc` works for ORK signing operations. VERIFIED (LEARNINGS-ratidefy-batch-001 L-20).
48
+
49
+ See [SDK Internals](concepts.md#sdk-internals) for architecture details.
50
+
51
+ ### Webpack Workarounds (Required)
52
+
53
+ Two webpack fixes are required for `@tidecloak/*` packages in Next.js:
54
+
55
+ 1. **`strictExportPresence: false`** — `@tidecloak/js` has incomplete re-exports from `heimdall-tide`. Without this, webpack errors on missing re-exports. Note: the property is `config.module.strictExportPresence` (boolean), NOT `reexportExportsPresence` (which is not a valid webpack 5 property). VERIFIED (learning-batch-004, L-04).
56
+
57
+ 2. **`@tidecloak/react` ESM alias** — `@tidecloak/react`'s CJS dist (`dist/cjs/index.js`) contains ESM `import` syntax. When `@tidecloak/nextjs` does `require("@tidecloak/react")`, webpack follows the CJS path and fails. Force resolution to the ESM dist. Use `path.resolve()` (not `require.resolve()` which throws `ERR_PACKAGE_PATH_NOT_EXPORTED`). Without this, the login page shows a redirect loop — the provider fails silently and auth state never initializes. VERIFIED (learning-batch-003, L-04).
58
+
59
+ ```typescript
60
+ import path from "path";
61
+ import type { NextConfig } from "next";
62
+
63
+ const nextConfig: NextConfig = {
64
+ webpack: (config) => {
65
+ // Fix 1: @tidecloak/js incomplete re-exports — suppress strict export checking
66
+ config.module.strictExportPresence = false;
67
+
68
+ // Fix 2: @tidecloak/react CJS dist contains ESM syntax
69
+ config.resolve.alias = {
70
+ ...config.resolve.alias,
71
+ "@tidecloak/react": path.resolve(
72
+ __dirname,
73
+ "node_modules/@tidecloak/react/dist/esm/index.js"
74
+ ),
75
+ };
76
+
77
+ return config;
78
+ },
79
+ };
80
+ ```
81
+
82
+ **Next.js 16+**: Turbopack is the default bundler. Since this webpack config is required for `@tidecloak/*`, use `next dev --webpack` in `package.json` scripts:
83
+
84
+ ```json
85
+ "scripts": {
86
+ "dev": "next dev --webpack"
87
+ }
88
+ ```
89
+
90
+ Without `--webpack`, Next.js 16 errors with "This build is using Turbopack, with a `webpack` config and no `turbopack` config."
91
+
92
+ **VERIFIED** (forseti-crypto-quickstart `next.config.ts`, Next.js 16.2.1 runtime error)
93
+
94
+ ### Scaffold Command
95
+
96
+ ```bash
97
+ npm init @tidecloak/nextjs@latest
98
+ ```
99
+
100
+ **RESOLVED_BY_VENDOR** (GAP-005): Scaffolds Next.js app with interactive prompts for realm/client creation, IGA setup, admin user linking. Use manual setup if scaffold behavior is unclear.
101
+
102
+ ---
103
+
104
+ ### Client-Side Setup (App Router)
105
+
106
+ **Provider setup**:
107
+
108
+ There are two provider patterns depending on how the adapter JSON is loaded:
109
+
110
+ **Pattern A: Import config directly (recommended for Next.js)**:
111
+ ```typescript
112
+ // app/providers.tsx
113
+ 'use client';
114
+
115
+ import { TideCloakProvider } from '@tidecloak/nextjs';
116
+ import tcConfig from '../../data/tidecloak.json';
117
+
118
+ export function Providers({ children }: { children: React.ReactNode }) {
119
+ return (
120
+ <TideCloakProvider config={tcConfig}>
121
+ {children}
122
+ </TideCloakProvider>
123
+ );
124
+ }
125
+ ```
126
+
127
+ **Pattern B: Fetch config from URL** (when `tidecloak.json` is in `public/`):
128
+ ```typescript
129
+ // app/providers.tsx
130
+ 'use client';
131
+
132
+ import { TideCloakContextProvider } from '@tidecloak/nextjs';
133
+
134
+ export function Providers({ children }: { children: React.ReactNode }) {
135
+ return (
136
+ <TideCloakContextProvider configUrl="/tidecloak.json">
137
+ {children}
138
+ </TideCloakContextProvider>
139
+ );
140
+ }
141
+ ```
142
+
143
+ **Critical**: `TideCloakContextProvider` uses the `configUrl` prop, NOT `configFilePath`. The `useTideCloak()` hook returns flat properties (`authenticated`, `login`, `logout`, `secureFetch`, `getToken`, `hasRealmRole`, etc.) — NOT a `tc` wrapper object. VERIFIED (LEARNINGS-ratidefy-batch-001 L-19).
144
+ ```
145
+
146
+ ```typescript
147
+ // app/layout.tsx
148
+ import { Providers } from './providers';
149
+
150
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
151
+ return (
152
+ <html lang="en">
153
+ <body>
154
+ <Providers>{children}</Providers>
155
+ </body>
156
+ </html>
157
+ );
158
+ }
159
+ ```
160
+
161
+ The provider reads all config (auth-server-url, realm, resource/clientId, redirect URIs) from `data/tidecloak.json`. Do not duplicate these values into `NEXT_PUBLIC_TIDECLOAK_*` env vars.
162
+
163
+ **DPoP configuration** **VERIFIED** (vendor confirmation, GAP-032 resolved):
164
+ ```typescript
165
+ <TideCloakContextProvider
166
+ useDPoP={{ mode: 'strict', alg: 'ES256' }}
167
+ // ... other props
168
+ >
169
+ ```
170
+
171
+ ES256 is the default and recommended algorithm. EdDSA also supported. DPoP is required for Tide's full security guarantees.
172
+
173
+ ---
174
+
175
+ ### Client-Side Auth State Access
176
+
177
+ ```typescript
178
+ // app/components/UserInfo.tsx
179
+ 'use client';
180
+ import { useTideCloak } from '@tidecloak/nextjs';
181
+
182
+ export function UserInfo() {
183
+ const { authenticated, getValueFromIdToken, login, logout, hasRealmRole } = useTideCloak();
184
+
185
+ if (!authenticated) {
186
+ return <button onClick={login}>Login</button>;
187
+ }
188
+
189
+ return (
190
+ <div>
191
+ <p>Welcome {getValueFromIdToken('preferred_username')}</p>
192
+ {hasRealmRole('admin') && <AdminButton />} {/* UI gating only */}
193
+ <button onClick={logout}>Logout</button>
194
+ </div>
195
+ );
196
+ }
197
+ ```
198
+
199
+ **Critical**: `hasRealmRole()` and `hasClientRole()` are UI gating, NOT real authorization. The SDK hook exports `hasRealmRole(role)` for realm roles and `hasClientRole(role, client?)` for client roles. There is no generic `hasRole()` on the hook. See [Protected Routes vs Protected APIs](feature-mapping.md#protected-routes-vs-protected-apis).
200
+
201
+ ---
202
+
203
+ ### Server-Side JWT Verification (App Router)
204
+
205
+ **Pattern** (tidecloak-nextjs-reference.md, keylessh `server/lib/auth/tideJWT.ts`):
206
+
207
+ ```typescript
208
+ // app/api/admin/users/route.ts
209
+ import { NextRequest, NextResponse } from 'next/server';
210
+ import { verifyTideJWT } from '@/lib/auth/tideJWT';
211
+
212
+ export async function GET(req: NextRequest) {
213
+ const authHeader = req.headers.get('authorization');
214
+ if (!authHeader) {
215
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
216
+ }
217
+
218
+ try {
219
+ const jwt = await verifyTideJWT(authHeader.replace('Bearer ', ''));
220
+
221
+ // Check role
222
+ if (!jwt.realm_access?.roles?.includes('admin')) {
223
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
224
+ }
225
+
226
+ // Proceed with admin operation
227
+ return NextResponse.json({ users: [...] });
228
+ } catch (err) {
229
+ return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
230
+ }
231
+ }
232
+ ```
233
+
234
+ **`verifyTideJWT()` implementation** **VERIFIED** (keylessh `server/lib/auth/tideJWT.ts`):
235
+
236
+ ```typescript
237
+ // lib/auth/tideJWT.ts
238
+ import { jwtVerify, createLocalJWKSet } from 'jose';
239
+ import type { JWTPayload } from 'jose';
240
+ import { loadTideConfig } from './tidecloakConfig';
241
+
242
+ const config = loadTideConfig();
243
+ if (!config.jwk) throw new Error('Missing jwk in tidecloak.json. Re-export adapter with IGA enabled.');
244
+ const JWKS = createLocalJWKSet(config.jwk); // Local only. Do not use createRemoteJWKSet.
245
+
246
+ export async function verifyTideJWT(token: string): Promise<JWTPayload> {
247
+ const { payload } = await jwtVerify(token, JWKS, {
248
+ issuer: `${config['auth-server-url'].replace(/\/+$/, '')}/realms/${config.realm}`,
249
+ });
250
+
251
+ // TideCloak access tokens use azp (authorized party) for the client ID.
252
+ if (payload.azp !== config.resource) {
253
+ throw new Error('Token azp does not match client');
254
+ }
255
+
256
+ const now = Math.floor(Date.now() / 1000);
257
+ if (payload.exp && payload.exp < now) {
258
+ throw new Error('Token expired');
259
+ }
260
+
261
+ return payload;
262
+ }
263
+ ```
264
+
265
+ **`loadTideConfig()` implementation** **VERIFIED** (keylessh `server/lib/auth/tidecloakConfig.ts`):
266
+
267
+ ```typescript
268
+ // lib/auth/tidecloakConfig.ts
269
+ import { readFileSync } from 'fs';
270
+
271
+ interface TideConfig {
272
+ realm: string;
273
+ 'auth-server-url': string;
274
+ resource: string;
275
+ 'ssl-required': string;
276
+ 'public-client': boolean;
277
+ 'confidential-port': number;
278
+ jwk: { keys: any[] }; // Tide extension
279
+ vendorId: string; // Tide extension
280
+ homeOrkUrl: string; // Tide extension
281
+ }
282
+
283
+ export function loadTideConfig(): TideConfig {
284
+ // Priority: env var > file
285
+ if (process.env.CLIENT_ADAPTER) {
286
+ return JSON.parse(process.env.CLIENT_ADAPTER);
287
+ }
288
+ if (process.env.TIDECLOAK_CONFIG_B64) {
289
+ return JSON.parse(Buffer.from(process.env.TIDECLOAK_CONFIG_B64, 'base64').toString());
290
+ }
291
+ return JSON.parse(readFileSync('data/tidecloak.json', 'utf-8'));
292
+ }
293
+ ```
294
+
295
+ **Agent implication**:
296
+ - Use embedded JWKS from adapter JSON only. Do not use `createRemoteJWKSet`. Do not fetch from `{realm}/protocol/openid-connect/certs`. If `jwk` is missing, re-export adapter with IGA enabled. (I-04)
297
+ - `jwk` field is only present when IGA is enabled on the realm. Validate at startup.
298
+
299
+ ### Server-Side JWT Verification via `@tidecloak/verify`
300
+
301
+ **Alternative pattern** **VERIFIED** (keylessh exemplar):
302
+
303
+ The `@tidecloak/verify` package provides a higher-level verification function. It is CJS — never use named ESM imports.
304
+
305
+ ```typescript
306
+ // @tidecloak/verify is CJS — use defensive interop pattern
307
+ import TideJWT from '@tidecloak/verify';
308
+ const _mod = (TideJWT as any)?.default ?? TideJWT;
309
+ const { verifyTideCloakToken } = _mod;
310
+
311
+ // config = parsed tidecloak.json adapter config
312
+ // token = raw JWT string (without "Bearer " prefix)
313
+ // allowedRoles = string[] of required realm roles (empty array to skip role check)
314
+ const payload = await verifyTideCloakToken(config, token, allowedRoles);
315
+ ```
316
+
317
+ **Anti-pattern**: `import { verifyTideCloakToken } from "@tidecloak/verify"` fails with `does not provide an export named 'verifyTideCloakToken'`. The package is CJS. Always use the default import with defensive interop.
318
+
319
+ **Express middleware using `@tidecloak/verify`:**
320
+
321
+ ```typescript
322
+ import TideJWT from '@tidecloak/verify';
323
+ const _mod = (TideJWT as any)?.default ?? TideJWT;
324
+ const { verifyTideCloakToken } = _mod;
325
+
326
+ async function authenticate(req, res, next) {
327
+ const token = req.headers.authorization?.substring(7);
328
+ if (!token) return res.status(401).json({ message: "No token" });
329
+
330
+ try {
331
+ const payload = await verifyTideCloakToken(config, token, []);
332
+ req.user = {
333
+ id: payload.sub,
334
+ username: payload.preferred_username,
335
+ email: payload.email,
336
+ roles: payload.realm_access?.roles || [],
337
+ };
338
+ next();
339
+ } catch (error) {
340
+ res.status(401).json({ message: "Token verification failed" });
341
+ }
342
+ }
343
+
344
+ app.use("/api/*", authenticate);
345
+ ```
346
+
347
+ ---
348
+
349
+ ### DPoP Verification (Server-Side)
350
+
351
+ **Pattern** **VERIFIED** (keylessh `server/auth.ts`):
352
+
353
+ ```typescript
354
+ // lib/auth/dpop.ts
355
+ import { jwtVerify, decodeJwt } from 'jose';
356
+ import { createHash } from 'crypto';
357
+
358
+ const jtiCache = new Map<string, number>(); // jti -> expiry timestamp
359
+ const JTI_TTL_MS = 2 * 60 * 1000; // 2 minutes
360
+
361
+ export async function verifyDPoP(
362
+ req: Request,
363
+ accessToken: string
364
+ ): Promise<void> {
365
+ const dpopHeader = req.headers.get('dpop');
366
+ if (!dpopHeader) {
367
+ throw new Error('DPoP header missing');
368
+ }
369
+
370
+ // Decode without verification first to get jwk
371
+ const unverified = decodeJwt(dpopHeader);
372
+ if (unverified.typ !== 'dpop+jwt') {
373
+ throw new Error('Invalid DPoP type');
374
+ }
375
+ if (unverified.alg !== 'ES256' && unverified.alg !== 'EdDSA') {
376
+ throw new Error('Invalid DPoP algorithm');
377
+ }
378
+
379
+ // Verify signature
380
+ const jwk = unverified.jwk as any;
381
+ const { payload } = await jwtVerify(dpopHeader, await importJWK(jwk));
382
+
383
+ // Verify htm and htu
384
+ const url = new URL(req.url);
385
+ if (payload.htm !== req.method) {
386
+ throw new Error('DPoP htm mismatch');
387
+ }
388
+ if (payload.htu !== `${url.protocol}//${url.host}${url.pathname}`) {
389
+ throw new Error('DPoP htu mismatch');
390
+ }
391
+
392
+ // Verify iat freshness (120s window)
393
+ const now = Math.floor(Date.now() / 1000);
394
+ if (!payload.iat || payload.iat < now - 120 || payload.iat > now + 120) {
395
+ throw new Error('DPoP iat out of range');
396
+ }
397
+
398
+ // Verify jti not replayed
399
+ const jti = payload.jti as string;
400
+ if (jtiCache.has(jti)) {
401
+ throw new Error('DPoP jti replayed');
402
+ }
403
+ jtiCache.set(jti, Date.now() + JTI_TTL_MS);
404
+
405
+ // Clean expired jtis
406
+ const expiredKeys = Array.from(jtiCache.entries())
407
+ .filter(([_, expiry]) => expiry < Date.now())
408
+ .map(([key, _]) => key);
409
+ expiredKeys.forEach(key => jtiCache.delete(key));
410
+
411
+ // Verify cnf.jkt in access token matches DPoP proof
412
+ const tokenPayload = decodeJwt(accessToken);
413
+ const expectedJkt = createHash('sha256').update(JSON.stringify(jwk)).digest('base64url');
414
+ if (tokenPayload.cnf?.jkt !== expectedJkt) {
415
+ throw new Error('DPoP jkt mismatch');
416
+ }
417
+ }
418
+ ```
419
+
420
+ **Agent implication**:
421
+ - DPoP verification is per-request; maintain in-memory `jti` cache with TTL
422
+ - DPoP is required for Tide's full security guarantees **VERIFIED** (vendor confirmation, GAP-032 resolved). ES256 default.
423
+
424
+ ---
425
+
426
+ ### E2EE Usage (Client-Side)
427
+
428
+ ```typescript
429
+ 'use client';
430
+ import { useTideCloak } from '@tidecloak/nextjs';
431
+
432
+ export function EncryptedForm() {
433
+ const { doEncrypt, doDecrypt } = useTideCloak();
434
+
435
+ async function handleSubmit(plaintext: string) {
436
+ // Encrypt before sending to server
437
+ const ciphertext = await doEncrypt('ssn', plaintext);
438
+ await fetch('/api/users', {
439
+ method: 'POST',
440
+ body: JSON.stringify({ ssn: ciphertext }),
441
+ });
442
+ }
443
+
444
+ async function handleLoad(ciphertext: string) {
445
+ // Decrypt after receiving from server
446
+ const plaintext = await doDecrypt('ssn', ciphertext);
447
+ console.log(plaintext);
448
+ }
449
+
450
+ return <form onSubmit={...}>...</form>;
451
+ }
452
+ ```
453
+
454
+ **Agent implication**:
455
+ - Requires roles: `_tide_ssn.selfencrypt`, `_tide_ssn.selfdecrypt`
456
+ - Both `Uint8Array` (binary) and string inputs supported **VERIFIED** (vendor confirmation, GAP-013 resolved)
457
+ - Requires online Fabric access **VERIFIED**
458
+
459
+ ---
460
+
461
+ ### Silent SSO Setup
462
+
463
+ **Required file** **VERIFIED** (tidecloak-nextjs-how-to-guide.md):
464
+
465
+ ```html
466
+ <!-- public/silent-check-sso.html -->
467
+ <html>
468
+ <body>
469
+ <script>
470
+ parent.postMessage(location.href, location.origin);
471
+ </script>
472
+ </body>
473
+ </html>
474
+ ```
475
+
476
+ **Corrupted state recovery pattern** **VERIFIED** (keylessh `client/src/contexts/AuthContext.tsx`, `main.tsx`):
477
+
478
+ ```typescript
479
+ // app/layout.tsx or pages/_app.tsx
480
+ import { useEffect } from 'react';
481
+
482
+ export default function RootLayout({ children }) {
483
+ useEffect(() => {
484
+ // Check for corrupted auth state on startup
485
+ const params = new URLSearchParams(window.location.search);
486
+ if (params.get('reset') === 'true') {
487
+ // Clear all auth state
488
+ const keys = Object.keys(localStorage);
489
+ keys.forEach(key => {
490
+ if (key.includes('iam') || key.includes('auth')) {
491
+ localStorage.removeItem(key);
492
+ }
493
+ });
494
+ window.location.href = '/';
495
+ return;
496
+ }
497
+
498
+ // Detect stale state: has IAM data but init failed
499
+ const hasIamData = Object.keys(localStorage).some(k =>
500
+ k.includes('iam') || k.includes('auth')
501
+ );
502
+
503
+ // If init timeout (10s) and stale data exists, offer reset
504
+ setTimeout(() => {
505
+ if (hasIamData && !tideCloakContext.authenticated) {
506
+ console.warn('Stale auth state detected. Append ?reset=true to clear.');
507
+ }
508
+ }, 10000);
509
+ }, []);
510
+
511
+ return <TideCloakContextProvider>{children}</TideCloakContextProvider>;
512
+ }
513
+ ```
514
+
515
+ **Agent implication**: Defensive pattern from keylessh; not SDK-provided.
516
+
517
+ ---
518
+
519
+ ### Request Interception / Route Protection (UI Only)
520
+
521
+ Next.js 16+ uses `proxy.ts` (or `src/proxy.ts`) for request interception. Next.js 15 and earlier use `middleware.ts`. Check the project's Next.js version before choosing the filename.
522
+
523
+ **Version detection**:
524
+ ```bash
525
+ # Detect Next.js major version from the installed package
526
+ NEXT_MAJOR=$(node -e "try{console.log(require('next/package.json').version.split('.')[0])}catch{console.log('unknown')}" 2>/dev/null)
527
+ # 16+ → proxy.ts | 15 or earlier → middleware.ts
528
+ ```
529
+
530
+ ```typescript
531
+ // proxy.ts (Next.js 16+) — called middleware.ts in Next.js 15 and earlier
532
+ import { NextResponse } from 'next/server';
533
+ import type { NextRequest } from 'next/server';
534
+
535
+ export function middleware(req: NextRequest) {
536
+ // WARNING: This is UI gating only, NOT real authorization
537
+ const session = req.cookies.get('SESSION_COOKIE_NAME');
538
+
539
+ if (!session && req.nextUrl.pathname.startsWith('/admin')) {
540
+ return NextResponse.redirect(new URL('/login', req.url));
541
+ }
542
+
543
+ return NextResponse.next();
544
+ }
545
+
546
+ export const config = {
547
+ matcher: '/admin/:path*',
548
+ };
549
+ ```
550
+
551
+ **Critical**: Proxy/middleware route protection is UI gating. Always verify JWT server-side in API routes.
552
+
553
+ **Version rule**: Next.js 16+ uses `proxy.ts`. Next.js 15 and earlier use `middleware.ts`. Always check the project's installed Next.js version before creating or renaming this file. If migrating from `middleware.ts` to `proxy.ts`, rename the file. Next.js provides a codemod for automated migration.
554
+
555
+ **Do not confuse** `proxy.ts` (Next.js request interception at the edge) with `lib/auth/protect.ts` (Tide auth helper with `withAuth`/`withRole`). They are unrelated files with different purposes.
556
+
557
+ ---
558
+
559
+ ## React (Vite/CRA) Implementation
560
+
561
+ ### Prerequisites
562
+
563
+ **Verified requirements** (tidecloak-react-tutorial-quickstart.md):
564
+ - React 18+
565
+ - Vite or Create React App
566
+ - TideCloak instance running
567
+ - IGA enabled on realm **VERIFIED** (vendor confirmation, batch-02 Q-04). Canonical ordering: license → IGA → E2EE.
568
+
569
+ ### Package Installation
570
+
571
+ ```bash
572
+ npm install @tidecloak/react
573
+ ```
574
+
575
+ **INFERRED** (A-11): Package assumed on public npm registry.
576
+
577
+ ---
578
+
579
+ ### SDK API Reference
580
+
581
+ **`useTideCloak()` hook** **VERIFIED** (SDK documentation, operational exemplars):
582
+
583
+ ```tsx
584
+ import { useTideCloak, Authenticated, Unauthenticated } from '@tidecloak/react';
585
+
586
+ const {
587
+ authenticated, // boolean - is user logged in
588
+ login, // () => void - redirect to TideCloak login
589
+ logout, // () => void - log out
590
+ token, // string - raw JWT access token
591
+ tokenExp, // number - token expiry timestamp
592
+ refreshToken, // () => Promise - refresh the token
593
+ getValueFromToken, // (key: string) => any - read JWT claim
594
+ getValueFromIdToken, // (key: string) => any - read ID token claim
595
+ hasRealmRole, // (role: string) => boolean
596
+ hasClientRole, // (role: string, client?: string) => boolean
597
+ isInitializing, // boolean - SDK still loading
598
+ doEncrypt, // E2EE encrypt
599
+ doDecrypt, // E2EE decrypt
600
+ IAMService, // direct access to IAMService
601
+ } = useTideCloak();
602
+ ```
603
+
604
+ **Guard components:**
605
+
606
+ ```tsx
607
+ <Authenticated> {/* renders only when logged in */}
608
+ <Unauthenticated> {/* renders only when logged out */}
609
+ ```
610
+
611
+ **Critical**: `hasRealmRole()` and `hasClientRole()` are UI gating, NOT real authorization. Server must verify JWT.
612
+
613
+ **Anti-pattern**: `tide-realm-admin` is a **client role** on the `realm-management` client, not a realm role. Use `hasClientRole("tide-realm-admin", "realm-management")`, not `hasRealmRole("tide-realm-admin")`.
614
+
615
+ **Anti-pattern**: Do not call `login()` before the SDK has initialized. Check `isInitializing` at the top of the component tree, BEFORE `<Authenticated>` / `<Unauthenticated>` guards render. See Provider Setup below.
616
+
617
+ ---
618
+
619
+ ### Provider Setup
620
+
621
+ **Anti-pattern**: The prop is `configUrl`, NOT `configFilePath`. Using the wrong prop falls through to the default `/adapter.json`, which does not exist, and Vite returns the HTML index page instead (`SyntaxError: Unexpected token '<'`).
622
+
623
+ ```typescript
624
+ // src/main.tsx (Vite) or src/index.tsx (CRA)
625
+ import React from 'react';
626
+ import ReactDOM from 'react-dom/client';
627
+ import { TideCloakContextProvider } from '@tidecloak/react';
628
+ import App from './App';
629
+
630
+ // Config file must be in public/tidecloak.json — do NOT import as a JS module
631
+ const initOptions = { enableDpop: true }; // DPoP required for Tide security; ES256 is the default algorithm
632
+
633
+ ReactDOM.createRoot(document.getElementById('root')!).render(
634
+ <React.StrictMode>
635
+ <TideCloakContextProvider
636
+ configUrl="/tidecloak.json"
637
+ initOptions={initOptions}
638
+ >
639
+ <App />
640
+ </TideCloakContextProvider>
641
+ </React.StrictMode>
642
+ );
643
+ ```
644
+
645
+ **`isInitializing` guard pattern** **VERIFIED** (SDK documentation, operational exemplars):
646
+
647
+ `<Unauthenticated>` renders during initialization because the user is not yet authenticated. This exposes child components before the SDK is ready. Block the entire tree first:
648
+
649
+ ```tsx
650
+ import { useTideCloak, Authenticated, Unauthenticated } from '@tidecloak/react';
651
+
652
+ function App() {
653
+ return (
654
+ <TideCloakContextProvider configUrl="/tidecloak.json" initOptions={{ enableDpop: true }}>
655
+ <AppContent />
656
+ </TideCloakContextProvider>
657
+ );
658
+ }
659
+
660
+ function AppContent() {
661
+ const { isInitializing } = useTideCloak();
662
+ if (isInitializing) return <p>Initializing TideCloak...</p>;
663
+ return (
664
+ <>
665
+ <Authenticated><UserContent /></Authenticated>
666
+ <Unauthenticated><LoginPage /></Unauthenticated>
667
+ </>
668
+ );
669
+ }
670
+ ```
671
+
672
+ **Common failure**: Calling `login()` before initialization completes produces `TideCloak client not initialized - call initIAM() first`.
673
+
674
+ ---
675
+
676
+ ### Auth State Access
677
+
678
+ ```typescript
679
+ // src/components/UserInfo.tsx
680
+ import { useTideCloak } from '@tidecloak/react';
681
+
682
+ export function UserInfo() {
683
+ const { authenticated, getValueFromIdToken, login, logout, hasRealmRole, doEncrypt, doDecrypt } = useTideCloak();
684
+
685
+ if (!authenticated) {
686
+ return <button onClick={login}>Login</button>;
687
+ }
688
+
689
+ return (
690
+ <div>
691
+ <p>Welcome {getValueFromIdToken('preferred_username')}</p>
692
+ {hasRealmRole('admin') && <AdminPanel />} {/* UI gating only */}
693
+ <button onClick={logout}>Logout</button>
694
+ </div>
695
+ );
696
+ }
697
+ ```
698
+
699
+ ---
700
+
701
+ ### Auth Bridge Pattern (keylessh)
702
+
703
+ **Pattern** **VERIFIED** (keylessh `client/src/contexts/AuthContext.tsx`):
704
+
705
+ Syncs IAMService (Vanilla JS) state into React context for apps using both.
706
+
707
+ ```typescript
708
+ // src/contexts/AuthContext.tsx
709
+ import { createContext, useContext, useEffect, useState } from 'react';
710
+ import { TideCloakContextProvider } from '@tidecloak/react';
711
+ import { IAMService } from '@tidecloak/js';
712
+
713
+ interface AuthContextType {
714
+ authenticated: boolean;
715
+ user: any | null;
716
+ login: () => void;
717
+ logout: () => void;
718
+ }
719
+
720
+ const AuthContext = createContext<AuthContextType | null>(null);
721
+
722
+ export function AuthProvider({ children }: { children: React.ReactNode }) {
723
+ const [authenticated, setAuthenticated] = useState(false);
724
+ const [user, setUser] = useState(null);
725
+
726
+ useEffect(() => {
727
+ const iam = IAMService; // singleton, direct import — do NOT call getInstance()
728
+
729
+ // Listen to IAM events
730
+ iam.on('tokenExpired', () => {
731
+ setAuthenticated(false);
732
+ setUser(null);
733
+ });
734
+
735
+ iam.on('authRefreshSuccess', async () => {
736
+ const token = await iam.getToken();
737
+ if (token) {
738
+ setAuthenticated(true);
739
+ setUser(await iam.getUserInfo());
740
+ }
741
+ });
742
+
743
+ iam.on('authRefreshError', () => {
744
+ setAuthenticated(false);
745
+ setUser(null);
746
+ });
747
+
748
+ // Initial state
749
+ iam.getToken().then(token => {
750
+ if (token) {
751
+ setAuthenticated(true);
752
+ iam.getUserInfo().then(setUser);
753
+ }
754
+ });
755
+ }, []);
756
+
757
+ return (
758
+ <AuthContext.Provider value={{ authenticated, user, login: iam.doLogin, logout: iam.doLogout }}>
759
+ <TideCloakContextProvider>{children}</TideCloakContextProvider>
760
+ </AuthContext.Provider>
761
+ );
762
+ }
763
+
764
+ export const useAuth = () => useContext(AuthContext)!;
765
+ ```
766
+
767
+ **Agent implication**: Bridge pattern is keylessh-specific; not SDK-prescribed. Use when integrating with existing IAMService code.
768
+
769
+ ---
770
+
771
+ ### Silent SSO Setup (React)
772
+
773
+ Same as Next.js:
774
+
775
+ ```html
776
+ <!-- public/silent-check-sso.html -->
777
+ <html>
778
+ <body>
779
+ <script>
780
+ parent.postMessage(location.href, location.origin);
781
+ </script>
782
+ </body>
783
+ </html>
784
+ ```
785
+
786
+ ---
787
+
788
+ ### Backend Integration (Express)
789
+
790
+ **Pattern** **VERIFIED** (keylessh `server/auth.ts`, `server/index.ts`):
791
+
792
+ ```typescript
793
+ // server/auth.ts
794
+ import express from 'express';
795
+ import { verifyTideJWT } from './lib/auth/tideJWT';
796
+ import { verifyDPoP } from './lib/auth/dpop';
797
+
798
+ export function requireAuth(requiredRole?: string) {
799
+ return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
800
+ const authHeader = req.headers.authorization;
801
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
802
+ return res.status(401).json({ error: 'Unauthorized' });
803
+ }
804
+
805
+ const token = authHeader.substring(7);
806
+
807
+ try {
808
+ // Verify DPoP if present
809
+ if (req.headers.dpop) {
810
+ await verifyDPoP(req, token);
811
+ }
812
+
813
+ // Verify JWT
814
+ const jwt = await verifyTideJWT(token);
815
+
816
+ // Check role if required
817
+ if (requiredRole && !jwt.realm_access?.roles?.includes(requiredRole)) {
818
+ return res.status(403).json({ error: 'Forbidden' });
819
+ }
820
+
821
+ req.user = jwt;
822
+ next();
823
+ } catch (err) {
824
+ return res.status(401).json({ error: 'Invalid token' });
825
+ }
826
+ };
827
+ }
828
+
829
+ // Usage
830
+ app.get('/api/admin/users', requireAuth('admin'), (req, res) => {
831
+ res.json({ users: [...] });
832
+ });
833
+ ```
834
+
835
+ ---
836
+
837
+ ### CSP Configuration (React/Express)
838
+
839
+ **Required** **VERIFIED** (vendor confirmation, GAP-028 resolved):
840
+
841
+ ```typescript
842
+ // server/index.ts
843
+ import helmet from 'helmet';
844
+
845
+ app.use(helmet({
846
+ contentSecurityPolicy: {
847
+ directives: {
848
+ frameSrc: ["'self'", '*'],
849
+ },
850
+ },
851
+ }));
852
+ ```
853
+
854
+ `frame-src '*'` required for ORK re-homing. No fixed domain list.
855
+
856
+ ---
857
+
858
+ ## Vanilla JS Implementation
859
+
860
+ ### Prerequisites
861
+
862
+ **Verified requirements** (tidecloak-js-tutorial.md):
863
+ - Modern browser (ES6+)
864
+ - TideCloak instance running
865
+ - IGA enabled on realm **VERIFIED** (vendor confirmation, batch-02 Q-04). Canonical ordering: license → IGA → E2EE.
866
+
867
+ ### Package Installation
868
+
869
+ ```bash
870
+ npm install @tidecloak/js
871
+ ```
872
+
873
+ **INFERRED** (A-11): Package assumed on public npm registry.
874
+
875
+ ---
876
+
877
+ ### IAMService Initialization
878
+
879
+ ```typescript
880
+ // src/auth.ts
881
+ import { IAMService } from '@tidecloak/js';
882
+
883
+ // IAMService is a pre-instantiated singleton — use it directly.
884
+ // Do NOT call IAMService.getInstance() — it does not exist.
885
+ const iam = IAMService;
886
+
887
+ // Pass the FULL adapter JSON from tidecloak.json, not a subset.
888
+ // Do NOT destructure into {url, realm, clientId} — initIAM reads
889
+ // "auth-server-url", "realm", "resource", "vendorId", "homeOrkUrl", etc.
890
+ const config = await fetch('/api/config').then(r => r.json());
891
+ await iam.initIAM({ ...config, useDPoP: { mode: 'strict', alg: 'ES256' } });
892
+ ```
893
+
894
+ ---
895
+
896
+ ### Event-Driven Auth State
897
+
898
+ ```typescript
899
+ // Listen to auth events
900
+ iam.on('tokenExpired', () => {
901
+ console.log('Token expired, redirecting to login');
902
+ iam.doLogin({ redirectUri: window.location.origin });
903
+ });
904
+
905
+ iam.on('authRefreshSuccess', async () => {
906
+ console.log('Token refreshed');
907
+ const user = await iam.getUserInfo();
908
+ updateUI(user);
909
+ });
910
+
911
+ iam.on('authRefreshError', (error) => {
912
+ console.error('Refresh failed', error);
913
+ iam.doLogin({ redirectUri: window.location.origin });
914
+ });
915
+ ```
916
+
917
+ **Agent implication**: Error handling from keylessh pattern **VERIFIED** (keylessh `AuthContext.tsx`).
918
+
919
+ ---
920
+
921
+ ### Login / Logout
922
+
923
+ ```typescript
924
+ // Login
925
+ document.getElementById('login-btn').addEventListener('click', () => {
926
+ iam.doLogin({ redirectUri: window.location.origin });
927
+ });
928
+
929
+ // Logout
930
+ document.getElementById('logout-btn').addEventListener('click', () => {
931
+ iam.doLogout({ redirectUri: window.location.origin });
932
+ });
933
+ ```
934
+
935
+ ---
936
+
937
+ ### Role Checks (UI Gating Only)
938
+
939
+ ```typescript
940
+ const hasAdminRole = iam.hasRealmRole('admin');
941
+
942
+ if (hasAdminRole) {
943
+ document.getElementById('admin-panel').style.display = 'block';
944
+ } else {
945
+ document.getElementById('admin-panel').style.display = 'none';
946
+ }
947
+ ```
948
+
949
+ **Critical**: This is UI gating, NOT authorization. Server must verify JWT.
950
+
951
+ ---
952
+
953
+ ### Secure API Calls
954
+
955
+ ```typescript
956
+ // Using secureFetch (SDK-provided DPoP-aware fetch)
957
+ const response = await iam.secureFetch('/api/admin/users', {
958
+ method: 'GET',
959
+ headers: {
960
+ 'Content-Type': 'application/json',
961
+ },
962
+ });
963
+
964
+ const users = await response.json();
965
+ ```
966
+
967
+ **Agent implication**: `secureFetch()` automatically adds `Authorization` and `DPoP` headers.
968
+
969
+ ---
970
+
971
+ ### E2EE (Vanilla JS)
972
+
973
+ ```typescript
974
+ // Encrypt
975
+ const plaintext = 'sensitive data';
976
+ const ciphertext = await iam.doEncrypt('ssn', plaintext);
977
+
978
+ // Send to server
979
+ await iam.secureFetch('/api/users', {
980
+ method: 'POST',
981
+ body: JSON.stringify({ ssn: ciphertext }),
982
+ });
983
+
984
+ // Decrypt
985
+ const response = await iam.secureFetch('/api/users/123');
986
+ const data = await response.json();
987
+ const plaintext = await iam.doDecrypt('ssn', data.ssn);
988
+ ```
989
+
990
+ ---
991
+
992
+ ### Silent SSO Setup (Vanilla JS)
993
+
994
+ Same file as React/Next.js:
995
+
996
+ ```html
997
+ <!-- public/silent-check-sso.html -->
998
+ <html>
999
+ <body>
1000
+ <script>
1001
+ parent.postMessage(location.href, location.origin);
1002
+ </script>
1003
+ </body>
1004
+ </html>
1005
+ ```
1006
+
1007
+ ---
1008
+
1009
+ ## Framework Comparison Matrix
1010
+
1011
+ | Feature | Next.js | React | Vanilla JS | Notes |
1012
+ |---------|---------|-------|------------|-------|
1013
+ | **Package** | `@tidecloak/nextjs` | `@tidecloak/react` | `@tidecloak/js` | INFERRED (A-11) |
1014
+ | **Provider** | `<TideCloakContextProvider>` | `<TideCloakContextProvider>` | `IAMService` (singleton, direct import) | VERIFIED |
1015
+ | **Auth state** | `useTideCloak()` hook | `useTideCloak()` hook | Event listeners | VERIFIED |
1016
+ | **Login/Logout** | Hook methods | Hook methods | `doLogin()` / `doLogout()` | VERIFIED |
1017
+ | **E2EE** | Hook methods | Hook methods | `doEncrypt()` / `doDecrypt()` | VERIFIED |
1018
+ | **Server-side JWT** | API routes | Express middleware | N/A (client-only) | VERIFIED (keylessh) |
1019
+ | **DPoP verification** | API routes | Express middleware | N/A (client-only) | VERIFIED (keylessh) |
1020
+ | **CSP setup** | `next.config.js` headers | Express `helmet()` | HTML meta tag | VERIFIED (keylessh) |
1021
+ | **Silent SSO** | `public/silent-check-sso.html` | `public/silent-check-sso.html` | `public/silent-check-sso.html` | VERIFIED |
1022
+ | **Route protection** | Middleware (UI only) | React Router guards (UI only) | Manual checks (UI only) | VERIFIED |
1023
+ | **API protection** | `verifyTideJWT()` in routes | `requireAuth()` middleware | Server-side separate | VERIFIED (keylessh) |
1024
+
1025
+ ---
1026
+
1027
+ ## Common Patterns Across Frameworks
1028
+
1029
+ ### Adapter JSON Loading
1030
+
1031
+ All frameworks need server-side config:
1032
+
1033
+ ```typescript
1034
+ // Priority: env var > file
1035
+ function loadTideConfig() {
1036
+ if (process.env.CLIENT_ADAPTER) {
1037
+ return JSON.parse(process.env.CLIENT_ADAPTER);
1038
+ }
1039
+ if (process.env.TIDECLOAK_CONFIG_B64) {
1040
+ return JSON.parse(Buffer.from(process.env.TIDECLOAK_CONFIG_B64, 'base64').toString());
1041
+ }
1042
+ return JSON.parse(fs.readFileSync('data/tidecloak.json', 'utf-8'));
1043
+ }
1044
+ ```
1045
+
1046
+ **VERIFIED** (keylessh `tidecloakConfig.ts`, bridge configs).
1047
+
1048
+ ---
1049
+
1050
+ ### Token Extraction (Server-Side)
1051
+
1052
+ ```typescript
1053
+ function extractToken(req: Request): string {
1054
+ const authHeader = req.headers.get('authorization') || req.headers.authorization;
1055
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
1056
+ throw new Error('Missing or invalid Authorization header');
1057
+ }
1058
+ return authHeader.substring(7);
1059
+ }
1060
+ ```
1061
+
1062
+ ---
1063
+
1064
+ ### Required Realm Roles
1065
+
1066
+ All frameworks require user has `_tide_enabled` role **VERIFIED** (vendor confirmation, GAP-031 resolved). Declare `_tide_enabled` in the realm.json template — `setUpTideRealm` does not create it automatically.
1067
+
1068
+ ---
1069
+
1070
+ ## Framework-Specific Gaps
1071
+
1072
+ ### Next.js
1073
+
1074
+ - Scaffold command behavior **RESOLVED_BY_VENDOR** (GAP-005)
1075
+ - App Router vs Pages Router differences undocumented **ASSUMED**
1076
+ - Middleware CSP enforcement vs `next.config.js` headers unclear **ASSUMED**
1077
+
1078
+ ### React
1079
+
1080
+ - CRA vs Vite configuration differences undocumented **ASSUMED**
1081
+ - Auth bridge pattern (IAMService + React Context) not SDK-prescribed **VERIFIED** (keylessh pattern)
1082
+
1083
+ ### Vanilla JS
1084
+
1085
+ - No server-side patterns (pure client library)
1086
+ - Module bundler compatibility (Webpack, Rollup, Vite) undocumented **ASSUMED**
1087
+
1088
+ ---
1089
+
1090
+ ## Post-Auth Redirect Handler
1091
+
1092
+ Every Tide-enabled app must have a working page at its configured `redirectUri`. See [canon/redirect-handler.md](redirect-handler.md) for full doctrine.
1093
+
1094
+ | Framework | Default path | Handler location |
1095
+ |-----------|-------------|-----------------|
1096
+ | Vanilla JS / Vite | `/auth/redirect` | `public/auth/redirect.html` (must include SDK script) |
1097
+ | React SPA | `/auth/redirect` | Route component or SPA fallback |
1098
+ | Next.js App Router | `/auth/redirect` | `app/auth/redirect/page.tsx` |
1099
+ | Next.js Pages Router | `/auth/redirect` | `pages/auth/redirect.tsx` |
1100
+
1101
+ ---
1102
+
1103
+ ## Key Dependencies
1104
+
1105
+ | Package | Stable version | Purpose |
1106
+ |---------|---------------|---------|
1107
+ | `@tidecloak/js` | 0.13.26 | Core SDK (vanilla JS) |
1108
+ | `@tidecloak/react` | 0.13.26 | React hooks and guards |
1109
+ | `@tidecloak/nextjs` | 0.13.26 | Next.js provider and hooks |
1110
+ | `@tidecloak/verify` | 0.13.26 | Server-side JWT verification (CJS) |
1111
+ | `heimdall-tide` | 0.13.26 | Policy signing, BasicCustomRequest |
1112
+ | `@tideorg/js` | 0.13.26 | Models, Contracts (Forseti) |
1113
+ | `asgard-tide` | 0.13.26 | Vendor validation |
1114
+
1115
+ All Tide packages are pre-1.0. Pin to exact versions in templates. See [canon/version-policy.md](version-policy.md) for the full version policy.
1116
+
1117
+ ---
1118
+
1119
+ ## Status Legend
1120
+
1121
+ - **VERIFIED** - Directly sourced from documentation or keylessh exemplar
1122
+ - **INFERRED** - Strongly implied by source material
1123
+ - **ASSUMED** - Operator guidance where sources are silent
1124
+ - **REQUIRES_RUNTIME_VALIDATION** - Single-app evidence; needs confirmation
1125
+ - **STILL_UNRESOLVED** - Open gap