@tideorg/mcp 1.4.2 → 1.9.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 (57) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/GAP_REGISTER.md +18 -8
  3. package/PRIVACY.md +41 -0
  4. package/README.md +204 -197
  5. package/adapters/AGENTS.md +3 -2
  6. package/adapters/CLAUDE.md +1 -1
  7. package/canon/anti-patterns.md +55 -62
  8. package/canon/concepts.md +46 -34
  9. package/canon/custom-contracts.md +12 -8
  10. package/canon/feature-mapping.md +27 -75
  11. package/canon/framework-matrix.md +69 -22
  12. package/canon/hosting-options.md +111 -0
  13. package/canon/iga-change-requests-api.md +211 -0
  14. package/canon/invariants.md +33 -35
  15. package/canon/security-gap-mapping.md +506 -0
  16. package/canon/security-runtime-probes.md +177 -0
  17. package/canon/tidecloak-bootstrap.md +21 -16
  18. package/canon/tidecloak-endpoints.md +60 -98
  19. package/canon/troubleshooting.md +115 -53
  20. package/canon/version-policy.md +8 -12
  21. package/mcp-server/dist/server.js +221 -19
  22. package/package.json +48 -45
  23. package/playbooks/add-auth-nextjs-existing.md +33 -17
  24. package/playbooks/add-auth-nextjs-fresh.md +1 -1
  25. package/playbooks/add-rbac-nextjs.md +7 -2
  26. package/playbooks/bootstrap-realm-from-template.md +33 -18
  27. package/playbooks/deploy-tidecloak-docker.md +31 -28
  28. package/playbooks/diagnose-missing-roles-or-claims.md +2 -2
  29. package/playbooks/initialize-admin-and-link-account.md +22 -19
  30. package/playbooks/protect-api-nextjs.md +40 -1
  31. package/playbooks/protect-aspnet-core-asgard.md +313 -0
  32. package/playbooks/protect-routes-nextjs.md +24 -10
  33. package/playbooks/provision-tidecloak-skycloak.md +213 -0
  34. package/playbooks/setup-forseti-e2ee.md +32 -50
  35. package/playbooks/setup-iga-admin-panel.md +113 -171
  36. package/playbooks/verify-jwt-server-side.md +20 -1
  37. package/prompts/build-private-customer-portal.md +1 -1
  38. package/prompts/security-gap-analysis.md +55 -0
  39. package/reference-apps/encrypted-communication/anti-patterns.md +3 -3
  40. package/reference-apps/encrypted-communication/bootstrap-sequence.md +2 -2
  41. package/reference-apps/encrypted-communication/scenario.md +2 -2
  42. package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +8 -8
  43. package/reference-apps/iga-admin-governance/anti-patterns.md +18 -16
  44. package/reference-apps/iga-admin-governance/bootstrap-sequence.md +10 -5
  45. package/reference-apps/iga-admin-governance/role-policy-matrix.md +12 -13
  46. package/reference-apps/iga-admin-governance/scenario.md +15 -13
  47. package/reference-apps/organisation-password-manager/bootstrap-sequence.md +5 -6
  48. package/reference-apps/policy-governed-signing/bootstrap-sequence.md +9 -9
  49. package/skills/tide-diagnostics/SKILL.md +1 -1
  50. package/skills/tide-integration/SKILL.md +9 -18
  51. package/skills/tide-mcp-qa/SKILL.md +139 -0
  52. package/skills/tide-rbac-and-e2ee/SKILL.md +5 -5
  53. package/skills/tide-reviewer/SKILL.md +1 -1
  54. package/skills/tide-security-analyst/SKILL.md +182 -0
  55. package/skills/tide-setup/SKILL.md +1 -1
  56. package/canon/delegation.md +0 -195
  57. package/playbooks/setup-server-delegation.md +0 -142
@@ -130,7 +130,7 @@ For code patterns and full details, see `canon/framework-matrix.md`.
130
130
  - **`silent-check-sso.html`**: Must exist in `public/`. See playbook `add-auth-nextjs-fresh`.
131
131
  - **Redirect handler**: Must exist at configured `redirectUri` path. Without it, login ends on 404. See `canon/redirect-handler.md`.
132
132
  - **Setup order**: License → IGA → E2EE. No other sequence works. See `canon/tidecloak-bootstrap.md`.
133
- - **Version policy**: Tide packages at `0.13.26`. No 0.99.x. See `canon/version-policy.md`.
133
+ - **Version policy**: Tide packages at `0.13.33`. No 0.99.x. See `canon/version-policy.md`.
134
134
  - **Docker images**: `tidecloak-dev` is production (full protocol). `tidecloak-stg-dev` is staging. Images are under `tideorg/` org (e.g. `tideorg/tidecloak-dev:latest`), NOT `tidecloak/`. Do NOT append `start-dev` to docker run — TideCloak images have a pre-configured entrypoint.
135
135
  - **`_tide_enabled`**: Declare in realm.json. Not auto-created.
