@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,816 @@
1
+ # Tide Core Concepts
2
+
3
+ Agent-operable definitions of Tide's cryptographic model and architecture.
4
+
5
+ **Rule**: Official Tide documentation uses marketing-heavy terminology. This file uses agent-facing names with official terms in parentheses.
6
+
7
+ ---
8
+
9
+ ## Never-Whole-Key Model (Ineffable Cryptography)
10
+
11
+ **What it is**: Cryptographic keys never exist in complete form at any point in their lifecycle. Keys are born fragmented, used blind, and destroyed atomically. **VERIFIED** (whitepaper tier1-article2, Architecture.md)
12
+
13
+ **Why it matters**: No single point of compromise can extract a complete key. No admin, server, ORK node, or attacker can forge tokens or decrypt data alone.
14
+
15
+ **How it works**:
16
+ - Keys generated via Distributed Key Generation (DKG) across ORK nodes using Nested Shamir Secret Sharing
17
+ - Each ORK holds one shard; threshold T ORKs must cooperate for any operation
18
+ - Shards never leave their ORK; operations use threshold signatures and blind protocols
19
+ - Only exception: Ragnarok (Fabric offboarding) deliberately reconstructs keys locally
20
+
21
+ **Agent implication**: Do not implement local key generation, storage, or backup. All cryptographic operations delegate to Fabric via TideCloak.
22
+
23
+ ---
24
+
25
+ ## Fabric (Cybersecurity Fabric)
26
+
27
+ **What it is**: Decentralized network of ORK nodes that collectively perform cryptographic operations. **VERIFIED** (intro.md, Architecture.md)
28
+
29
+ **Official name**: "Cybersecurity Fabric" is marketing. Use "Fabric" or "ORK network" in code and docs.
30
+
31
+ **Deployment configurations**:
32
+ - Mainnet: 20 ORKs, T=14 threshold **VERIFIED** (whitepaper tier1-article2)
33
+ - Test: 5 ORKs, T=3 threshold **VERIFIED** (keylessh `start.sh`)
34
+ - Threshold is configurable via `TIDE_VENDOR_THRESHOLD_SIGNING` / `TIDE_VENDOR_THRESHOLD_TOTAL` env vars **VERIFIED** (keylessh `start.sh`)
35
+
36
+ **Security assumption**: System remains secure if fewer than 30% of ORKs are compromised (honest minority model). On mainnet: safe if <7 of 20 ORKs compromised. **VERIFIED** (Threat-model.md)
37
+
38
+ **Agent implication**: Never hardcode 14/20. Treat threshold as deployment-variable.
39
+
40
+ ---
41
+
42
+ ## ORK (Orchestrated Recluders of Keys)
43
+
44
+ **What it is**: Individual nodes in the Fabric. Each holds one shard of distributed keys. **VERIFIED** (tier1-article2)
45
+
46
+ **Official name**: "Orchestrated Recluders of Keys" is a backronym. Use "ORK" or "ORK node".
47
+
48
+ **Responsibilities**:
49
+ - Store one key shard per distributed key
50
+ - Perform threshold cryptographic operations (signing, decryption, PRISM verification)
51
+ - Independently verify claims before partial-signing
52
+ - Execute Forseti policy contracts
53
+ - Coordinate via TWELVE MAP directory service
54
+
55
+ **Agent implication**: ORK endpoints are discovered via adapter JSON (`homeOrkUrl`) or TWELVE MAP, not hardcoded.
56
+
57
+ ---
58
+
59
+ ## Threshold Password Authentication (BYOiD)
60
+
61
+ **What it is**: Authentication model where user password is verified via threshold protocol without any party learning the password. No password hashes stored anywhere. **VERIFIED** (byoid.md, tier2-protocol-prism.mdx)
62
+
63
+ **Official name**: "Bring Your Own Identity (BYOiD)" sounds like federated login. It is not. It is threshold password authentication using PRISM (Threshold Oblivious Pseudorandom Function).
64
+
65
+ **How it works**:
66
+ 1. User enters password in browser
67
+ 2. SWE coordinates PRISM protocol across T+ ORKs
68
+ 3. Each ORK applies its CMK shard to the password challenge
69
+ 4. Browser assembles partial results into authentication proof
70
+ 5. No ORK, server, or attacker learns the password
71
+
72
+ **Agent implication**: Standard login flows (`doLogin()`, `useTideCloak()`) work transparently. Do not implement custom password verification. PRISM is SDK-internal.
73
+
74
+ **Common confusion**: BYOiD is NOT OAuth2/OIDC federated login. It uses OIDC protocol framing but authentication is threshold-verified, not server-checked.
75
+
76
+ ---
77
+
78
+ ## Threshold JWT Signing (VVK)
79
+
80
+ **What it is**: JWT tokens are signed by threshold protocol across ORKs, not by TideCloak server. Each VVK ORK independently verifies all claims before partial-signing. **VERIFIED** (tier2-protocol-vvk-jwt-signing.mdx)
81
+
82
+ **How it works**:
83
+ 1. TideCloak requests JWT signing from Fabric
84
+ 2. Each VVK ORK performs 12-gate verification chain independently **INFERRED** (whitepaper describes gates; specific gate list not in SDK docs - GAP-025)
85
+ 3. If all gates pass, ORK produces partial signature
86
+ 4. T+ partial signatures combine into final JWT
87
+ 5. If any ORK rejects a claim, signing fails
88
+
89
+ **Agent implication**:
90
+ - JWT verification uses embedded JWKS from adapter JSON only. Do not use `createRemoteJWKSet`. **VERIFIED** (keylessh `tideJWT.ts`)
91
+ - Adapter JSON field `jwk: { keys: JWK[] }` contains public keys for local verification **VERIFIED** (keylessh `tidecloakConfig.ts`)
92
+ - Server-side verification pattern: extract JWT → verify signature against adapter JWKS → check `iss`, `azp`, `exp`, `iat` → validate roles **VERIFIED** (keylessh `tideJWT.ts`)
93
+
94
+ **Common confusion**: This is NOT standard Keycloak JWT signing. TideCloak delegates signing to Fabric. Do not fetch JWKS from the remote `{realm}/protocol/openid-connect/certs` endpoint. Use only the embedded `jwk` from `tidecloak.json`. See I-04.
95
+
96
+ ---
97
+
98
+ ## IGA (Identity Governance & Administration)
99
+
100
+ **What it is**: Quorum-enforced governance layer. Admin changes to roles, users, clients require multi-admin approval sealed by VVK threshold signatures. **VERIFIED** (IGA.mdx, SetupIGA.md, keylessh `init-tidecloak.sh`)
101
+
102
+ **Official docs weakness**: Reads as "approval workflow" without emphasis on threshold cryptography. IGA approval is cryptographic enforcement, not procedural.
103
+
104
+ **How it works**:
105
+ 1. Admin proposes change (create user, modify role, etc.) → draft created automatically for 18 specific admin API actions **VERIFIED** (vendor confirmation, GAP-041 resolved). Not every mutating endpoint creates a draft — many operations (updating user attributes, updating realm settings, creating groups, managing identity providers) execute immediately. Master realm and IGA-disabled realms are always exempt.
106
+ 2. Change-set enters approval queue with `draftRecordId`, `changeSetType`, `actionType`
107
+ 3. Required quorum of admins approve: `max(1, floor(TotalAdmins * 0.7))` **VERIFIED** (SetupIGA.md)
108
+ 4. Approved change is sealed by VVK threshold signatures (Authorization Proofing)
109
+ 5. Change is committed to realm
110
+ 6. VVK ORKs verify future JWT claims against these proofs
111
+
112
+ **Change-set lifecycle** **VERIFIED** (CHANGE_REQUEST_API.md):
113
+ ```
114
+ DRAFT → PENDING → APPROVED → ACTIVE
115
+ → DENIED
116
+ (cancel at any stage)
117
+ ```
118
+
119
+ **Change-set API endpoints** **VERIFIED** (CHANGE_REQUEST_API.md, all exemplar init scripts):
120
+ ```
121
+ GET /admin/realms/{realm}/tide-admin/change-set/counts → lightweight counts
122
+ GET /admin/realms/{realm}/tide-admin/change-set/all/requests → all change requests
123
+ GET /admin/realms/{realm}/tide-admin/change-set/{type}/requests → by type (users, roles, clients, groups)
124
+ POST /admin/realms/{realm}/tide-admin/change-set/sign/batch → approve (sign)
125
+ POST /admin/realms/{realm}/tide-admin/change-set/commit/batch → commit approved changes
126
+ POST /admin/realms/{realm}/tide-admin/change-set/cancel/batch → cancel pending changes
127
+ POST /admin/realms/{realm}/tideAdminResources/add-review → submit enclave-signed review (multipart)
128
+ POST /admin/realms/{realm}/tideAdminResources/add-rejection → reject (multipart/form-data, NOT JSON)
129
+ ```
130
+
131
+ **Signing modes** **VERIFIED** (CHANGE_REQUEST_API.md):
132
+ - **FirstAdmin**: Signs immediately with VRK. `requiresApprovalPopup: false`. Used during initial setup before multi-admin is configured.
133
+ - **MultiAdmin**: Returns enclave challenge. `requiresApprovalPopup: true`. Client must decode `changeSetDraftRequests` (base64), present to enclave for admin to sign, then submit via `add-review`.
134
+
135
+ **Policy resolution**: When signing, the system uses the `tide-realm-admin` role's policy by default. Pass `policyRoleId` in the sign request to use a role-specific policy instead. **VERIFIED** (CHANGE_REQUEST_API.md)
136
+
137
+ **Note**: The SSH policy `threshold` (per-role approval count for Forseti policy signing) is distinct from the IGA admin quorum (`max(1, floor(N*0.7))`). These are independent systems.
138
+
139
+ **Agent implication**:
140
+ - Enable IGA via `POST /admin/realms/{realm}/tide-admin/toggle-iga` **VERIFIED** (all exemplar init scripts)
141
+ - Canonical setup ordering: license (`setUpTideRealm`) → IGA (`toggle-iga`) → E2EE. This is the only valid sequence. **VERIFIED** (vendor confirmation, batch-02 Q-04, A-21/A-22 resolved). License required for all Tide features (BYOiD login, signing, IGA, vouchers). IGA required for E2EE because `jwk` only injected when IGA enabled. BYOiD-only (license + no IGA) is a valid deployment mode but has no E2EE. DPoP is standard Keycloak, not gated by Tide licensing.
142
+ - After IGA commit, new roles are NOT immediately visible in the doken. Token refresh is required, and propagation may take up to 120s **VERIFIED** (test-cases F3, F6)
143
+ - Change requests have a hardcoded 1-month (2628000 seconds) cryptographic expiry enforced by the Tide enclave. After this window, signing requests are rejected. No automatic database cleanup — stale drafts persist until manually cancelled. Expiry not configurable. **VERIFIED** (vendor confirmation, batch-02 Q-08, A-25 resolved)
144
+ - **ACTIVE vs DRAFT distinction** **VERIFIED** (vendor confirmation, GAP-041 resolved): Role creation (`POST .../roles`) and default-role assignment get `ACTIVE` status (recorded for audit but non-blocking, no sign/commit needed). All other draft-triggering actions get `DRAFT` status requiring quorum sign/commit. This matters for automation: agents creating roles do not need sign/commit, but agents assigning those roles to users do.
145
+
146
+ **Common confusion**: IGA is NOT generic approval workflow. Approval is sealed by threshold signatures. No single admin can bypass.
147
+
148
+ ---
149
+
150
+ ## Threshold E2EE (Hermetic E2EE)
151
+
152
+ **What it is**: End-to-end encryption where decryption requires threshold participation from Fabric ORKs. Plaintext exists only on user's device. **VERIFIED** (hermetic-e2ee.md, tier2-protocol-hermetic-e2ee.mdx)
153
+
154
+ **Official name**: "Hermetic E2EE" is marketing. Use "threshold E2EE".
155
+
156
+ **How it works** **VERIFIED** (vendor confirmation, GAP-009 resolved):
157
+ 1. User calls `doEncrypt(tag, plaintext)` in browser
158
+ 2. A fresh random encryption key is generated per call (<32B: fresh ElGamal scalar; >=32B: fresh 32-byte AES key via `crypto.getRandomValues()`). No key reuse across calls.
159
+ 3. Data encrypted locally; the per-call key is ElGamal-encrypted via threshold across ORKs
160
+ 4. Ciphertext (TideMemory envelope) stored in application
161
+ 5. User calls `doDecrypt(tag, ciphertext)` → Fabric threshold-decrypts the per-call key → plaintext recovered locally
162
+ 6. Separately, a session key (`t.ssk`) in the doken authenticates the user to ORKs but does NOT encrypt data. Lives in ORK enclave iframe; destroyed on tab close/logout. Adapters have no encryption key state to manage.
163
+
164
+ **Tag-based role enforcement** **VERIFIED** (e2ee.md):
165
+ - Roles: `_tide_<tag>.selfencrypt` and `_tide_<tag>.selfdecrypt`
166
+ - Enforcement is cryptographic: Fabric won't decrypt without role proof in JWT
167
+ - 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)
168
+
169
+ **Agent implication**:
170
+ - Use SDK methods `doEncrypt()`/`doDecrypt()` **VERIFIED** (SDK references)
171
+ - Both `Uint8Array` (binary) and string inputs supported **VERIFIED** (vendor confirmation, GAP-013 resolved). Strings are converted via `TextEncoder`. Base64 encoding is used for JSON transport (~33% overhead).
172
+ - No application-level maximum payload size **VERIFIED** (vendor confirmation, GAP-013 resolved). Payloads under 32 bytes are encrypted directly with ElGamal; payloads of 32 bytes or more use a hybrid scheme (AES-256-GCM for the data, ElGamal for the symmetric key). Practical transmission ceiling is the ASP.NET Core Kestrel default request body limit (~28.6 MB), reconfigurable per deployment.
173
+ - Self-encryption and policy-based encryption share the same size characteristics.
174
+ - Ciphertext is a TideMemory-serialized envelope (SerializedField v1). Returns base64 string when input was string, raw `Uint8Array` when input was `Uint8Array`. Overhead ~157B (small data) / ~217B (large data) before base64. Format versioned (v1), stable within major SDK version. **VERIFIED** (vendor confirmation, batch-02 Q-05, A-28 resolved)
175
+ - E2EE requires online Fabric access; no offline decryption **VERIFIED** (hermetic-e2ee.md)
176
+ - Both `selfencrypt` AND `selfdecrypt` roles needed to round-trip **INFERRED** (roles are separate; no source states granting one implies the other - A-27)
177
+
178
+ **Common confusion**: This is NOT client-side encryption with server-stored keys. Decryption requires live Fabric threshold participation.
179
+
180
+ ---
181
+
182
+ ## SWE (Secure Web Enclave)
183
+
184
+ **What it is**: Browser-delivered JavaScript module that coordinates cryptographic operations. Untrusted dealer: coordinates but cannot extract keys or forge proofs. Verified by SRI hash. **VERIFIED** (SWE.md, tier1-article7)
185
+
186
+ **Official name**: "Secure Web Enclave" oversells trust level. SWE is explicitly an untrusted dealer. Use "SWE (untrusted dealer)" in docs to prevent confusion.
187
+
188
+ **How it works**:
189
+ 1. Browser loads SWE iframe from Tide domain
190
+ 2. SWE code is SRI-verified: `integrity="sha384-..."` hash must match
191
+ 3. SWE coordinates with ORKs but never learns complete keys or plaintext
192
+ 4. ORKs verify SWE's instructions independently; untrusted SWE cannot bypass threshold enforcement
193
+
194
+ **CSP requirements** **VERIFIED** (vendor confirmation, GAP-028 resolved):
195
+ ```
196
+ frame-src 'self' *
197
+ ```
198
+ `frame-src '*'` required for ORK re-homing. Without correct CSP, SWE iframe silently fails.
199
+
200
+ **Agent implication**:
201
+ - Include `silent-check-sso.html` in `public/` for silent token refresh **VERIFIED** (docs + keylessh)
202
+ - Do not modify or vendor SWE code; SRI hash verification will fail
203
+ - SWE failure symptoms: login hangs, E2EE operations timeout, no visible errors (check browser console for CSP violations)
204
+
205
+ ---
206
+
207
+ ## Forseti Policy Engine
208
+
209
+ **What it is**: Programmable policy engine. C# contracts run in sandboxed VmHost on every ORK. Policy decisions are majority-enforced. **VERIFIED** (Forseti.mdx, tier2-protocol-forseti.mdx, keylessh `sshPolicy.ts`)
210
+
211
+ **How it works**:
212
+ 1. Application creates Forseti contract (C# code) defining policy rules
213
+ 2. Contract is submitted to ORKs via `createTideRequest()`
214
+ 3. Each ORK compiles and executes contract in sandbox
215
+ 4. Majority of ORKs must approve for operation to proceed
216
+ 5. No single ORK bypass; compromised ORK cannot override policy
217
+
218
+ **Contract structure** **VERIFIED** (keylessh `sshPolicy.ts`):
219
+ ```typescript
220
+ {
221
+ contract: "using System; ...", // C# code
222
+ modelName: "BasicCustom<APP>:BasicCustom<1>", // model ID
223
+ authFlow: "Policy:1", // protocol version
224
+ authorizer: tc.doken, // VVK-signed session token
225
+ challengeData: { ... } // application-specific data
226
+ }
227
+ ```
228
+
229
+ **Model IDs** **VERIFIED** (keylessh `sshPolicy.ts`):
230
+ - `BasicCustom<X>:BasicCustom<1>` - implicit auth, no popup
231
+ - `DynamicCustom<X>:DynamicCustom<1>` - dynamic challenge data
232
+ - `DynamicApprovedCustom<X>:DynamicApprovedCustom<1>` - requires operator approval popup
233
+
234
+ **REQUIRES_RUNTIME_VALIDATION**: Single-app evidence. `<X>` is app-specific (keylessh uses `<SSH>`). Prefix pattern may be Forseti convention.
235
+
236
+ **Built-in default contracts** **VERIFIED** (vendor confirmation, GAP-046 resolved):
237
+
238
+ Four pre-registered contracts in the ORK factory — no need to upload via the contract deployment API:
239
+
240
+ | Contract | Purpose | Validates | Parameters |
241
+ |----------|---------|-----------|------------|
242
+ | `GenericResourceAccessThresholdRoleContract` (`GenericResourceAccessThresholdRole:1`) | Role-gated multi-approval on **client roles** | Approvers only | `role` (string), `resource` (string), `threshold` (number) |
243
+ | `GenericRealmAccessThresholdRoleContract` | Same but checks **realm roles** | Approvers only | `role`, `resource`, `threshold` |
244
+ | `SimpleTagBasedDecryptionContract` | Tag-based decryption. Checks `_tide_{tag}.encrypt`/`.decrypt` roles. One built-in option, not the standard Forseti flow. | Data + Executor | tag from data context |
245
+ | `HederaTX` | Hedera transaction contract | Unknown | Unknown |
246
+
247
+ Use default contracts for simple role-gated multi-approval. Write custom contracts for executor validation, data validation, or complex logic. `GenericResourceAccessThresholdRoleContract` only validates **approvers** (not executor); its `ValidateData` is a no-op and `ValidateExecutor` is not implemented.
248
+
249
+ **C# contract API surface** **VERIFIED** (forseti-crypto-quickstart, tidewarden, test-cases):
250
+
251
+ Every contract implements `IAccessPolicy` from `Ork.Forseti.Sdk` and defines up to three validation methods:
252
+
253
+ | Method | When it runs | Context object | Key properties |
254
+ |--------|-------------|---------------|----------------|
255
+ | `ValidateData(DataContext ctx)` | Always | `ctx.RequestId` (e.g. `"PolicyEnabledEncryption:1"`), `ctx.Data` (positional byte buffer), `ctx.DynamicData`, `ctx.Policy.ExecutionType`, `ctx.Policy.ApprovalType` | |
256
+ | `ValidateApprovers(ApproversContext ctx)` | When `ApprovalType.EXPLICIT` | `ctx.Dokens` (array of doken bytes) | `DokenDto.WrapAll(ctx.Dokens)` wraps all approvers |
257
+ | `ValidateExecutor(ExecutorContext ctx)` | When `ExecutionType.PRIVATE` | `ctx.Doken` (single doken bytes), `ctx.DynamicData` | `new DokenDto(ctx.Doken)` wraps executor |
258
+
259
+ **Data access in contracts**: `ctx.Data` is a positional byte buffer, NOT key-value. Read fields with `data.GetValue(index)` or `data.TryGetValue(index, out var field)`. Tags are extracted by iterating indices.
260
+
261
+ **TideMemory wire format**: Fields are packed sequentially — each field is a 4-byte little-endian length prefix followed by that many data bytes. `ctx.DynamicData` uses the same format; read with `TryReadField(buffer, index, out result)`.
262
+
263
+ **Executor validation pattern**:
264
+ ```csharp
265
+ var executor = new DokenDto(ctx.Doken);
266
+ return Decision
267
+ .RequireNotExpired(executor)
268
+ .RequireRole(executor, roleName); // 2-arg: realm role
269
+ // or: .RequireRole(executor, resource, role); // 3-arg: client role
270
+ ```
271
+
272
+ **Approver validation pattern**:
273
+ ```csharp
274
+ var approvers = DokenDto.WrapAll(ctx.Dokens);
275
+ return Decision
276
+ .Require(approvers.Count > 0, "No approvers")
277
+ .RequireAnyWithRole(approvers, resource, role);
278
+ ```
279
+
280
+ **Direction detection**: `ctx.RequestId == "PolicyEnabledEncryption:1"` for encrypt, `"PolicyEnabledDecryption:1"` for decrypt. Contract must check this to apply direction-specific logic.
281
+
282
+ **Error surface**: `PolicyDecision.Deny("message")` — deny messages are string-based and propagated to the client. **VERIFIED** (test-cases F11 negative tests)
283
+
284
+ **Forseti sandbox security model** **VERIFIED** (vendor confirmation, GAP-008 resolved): Five-layer model. Contracts compiled with Roslyn against .NET 8.0 references plus three SDK assemblies (`Ork.Forseti.Sdk`, `Cryptide`, `Ork.Shared`). After compilation, IL vetting enforces a block-list of forbidden namespace prefixes: `System.IO`, `System.Net`, `System.Diagnostics`, `System.Threading`, `System.Reflection`, `System.Runtime.InteropServices`, `System.Reflection.Emit`, `Microsoft.Win32`. `System.Console` and `System.Runtime.CompilerServices.Unsafe` are always hard-blocked. Non-deterministic calls (`DateTime.Now`, `Guid.NewGuid`, `Random`, etc.) blocked by default. Static constructors banned. A contract referencing a blocked namespace compiles but fails IL vetting with `BadPolicy.ForbiddenCall` at upload time. Each policy runs in an isolated `AssemblyLoadContext` inside a separate VmHost process with OS-level CPU/memory limits and gas metering (default 50,000 gas). `Claim(key)` = 5 gas, `Log(message)` = 25-30 gas. Throws `OutOfGasException` when exhausted.
285
+
286
+ **Doken delivery**: Doken is an extra field (`"doken"`) in the OIDC token endpoint response body. Standard OIDC libraries discard unknown fields. To capture it, intercept the raw HTTP response before the OIDC library deserializes it. Doken is delivered at both initial login and token refresh. **VERIFIED** (tidewarden `DokenCapturingClient`)
287
+
288
+ **Agent implication**:
289
+ - Gas metering: default 50,000 gas. `Claim(key)` = 5 gas, `Log(message)` = 25-30 gas. Throws `OutOfGasException` when exhausted. **VERIFIED** (vendor confirmation, GAP-008 resolved)
290
+ - Sandbox: five-layer model fully documented. Namespace block-list enforced via IL vetting. **VERIFIED** (vendor confirmation, GAP-008 resolved)
291
+ - Contract errors propagated to client as string messages: `PolicyDecision.Deny("message")`, `BadPolicy.ForbiddenCall:{target}`, `BadPolicy.EntryTypeNotFound`, `BadPolicy.BudgetExceeded`, `VmHost.Timeout`, `OutOfGasException`. ORK error codes are not systematically surfaced — they arrive as opaque strings. **VERIFIED** (vendor confirmation, GAP-004 resolved). Internal ORK compilation error taxonomy still opaque (GAP-018).
292
+ - Doken accessed via `tc.doken` property **VERIFIED** (keylessh `tideSsh.ts`)
293
+ - Doken is VVK-signed (EdDSA), session-bound. **VERIFIED** (vendor confirmation, GAP-017 resolved)
294
+ - Doken `exp` copied directly from SSO access token `exp` at issuance — they share the same expiration. Auto-reissued when access token is refreshed; SDK handles transparently including mid-operation refresh via `dokenRefreshCallback`. No independent doken TTL configuration.
295
+ - Doken revocation: operates at the signature level via per-user blocklist (`RevokedAuthorizerService`). No explicit "revoke this doken" API.
296
+ - Role claims (`realm_access`, `resource_access`) are snapshot into the doken at issuance and only update on the next token refresh — this explains the up to 120s delay between IGA role changes and doken reflection.
297
+ - A fully-expired doken cannot be refreshed — the user must reauthenticate. Refresh must happen before expiry, not after.
298
+
299
+ **Common confusion**: Forseti is NOT authorization middleware config. Real C# contracts execute in every ORK. No single bypass point.
300
+
301
+ ---
302
+
303
+ ## DPoP (Demonstration of Proof-of-Possession)
304
+
305
+ **What it is**: RFC 9449 standard. Binds access tokens to a client key pair. Server verifies proof on every request. **VERIFIED** (RFC 9449, keylessh `auth.ts`, `AuthContext.tsx`)
306
+
307
+ **TideCloak DPoP supports two signing algorithms** **VERIFIED** (vendor confirmation, GAP-049 resolved):
308
+ - **ES256** (ECDSA P-256) — **default**. Recommended for new projects due to universal browser WebCrypto support.
309
+ - **EdDSA** (Ed25519) — supported, with automatic fallback logic for browsers that lack Ed25519.
310
+
311
+ The algorithm is negotiated at runtime via the server's `dpop_signing_alg_values_supported` OIDC metadata field. Apps may override the default with `useDPoP.alg`. ES384/ES512 are declared in types but not implemented (would throw at runtime).
312
+
313
+ ```typescript
314
+ // Recommended for new projects (ES256 is the default)
315
+ useDPoP: { mode: 'strict', alg: 'ES256' }
316
+ // EdDSA also supported
317
+ useDPoP: { mode: 'strict', alg: 'EdDSA' }
318
+ ```
319
+
320
+ **DPoP enablement**: DPoP is configured per-client in TideCloak via the client attribute `"dpop.bound.access.tokens": "true"`. This is NOT a realm-wide setting. **VERIFIED** (test-cases realm.json)
321
+
322
+ **DPoP client SDK**: DPoP requires the `TideCloak` class (not `IAMService`) with `enableDpop: true` in init options:
323
+ ```typescript
324
+ const tc = new TideCloak({ url, realm, clientId, vendorId, homeOrkUrl, ... });
325
+ await tc.init({ onLoad: "check-sso", pkceMethod: "S256", enableDpop: true });
326
+ // tc.secureFetch() auto-attaches DPoP headers
327
+ ```
328
+ **VERIFIED** (test-cases `DPoPAuthProvider.tsx`)
329
+
330
+ **Server-side verification pattern** **VERIFIED** (keylessh `server/auth.ts`, test-cases `dpop-protected/route.ts`):
331
+ 1. Extract `Authorization: DPoP <token>` (NOT `Bearer`)
332
+ 2. Extract `DPoP` header (the proof JWT)
333
+ 3. Verify JWT structure: `typ: "dpop+jwt"`, `alg` matches expected (EdDSA or ES256)
334
+ 4. Verify signature against `jwk` in DPoP proof
335
+ 5. Check `htm` (HTTP method) matches request method
336
+ 6. Check `htu` (HTTP URI) matches request URL (no query string)
337
+ 7. Check `iat` timestamp (120s freshness window)
338
+ 8. Check `jti` not replayed (2-min TTL in-memory cache)
339
+ 9. Extract access token, verify `cnf.jkt` matches DPoP proof thumbprint
340
+
341
+ Alternative: use `oauth2-dpop` npm package + `jose` for simpler verification. **VERIFIED** (test-cases `dpop-protected/route.ts`)
342
+
343
+ **Agent implication**:
344
+ - DPoP required for Tide's full security guarantees **VERIFIED** (vendor confirmation, GAP-032 resolved). Disabling degrades security.
345
+ - ES256 is the default algorithm. EdDSA supported with automatic fallback. ES384/ES512 not implemented.
346
+ - SDK provides DPoP client-side via `TideCloak.secureFetch()`; server must verify proof
347
+ - Replay protection requires in-memory `jti` cache with TTL
348
+
349
+ **Common confusion**: DPoP is NOT bearer token. Authorization header uses `DPoP` scheme, not `Bearer`. Each request requires fresh proof. Do not cache DPoP headers.
350
+
351
+ ---
352
+
353
+ ## Adapter JSON
354
+
355
+ **What it is**: Configuration file exported from TideCloak Admin Console. Extends Keycloak adapter format with Tide-specific fields. **VERIFIED** (keylessh `tidecloakConfig.ts`)
356
+
357
+ **Tide extensions**:
358
+ - `jwk: { keys: JWK[] }` - Embedded JWKS for local JWT verification (only present when IGA is enabled) **VERIFIED**
359
+ - `vendorId: string` - Tide vendor identifier **VERIFIED**
360
+ - `homeOrkUrl: string` - Home ORK endpoint **VERIFIED**
361
+ - `client-origin-auth-{origin}: string` - ORK-network-produced signature attesting a web origin is authorized for this client. Required for SDK enclave initialization. One entry per allowed web origin per client. No browser-specific variants. **VERIFIED** (vendor confirmation, GAP-048 resolved)
362
+
363
+ **Storage locations** **VERIFIED** (keylessh `tidecloakConfig.ts`, bridge configs):
364
+ - File: `data/tidecloak.json` (default)
365
+ - Env var: `CLIENT_ADAPTER` (JSON string)
366
+ - Env var: `TIDECLOAK_CONFIG_B64` (base64-encoded JSON)
367
+
368
+ **Agent implication**:
369
+ - `jwk` field is only present when IGA is enabled on the realm. Validate its presence at startup.
370
+ - Adapter JSON enables local JWT verification. Do not use `createRemoteJWKSet` or fetch JWKS remotely. If `jwk` is missing, re-export adapter with IGA enabled. (I-04)
371
+ - Export via vendor endpoint: `GET /admin/realms/{realm}/clients/{client-id}/vendorResources/get-installations-provider?providerId=keycloak-oidc-keycloak-json` **VERIFIED** (vendor confirmation, GAP-044 resolved). There is a single provider ID: `keycloak-oidc-keycloak-json`. The string `tidecloak-oidc-keycloak-json` does not exist in the codebase and should not be used.
372
+ - The config loader must select the `client-origin-auth-{window.location.origin}` entry and pass it to the SDK as `clientOriginAuth`. If no matching entry exists, enclave initialization will fail. **VERIFIED** (vendor confirmation, GAP-048 resolved)
373
+
374
+ **Common confusion**: Adapter JSON is NOT standard Keycloak format. Do not use generic Keycloak adapter parsers.
375
+
376
+ ---
377
+
378
+ ## Key Types (Theory Backing)
379
+
380
+ Most key types are theory-only; not directly referenced in SDK code.
381
+
382
+ | Key Type | Purpose | SDK Visibility | Source |
383
+ |----------|---------|----------------|--------|
384
+ | **CMK** (Consumer Master Key) | User identity key for BYOiD auth | Transparent to SDK | tier2-protocol-account-creation-keygen.mdx |
385
+ | **VVK** (Vendor Verifiable Key) | Organization JWT signing key | `vendorId` in adapter JSON | tier2-protocol-vvk-jwt-signing.mdx |
386
+ | **CVK** (Consumer Vendor Key) | Per-user personal authority key for Forseti, E2EE | `executeSignRequest()`, `doEncrypt()`/`doDecrypt()` | tier2-protocol-cvk-session.mdx, keylessh `tideSsh.ts` |
387
+ | **VRK** (Vendor Random Key) | Monthly TideCloak-Fabric payment key | Theory-only | tier2-protocol-vendor-licensing.mdx |
388
+ | **DVK** (Device Vendor Key) | Hardware biometric key on authenticator app | Theory-only | tier2-protocol-authenticator-app.mdx |
389
+ | **BRK** (Browser Key) | localStorage cross-verification key | Theory-only | tier2-protocol-authenticator-app.mdx |
390
+ | **RGK** (Ragnarok Key) | Offboarding reconstruction key | Theory-only | tier2-protocol-ragnarok.mdx |
391
+
392
+ **Agent implication**: Focus on CMK, VVK, CVK. Others are protocol backing, not SDK-facing.
393
+
394
+ ---
395
+
396
+ ## Ragnarok (Fabric Offboarding)
397
+
398
+ **What it is**: Deliberate offboarding from Fabric. The sole deviation from the never-whole-key invariant. Reconstructs keys locally so TideCloak can operate independently. Irreversible. **VERIFIED** (ragnarok.md, tier2-protocol-ragnarok.mdx)
399
+
400
+ **Official name**: "Ragnarok" is whimsical for a destructive operation. Use "Fabric offboarding (Ragnarok)" in docs.
401
+
402
+ **How it works**:
403
+ 1. Quorum of admins approve Ragnarok
404
+ 2. Keys reconstructed locally via nested threshold (RGK)
405
+ 3. TideCloak switches to local EdDSA signing (Heimdall handler)
406
+ 4. Fabric integration disabled permanently
407
+
408
+ **Agent implication**:
409
+ - Ragnarok is irreversible once quorum-approved **ASSUMED** (ragnarok.md describes finalization but no rollback - A-24)
410
+ - Post-Ragnarok details undocumented **STILL_UNRESOLVED** (GAP-021)
411
+ - keylessh does not implement Ragnarok
412
+
413
+ ---
414
+
415
+ ## Required Roles and Configuration
416
+
417
+ **`_tide_enabled` role** **VERIFIED** (vendor confirmation, GAP-031 resolved):
418
+ - `setUpTideRealm` runs after realm import and does not auto-create `_tide_enabled`
419
+ - Declare `_tide_enabled` in the realm template so the role already exists when setup continues
420
+ - If omitted from the imported realm, it must be created and assigned manually afterward
421
+ - Must be in every user's default role set
422
+ - Without it, Tide cryptographic operations fail
423
+
424
+ **`tidebrowser` flow** **VERIFIED** (confirmed across 4 exemplar realm.json files):
425
+ - Tide-specific browser authentication flow
426
+ - Set as `browserFlow` in realm.json
427
+ - Replaces standard Keycloak browser flow
428
+
429
+ **Token lifetimes** **VERIFIED** (confirmed across all exemplar realm.json files):
430
+ - `accessTokenLifespan: 600` (10 min)
431
+ - `ssoSessionIdleTimeout: 1800` (30 min)
432
+ - `ssoSessionMaxLifespan: 36000` (10 hours)
433
+
434
+ ---
435
+
436
+ ## Undocumented Admin Endpoints
437
+
438
+ Discovered in keylessh; not in API documentation.
439
+
440
+ | Endpoint | Purpose | Evidence |
441
+ |----------|---------|----------|
442
+ | `POST /admin/realms/{realm}/vendorResources/setUpTideRealm` | Initialize Tide features (email + terms) | `init-tidecloak.sh` |
443
+ | `POST /admin/realms/{realm}/tide-admin/toggle-iga` | Enable/disable IGA | `init-tidecloak.sh` |
444
+ | `POST /admin/realms/{realm}/users/{user-id}/tideAdminResources/get-required-action-link` | Generate account linking URL | `init-tidecloak.sh` |
445
+ | `GET /admin/realms/{realm}/clients/{client-id}/vendorResources/get-installations-provider?providerId=keycloak-oidc-keycloak-json` | Download adapter JSON with Tide extensions. Only valid providerId: `keycloak-oidc-keycloak-json`. `tidecloak-oidc-keycloak-json` does not exist. | `init-tidecloak.sh`, vendor confirmation (GAP-044 resolved) |
446
+
447
+ **PARTIALLY_RESOLVED** (GAP-029): Most endpoint paths observed. `setUpTideRealm` fully documented (vendor confirmation, batch-02 Q-03). `get-required-action-link` request/response schema still missing.
448
+
449
+ ---
450
+
451
+ ## Account Linking Onboarding
452
+
453
+ **PARTIALLY_RESOLVED_BY_KEYLESSH** (GAP-030):
454
+
455
+ New admin users must complete account linking:
456
+ 1. Generate time-limited URL via `get-required-action-link` (default: 12 hours lifespan)
457
+ 2. User opens link, completes linking in browser
458
+ 3. Poll user attributes for `tideUserKey` and `vuid`
459
+ 4. When present, linking complete
460
+
461
+ **Evidence**: keylessh `init-tidecloak.sh`
462
+
463
+ **Unclear**: Whether mandatory for non-admin users or non-IGA setups.
464
+
465
+ ---
466
+
467
+ ## Settlement Layer (Theory-Only)
468
+
469
+ **Anonymous Voucher**: Micro-payment token gating every ORK operation. Triple-blinded: ORK identity, vendor identity, user ID independently obfuscated. **VERIFIED** (tier2-protocol-anonymous-voucher.mdx)
470
+
471
+ **VUID (Vendor User ID)**: Mathematically unlinkable identifier across vendors. Mapped to `vuid` JWT claim. **VERIFIED** (tier2-protocol-double-blind-tss.mdx, keylessh `realm.json`)
472
+
473
+ **Agent implication**: Vouchers are a universal Tide Network requirement — every cryptographic operation requires a voucher. For TideCloak apps using `@tidecloak/js`, the SDK automatically constructs the voucher URL from the user's access token — developers do not need to build this URL manually. **VERIFIED** (vendor confirmation, GAP-051 resolved). Non-TideCloak integrations must operate their own voucher faucet and pass its URL to the Heimdall SDK directly.
474
+
475
+ ---
476
+
477
+ ## Voucher Gates (Dual Role Requirement)
478
+
479
+ **What it is**: For policy-governed VVK encryption, users need TWO types of roles: voucher gate roles and Forseti contract roles. The voucher system in `VoucherResource.java` independently gates whether encrypt/decrypt operations are allowed for a session before the Forseti contract even runs. **VERIFIED** (operational exemplars: forseti-crypto-quickstart, tidecloak-test-cases)
480
+
481
+ **Two encryption models** **VERIFIED** (test-cases F7, F9, vendor confirmation batch-02 Q-10):
482
+
483
+ | Pattern | Role names | Encryption model | SDK call |
484
+ |---------|-----------|-----------------|----------|
485
+ | Self-encryption | `_tide_{tag}.selfencrypt` / `_tide_{tag}.selfdecrypt` | User-bound (only encryptor can decrypt) | `doEncrypt([{data, tags}])` — no policy bytes |
486
+ | Policy-based | No `_tide_{tag}` roles required. Tags are opaque data; authorization defined by Forseti contract | Shared (any doken satisfying the contract can decrypt) | `doEncrypt([{data, tags}], policyBytes)` — with policy bytes |
487
+
488
+ For self-encryption, the SDK checks `_tide_{tag}.selfencrypt`/`.selfdecrypt` roles on the client and ORK. For policy-based encryption, the SDK skips the tag-role check entirely (`tidecloak.js:703-704`); authorization is handled by the Forseti contract, which can check any Keycloak role (not necessarily `_tide_`-prefixed).
489
+
490
+ **Voucher gate roles**: The voucher system still requires `_tide_*` roles to fund ORK operations, even for policy-based encryption. But you do not need per-tag `_tide_{tag}.encrypt`/`.decrypt` roles — any `_tide_*` role (e.g., `_tide_enabled`, or a generic `_tide_x.selfdecrypt`) satisfies the `vendorsign` voucher check. For `vendordecrypt`, a role matching `_tide_*.selfdecrypt` or `_tide_*.decrypt` is needed.
491
+
492
+ **Forseti contract roles** (e.g., `shared-data-access`): Checked by the custom contract's `ValidateExecutor`. Controls WHO can actually decrypt. Regular realm role, NOT `_tide_*` prefixed. Only relevant for policy-based encryption.
493
+
494
+ **Voucher enforcement rules** **VERIFIED** (vendor confirmation, GAP-051 resolved):
495
+ The `tidevouchers/fromUserSession` endpoint performs role-based voucher authorization before issuing vouchers:
496
+ - `signin` and `updateaccount`: always allowed
497
+ - `vendorsign` (encrypt): allowed if user has ANY `_tide_*` role, an authorizer role, or `tide-realm-admin`
498
+ - `vendordecrypt` (decrypt): allowed if user has a role matching `_tide_*.selfdecrypt` or `_tide_*.decrypt`
499
+
500
+ **Without the voucher**: ORKs refuse before the Forseti contract runs. Error: `"session ID ... has not been allowed a voucher action of type vendordecrypt"`.
501
+
502
+ **Self-encryption role assignment pattern**:
503
+ ```json
504
+ {
505
+ "roles": {
506
+ "realm": [
507
+ { "name": "_tide_secret.selfencrypt", "description": "Voucher gate: enables self-encryption" },
508
+ { "name": "_tide_secret.selfdecrypt", "description": "Voucher gate: enables self-decryption" }
509
+ ]
510
+ }
511
+ }
512
+ ```
513
+
514
+ **Policy-based encryption role assignment pattern** (simplified approach — vendor confirmation, batch-02 Q-10):
515
+ ```json
516
+ {
517
+ "roles": {
518
+ "realm": [
519
+ { "name": "shared-data-access", "description": "Forseti contract role: who can encrypt/decrypt" },
520
+ { "name": "_tide_x.selfencrypt", "description": "Voucher gate: enables vendorsign (generic tag)" },
521
+ { "name": "_tide_x.selfdecrypt", "description": "Voucher gate: enables vendordecrypt (generic tag)" }
522
+ ]
523
+ }
524
+ }
525
+ ```
526
+
527
+ Policy-based encryption does NOT require per-tag `_tide_{tag}.encrypt`/`.decrypt` roles. The SDK skips the tag-role check when a `decryption_policy` is provided. Use a generic voucher gate (e.g., `_tide_x.selfencrypt`/`_tide_x.selfdecrypt` with tag `x`) to satisfy the voucher system. Actual access control is entirely in the Forseti contract checking the business role (e.g., `shared-data-access`).
528
+
529
+ Note: The `SimpleTagBasedDecryptionContract` is one built-in default contract that does check `_tide_{tag}.encrypt`/`.decrypt` roles, but the standard Forseti flow uses custom contracts where authorization is fully contract-defined.
530
+
531
+ **Agent implication**: After adding roles to a running realm, users must refresh their token (or log out and back in) for the doken to pick up new roles. Propagation may take up to 120s. **VERIFIED** (test-cases F3, F7)
532
+
533
+ **Anti-pattern**: Using `_tide_*` roles in your Forseti contract's `ValidateExecutor`. The voucher gate roles are for ORK operation gating only. Your contract should check a regular realm role.
534
+
535
+ **Anti-pattern**: Using self-encryption roles (`selfencrypt`/`selfdecrypt`) when you need shared access. Self-encryption is identity-bound. Use policy-based encryption with a Forseti contract for shared data. Policy-based encryption does not require per-tag `_tide_` roles — authorization is contract-defined.
536
+
537
+ **Anti-pattern**: Renaming `selfencrypt`/`selfdecrypt` to `encrypt`/`decrypt` and expecting this to enable shared decryption. The role suffix does not change the encryption model. The SDK call path determines whether encryption is self-bound or policy-governed. See AP-26 in `canon/anti-patterns.md`.
538
+
539
+ ---
540
+
541
+ ## Self-Encryption vs Policy-Governed VVK Encryption
542
+
543
+ **What it is**: Two completely separate encryption models with different key binding, access semantics, and SDK call paths. **VERIFIED** (operational exemplars: forseti-crypto-quickstart)
544
+
545
+ **Model 1: Self-encryption** (no policy):
546
+ - SDK calls: `doEncrypt(data)` / `doDecrypt(data)` on `useTideCloak()`
547
+ - Binds ciphertext to the encrypting user's identity (CVK)
548
+ - Only that user can decrypt
549
+ - Giving another user `selfdecrypt` role does NOT let them decrypt your data
550
+ - Use for: personal vaults, user-private data
551
+
552
+ **Model 2: Policy-governed VVK encryption** (with signed policy):
553
+ - SDK calls: `IAMService.doEncrypt(data, signedPolicyBytes)` / `IAMService.doDecrypt(data, signedPolicyBytes)`
554
+ - Encrypts with VVK (organizational key distributed across ORKs)
555
+ - Any user whose doken satisfies the Forseti contract can decrypt
556
+ - Requires a signed Forseti policy with a custom contract
557
+ - Use for: shared data, group messaging, multi-user access
558
+
559
+ **Critical distinction**: The `doEncrypt`/`doDecrypt` on `useTideCloak()` are convenience wrappers that do NOT pass the policy parameter. For shared encryption, call `IAMService` directly.
560
+
561
+ **Agent implication**: Never use self-encryption for shared data. Never assume that assigning decrypt roles to multiple users enables cross-user decryption under self-encryption.
562
+
563
+ **Anti-pattern**: Using `doEncrypt`/`doDecrypt` from `useTideCloak()` and expecting other users to decrypt the result. Self-encryption is identity-bound; shared access requires `IAMService` with a signed policy.
564
+
565
+ ---
566
+
567
+ ## Policy Signing (5-Step Flow)
568
+
569
+ **What it is**: VVK policy signing requires a 5-step flow through the ORK threshold network. Raw `policy.toBytes()` without a VVK signature is rejected. **VERIFIED** (operational exemplars: forseti-crypto-quickstart)
570
+
571
+ **The 5 steps**:
572
+
573
+ ```js
574
+ const tc = IAMService._tc;
575
+
576
+ // Step 1: Initialize the request
577
+ const initialized = await tc.createTideRequest(request.encode());
578
+
579
+ // Step 2: Operator approval (opens popup for user to sign)
580
+ const approvalResults = await tc.requestTideOperatorApproval([
581
+ { id: "policy-sign", request: initialized },
582
+ ]);
583
+ if (approvalResults[0].status !== "approved") throw new Error("Denied");
584
+
585
+ // Step 3: Decode approved request and ATTACH THE ADMIN POLICY
586
+ const approvedRequest = BaseTideRequest.decode(approvalResults[0].request);
587
+ const adminPolicyBytes = await fetchAdminPolicyFromTideCloak(token);
588
+ approvedRequest.addPolicy(adminPolicyBytes);
589
+
590
+ // Step 4: Execute sign request (waitForAll MUST be true)
591
+ const signatures = await tc.executeSignRequest(approvedRequest.encode(), true);
592
+
593
+ // Step 5: Attach VVK signature to the policy object
594
+ policy.signature = signatures[0];
595
+ const signedBytes = policy.toBytes(); // NOW has VVK signature
596
+ ```
597
+
598
+ **What fails if each step is skipped**:
599
+ - Skip step 2 (popup): `executeSignRequest` alone does not sign policies
600
+ - Skip step 3 (admin policy): ORKs reject with `"Policy supplied has not been signed"`
601
+ - Skip step 4 (executeSignRequest): approval popup alone does not produce the VVK signature
602
+ - Forget `policy.signature = signatures[0]`: policy bytes have no signature, ORKs reject
603
+
604
+ **Critical pitfalls**:
605
+ - 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.
606
+ - Do NOT store signed policy bytes to the server until verified. A failed signing attempt storing unsigned bytes causes subsequent calls to fetch stale bytes and silently fail.
607
+
608
+ **Agent implication**: Copy the exact pattern from the forseti-crypto-quickstart reference implementation. Deviating from the step order or parameter passing causes ORK byte serialization failures.
609
+
610
+ **Anti-pattern**: Attempting to sign policies from the init script or backend. The backend receives `requiresApprovalPopup: true` and cannot complete the signing. Policy signing MUST happen from the frontend after the admin logs in.
611
+
612
+ ---
613
+
614
+ ## SDK Internals
615
+
616
+ **What it is**: Internal structure of Tide SDK packages and their exports. **VERIFIED** (operational exemplars: forseti-crypto-quickstart, tidewarden, keylessh)
617
+
618
+ **Package responsibilities** **VERIFIED** (runtime confirmation, GAP-042):
619
+ - **`@tidecloak/nextjs`** — Next.js integration layer. Exports `TideCloakProvider`, `useTideCloak`, `useAuthCallback`, `Authenticated`/`Unauthenticated`, guard components. Does NOT export `Models`, `PolicySignRequest`, `BaseTideRequest`, `ApprovalType`, `ExecutionType`, or any policy/model helpers. Importing these from `@tidecloak/nextjs` returns `undefined` at runtime.
620
+ - **`@tideorg/js`** — Exports `Models` (`Policy`, `BaseTideRequest`, `ExecutionType`, `ApprovalType`) and `Contracts` (`GenericResourceAccessThresholdRoleContract`). **This is the preferred import source for Models and Contracts.**
621
+ - **`heimdall-tide`** — Exports `PolicySignRequest`. Use `PolicySignRequest.New(policy)` to construct policy signing requests. **This is the preferred import source for PolicySignRequest.**
622
+ - **`@tidecloak/js`** — Core TideCloak JS SDK. Exports `IAMService`, `TideCloak` (for DPoP). Re-exports `Models` from `@tideorg/js` and `PolicySignRequest` from `heimdall-tide`, but the re-exports may be incomplete. Prefer importing from the source packages directly.
623
+ - **`asgard-tide`** — Vendor validation library.
624
+
625
+ **Package-boundary rule**: `@tidecloak/nextjs` is the Next.js-facing auth/provider/hooks layer. It does NOT export policy/model helpers (`Models`, `PolicySignRequest`, `BaseTideRequest`, `ApprovalType`, `ExecutionType`). When those helpers are needed, import `Models` from `@tideorg/js` and `PolicySignRequest` from `heimdall-tide`. Importing them from `@tidecloak/nextjs` produces `undefined` at runtime, causing destructuring failures like `Cannot destructure property 'Policy' of 'Models' as it is undefined`.
626
+
627
+ **Import rules**:
628
+ ```js
629
+ // Models and Contracts: import from @tideorg/js (NOT @tidecloak/nextjs)
630
+ import { Models, Contracts } from "@tideorg/js";
631
+ const { Policy, ApprovalType, ExecutionType, BaseTideRequest } = Models;
632
+
633
+ // PolicySignRequest: import from heimdall-tide (NOT @tidecloak/nextjs)
634
+ import { PolicySignRequest } from "heimdall-tide";
635
+
636
+ // IAMService and TideCloak: import from @tidecloak/js
637
+ import { IAMService } from "@tidecloak/js";
638
+ import { TideCloak } from "@tidecloak/js"; // for DPoP flows
639
+
640
+ // Policy signing methods live on the TideCloak instance, not IAMService:
641
+ const tc = IAMService._tc; // underlying TideCloak instance
642
+ await tc.createTideRequest(request);
643
+ await tc.requestTideOperatorApproval(requests);
644
+ await tc.executeSignRequest(request, waitForAll);
645
+
646
+ // Next.js hooks/provider: from @tidecloak/nextjs
647
+ import { useTideCloak, TideCloakProvider } from "@tidecloak/nextjs";
648
+ ```
649
+
650
+ **Next.js webpack workaround**: `@tidecloak/js` re-exports symbols from `heimdall-tide` that may not resolve. Add to `next.config.ts`:
651
+ ```typescript
652
+ config.module.strictExportPresence = false;
653
+ ```
654
+ Also add the `@tidecloak/react` ESM alias. See `canon/framework-matrix.md` for the complete webpack config. VERIFIED (learning-batch-004, L-04).
655
+
656
+ **Model IDs format**: `"{name}:{version}"`. Examples: `"Policy:1"`, `"PolicyEnabledEncryption:1"`, `"PolicyEnabledDecryption:1"`. Check `ModelRegistry.js` in `@tideorg/js/dist/Models/` for correct names. Never guess model names.
657
+
658
+ **Agent implication**: Always use the documented import paths. The #1 source of policy-signing failures is importing from the wrong package or incorrect destructuring.
659
+
660
+ **Anti-pattern**: Importing `Models` or `PolicySignRequest` from `@tidecloak/nextjs`. This package does not export them. Dynamic imports like `await import("@tidecloak/nextjs")` return `undefined` for these symbols, producing `Cannot destructure property 'Policy' of 'Models' as it is undefined`. Fix: import `Models` from `@tideorg/js` and `PolicySignRequest` from `heimdall-tide`.
661
+
662
+ **Anti-pattern**: Destructuring `{ ApprovalType, ExecutionType }` from `Models.Policy` instead of `Models`. `Models.Policy` is the Policy class itself. This gives `undefined`, causing `Cannot read properties of undefined (reading 'IMPLICIT')`.
663
+
664
+ ### `@tideorg/js` Export Reference (v0.13.26)
665
+
666
+ `@tideorg/js` exports 7 namespaces. Only `Models` and `Contracts` are needed for app-level policy work. The others are internal SDK plumbing.
667
+
668
+ **Top-level namespaces**: `Models`, `Contracts`, `Clients`, `Cryptide`, `Flow`, `Math`, `Tools` (also aliased as `Utils`).
669
+
670
+ #### Models (app-facing — used in policy signing)
671
+
672
+ | Export | Type | Purpose |
673
+ |--------|------|---------|
674
+ | `Models.Policy` | class | Construct a Forseti policy. Constructor: `{ version, contractId, modelId, keyId, approvalType, executionType, params }`. Methods: `toBytes()`, static `from()`. Property: `signature` (set after signing). |
675
+ | `Models.ApprovalType` | enum | `EXPLICIT` (0) = separate approver dokens needed. `IMPLICIT` (1) = no separate approvers. |
676
+ | `Models.ExecutionType` | enum | `PRIVATE` (0) = runs `ValidateExecutor` on user's doken. `PUBLIC` (1) = no executor check. |
677
+ | `Models.BaseTideRequest` | class | Base request for Tide operations. Methods: `encode()`, `decode()`, `addPolicy()`, `addApproval()`, `addAuthorizer()`, `setCustomExpiry()`, `getPolicy()`, `hasPolicy()`, `isInitialized()`, `replicate()`. |
678
+ | `Models.Doken` | class | Authorization/identity token. Properties: `header`, `payload` (`sessionKey`, `tideuserkey`, `vuid`, `homeOrk`, `exp`, `aud`, `realm_access`, `resource_access`), `signature`. Methods: `isExpired()`, `validate()`, `verify()`, `serialize()`. |
679
+ | `Models.SerializedField` | class | Encrypted field envelope (TideMemory format v1). Static: `create()`, `deserialize()`. |
680
+ | `Models.Datum` | class | Data element with tag. Properties: `data`, `tag`. |
681
+ | `Models.AuthRequest` | class | Authentication request. Properties: `keyId`, `purpose`, `keyPub`, `expiry`, `sessionId`. |
682
+ | `Models.VendorData` | class | Vendor data. Properties: `VUID`, `gCMKAuth`, `blindSig`, `AuthToken`. |
683
+ | `Models.VendorSettings` | class | Vendor config. Properties: `regOn`, `backupOn`, `imageURL`, `logoURL`. |
684
+ | `Models.EnclaveEntry` | class | Enclave entry. Properties: `username`, `persona`, `expired`, `userInfo`, `orksBitwise`, `selfRequesti`, `sessKey`. |
685
+ | `Models.PolicyParameters` | class | Policy parameter storage. Methods: `tryGetParameter()`, `getParameter()`, `toBytes()`. |
686
+ | `Models.ModelRegistry` | class | Registry for model builders. Static: `getHumanReadableModelBuilder()`. |
687
+ | `Models.HumanReadableModelBuilder` | class | Builds human-readable request representations. Methods: `getDetailsMap()`, `getRequestDataJson()`, `getExpiry()`. |
688
+ | `Models.Infos.KeyInfo` | class | User key info. Properties: `UserId`, `UserPublic`, `UserM`, `OrkInfo`. |
689
+ | `Models.Infos.OrkInfo` | class | ORK node info. Properties: `orkID`, `orkPublic`, `orkURL`, `orkPaymentPublic`. |
690
+
691
+ #### Contracts (app-facing — used for contract validation)
692
+
693
+ | Export | Type | Purpose |
694
+ |--------|------|---------|
695
+ | `Contracts.BaseContract` | abstract class | Base for contract validation. Abstract: `validateData()`, `validateApprovers()`, `validateExecutor()`. Method: `testPolicy()`. |
696
+ | `Contracts.GenericRealmAccessThresholdRoleContract` | class | Built-in contract checking realm roles with threshold approval. |
697
+ | `Contracts.GenericResourceAccessThresholdRoleContract` | class | Built-in contract checking client/resource roles with threshold approval. |
698
+ | `Contracts.Doken` | class | Doken for contract context. Methods: `hasResourceAccessRole()`, `hasRealmAccessRole()`, `hasVuid()`. |
699
+
700
+ #### Tools (utility — used for serialization)
701
+
702
+ | Export | Type | Purpose |
703
+ |--------|------|---------|
704
+ | `Tools.TideMemory` | class (extends Uint8Array) | Tide's binary serialization format. Static: `CreateFromArray()`, `Create()`. Methods: `WriteValue()`, `GetValue()`, `TryGetValue()`. |
705
+ | `Tools.Threshold` | constant | Default threshold (14). |
706
+ | `Tools.Max` | constant | Default max ORKs (20). |
707
+ | `Tools.CurrentTime()` | function | Current timestamp. |
708
+ | `Tools.WaitForNumberofORKs()` | function | Wait for threshold ORK responses. |
709
+
710
+ #### Cryptide (internal — rarely needed by app code)
711
+
712
+ Low-level cryptographic operations. Sub-namespaces: `Encryption` (ElGamal, AES, DH), `Signing` (EdDSA, BlindSig, signature formats), `Hashing` (SHA256, SHA512, HMAC), `Ed25519` (curve point math), `Components` (key component system), `Serialization` (byte conversion utilities), `Interpolation` (Lagrange interpolation for threshold), `Math` (modular arithmetic).
713
+
714
+ Key class: `Cryptide.TideKey` — key management. Static: `NewKey()`, `FromSerializedComponent()`. Methods: `sign()`, `verify()`, `asymmetricEncrypt()`, `asymmetricDecrypt()`.
715
+
716
+ #### Flow (internal — SDK orchestration)
717
+
718
+ High-level operation flows. Sub-namespaces: `DecryptionFlows`, `EncryptionFlows`, `SigningFlows`, `VoucherFlows`.
719
+
720
+ Key class: `Flow.EncryptionFlows.PolicyAuthorizedEncryptionFlow` — policy-based encrypt/decrypt. Methods: `encrypt()`, `decrypt()`, `createEncryptionRequest()`, `createDecryptionRequest()`, `commitEncrypt()`, `commitDecrypt()`.
721
+
722
+ Key class: `Flow.SigningFlows.dVVKSigningFlow2Step` — two-step VVK signing. Methods: `preSign()`, `sign()`, `getVouchers()`.
723
+
724
+ #### Math (internal — threshold crypto math)
725
+
726
+ | Export | Type | Purpose |
727
+ |--------|------|---------|
728
+ | `Math.KeyDecryption.GetKeys()` | function | Recover keys from decryption shares. |
729
+ | `Math.KeySigning.PreSign()` | function | Prepare threshold signature. |
730
+ | `Math.KeySigning.Sign()` | function | Finalize threshold signature. |
731
+
732
+ ---
733
+
734
+ ## Admin Policy Endpoint
735
+
736
+ **What it is**: The correct endpoint for fetching the signed admin policy needed in step 3 of policy signing. Two endpoints exist; one is wrong. **VERIFIED** (operational exemplars: forseti-crypto-quickstart, keylessh)
737
+
738
+ **Endpoints**:
739
+ ```
740
+ WRONG: GET /admin/realms/{realm}/tide-admin/realm-policy
741
+ Returns: { status: "none" } even when the policy exists
742
+
743
+ CORRECT: GET /realms/{realm}/tide-policy-resources/admin-policy
744
+ Returns: base64-encoded bytes (does not require admin credentials)
745
+ ```
746
+
747
+ **Response handling**: The correct endpoint returns base64 text, not raw binary. Decode on backend:
748
+ ```js
749
+ // Backend proxy
750
+ const b64 = await tcRes.text();
751
+ const raw = Buffer.from(b64, "base64");
752
+ const bytes = Array.from(new Uint8Array(raw));
753
+ res.json({ policyBytes: bytes });
754
+
755
+ // Frontend
756
+ const data = await res.json();
757
+ const adminPolicyBytes = new Uint8Array(data.policyBytes);
758
+ ```
759
+
760
+ **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 fails with `Index out of range` because the policy structure is garbage.
761
+
762
+ **Agent implication**: Always base64-decode the response. Proxy through your backend since this is a non-admin endpoint.
763
+
764
+ **Anti-pattern**: Using `GET /admin/realms/{realm}/tide-admin/realm-policy` to fetch the admin policy. This endpoint checks status only and returns `{ status: "none" }` regardless of whether the policy exists.
765
+
766
+ ---
767
+
768
+ ## Realm Policy Setup
769
+
770
+ **What it is**: Endpoints for creating and managing the realm admin policy. The realm admin policy must exist before encryption policies can be signed. **VERIFIED** (operational exemplars: forseti-crypto-quickstart)
771
+
772
+ **Endpoints**:
773
+ ```
774
+ GET /tide-admin/realm-policy -> check status
775
+ POST /tide-admin/realm-policy/pending -> create pending policy (needs templateId + contractCode)
776
+ POST /tide-admin/realm-policy/commit -> commit after approval
777
+ GET /tide-admin/policy-templates -> list templates
778
+ POST /tide-admin/policy-templates -> create template
779
+ ```
780
+
781
+ **Setup sequence**:
782
+ 1. Create a policy template via `POST /tide-admin/policy-templates`
783
+ 2. Create a pending realm policy via `POST /tide-admin/realm-policy/pending` with `templateId` and `contractCode`
784
+ 3. Approve via enclave (admin browser required)
785
+ 4. Commit via `POST /tide-admin/realm-policy/commit`
786
+
787
+ **Storing policy on a role** (`init-cert` endpoint) **VERIFIED** (vendor confirmation, GAP-039 resolved):
788
+ ```
789
+ POST /admin/realms/{realm}/tide-iga-provider/role-policy/{roleId}/init-cert
790
+ Content-Type: application/json
791
+
792
+ {
793
+ "initCert": "<base64 signed policy bytes, required>",
794
+ "initCertSig": "<base64 raw VVK signature, optional>"
795
+ }
796
+ ```
797
+ There is no `doken` field — this is a pure storage endpoint persisting to `TideRoleDraftEntity`. Returns `{"message":"initCert updated for role <roleId>"}` on success, 404 if role not found, 400 if `initCert` missing. The stored policy is consumed during multi-admin change-set signing/commit to produce vendor-signed access proofs.
798
+
799
+ Note: path is `tide-iga-provider`, not `tide-admin`. The JS admin client does not have this endpoint wired up — use raw HTTP.
800
+
801
+ **Critical requirement**: `templateId` is REQUIRED when creating a pending realm policy. Omitting it causes the request to fail.
802
+
803
+ **Agent implication**: The realm admin policy is a prerequisite for all encryption policy signing. Verify it exists before attempting to sign encryption policies.
804
+
805
+ **Anti-pattern**: Attempting to sign encryption policies without first setting up the realm admin policy. The ORKs will reject with `"Policy supplied has not been signed"` because there is no admin policy to attach in step 3 of the policy signing flow.
806
+
807
+ ---
808
+
809
+ ## Status Legend
810
+
811
+ - **VERIFIED** - Directly sourced from documentation or exemplar evidence (keylessh, forseti-crypto-quickstart, tidewarden, test-cases, CHANGE_REQUEST_API.md)
812
+ - **INFERRED** - Strongly implied by source material
813
+ - **ASSUMED** - Operator guidance where sources are silent
814
+ - **REQUIRES_RUNTIME_VALIDATION** - Single-app evidence; needs confirmation across apps
815
+ - **STILL_UNRESOLVED** - Open gap
816
+ - **PARTIALLY_RESOLVED** - Partial evidence; gaps remain