@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,756 @@
1
+ # Set Up Forseti-Governed E2EE (Policy-Based Shared Encryption)
2
+
3
+ Implement end-to-end encrypted data sharing using Tide's Forseti policy contracts and VVK encryption. This is the most complex Tide integration. Follow every step exactly.
4
+
5
+ **CRITICAL**: Self-encryption and policy-governed encryption are completely different mechanisms. Do not mix them. Do not skip the policy signing flow. Do not improvise byte serialization.
6
+
7
+ ---
8
+
9
+ ## When to Use
10
+
11
+ - Encrypting data that multiple users must decrypt (shared secrets, messages, credentials)
12
+ - Enforcing role-based access to encrypted data via Forseti contracts on the ORK network
13
+ - Building scoped group encryption (e.g., team vaults, org-level shared data)
14
+ - Requiring threshold-governed operations with custom business rules
15
+
16
+ **Do not use** for private-only encryption where only the encrypting user needs access. Use self-encryption instead.
17
+
18
+ ---
19
+
20
+ ## Prerequisites
21
+
22
+ - TideCloak realm deployed with ORK network connected
23
+ - `@tidecloak/js` and `heimdall-tide` packages installed
24
+ - `IAMService` initialized via `useTideCloak()` or equivalent
25
+ - Admin user with `tide-realm-admin` role (required for policy signing)
26
+ - Backend API for storing signed policy bytes
27
+ - Familiarity with [protect-api-nextjs.md](protect-api-nextjs.md) and [add-rbac-nextjs.md](add-rbac-nextjs.md)
28
+
29
+ ---
30
+
31
+ ## Two Encryption Models
32
+
33
+ ### 1. Self-Encryption (Private Data, NO Sharing)
34
+
35
+ Uses `doEncrypt`/`doDecrypt` from `useTideCloak()` WITHOUT a policy parameter. Ciphertext is bound to the encrypting user's identity. Only that user can ever decrypt. Giving another user the same `selfdecrypt` role does NOT let them decrypt your data.
36
+
37
+ ```tsx
38
+ const { doEncrypt, doDecrypt } = useTideCloak();
39
+
40
+ // Private: ONLY I can ever decrypt this
41
+ const encrypted = await doEncrypt([{ data: "my secret", tags: ["vault"] }]);
42
+ const plaintext = await doDecrypt([{ encrypted: encrypted[0], tags: ["vault"] }]);
43
+ // plaintext only exists in browser memory
44
+ ```
45
+
46
+ Requires `_tide_{tag}.selfencrypt` / `_tide_{tag}.selfdecrypt` realm roles.
47
+
48
+ ### 2. Policy-Governed VVK Encryption (Shared Data)
49
+
50
+ Uses `IAMService.doEncrypt(data, policyBytes)` WITH a signed policy. Encrypts with VVK (organizational key distributed across ORKs). Any user whose doken satisfies the Forseti policy can decrypt.
51
+
52
+ ```jsx
53
+ const { IAMService } = useTideCloak();
54
+
55
+ // Encrypt with VVK + policy (shared: anyone with the right role can decrypt)
56
+ const encrypted = await IAMService.doEncrypt(
57
+ [{ data: "shared message", tags: ["shared"] }],
58
+ signedPolicyBytes // MUST be VVK-signed Uint8Array
59
+ );
60
+
61
+ // Decrypt (any user whose doken satisfies the Forseti contract)
62
+ const decrypted = await IAMService.doDecrypt(
63
+ [{ encrypted: ciphertext, tags: ["shared"] }],
64
+ signedPolicyBytes
65
+ );
66
+ ```
67
+
68
+ The `doEncrypt`/`doDecrypt` on `useTideCloak()` are convenience wrappers that do NOT pass the policy parameter through. For shared encryption, call `IAMService` directly.
69
+
70
+ ---
71
+
72
+ ## Step 1: Write the Forseti Contract (C#)
73
+
74
+ Every contract implements `IAccessPolicy` from `Ork.Forseti.Sdk`. Without the `using` directive, the ORK compiler fails with `"The type or namespace name 'IAccessPolicy' could not be found"`.
75
+
76
+ ```csharp
77
+ using Ork.Forseti.Sdk;
78
+
79
+ public class Contract : IAccessPolicy
80
+ {
81
+ [PolicyParam(Required = true)]
82
+ public string Role { get; set; }
83
+
84
+ public PolicyDecision ValidateData(DataContext ctx)
85
+ {
86
+ if (ctx.Data == null || ctx.Data.Length == 0)
87
+ return PolicyDecision.Deny("No data provided");
88
+ return PolicyDecision.Allow();
89
+ }
90
+
91
+ public PolicyDecision ValidateExecutor(ExecutorContext ctx)
92
+ {
93
+ var executor = new DokenDto(ctx.Doken);
94
+ return Decision
95
+ .RequireNotExpired(executor)
96
+ .RequireRole(executor, Role);
97
+ }
98
+ }
99
+ ```
100
+
101
+ For contracts that also need approver validation (EXPLICIT approval mode):
102
+
103
+ ```csharp
104
+ using Ork.Forseti.Sdk;
105
+
106
+ public class Contract : IAccessPolicy
107
+ {
108
+ [PolicyParam(Required = true)]
109
+ public string Role { get; set; }
110
+
111
+ [PolicyParam(Required = true)]
112
+ public string Resource { get; set; }
113
+
114
+ public PolicyDecision ValidateData(DataContext ctx)
115
+ {
116
+ if (ctx.Data == null || ctx.Data.Length == 0)
117
+ return PolicyDecision.Deny("No data provided");
118
+ return PolicyDecision.Allow();
119
+ }
120
+
121
+ public PolicyDecision ValidateApprovers(ApproversContext ctx)
122
+ {
123
+ // M-of-N approval (for EXPLICIT policies)
124
+ var approvers = DokenDto.WrapAll(ctx.Dokens);
125
+ return Decision
126
+ .RequireAnyWithRole(approvers, Resource, Role);
127
+ }
128
+
129
+ public PolicyDecision ValidateExecutor(ExecutorContext ctx)
130
+ {
131
+ // Check executor has required role (for PRIVATE policies)
132
+ var executor = new DokenDto(ctx.Doken);
133
+ return Decision
134
+ .RequireNotExpired(executor)
135
+ .RequireRole(executor, Resource, Role);
136
+ }
137
+ }
138
+ ```
139
+
140
+ **Advanced contract features** (from forseti-crypto-quickstart):
141
+ - Separate `EncryptionRealmRole` and `DecryptionRealmRole` params (both optional)
142
+ - Detect encrypt vs decrypt via `ctx.RequestId` (`PolicyEnabledEncryption:1` / `PolicyEnabledDecryption:1`)
143
+ - Read tags from `ctx.Data` and enforce `DecryptTimeLock:{epoch}` tags (block decryption until a timestamp)
144
+
145
+ ---
146
+
147
+ ## Step 2: Compute the Contract Hash
148
+
149
+ **There is no REST API for deploying Forseti contracts.** Do not use `PUT` or `POST` to `/tide-admin/forseti-contracts` — these endpoints do not exist (404/405). Contract deployment happens via `PolicySignRequest.addForsetiContractToUpload(contractSource)` in the browser signing flow (Step 4). VERIFIED (session-001, LEARNINGS-batch-007 L-06).
150
+
151
+ The `contractId` must be the SHA-512 hash of the EXACT contract source. Compute it locally:
152
+
153
+ ```js
154
+ async function computeContractHash(source) {
155
+ const data = new TextEncoder().encode(source);
156
+ const hash = await crypto.subtle.digest("SHA-512", data);
157
+ return Array.from(new Uint8Array(hash))
158
+ .map(b => b.toString(16).padStart(2, "0")).join("").toUpperCase();
159
+ }
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Step 3: Construct the Policy
165
+
166
+ Policy uses IMPLICIT + PRIVATE:
167
+ - **IMPLICIT** approval: no separate approver dokens needed
168
+ - **PRIVATE** execution: runs `ValidateExecutor` on the user's doken
169
+
170
+ **CRITICAL import path**: `ApprovalType` and `ExecutionType` are direct exports from `Models`, alongside `Policy`. They are NOT nested under `Models.Policy` (which is the class itself, not a namespace).
171
+
172
+ ```js
173
+ const { Models } = await import("@tideorg/js");
174
+ const { Policy, ApprovalType, ExecutionType } = Models;
175
+
176
+ // vendorId MUST come from the adapter JSON config (data/tidecloak.json).
177
+ // Do NOT read it from IAMService._tc.vendorId — that property is undefined.
178
+ // import tcConfig from '../../data/tidecloak.json';
179
+ const vendorId = tcConfig.vendorId; // or tcConfig["vendorId"]
180
+
181
+ const policy = new Policy({
182
+ version: "3",
183
+ contractId: contractHash,
184
+ // modelId MUST be an array of specific model IDs, NOT "any"
185
+ // These are the ORK request types the policy governs
186
+ modelId: ["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"],
187
+ keyId: vendorId, // from adapter JSON, not from IAMService
188
+ approvalType: ApprovalType.IMPLICIT,
189
+ executionType: ExecutionType.PRIVATE,
190
+ params: new Map([["Role", "shared-data-access"]]),
191
+ });
192
+ ```
193
+
194
+ **Wrong:**
195
+ ```js
196
+ // WRONG: destructuring from Models.Policy (a class, not a namespace)
197
+ const { ApprovalType, ExecutionType } = Models.Policy;
198
+
199
+ // WRONG: using "any" instead of specific model IDs
200
+ modelId: "any"
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Step 4: Sign the Policy (5-Step Flow)
206
+
207
+ Raw `policy.toBytes()` will be rejected by the ORKs with `"Policy supplied has not been signed"`. Policy signing is a 5-step process.
208
+
209
+ ### CRITICAL: Import Paths
210
+
211
+ `PolicySignRequest` MUST come from `heimdall-tide`. Despite broken package exports for some symbols, `PolicySignRequest` itself works and handles the draft serialization format that the ORK expects. Do NOT try to build the policy signing request manually with `BaseTideRequest` + `TideMemory`.
212
+
213
+ ```js
214
+ const { PolicySignRequest } = await import("heimdall-tide");
215
+ const { Models } = await import("@tideorg/js");
216
+ const { BaseTideRequest } = Models;
217
+ ```
218
+
219
+ ### The 5 Steps
220
+
221
+ ```js
222
+ const tc = IAMService._tc;
223
+ if (!tc) throw new Error("TideCloak not initialized — user must be logged in before policy signing");
224
+
225
+ // Step 1: Build and initialize the request
226
+ const policyRequest = PolicySignRequest.New(policy);
227
+ policyRequest.addForsetiContractToUpload(CONTRACT_SOURCE);
228
+ policyRequest.setCustomExpiry(604800); // 1 week
229
+
230
+ const initializedBytes = await tc.createTideRequest(policyRequest.encode());
231
+ const initialized = BaseTideRequest.decode(initializedBytes);
232
+
233
+ // Step 2: Operator approval (opens popup for admin to sign)
234
+ const approvalResults = await tc.requestTideOperatorApproval([
235
+ { id: "policy-sign", request: initialized.encode() },
236
+ ]);
237
+ if (approvalResults[0].status !== "approved") throw new Error("Denied");
238
+
239
+ // Step 3: Decode approved request and ATTACH THE ADMIN POLICY
240
+ const approvedRequest = BaseTideRequest.decode(approvalResults[0].request);
241
+ const adminPolicyBytes = await fetchAdminPolicyFromTideCloak(token);
242
+ approvedRequest.addPolicy(adminPolicyBytes);
243
+
244
+ // Step 4: Execute sign request (waitForAll MUST be true)
245
+ const signatures = await tc.executeSignRequest(approvedRequest.encode(), true);
246
+
247
+ // Step 5: Attach VVK signature to the policy object
248
+ policy.signature = signatures[0];
249
+ const signedBytes = policy.toBytes(); // NOW has VVK signature
250
+ ```
251
+
252
+ **Reference implementation wrapper functions** (from forseti-crypto-quickstart `AuthProvider.tsx`). This is the EXACT pattern that works. Do not deviate:
253
+
254
+ ```js
255
+ const initializeTideRequest = async (request) => {
256
+ return BaseTideRequest.decode(
257
+ await IAMService._tc?.createTideRequest(request.encode())
258
+ );
259
+ };
260
+
261
+ const approveTideRequests = async (requests) => {
262
+ return await IAMService._tc?.requestTideOperatorApproval(requests);
263
+ };
264
+
265
+ const executeTideRequest = async (request, waitForAll = false) => {
266
+ return await IAMService._tc?.executeSignRequest(request, waitForAll);
267
+ };
268
+ ```
269
+
270
+ ### What Fails If You Skip Each Step
271
+
272
+ | Skipped Step | Error |
273
+ |---|---|
274
+ | Step 2 (approval popup) | `executeSignRequest` alone does not sign policies |
275
+ | Step 3 (admin policy) | ORKs reject: `"Policy supplied has not been signed"` |
276
+ | Step 4 (executeSignRequest) | Approval popup alone does not produce the VVK signature |
277
+ | Step 5 (signature assignment) | `policy.toBytes()` has no signature, ORKs reject |
278
+
279
+ ### Critical Pitfalls
280
+
281
+ - Do NOT call `addPolicy(policy.toBytes())` on the `BaseTideRequest` during construction. Attaching an unsigned policy causes `createTideRequest` to fail immediately. Only call `addPolicy()` in step 3 to attach the signed admin policy.
282
+ - Do NOT store signed policy bytes to the server until verified. If a failed signing attempt stores unsigned bytes, subsequent calls fetch stale bytes and silently fail.
283
+
284
+ ---
285
+
286
+ ## Step 5: Get the Admin Policy (Correct Endpoint)
287
+
288
+ ```
289
+ WRONG: GET /admin/realms/{realm}/tide-admin/realm-policy
290
+ -> { status: "none" } (even when the policy exists)
291
+
292
+ CORRECT: GET /realms/{realm}/tide-policy-resources/admin-policy
293
+ -> base64-encoded bytes (public, no auth required)
294
+ ```
295
+
296
+ The correct endpoint does not require master admin credentials. Proxy through your backend.
297
+
298
+ **CORS: Browser cannot fetch this endpoint directly.** TideCloak and the app are on different origins. Direct browser fetch returns an opaque/blocked response. Create a same-origin API route that proxies the request server-side. VERIFIED (session-001).
299
+
300
+ **If the proxy route is protected with `withAuth` (correct per I-03), the client must use authenticated fetch** (e.g., `appFetch` or `secureFetch` with Bearer header). A bare `fetch()` without Authorization returns 401. The resulting empty/error response produces undefined policy bytes, causing `PolicyAuthorizationFlowException: "Model does not have a policy passed with it"` at signing time. VERIFIED (LEARNINGS-batch-009 L-08).
301
+
302
+ **There is no REST API for deploying Forseti contracts.** Do not try `PUT` or `POST` to `/tide-admin/forseti-contracts` — these endpoints do not exist on the dev image (404/405). Contract deployment happens via `PolicySignRequest.addForsetiContractToUpload(contractSource)` in the browser signing flow (Step 4). VERIFIED (session-001).
303
+
304
+ **The response is base64-encoded, not raw binary.** You must decode before using:
305
+
306
+ ```js
307
+ // Backend proxy
308
+ const b64 = await tcRes.text();
309
+ const raw = Buffer.from(b64, "base64");
310
+ const bytes = Array.from(new Uint8Array(raw));
311
+ res.json({ policyBytes: bytes });
312
+
313
+ // Frontend
314
+ const data = await res.json();
315
+ const adminPolicyBytes = new Uint8Array(data.policyBytes);
316
+ ```
317
+
318
+ **Common mistake:** Treating the base64 string as raw bytes (passing each character's char code as a byte value). If admin policy bytes start with `[65, 81, 65, 65, ...]` (ASCII for "AQAA..."), you are passing base64 text as byte values instead of decoding it. The ORK will fail with `Index out of range` because the policy structure is garbage.
319
+
320
+ **Common mistake:** Malformed URL from trailing slash in `auth-server-url`. The adapter JSON `auth-server-url` may include a trailing slash (e.g., `http://localhost:8080/`). Constructing the admin policy URL without stripping it produces `http://localhost:8080//realms/...` (double slash), which returns HTML or 404 instead of policy bytes. Always normalize:
321
+ ```js
322
+ const baseUrl = config["auth-server-url"].replace(/\/+$/, "");
323
+ const adminPolicyUrl = `${baseUrl}/realms/${config.realm}/tide-policy-resources/admin-policy`;
324
+ ```
325
+
326
+ ---
327
+
328
+ ## Step 6: Set Up Dual Role Requirement (Voucher Gates + Forseti Contract Roles)
329
+
330
+ Even though policy-governed encryption uses the VVK and the Forseti contract uses a regular role for access control, the TideCloak voucher system independently gates whether encrypt/decrypt operations are even allowed for a session.
331
+
332
+ The voucher system (in `VoucherResource.java`) performs role-based voucher authorization **VERIFIED** (vendor confirmation, GAP-051 resolved):
333
+ - `signin` and `updateaccount`: always allowed
334
+ - `vendorsign` (encrypt): allowed if user has ANY `_tide_*` role, an authorizer role, or `tide-realm-admin`
335
+ - `vendordecrypt` (decrypt): allowed if user has a role matching `_tide_*.selfdecrypt` or `_tide_*.decrypt`
336
+
337
+ Without the voucher, the ORK refuses to participate before the Forseti contract even runs. Error: `"session ID ... has not been allowed a voucher action of type vendordecrypt"`
338
+
339
+ **Policy-based encryption does NOT require per-tag `_tide_{tag}.encrypt`/`.decrypt` roles** (vendor confirmation, batch-02 Q-10). The SDK skips the tag-role check when a `decryption_policy` is provided. Authorization is handled entirely by the Forseti contract, which can check any Keycloak role. You only need `_tide_*` roles to satisfy the voucher system.
340
+
341
+ You need TWO types of roles:
342
+
343
+ | Role Type | Example | Purpose |
344
+ |-----------|---------|---------|
345
+ | Voucher gate (generic) | `_tide_x.selfencrypt` / `_tide_x.selfdecrypt` | Enables the voucher action type. Use a single generic tag (e.g., `x`) for all policy-based encrypt/decrypt. Does NOT control who can decrypt. |
346
+ | Forseti contract role | `shared-data-access` | Checked by the contract's `ValidateExecutor`. Controls WHO can actually decrypt. Regular realm role, NOT `_tide_*`. |
347
+
348
+ ```json
349
+ // realm.json: need BOTH types of roles
350
+ {
351
+ "roles": {
352
+ "realm": [
353
+ { "name": "shared-data-access", "description": "Forseti contract role: who can encrypt/decrypt" },
354
+ { "name": "_tide_x.selfencrypt", "description": "Voucher gate: enables vendorsign (generic tag)" },
355
+ { "name": "_tide_x.selfdecrypt", "description": "Voucher gate: enables vendordecrypt (generic tag)" }
356
+ ]
357
+ }
358
+ }
359
+ ```
360
+
361
+ All three roles should be in the default composite role so all users get them. To restrict decryption access, remove `shared-data-access` from specific users (not the voucher gate roles). Use tag `x` in all `doEncrypt`/`doDecrypt` calls.
362
+
363
+ After adding roles to a running realm, users must log out and back in for their doken to pick up the new roles.
364
+
365
+ Note: The `SimpleTagBasedDecryptionContract` is one built-in default that checks `_tide_{tag}.encrypt`/`.decrypt` roles, but the standard Forseti flow uses custom contracts where authorization is contract-defined — no per-tag `_tide_` roles needed.
366
+
367
+ ---
368
+
369
+ ## Step 7: Store Signed Policy Bytes Server-Side
370
+
371
+ Signed policy bytes are needed by EVERY user who encrypts or decrypts. Store them on the server, not in localStorage (per-browser) or in-memory (lost on refresh).
372
+
373
+ ```
374
+ ENCRYPT (sender):
375
+ 1. getSignedPolicy() -> tries server, falls back to sign via enclave + store
376
+ 2. Returns { bytes, key }
377
+ 3. IAMService.doEncrypt(data, bytes)
378
+ 4. POST /messages with { encryptedMessage, policyKey: key }
379
+
380
+ DECRYPT (recipient):
381
+ 1. GET /messages/inbox -> each message has policyKey
382
+ 2. fetchSignedPolicy(token, msg.policyKey) -> fetches from server by key
383
+ 3. IAMService.doDecrypt(data, bytes)
384
+ ```
385
+
386
+ Store the policy key with each encrypted message so the recipient knows which signed policy to fetch. Different messages may use different policies (different contracts, roles, sharing groups).
387
+
388
+ ---
389
+
390
+ ## Group Lifecycle (CREATED -> POLICY_SETUP -> READY)
391
+
392
+ A group requires TWO signed policies before it is operational. Only a `tide-realm-admin` can sign these.
393
+
394
+ | Policy | Purpose | Without It |
395
+ |--------|---------|------------|
396
+ | Role policy (init-cert) | Enables VVK-signed UserContext creation when members are assigned | IGA can approve but ORKs will not produce a cryptographic proof |
397
+ | Encryption policy | Enables VVK encrypt/decrypt with scoped tag + Forseti contract | ORKs reject: `"Policy supplied has not been signed"` |
398
+
399
+ ```
400
+ Group lifecycle states:
401
+ CREATED -- role exists, role policy attached, no policies signed
402
+ POLICY_SETUP -- admin has signed one of the two policies
403
+ READY -- both policies signed, group is usable
404
+
405
+ Frontend gates:
406
+ - "Add Member" disabled until READY
407
+ - "Send" / encrypt disabled until READY
408
+ - Admin sees setup checklist:
409
+ [ ] Sign role policy (init-cert) -- approval popup
410
+ [ ] Sign encryption policy -- approval popup
411
+ [x] Group is ready for use
412
+ ```
413
+
414
+ Signing policies lazily (on first use) causes confusing errors for non-admin users who cannot sign policies themselves. Better UX: admin completes setup, then the group is ready.
415
+
416
+ **The two policies serve completely different purposes and are signed through different APIs:**
417
+
418
+ | Policy | What It Does | How to Sign | API |
419
+ |--------|-------------|-------------|-----|
420
+ | Role policy (init-cert) | Enables VVK-signed UserContext when members are assigned roles | `POST /tide-iga-provider/role-policy/{roleId}/init-cert` | IGA admin API |
421
+ | Encryption policy | Enables VVK encrypt/decrypt with Forseti contract | `PolicySignRequest.New(policy)` via enclave approval popup | Direct ORK signing flow |
422
+
423
+ ### Role Policy (init-cert) Signing Flow
424
+
425
+ ```js
426
+ // 1. Build the policy for role signing (same Policy constructor)
427
+ const policy = new Policy({
428
+ version: "3",
429
+ contractId: contractHash,
430
+ modelId: ["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"],
431
+ keyId: vendorId,
432
+ approvalType: ApprovalType.IMPLICIT,
433
+ executionType: ExecutionType.PRIVATE,
434
+ params: new Map([["Role", "shared-data-access"]]),
435
+ });
436
+
437
+ // 2. Same PolicySignRequest.New -> createTideRequest -> approval popup -> executeSignRequest
438
+ const policyRequest = PolicySignRequest.New(policy);
439
+ policyRequest.addForsetiContractToUpload(contractCode);
440
+ policyRequest.setCustomExpiry(604800);
441
+
442
+ const initialized = BaseTideRequest.decode(
443
+ await tc.createTideRequest(policyRequest.encode())
444
+ );
445
+ const approvals = await tc.requestTideOperatorApproval([
446
+ { id: "role-policy", request: initialized.encode() }
447
+ ]);
448
+ const approved = BaseTideRequest.decode(approvals[0].request);
449
+ approved.addPolicy(adminPolicyBytes);
450
+ const signatures = await tc.executeSignRequest(approved.encode(), true);
451
+
452
+ policy.signature = signatures[0];
453
+ const signedPolicyData = bytesToBase64(policy.toBytes());
454
+ const signedPolicySignature = bytesToBase64(signatures[0]);
455
+
456
+ // 3. DIFFERENCE: send to TideCloak's init-cert endpoint (not stored locally)
457
+ // Note: path is tide-iga-provider, not tide-admin. No doken field — pure storage endpoint.
458
+ await fetch(`/admin/realms/${realm}/tide-iga-provider/role-policy/${roleId}/init-cert`, {
459
+ method: "POST",
460
+ headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
461
+ body: JSON.stringify({
462
+ initCert: signedPolicyData,
463
+ initCertSig: signedPolicySignature
464
+ })
465
+ });
466
+ ```
467
+
468
+ ### Encryption Policy Signing Flow
469
+
470
+ Same steps 1-2 as above, but in step 3 store the signed bytes on YOUR server:
471
+
472
+ ```js
473
+ policy.signature = signatures[0];
474
+ const signedBytes = policy.toBytes();
475
+ await storeSignedPolicy(policyKey, Array.from(signedBytes));
476
+ ```
477
+
478
+ **What the init script can do (server-side, no browser):**
479
+ - Attach Forseti policy to a role: `PUT /tide-admin/ssh-policies`
480
+
481
+ These are prep steps. None produce VVK-signed bytes.
482
+
483
+ **What the frontend must do (requires admin's browser enclave):**
484
+ - Sign both policies via the `PolicySignRequest` flow above.
485
+
486
+ ---
487
+
488
+ ## The Complete Flow for Scoped Shared Encryption
489
+
490
+ ```
491
+ 1. ADMIN SETUP (one-time per group):
492
+ - Backend creates role: group-member:{groupId}
493
+ - Backend attaches Forseti policy to role: PUT /tide-admin/ssh-policies
494
+ - Frontend: admin signs role policy via approval popup (init-cert stored)
495
+ - Frontend: admin signs encryption policy via approval popup (stored on server)
496
+ - Both policies signed -- group transitions to READY
497
+
498
+ 2. USER ASSIGNMENT (per user, per scope -- MANUAL APPROVAL REQUIRED):
499
+ - Backend creates IGA draft (assigns role to user)
500
+ - Frontend shows pending change request to group admin
501
+ - Admin clicks Approve -- approval enclave popup -- signs UserContext
502
+ - Frontend submits signed result -- backend commits
503
+ - User refreshes token -- doken now includes group-member:{groupId}
504
+
505
+ 3. ENCRYPT (sender):
506
+ - Fetch signed encryption policy from server
507
+ - IAMService.doEncrypt([{ data, tags: ["group:{groupId}"] }], signedPolicy)
508
+ - ORKs verify: sender's doken has group-member:{groupId} in AccessProof
509
+ - VVK encrypts with scoped tag
510
+
511
+ 4. DECRYPT (recipient):
512
+ - Fetch signed policy from server (by policyKey stored with message)
513
+ - IAMService.doDecrypt([{ encrypted, tags: ["group:{groupId}"] }], signedPolicy)
514
+ - ORKs verify: recipient's doken has group-member:{groupId} in AccessProof
515
+ - VVK decrypts -- plaintext only in recipient's browser
516
+ ```
517
+
518
+ **Why this prevents unauthorized access:**
519
+ - Tags without a matching UserContext are rejected by the ORKs
520
+ - UserContexts are VVK-signed and bound to a specific user's `tideUserKey`
521
+ - A user cannot forge or reuse another user's UserContext
522
+ - Role assignment goes through IGA governance with manual approval enclave signing
523
+ - Backend cannot bypass approval -- `requiresApprovalPopup: true` is enforced
524
+
525
+ ---
526
+
527
+ ## IGA Role Assignment Flow (Manual Approval Required)
528
+
529
+ Role assignments that produce VVK-signed UserContexts MUST be manually approved through the approval enclave in the admin's browser. You cannot automate this from a backend script.
530
+
531
+ What happens when you try to auto-approve:
532
+
533
+ ```
534
+ POST /tide-admin/change-set/sign
535
+ Response: { "requiresApprovalPopup": true, "changeSetDraftRequests": "base64..." }
536
+ ```
537
+
538
+ The `requiresApprovalPopup: true` response means the backend CANNOT complete the signing. The VVK-signed UserContext binds a specific user's `tideUserKey` to specific roles. This signing must happen through the ORK threshold network via the Tide enclave running in the admin's browser.
539
+
540
+ **Always use `/batch` endpoints for approval:**
541
+
542
+ ```js
543
+ // 1. Backend creates IGA draft (role assignment)
544
+ POST /admin/realms/{realm}/users/{userId}/role-mappings/realm
545
+
546
+ // 2. Frontend fetches pending requests
547
+ GET /admin/realms/{realm}/tide-admin/change-set/users/requests
548
+
549
+ // 3. Frontend calls sign/batch (one call for all selected)
550
+ POST /admin/realms/{realm}/tide-admin/change-set/sign/batch
551
+ { changeSets: [{ changeSetId, changeSetType, actionType, policyRoleId? }] }
552
+
553
+ // 4. Collect all that need enclave, pass to ONE approval popup
554
+ const enclaveRequests = results
555
+ .filter(r => r.requiresApprovalPopup)
556
+ .map(r => ({ id: r.changesetId, request: base64ToBytes(r.changeSetDraftRequests) }));
557
+ const approvals = await tc.requestTideOperatorApproval(enclaveRequests);
558
+
559
+ // 5. Submit each signed review via add-review (FormData, NOT JSON)
560
+ POST /admin/realms/{realm}/tideAdminResources/add-review
561
+
562
+ // 6. Commit all in batch
563
+ POST /admin/realms/{realm}/tide-admin/change-set/commit/batch
564
+ { changeSets: [{ changeSetId, changeSetType, actionType }] }
565
+ ```
566
+
567
+ **The add-review endpoint uses FormData, not JSON:**
568
+
569
+ ```js
570
+ const formData = new FormData();
571
+ formData.append("changeSetId", id);
572
+ formData.append("actionType", actionType);
573
+ formData.append("changeSetType", changeSetType);
574
+ formData.append("requests", btoa(String.fromCharCode(...signedBytes)));
575
+ ```
576
+
577
+ After commit: the user must refresh their token (or re-login) for the new role to appear in their doken. Use `IAMService.forceUpdateToken()` or `useTideCloak().forceRefreshToken()`.
578
+
579
+ ---
580
+
581
+ ## CRITICAL: Import Paths and PolicySignRequest
582
+
583
+ These imports are the #1 source of policy-signing failures. Get them wrong and the code crashes before reaching the ORK.
584
+
585
+ **Package-boundary rule**: `@tidecloak/nextjs` is the Next.js hooks/provider layer. It does NOT export `Models`, `PolicySignRequest`, `BaseTideRequest`, `ApprovalType`, or `ExecutionType`. Importing them from `@tidecloak/nextjs` returns `undefined`, producing `Cannot destructure property 'Policy' of 'Models' as it is undefined`. Import `Models` from `@tideorg/js` and `PolicySignRequest` from `heimdall-tide`.
586
+
587
+ **Correct imports:**
588
+
589
+ ```js
590
+ // Models: from @tideorg/js (NOT @tidecloak/nextjs)
591
+ const { Models } = await import("@tideorg/js");
592
+ const { Policy, ApprovalType, ExecutionType, BaseTideRequest } = Models;
593
+ // Models.Policy is the Policy CLASS, not a namespace
594
+ // ApprovalType, ExecutionType are direct exports from Models alongside Policy
595
+ // NOT: const { ApprovalType } = Models.Policy; // WRONG -- Policy is a class, not a namespace
596
+
597
+ // PolicySignRequest: from heimdall-tide (NOT @tidecloak/nextjs)
598
+ const { PolicySignRequest } = await import("heimdall-tide");
599
+ const policyRequest = PolicySignRequest.New(policy);
600
+ policyRequest.addForsetiContractToUpload(CONTRACT_SOURCE);
601
+ policyRequest.setCustomExpiry(604800); // 1 week
602
+ ```
603
+
604
+ Do NOT try to build the policy signing request manually with `BaseTideRequest` + `TideMemory`. The ORK's `PolicySignRequestFactory` expects a specific internal structure that `PolicySignRequest.New(policy)` produces correctly. Building this manually produces different byte layouts that the ORK cannot parse.
605
+
606
+ **After createTideRequest, decode the result:**
607
+
608
+ ```js
609
+ // createTideRequest returns raw bytes. Decode back to BaseTideRequest
610
+ // before passing to approval. Skipping this causes serialization errors.
611
+ const initializedBytes = await tc.createTideRequest(policyRequest.encode());
612
+ const initializedRequest = BaseTideRequest.decode(initializedBytes);
613
+
614
+ // Pass the re-encoded request to approval
615
+ const approvalResults = await tc.requestTideOperatorApproval([
616
+ { id: "policy-sign", request: initializedRequest.encode() },
617
+ ]);
618
+ ```
619
+
620
+ ### SDK Internals (Reference)
621
+
622
+ - `@tidecloak/js` exports: `IAMService`, `TideCloak`, `RequestEnclave`, `ApprovalEnclaveNew`, `Tools`, `AdminAPI`
623
+ - `@tideorg/js` exports: `Models` (`Policy`, `BaseTideRequest`, `ApprovalType`, `ExecutionType`), `Contracts`
624
+ - `heimdall-tide` exports: `PolicySignRequest`
625
+ - `Tools` includes: `TideMemory` (Tide's serialization format)
626
+ - `IAMService._tc` gives access to the underlying `TideCloak` instance for `createTideRequest()` and `executeSignRequest()`
627
+ - `RequestEnclave.encrypt(data, policy)` switches flow: `"encrypt"` (self) vs `"policy encrypt"` (VVK) based on whether policy is provided
628
+ - Model IDs must match exactly what the ORK registry expects. Check `ModelRegistry.js` in `@tideorg/js/dist/Models/` for the correct `_name` and `_version`. Policy signing is `"Policy:1"` (not `"PolicySign:1"`).
629
+
630
+ ---
631
+
632
+ ## Common Mistakes That Cause Index out of range / PreSign Failures
633
+
634
+ 1. Destructuring `{ ApprovalType, ExecutionType }` from `Models.Policy` instead of `Models` (Models.Policy is the class, not a namespace)
635
+ 2. Building the request manually instead of using `PolicySignRequest.New(policy)`
636
+ 3. Not decoding `createTideRequest` result via `BaseTideRequest.decode()` before passing to approval
637
+ 4. Calling `addPolicy(policy.toBytes())` on the request during construction (attaching unsigned policy)
638
+ 5. Using wrong model ID (e.g. `"PolicySign:1"` instead of `"Policy:1"`)
639
+ 6. Using `modelId: "any"` instead of `["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"]` in Policy constructor
640
+ 7. Not using `PolicySignRequest.New(policy)` from `heimdall-tide` to construct the request
641
+
642
+ **If you get `Index out of range` in `Policy.From` during `PreSign`:** Do NOT try to debug the byte serialization. Instead, copy the exact working pattern from the forseti-crypto-quickstart reference implementation:
643
+
644
+ 1. Copy `~/forseti-crypto-quickstart/template-ts-app/components/AuthProvider.tsx` for the `initializeTideRequest`, `approveTideRequests`, `executeTideRequest` wrapper functions
645
+ 2. Copy `~/forseti-crypto-quickstart/template-ts-app/app/home/page.tsx` lines 740-830 for the policy creation, initialization, approval, and commit flow
646
+ 3. Copy `~/forseti-crypto-quickstart/template-ts-app/lib/forsetiContract.ts` for the contract source and `computeContractId` function
647
+
648
+ The forseti-crypto-quickstart is the reference implementation that works against the live ORK network. If your code diverges from this pattern in any way (different import paths, different function call order, different parameter passing), the ORK byte serialization will fail. Do not improvise.
649
+
650
+ ---
651
+
652
+ ## Verification Checklist
653
+
654
+ - [ ] `contractId` (SHA-512 of contract source) matches what is passed to `PolicySignRequest.addForsetiContractToUpload`
655
+ - [ ] Policy constructed with `ApprovalType.IMPLICIT` and `ExecutionType.PRIVATE` from `Models` (not `Models.Policy`)
656
+ - [ ] `modelId` is `["PolicyEnabledEncryption:1", "PolicyEnabledDecryption:1"]` (array, not string)
657
+ - [ ] `PolicySignRequest` imported from `heimdall-tide`
658
+ - [ ] `BaseTideRequest.decode()` called on `createTideRequest` result before approval
659
+ - [ ] Admin policy fetched from `GET /realms/{realm}/tide-policy-resources/admin-policy` (not the admin API endpoint)
660
+ - [ ] Admin policy bytes are base64-decoded (not raw char codes)
661
+ - [ ] Approval popup opens and admin approves
662
+ - [ ] `executeSignRequest` called with `waitForAll = true`
663
+ - [ ] `policy.signature = signatures[0]` assigned before `policy.toBytes()`
664
+ - [ ] Signed policy bytes stored server-side (not localStorage)
665
+ - [ ] Voucher gate roles assigned to users (generic `_tide_x.selfencrypt`/`_tide_x.selfdecrypt` for policy-based, or per-tag for self-encryption)
666
+ - [ ] Forseti contract role (`shared-data-access`) assigned to users
667
+ - [ ] Users logged out and back in after role changes
668
+ - [ ] `IAMService.doEncrypt(data, signedPolicyBytes)` succeeds
669
+ - [ ] `IAMService.doDecrypt(data, signedPolicyBytes)` succeeds for authorized user
670
+ - [ ] Decrypt fails for user without the Forseti contract role
671
+ - [ ] Group lifecycle: both role policy and encryption policy signed before marking READY
672
+
673
+ ---
674
+
675
+ ## Common Failures
676
+
677
+ ### "Policy supplied has not been signed"
678
+
679
+ **Cause:** Using raw `policy.toBytes()` without completing the 5-step signing flow, or only completing some steps.
680
+ **Fix:** Complete all 5 steps. Verify `policy.signature = signatures[0]` is called before `policy.toBytes()`.
681
+
682
+ ### "The type or namespace name 'IAccessPolicy' could not be found"
683
+
684
+ **Cause:** Missing `using Ork.Forseti.Sdk;` in the contract source.
685
+ **Fix:** Add the `using` directive as the first line of the contract.
686
+
687
+ ### "Policy refers to wrong contract"
688
+
689
+ **Cause:** `contractId` (SHA-512 hash) does not match the deployed contract source. Common reasons: whitespace or `using` differences between local and deployed copies, OR the hash is lowercase but the ORK stores it uppercase. The ORK comparison is **case-sensitive**.
690
+ **Fix:** Fetch `contractHash` from `GET /tide-admin/forseti-contracts` instead of computing locally. If computing locally, the hex string MUST be uppercase (`.toUpperCase()`).
691
+
692
+ ### `Index out of range` in `Policy.From` during `PreSign`
693
+
694
+ **Cause:** One of the 7 common mistakes listed above. Most likely: wrong import path for `ApprovalType`/`ExecutionType`, or not using `PolicySignRequest.New()`.
695
+ **Fix:** Copy the exact pattern from forseti-crypto-quickstart. Do not debug byte serialization.
696
+
697
+ ### "session ID ... has not been allowed a voucher action of type vendordecrypt"
698
+
699
+ **Cause:** User is missing a `_tide_*` voucher gate role that matches the tag used in encrypt/decrypt calls.
700
+ **Fix:** For self-encryption, assign `_tide_{tag}.selfdecrypt`. For policy-based encryption, assign a generic voucher gate (e.g., `_tide_x.selfdecrypt` with tag `x`). User must re-login after role assignment.
701
+
702
+ ### `{ status: "none" }` when fetching admin policy
703
+
704
+ **Cause:** Using the wrong endpoint `GET /admin/realms/{realm}/tide-admin/realm-policy`.
705
+ **Fix:** Use `GET /realms/{realm}/tide-policy-resources/admin-policy`.
706
+
707
+ ### Admin policy bytes start with `[65, 81, 65, 65, ...]`
708
+
709
+ **Cause:** Passing base64 text as raw byte values instead of decoding.
710
+ **Fix:** `Buffer.from(b64, "base64")` on the backend before sending to frontend.
711
+
712
+ ### `requiresApprovalPopup: true` from backend role assignment
713
+
714
+ **Cause:** Trying to auto-approve IGA role assignments from backend code.
715
+ **Fix:** IGA role assignments must go through the approval enclave in the admin's browser. Use the batch endpoint flow described in the IGA Role Assignment section.
716
+
717
+ ### Decrypt fails silently after adding roles
718
+
719
+ **Cause:** User's doken does not reflect the new roles yet.
720
+ **Fix:** Call `IAMService.forceUpdateToken()` or have the user re-login after role changes.
721
+
722
+ ---
723
+
724
+ ## Do Not Do This
725
+
726
+ ### Do Not Mix Self-Encryption with Policy-Governed Encryption
727
+
728
+ ```js
729
+ // WRONG: using doEncrypt from useTideCloak() for shared data
730
+ const { doEncrypt } = useTideCloak();
731
+ const encrypted = await doEncrypt([{ data: "shared", tags: ["shared"] }]);
732
+ // This uses self-encryption. Only you can decrypt. No policy, no sharing.
733
+ ```
734
+
735
+ Use `IAMService.doEncrypt(data, signedPolicyBytes)` for shared data.
736
+
737
+ ### Do Not Skip Voucher Gate Roles
738
+
739
+ Even with a valid Forseti contract role, the ORK refuses to participate without `_tide_*` voucher gate roles. Both role types are required. For policy-based encryption, a generic voucher gate (e.g., `_tide_x.selfencrypt`/`_tide_x.selfdecrypt`) is sufficient — you do not need per-tag `_tide_{tag}.encrypt`/`.decrypt` roles.
740
+
741
+ ### Do Not Sign Policies Lazily
742
+
743
+ Signing policies on first use causes confusing errors for non-admin users. Sign both policies during admin setup before marking the group as ready.
744
+
745
+ ### Do Not Store Unsigned Policy Bytes
746
+
747
+ If a signing attempt fails partway through and you store the result, all subsequent encrypt/decrypt calls silently fail. Only store bytes after verifying the full 5-step flow completed.
748
+
749
+ ---
750
+
751
+ ## References
752
+
753
+ - [add-rbac-nextjs.md](add-rbac-nextjs.md) - Role-based access control
754
+ - [protect-api-nextjs.md](protect-api-nextjs.md) - Server-side JWT verification
755
+ - Source: `~/forseti-crypto-quickstart/template-ts-app/` - Reference implementation (simplest working example)
756
+ - Source: `~/tidecloak-test-cases/test-app/src/lib/database/policyDb.ts` - EXPLICIT + PRIVATE multi-party approval reference