136
136
  - **`tide-realm-admin`**: Client role on `realm-management`, not a realm role.
@@ -112,7 +112,7 @@ const keyPair = await crypto.subtle.generateKey(
112
112
  **Correct approach**:
113
113
  ```typescript
114
114
  // ✅ CORRECT: Delegate to SDK/Fabric
115
- const ciphertext = await iam.doEncrypt('tag', plaintext);
115
+ const ciphertext = await iam.doEncrypt([{ data: plaintext, tags: ['tag'] }]);
116
116
  const signature = await tc.executeSignRequest(signRequest);
117
117
 
118
118
  // Keys never exist locally; operations use Fabric threshold protocols
@@ -133,7 +133,7 @@ async function offlineDecrypt(ciphertext) {
133
133
  if (!navigator.onLine && cachedSessionKey) {
134
134
  return decryptLocally(ciphertext, cachedSessionKey);
135
135
  }
136
- return iam.doDecrypt('tag', ciphertext);
136
+ return iam.doDecrypt([{ encrypted: ciphertext, tags: ['tag'] }]);
137
137
  }
138
138
  ```
139
139
 
@@ -152,7 +152,7 @@ async function decrypt(ciphertext) {
152
152
  if (!navigator.onLine) {
153
153
  throw new Error('E2EE requires online Fabric access');
154
154
  }
155
- return iam.doDecrypt('tag', ciphertext);
155
+ return iam.doDecrypt([{ encrypted: ciphertext, tags: ['tag'] }]);
156
156
  }
157
157
 
158
158
  // Handle offline state at application level, not E2EE level
@@ -206,8 +206,8 @@ app.post('/api/admin/create-user', async (req, res) => {
206
206
  body: JSON.stringify(req.body)
207
207
  });
208
208
 
209
- // Admin must then approve via /tide-admin/change-set/sign/batch
210
- // and commit via /tide-admin/change-set/commit/batch
209
+ // Admin must then approve via /iga/change-requests/{id}/authorize
210
+ // and commit via /iga/change-requests/{id}/commit (canon/iga-change-requests-api.md)
211
211
  return res.json({ status: 'pending_approval' });
212
212
  });
213
213
 
@@ -254,7 +254,7 @@ app.get('/api/protected', async (req, res) => {
254
254
  });
255
255
  ```
256
256
 
257
- **Note**: DPoP is required for Tide's full security guarantees **VERIFIED** (vendor confirmation, GAP-032 resolved). ES256 is the default algorithm; EdDSA also supported.
257
+ **Note**: DPoP is not merely "supported" it is **ENABLED and ENFORCED by default** in the TideCloak SDK (`useDPoP` defaults to `{ mode: 'strict' }`). Treat DPoP as on unless it has been explicitly disabled (`useDPoP: false`). Skipping server-side DPoP handling when the SDK default is in force will break every request. ES256 is the default algorithm; EdDSA also supported. **VERIFIED** (`tidecloak-js: packages/tidecloak-js/src/IAMService.js`).
258
258
 
259
259
  **Related**: Invariants I-12, [Concepts](concepts.md#dpop-demonstration-of-proof-of-possession)
260
260
 
@@ -412,16 +412,13 @@ if (checkSSHPolicy(currentUser, targetServer)) {
412
412
 
413
413
  **Correct approach**:
414
414
  ```typescript
415
- // ✅ CORRECT: Submit contract to Fabric for distributed execution
416
- const signRequest = await tc.createTideRequest({
417
- contract: sshPolicyContract, // C# code executed on every ORK
418
- modelName: 'DynamicApprovedCustom<SSH>:DynamicApprovedCustom<1>',
419
- authFlow: 'Policy:1',
420
- authorizer: tc.doken,
421
- challengeData: { user: currentUser, destination: targetServer }
422
- });
415
+ // ✅ CORRECT: Submit contract to Fabric for distributed execution.
416
+ // createTideRequest takes a single pre-encoded Uint8Array (a BaseTideRequest),
417
+ // NOT an object literal. Build the request, encode it, then submit (see AP-53).
418
+ const request = new BaseTideRequest(name, version, 'Policy:1', draft); // draft carries the C# contract
419
+ const initializedBytes = await tc.createTideRequest(request.encode());
423
420
 
424
- const result = await tc.executeSignRequest(signRequest);
421
+ const result = await tc.executeSignRequest(initializedBytes);
425
422
  // ORKs have independently evaluated policy; majority approved or rejected
426
423
  ```
427
424
 
@@ -541,9 +538,10 @@ export async function appFetch(path: string, init?: RequestInit): Promise<Respon
541
538
  ```bash
542
539
  # ✅ CORRECT: Export Tide adapter with extensions
543
540
  curl -X GET \
544
- "${TIDECLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_ID}/vendorResources/get-installations-provider?providerId=keycloak-oidc-keycloak-json" \
541
+ "${TIDECLOAK_URL}/admin/realms/${REALM}/vendorResources/get-installations-provider?clientId=${CLIENT_ID}&providerId=keycloak-oidc-keycloak-json" \
545
542
  -H "Authorization: Bearer ${ADMIN_TOKEN}" \
546
543
  > data/tidecloak.json
544
+ # Note: realm-level endpoint; the client is a query param (clientId=), NOT a /clients/{id}/ path segment.
547
545
 
548
546
  # Verify Tide extensions present
549
547
  jq 'has("jwk") and has("vendorId") and has("homeOrkUrl")' data/tidecloak.json
@@ -742,7 +740,7 @@ const isAdmin = jwt.realm_access.roles.includes('admin');
742
740
 
743
741
  if (canEncryptSSN) {
744
742
  // Allow SSN encryption only
745
- await iam.doEncrypt('ssn', plaintext);
743
+ await iam.doEncrypt([{ data: plaintext, tags: ['ssn'] }]);
746
744
  }
747
745
 
748
746
  if (isAdmin) {
@@ -816,12 +814,12 @@ if (claims.realm_access.roles.includes('admin')) {
816
814
 
817
815
  **Correct approach**:
818
816
  ```typescript
819
- // ✅ CORRECT: Always verify signature against embedded JWKS
820
- import TideJWT from "@tidecloak/verify";
821
- const _mod = (TideJWT as any)?.default ?? TideJWT;
822
- const { verifyTideCloakToken } = _mod;
817
+ // ✅ CORRECT: Always verify signature against embedded JWKS.
818
+ // Named import works (the package ships dual CJS/ESM exports on current versions).
819
+ import { verifyTideCloakToken } from "@tidecloak/verify";
823
820
 
824
- const verified = await verifyTideCloakToken(token, tidecloakConfig);
821
+ // Argument order is (config, token, allowedRoles?) — CONFIG FIRST, then the token.
822
+ const verified = await verifyTideCloakToken(tidecloakConfig, token);
825
823
  // Only trust claims from verified token
826
824
  ```
827
825
 
@@ -883,7 +881,7 @@ app.get('/api/records/:id', async (req, res) => {
883
881
  **Correct approach**:
884
882
  ```typescript
885
883
  // ✅ CORRECT: Decrypt only in the browser via SDK
886
- const plaintext = await iam.doDecrypt('medical', ciphertext);
884
+ const plaintext = await iam.doDecrypt([{ encrypted: ciphertext, tags: ['medical'] }]);
887
885
  // Plaintext never leaves the browser
888
886
  // Server stores and serves only ciphertext
889
887
  ```
@@ -897,12 +895,12 @@ const plaintext = await iam.doDecrypt('medical', ciphertext);
897
895
  **What it looks like**:
898
896
  ```typescript
899
897
  // ❌ WRONG: No tag
900
- const encrypted = await iam.doEncrypt('', plaintext);
898
+ const encrypted = await iam.doEncrypt([{ data: plaintext, tags: [] }]);
901
899
 
902
900
  // ❌ WRONG: Same tag for everything
903
- const encSSN = await iam.doEncrypt('data', ssn);
904
- const encMedical = await iam.doEncrypt('data', medicalRecord);
905
- const encNotes = await iam.doEncrypt('data', internalNotes);
901
+ const encSSN = await iam.doEncrypt([{ data: ssn, tags: ['data'] }]);
902
+ const encMedical = await iam.doEncrypt([{ data: medicalRecord, tags: ['data'] }]);
903
+ const encNotes = await iam.doEncrypt([{ data: internalNotes, tags: ['data'] }]);
906
904
  ```
907
905
 
908
906
  **Why it's wrong**:
@@ -916,9 +914,9 @@ const encNotes = await iam.doEncrypt('data', internalNotes);
916
914
  **Correct approach**:
917
915
  ```typescript
918
916
  // ✅ CORRECT: Meaningful tags per sensitivity level
919
- const encSSN = await iam.doEncrypt('ssn', ssn); // _tide_ssn.selfdecrypt
920
- const encMedical = await iam.doEncrypt('medical', record); // _tide_medical.selfdecrypt
921
- const encNotes = await iam.doEncrypt('notes', notes); // _tide_notes.selfdecrypt
917
+ const encSSN = await iam.doEncrypt([{ data: ssn, tags: ['ssn'] }]); // _tide_ssn.selfdecrypt
918
+ const encMedical = await iam.doEncrypt([{ data: record, tags: ['medical'] }]); // _tide_medical.selfdecrypt
919
+ const encNotes = await iam.doEncrypt([{ data: notes, tags: ['notes'] }]); // _tide_notes.selfdecrypt
922
920
  // Each tag has separate encrypt/decrypt roles with different user assignments
923
921
  ```
924
922
 
@@ -932,11 +930,11 @@ const encNotes = await iam.doEncrypt('notes', notes); // _tide_notes.selfde
932
930
  ```typescript
933
931
  // ❌ WRONG: Encrypt with doEncrypt, expect other users to doDecrypt
934
932
  // User A encrypts
935
- const ciphertext = await iam.doEncrypt('shared', data);
933
+ const ciphertext = await iam.doEncrypt([{ data, tags: ['shared'] }]);
936
934
  await api.saveSharedDoc(ciphertext);
937
935
 
938
936
  // User B tries to decrypt (FAILS even with _tide_shared.selfdecrypt role)
939
- const plaintext = await iam.doDecrypt('shared', ciphertext);
937
+ const plaintext = await iam.doDecrypt([{ encrypted: ciphertext, tags: ['shared'] }]);
940
938
  ```
941
939
 
942
940
  **Why it's wrong**:
@@ -962,9 +960,10 @@ const ciphertext = await iam.doEncrypt(data, signedPolicyBytes);
962
960
 
963
961
  **What it looks like**:
964
962
  ```csharp
965
- // ❌ WRONG: Checking voucher gate role in contract
966
- public override bool ValidateExecutor(UserContext user) {
967
- return user.HasRole("_tide_medical.selfdecrypt");
963
+ // ❌ WRONG: Checking voucher gate role in contract (also: role lives on the doken, not a "user")
964
+ public PolicyDecision ValidateExecutor(ExecutorContext ctx) {
965
+ return ctx.Doken.HasRole("_tide_medical.selfdecrypt")
966
+ ? PolicyDecision.Allow() : PolicyDecision.Deny("no");
968
967
  }
969
968
  ```
970
969
 
@@ -978,9 +977,11 @@ public override bool ValidateExecutor(UserContext user) {
978
977
 
979
978
  **Correct approach**:
980
979
  ```csharp
981
- // ✅ CORRECT: Check a regular realm role
982
- public override bool ValidateExecutor(UserContext user) {
983
- return user.HasRole("shared-data-access");
980
+ // ✅ CORRECT: Check a regular realm role on the executor's doken (DokenDto.HasRole),
981
+ // not a "user" object. The role check target is the doken, per Ork.Forseti.Sdk.
982
+ public PolicyDecision ValidateExecutor(ExecutorContext ctx) {
983
+ return ctx.Doken.HasRole("shared-data-access")
984
+ ? PolicyDecision.Allow() : PolicyDecision.Deny("not allowed");
984
985
  }
985
986
  // Voucher gate roles (_tide_*) are assigned separately to enable the ORK operation
986
987
  ```
@@ -1137,31 +1138,23 @@ if (useTideCloak().hasClientRole('tide-realm-admin', 'realm-management')) {
1137
1138
 
1138
1139
  ---
1139
1140
 
1140
- ## AP-30: Using Named ESM Imports with @tidecloak/verify
1141
-
1142
- **What it looks like**:
1143
- ```typescript
1144
- // ❌ WRONG: Named ESM import from CJS package
1145
- import { verifyTideCloakToken } from "@tidecloak/verify";
1146
- // Error: does not provide an export named 'verifyTideCloakToken'
1147
- ```
1148
-
1149
- **Why it's wrong**:
1150
- - `@tidecloak/verify` is a CJS package, not ESM
1151
- - Named imports fail because CJS modules export a single `module.exports` object
1152
- - CJS interop varies by runtime (tsx, node, bundlers wrap `.default` differently)
1141
+ ## AP-30: (Resolved) @tidecloak/verify Import Style
1153
1142
 
1154
- **Consequence**: Import error at startup. Server-side JWT verification completely broken.
1143
+ **Status**: The old CJS-only interop workaround is **no longer required** on current versions. `@tidecloak/verify` now ships a proper dual `exports` map (`import` → ESM, `require` → CJS), so a plain named import works in both ESM and CJS callers.
1155
1144
 
1156
1145
  **Correct approach**:
1157
1146
  ```typescript
1158
- // ✅ CORRECT: Default import with CJS interop
1159
- import TideJWT from "@tidecloak/verify";
1160
- const _mod = (TideJWT as any)?.default ?? TideJWT;
1161
- const { verifyTideCloakToken } = _mod;
1147
+ // ✅ CORRECT: Plain named import (works in ESM and CJS on current versions)
1148
+ import { verifyTideCloakToken } from "@tidecloak/verify";
1149
+ // CJS callers: const { verifyTideCloakToken } = require("@tidecloak/verify");
1150
+
1151
+ // Argument order is (config, token, allowedRoles?) — config FIRST (see AP-20).
1152
+ const payload = await verifyTideCloakToken(tidecloakConfig, token);
1162
1153
  ```
1163
1154
 
1164
- **Related**: AP-01
1155
+ Do NOT reintroduce the `const _mod = (TideJWT as any)?.default ?? TideJWT` dance — it targeted an older single-export build. If a named import genuinely fails, you are on a very old version; upgrade rather than adding interop shims.
1156
+
1157
+ **Related**: AP-01, AP-20
1165
1158
 
1166
1159
  ---
1167
1160
 
@@ -1298,7 +1291,7 @@ volumes:
1298
1291
  ```bash
1299
1292
  # ❌ WRONG: Skip license activation, jump to IGA
1300
1293
  curl -X POST "$KC_URL/admin/realms" -d '{"realm":"myrealm","enabled":true}'
1301
- curl -X POST "$KC_URL/admin/realms/myrealm/vendorResources/toggle-iga"
1294
+ curl -X POST "$KC_URL/admin/realms/myrealm/tide-admin/toggle-iga"
1302
1295
  # Missing: setUpTideRealm, approve/commit, user creation, invite link...
1303
1296
  ```
1304
1297
 
@@ -1313,9 +1306,9 @@ curl -X POST "$KC_URL/admin/realms/myrealm/vendorResources/toggle-iga"
1313
1306
  ```bash
1314
1307
  # ✅ CORRECT: Follow exact sequence
1315
1308
  # 1. Create realm
1316
- # 2. POST /vendorResources/setUpTideRealm (with email)
1317
- # 3. POST /vendorResources/toggle-iga
1318
- # 4. Approve + commit client change requests
1309
+ # 2. POST /admin/realms/{realm}/vendorResources/setUpTideRealm (form-urlencoded, with email)
1310
+ # 3. POST /admin/realms/{realm}/tide-admin/toggle-iga (NOT /vendorResources/toggle-iga)
1311
+ # 4. Approve + commit client change requests (iga/change-requests/{id}/authorize|commit)
1319
1312
  # 5. Create admin user + assign tide-realm-admin
1320
1313
  # 6. Generate invite link + wait for account linking
1321
1314
  # 7. Approve + commit user change requests
@@ -2054,7 +2047,7 @@ After `executeSignRequest`, the result is the VVK signature array. Do not store
2054
2047
 
2055
2048
  ## AP-56: Wrong Forseti Contract Namespace
2056
2049
 
2057
- Do not use `using Tide.Ork.Classes.Forseti;` or `using Tide.Forseti;` — these namespaces do not exist. The correct namespace is `using Ork.Forseti.Sdk;`. The contract class must be named `Contract` and implement `IAccessPolicy`. Method signatures use context objects: `ValidateData(DataContext ctx)`, `ValidateApprovers(ApproversContext ctx)`, `ValidateExecutor(ExecutorContext ctx)`. Use `PolicyDecision.Allow()` not `PolicyDecision.Approve()`. VERIFIED (ripple-learnings L-13).
2050
+ Do not use `using Tide.Ork.Classes.Forseti;` or `using Tide.Forseti;` — these namespaces do not exist. The correct namespace is `using Ork.Forseti.Sdk;`. The contract class must implement `IAccessPolicy`; the class NAME is not fixed — the ORK resolves the entry type from the request's stored `EntryType` (`asm.GetType(req.EntryType)`), so `Contract` is only a convention, not a requirement. Method signatures use context objects: `ValidateData(DataContext ctx)` (always called), `ValidateApprovers(ApproversContext ctx)` (when approvalType == EXPLICIT), `ValidateExecutor(ExecutorContext ctx)` (when executorType == PRIVATE). Use `PolicyDecision.Allow()` not `PolicyDecision.Approve()`. VERIFIED (`ork: Ork.Forseti.Sdk/Contracts/IAccessPolicy.cs`, entry-type resolution in `Ork.Forseti.VmHost/Program.cs`).
2058
2051
 
2059
2052
  ---
2060
2053
 
package/canon/concepts.md CHANGED
@@ -103,29 +103,28 @@ Agent-operable definitions of Tide's cryptographic model and architecture.
103
103
 
104
104
  **How it works**:
105
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`
106
+ 2. The change request enters the approval queue keyed by `id`, with `entityType`, `actionType`
107
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)
108
+ 4. Approved change is sealed by VVK threshold signatures (Authorization Proofing) — in Tide mode; Tideless mode records a username attestation (no crypto)
109
109
  5. Change is committed to realm
110
110
  6. VVK ORKs verify future JWT claims against these proofs
111
111
 
112
- **Change-set lifecycle** **VERIFIED** (CHANGE_REQUEST_API.md):
112
+ **Change-request lifecycle** **VERIFIED** (qea-iga-api.md):
113
113
  ```
114
- DRAFT → PENDING → APPROVED → ACTIVE
115
- → DENIED
116
- (cancel at any stage)
114
+ PENDING → APPROVED (committed) (ACTIVE)
115
+ → DENIED
116
+ CANCELLED
117
117
  ```
118
118
 
119
- **Change-set API endpoints** **VERIFIED** (CHANGE_REQUEST_API.md, all exemplar init scripts):
119
+ **Change-request API endpoints** current `/iga/change-requests/...` surface (replaces legacy `/tide-admin/change-set/...`, GAP-065). **Full spec: `canon/iga-change-requests-api.md`.**
120
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)
121
+ GET /admin/realms/{realm}/iga/change-requests?status=PENDING list (objects keyed by id)
122
+ GET /admin/realms/{realm}/iga/change-requests/{id} one change request
123
+ POST /admin/realms/{realm}/iga/change-requests/{id}/authorize approve (sign), body {} optional
124
+ POST /admin/realms/{realm}/iga/change-requests/bulk-authorize batch approve
125
+ POST /admin/realms/{realm}/iga/change-requests/{id}/commit → commit (412 if under threshold)
126
+ POST /admin/realms/{realm}/iga/change-requests/{id}/deny reject
127
+ GET/POST /admin/realms/{realm}/iga/change-requests/{id}/approval-model Tide-mode enclave two-phase sign
129
128
  ```
130
129
 
131
130
  **Signing modes** **VERIFIED** (CHANGE_REQUEST_API.md):
@@ -154,11 +153,11 @@ POST /admin/realms/{realm}/tideAdminResources/add-rejection → rejec
154
153
  **Official name**: "Hermetic E2EE" is marketing. Use "threshold E2EE".
155
154
 
156
155
  **How it works** **VERIFIED** (vendor confirmation, GAP-009 resolved):
157
- 1. User calls `doEncrypt(tag, plaintext)` in browser
156
+ 1. User calls `doEncrypt([{ data: plaintext, tags: [tag] }])` in browser
158
157
  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
158
  3. Data encrypted locally; the per-call key is ElGamal-encrypted via threshold across ORKs
160
159
  4. Ciphertext (TideMemory envelope) stored in application
161
- 5. User calls `doDecrypt(tag, ciphertext)` → Fabric threshold-decrypts the per-call key → plaintext recovered locally
160
+ 5. User calls `doDecrypt([{ encrypted: ciphertext, tags: [tag] }])` → Fabric threshold-decrypts the per-call key → plaintext recovered locally
162
161
  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
162
 
164
163
  **Tag-based role enforcement** **VERIFIED** (e2ee.md):
@@ -319,13 +318,17 @@ useDPoP: { mode: 'strict', alg: 'EdDSA' }
319
318
 
320
319
  **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
320
 
322
- **DPoP client SDK**: DPoP requires the `TideCloak` class (not `IAMService`) with `enableDpop: true` in init options:
321
+ **DPoP client SDK**: DPoP is enabled and enforced by default across the TideCloak SDKs. Configure it via the `useDPoP` option (there is no `enableDpop` flag — that option does not exist in the current SDK). Use the `TideCloak` class (not `IAMService`) for the DPoP-signing client:
323
322
  ```typescript
324
323
  const tc = new TideCloak({ url, realm, clientId, vendorId, homeOrkUrl, ... });
325
- await tc.init({ onLoad: "check-sso", pkceMethod: "S256", enableDpop: true });
324
+ // useDPoP defaults to { mode: 'strict' }; pass it explicitly to set alg or relax the mode:
325
+ // useDPoP: false → disable DPoP entirely
326
+ // useDPoP: { mode: 'auto' } → use DPoP only when the realm advertises it
327
+ // useDPoP: { mode: 'strict', alg: 'ES256' } → require DPoP (default), pin the algorithm
328
+ await tc.init({ onLoad: "check-sso", pkceMethod: "S256", useDPoP: { mode: "strict", alg: "ES256" } });
326
329
  // tc.secureFetch() auto-attaches DPoP headers
327
330
  ```
328
- **VERIFIED** (test-cases `DPoPAuthProvider.tsx`)
331
+ **VERIFIED** (tidecloak-js `IAMService.js` on `main` — `useDPoP` default `{ mode: 'strict' }`)
329
332
 
330
333
  **Server-side verification pattern** **VERIFIED** (keylessh `server/auth.ts`, test-cases `dpop-protected/route.ts`):
331
334
  1. Extract `Authorization: DPoP <token>` (NOT `Bearer`)
@@ -368,7 +371,7 @@ Alternative: use `oauth2-dpop` npm package + `jose` for simpler verification. **
368
371
  **Agent implication**:
369
372
  - `jwk` field is only present when IGA is enabled on the realm. Validate its presence at startup.
370
373
  - 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.
374
+ - Export via vendor endpoint: `GET /admin/realms/{realm}/vendorResources/get-installations-provider?clientId={client-uuid}&providerId=keycloak-oidc-keycloak-json` **VERIFIED** (vendor confirmation, GAP-044 resolved). This is the **realm-level** endpoint; the client is passed as the `clientId={uuid}` query param, NOT as a `/clients/{id}/` path segment (the per-client path returns a minimal adapter missing `jwk`). 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
375
  - 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
376
 
374
377
  **Common confusion**: Adapter JSON is NOT standard Keycloak format. Do not use generic Keycloak adapter parsers.
@@ -442,7 +445,7 @@ Discovered in keylessh; not in API documentation.
442
445
  | `POST /admin/realms/{realm}/vendorResources/setUpTideRealm` | Initialize Tide features (email + terms) | `init-tidecloak.sh` |
443
446
  | `POST /admin/realms/{realm}/tide-admin/toggle-iga` | Enable/disable IGA | `init-tidecloak.sh` |
444
447
  | `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) |
448
+ | `GET /admin/realms/{realm}/vendorResources/get-installations-provider?clientId={client-uuid}&providerId=keycloak-oidc-keycloak-json` | Download adapter JSON with Tide extensions. Realm-level endpoint; client passed as `clientId={uuid}` query param (NOT a `/clients/{id}/` path segment). Only valid providerId: `keycloak-oidc-keycloak-json`. `tidecloak-oidc-keycloak-json` does not exist. | `init-tidecloak.sh`, vendor confirmation (GAP-044 resolved) |
446
449
 
447
450
  **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
451
 
@@ -661,11 +664,13 @@ Also add the `@tidecloak/react` ESM alias. See `canon/framework-matrix.md` for t
661
664
 
662
665
  **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
666
 
664
- ### `@tideorg/js` Export Reference (v0.13.26)
667
+ ### `@tideorg/js` Export Reference (v0.13.33)
665
668
 
666
- `@tideorg/js` exports 7 namespaces. Only `Models` and `Contracts` are needed for app-level policy work. The others are internal SDK plumbing.
669
+ `@tideorg/js` exports 8 namespaces. Only `Models` and `Contracts` are needed for app-level policy work. The others are internal SDK plumbing.
667
670
 
668
- **Top-level namespaces**: `Models`, `Contracts`, `Clients`, `Cryptide`, `Flow`, `Math`, `Tools` (also aliased as `Utils`).
671
+ **Top-level namespaces**: `Models`, `Contracts`, `Clients`, `Cryptide`, `Errors`, `Flow`, `Math`, `Tools` (also aliased as `Utils`).
672
+
673
+ **Top-level convenience re-exports** (alongside the namespaces): `TideError` and `TideJsErrorCodes` (from `Errors`), and `RecentRequestsBuffer` (from `Clients`). The `Errors` namespace carries the SDK's structured error-reporting surface. **VERIFIED** (tide-js `index.ts` on `main`).
669
674
 
670
675
  #### Models (app-facing — used in policy signing)
671
676
 
@@ -733,24 +738,31 @@ Key class: `Flow.SigningFlows.dVVKSigningFlow2Step` — two-step VVK signing. Me
733
738
 
734
739
  ## Admin Policy Endpoint
735
740
 
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)
741
+ **What it is**: How to fetch the signed admin policy needed in step 3 of policy signing. **VERIFIED** (custom-contracts.md; LEARNINGS-ratidefy-batch-001 L-22)
737
742
 
738
743
  **Endpoints**:
739
744
  ```
740
745
  WRONG: GET /admin/realms/{realm}/tide-admin/realm-policy
741
746
  Returns: { status: "none" } even when the policy exists
742
747
 
743
- CORRECT: GET /realms/{realm}/tide-policy-resources/admin-policy
744
- Returns: base64-encoded bytes (does not require admin credentials)
748
+ WRONG: GET /realms/{realm}/tide-policy-resources/admin-policy
749
+ This public endpoint does NOT exist on current main.
750
+
751
+ CORRECT: GET /admin/realms/{realm}/iga/role-policies (server-side, admin bearer token)
752
+ Returns: an array of role-policy records; the signed policy bytes
753
+ live in each record's `policy` field (base64), alongside `policySig`.
745
754
  ```
746
755
 
747
- **Response handling**: The correct endpoint returns base64 text, not raw binary. Decode on backend:
756
+ **Response handling**: Fetch server-side with an admin bearer token, pick the `admin-policy` record, and decode its `policy` field. Proxy the bytes to the browser:
748
757
  ```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 });
758
+ // Backend proxy (admin token required)
759
+ const rpUrl = `${authServerUrl}/admin/realms/${realm}/iga/role-policies`;
760
+ const rolePolicies = await fetch(rpUrl, {
761
+ headers: { Authorization: `Bearer ${adminToken}` },
762
+ }).then(r => r.json());
763
+ const adminPolicyB64 = rolePolicies.find(p => p.name === 'admin-policy')?.policy;
764
+ const raw = Buffer.from(adminPolicyB64, "base64");
765
+ res.json({ policyBytes: Array.from(new Uint8Array(raw)) });
754
766
 
755
767
  // Frontend
756
768
  const data = await res.json();
@@ -259,7 +259,7 @@ Gas limit is 50,000. Exceeding it throws `OutOfGasException` and the operation f
259
259
  ### Step 1: Upload Contract to TideCloak (Optional)
260
260
 
261
261
  ```
262
- PUT /admin/realms/{realm}/tide-admin/forseti-contracts
262
+ POST /admin/realms/{realm}/iga/forseti-contracts
263
263
  Content-Type: application/json
264
264
  Authorization: Bearer <admin-token>
265
265
 
@@ -269,7 +269,7 @@ Authorization: Bearer <admin-token>
269
269
  }
270
270
  ```
271
271
 
272
- The ORK compiles the contract in its sandbox and stores it. The contract ID is `SHA512(source code)`.
272
+ The ORK compiles the contract in its sandbox and stores it. The response carries the stored contract's `contractHash` (`SHA512(source code)`).
273
273
 
274
274
  Note: This endpoint may not be available on all TideCloak images. If it returns 404/405, the contract is deployed through the policy signing flow (Step 3) instead — the contract source is included in the contract transport.
275
275
 
@@ -305,11 +305,15 @@ Important:
305
305
 
306
306
  Policy deployment requires the realm's **admin policy** to authorize the operation. The admin policy is pre-signed during realm setup (IGA bootstrap) and must be attached to every policy creation request. VERIFIED (LEARNINGS-ratidefy-batch-001 L-22).
307
307
 
308
- **Fetch the admin policy** (server-side — endpoint does not support CORS):
308
+ **Fetch the admin policy** (server-side, admin bearer token the signed policy is stored as a role policy). The public `tide-policy-resources/admin-policy` endpoint is not present on current main; retrieve the signed policy bytes from the admin IGA surface instead:
309
309
  ```typescript
310
- // Server-side: proxy this to the browser, or pre-download as static file
311
- const adminPolicyUrl = `${authServerUrl}/realms/${realm}/tide-policy-resources/admin-policy`;
312
- const adminPolicyB64 = await fetch(adminPolicyUrl).then(r => r.text());
310
+ // Server-side (admin token required): read the signed role/admin policy.
311
+ const rpUrl = `${authServerUrl}/admin/realms/${realm}/iga/role-policies`;
312
+ const rolePolicies = await fetch(rpUrl, {
313
+ headers: { Authorization: `Bearer ${adminToken}` },
314
+ }).then(r => r.json());
315
+ // Each record carries `policy` (base64 signed bytes) + `policySig`.
316
+ const adminPolicyB64 = rolePolicies.find(p => p.name === 'admin-policy')?.policy;
313
317
  const adminPolicyBytes = Uint8Array.from(atob(adminPolicyB64), c => c.charCodeAt(0));
314
318
  ```
315
319
 
@@ -358,7 +362,7 @@ const signedPolicyBytes = policy.toBytes(); // Store THESE bytes
358
362
  - Use `tc.createTideRequest()` → `tc.requestTideOperatorApproval()` → `tc.executeSignRequest()` — the React context's `initializeTideRequest` does not expose the approve/execute steps. VERIFIED (LEARNINGS-ratidefy-batch-001 L-24).
359
363
  - `initializeTideRequest` returns a **new object** — it does NOT mutate in place. If using it, capture the return value (AP-59).
360
364
  - Store `policy.toBytes()` (with signature attached), NOT `request.encode()` (AP-57).
361
- - Admin policy is fetched from `/realms/{realm}/tide-policy-resources/admin-policy`. This endpoint has no CORS fetch server-side and proxy to browser (see RB-008). VERIFIED (LEARNINGS-ratidefy-batch-001 L-22).
365
+ - Admin policy is fetched server-side (admin bearer) from `/admin/realms/{realm}/iga/role-policies` — the signed bytes live in each record's `policy` field. The old public `tide-policy-resources/admin-policy` endpoint is not present on current main; proxy the fetched bytes to the browser (see RB-008).
362
366
 
363
367
  ### Step 4: Use the Signed Policy
364
368
 
@@ -506,7 +510,7 @@ Available: `System`, `System.Linq`, `System.Collections.Generic`, `System.Text`,
506
510
  - **Wrong context properties**: Use `ctx.Dokens` (not `ctx.Approvers`) in `ValidateApprovers`, `ctx.Doken` (not `ctx.Executor`) in `ValidateExecutor`. Wrap with `DokenDto.WrapAll(ctx.Dokens)` and `new DokenDto(ctx.Doken)`. VERIFIED (LEARNINGS-ratidefy-batch-001 L-23).
507
511
  - **Params as plain object**: Use `[['Role', 'admin']]` not `{ Role: 'admin' }` (AP-54)
508
512
  - **Store request.encode() as policy**: Use `policy.toBytes()` not `request.encode()` (AP-57). Related: AP-55 (don't store raw VVK sig either)
509
- - **Missing admin policy**: Policy deployment requires the realm's admin policy attached to the request. Fetch from `/realms/{realm}/tide-policy-resources/admin-policy` (AP, LEARNINGS-ratidefy-batch-001 L-22)
513
+ - **Missing admin policy**: Policy deployment requires the realm's admin policy attached to the request. Fetch (admin bearer) from `/admin/realms/{realm}/iga/role-policies` (`policy` field). The old `tide-policy-resources/admin-policy` endpoint is not on current main. (AP, LEARNINGS-ratidefy-batch-001 L-22)
510
514
  - **JSON to createTideRequest**: Pass `Uint8Array` from `signRequest.encode()` (AP-53)
511
515
  - **Import BasicCustomRequest from wrong package**: Use `asgard-tide`, not `@tideorg/js` or `@tidecloak/js` (LEARNINGS-ratidefy-batch-001 L-11)
512
516
  - **Import from @tidecloak/nextjs**: Use `@tidecloak/js` for Models, `heimdall-tide` for PolicySignRequest, `asgard-tide` for BasicCustomRequest