@tideorg/mcp 1.4.1 → 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.
- package/CHANGELOG.md +81 -0
- package/GAP_REGISTER.md +19 -9
- package/PRIVACY.md +41 -0
- package/README.md +204 -120
- package/adapters/AGENTS.md +3 -2
- package/adapters/CLAUDE.md +1 -1
- package/adapters/replit.md +0 -0
- package/canon/anti-patterns.md +55 -62
- package/canon/concepts.md +46 -34
- package/canon/custom-contracts.md +12 -8
- package/canon/feature-mapping.md +27 -75
- package/canon/framework-matrix.md +69 -22
- package/canon/hosting-options.md +111 -0
- package/canon/iga-change-requests-api.md +211 -0
- package/canon/invariants.md +33 -35
- package/canon/redirect-handler.md +0 -0
- package/canon/security-gap-mapping.md +506 -0
- package/canon/security-runtime-probes.md +177 -0
- package/canon/tidecloak-bootstrap.md +84 -15
- package/canon/tidecloak-endpoints.md +60 -98
- package/canon/troubleshooting.md +201 -53
- package/canon/version-policy.md +8 -12
- package/mcp-server/dist/http.d.ts +2 -0
- package/mcp-server/dist/http.js +46 -0
- package/mcp-server/dist/index.d.ts +0 -0
- package/mcp-server/dist/index.js +2 -601
- package/mcp-server/dist/server.d.ts +2 -0
- package/mcp-server/dist/server.js +617 -0
- package/package.json +48 -45
- package/playbooks/add-auth-nextjs-existing.md +33 -17
- package/playbooks/add-auth-nextjs-fresh.md +1 -1
- package/playbooks/add-rbac-nextjs.md +7 -2
- package/playbooks/bootstrap-realm-from-template.md +33 -18
- package/playbooks/configure-e2ee-roles-and-policies.md +0 -0
- package/playbooks/deploy-tidecloak-docker.md +31 -28
- package/playbooks/diagnose-broken-login.md +0 -0
- package/playbooks/diagnose-missing-roles-or-claims.md +2 -2
- package/playbooks/initialize-admin-and-link-account.md +22 -19
- package/playbooks/migrate-from-existing-auth.md +0 -0
- package/playbooks/protect-api-nextjs.md +40 -1
- package/playbooks/protect-aspnet-core-asgard.md +313 -0
- package/playbooks/protect-routes-nextjs.md +24 -10
- package/playbooks/provision-tidecloak-skycloak.md +213 -0
- package/playbooks/setup-forseti-e2ee.md +32 -50
- package/playbooks/setup-iga-admin-panel.md +113 -171
- package/playbooks/start-tidecloak-dev.md +0 -0
- package/playbooks/verify-jwt-server-side.md +20 -1
- package/prompts/add-admin-approval-flow.md +0 -0
- package/prompts/build-private-customer-portal.md +1 -1
- package/prompts/migrate-generic-auth-to-tide.md +0 -0
- package/prompts/secure-existing-app.md +0 -0
- package/prompts/security-gap-analysis.md +55 -0
- package/reference-apps/INDEX.md +0 -0
- package/reference-apps/encrypted-communication/anti-patterns.md +3 -3
- package/reference-apps/encrypted-communication/bootstrap-sequence.md +2 -2
- package/reference-apps/encrypted-communication/manifest.yaml +0 -0
- package/reference-apps/encrypted-communication/role-policy-matrix.md +0 -0
- package/reference-apps/encrypted-communication/scenario.md +2 -2
- package/reference-apps/git-pr-signing-service/anti-patterns.md +0 -0
- package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +8 -8
- package/reference-apps/git-pr-signing-service/manifest.yaml +0 -0
- package/reference-apps/git-pr-signing-service/role-policy-matrix.md +0 -0
- package/reference-apps/git-pr-signing-service/scenario.md +0 -0
- package/reference-apps/iga-admin-governance/anti-patterns.md +18 -16
- package/reference-apps/iga-admin-governance/bootstrap-sequence.md +10 -5
- package/reference-apps/iga-admin-governance/manifest.yaml +0 -0
- package/reference-apps/iga-admin-governance/role-policy-matrix.md +12 -13
- package/reference-apps/iga-admin-governance/scenario.md +15 -13
- package/reference-apps/organisation-password-manager/anti-patterns.md +0 -0
- package/reference-apps/organisation-password-manager/bootstrap-sequence.md +5 -6
- package/reference-apps/organisation-password-manager/manifest.yaml +0 -0
- package/reference-apps/organisation-password-manager/role-policy-matrix.md +0 -0
- package/reference-apps/organisation-password-manager/scenario.md +0 -0
- package/reference-apps/policy-governed-signing/anti-patterns.md +0 -0
- package/reference-apps/policy-governed-signing/bootstrap-sequence.md +9 -9
- package/reference-apps/policy-governed-signing/manifest.yaml +0 -0
- package/reference-apps/policy-governed-signing/role-policy-matrix.md +0 -0
- package/reference-apps/policy-governed-signing/scenario.md +0 -0
- package/skills/tide-diagnostics/SKILL.md +1 -1
- package/skills/tide-integration/SKILL.md +9 -18
- package/skills/tide-learning-capture/SKILL.md +0 -0
- package/skills/tide-mcp-qa/SKILL.md +139 -0
- package/skills/tide-rbac-and-e2ee/SKILL.md +5 -5
- package/skills/tide-reviewer/SKILL.md +1 -1
- package/skills/tide-route-and-api-protection/SKILL.md +0 -0
- package/skills/tide-scenario-resolver/SKILL.md +0 -0
- package/skills/tide-security-analyst/SKILL.md +182 -0
- package/skills/tide-setup/SKILL.md +1 -1
- package/skills/tide-solutions-architect/SKILL.md +0 -0
- package/canon/delegation.md +0 -195
- package/playbooks/setup-server-delegation.md +0 -142
package/canon/anti-patterns.md
CHANGED
|
@@ -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'
|
|
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'
|
|
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'
|
|
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 /
|
|
210
|
-
// and commit via /
|
|
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
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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(
|
|
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}/
|
|
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'
|
|
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
|
|
821
|
-
|
|
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
|
-
|
|
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'
|
|
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(
|
|
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'
|
|
904
|
-
const encMedical = await iam.doEncrypt('data'
|
|
905
|
-
const encNotes = await iam.doEncrypt('data'
|
|
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'
|
|
920
|
-
const encMedical = await iam.doEncrypt('medical'
|
|
921
|
-
const encNotes = await iam.doEncrypt('notes'
|
|
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'
|
|
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'
|
|
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
|
|
967
|
-
return
|
|
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
|
-
|
|
983
|
-
|
|
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:
|
|
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
|
-
**
|
|
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:
|
|
1159
|
-
import
|
|
1160
|
-
const
|
|
1161
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
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.
|
|
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-
|
|
112
|
+
**Change-request lifecycle** **VERIFIED** (qea-iga-api.md):
|
|
113
113
|
```
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
PENDING → APPROVED (committed) → (ACTIVE)
|
|
115
|
+
→ DENIED
|
|
116
|
+
→ CANCELLED
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
**Change-
|
|
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}/
|
|
122
|
-
GET /admin/realms/{realm}/
|
|
123
|
-
|
|
124
|
-
POST /admin/realms/{realm}/
|
|
125
|
-
POST /admin/realms/{realm}/
|
|
126
|
-
POST /admin/realms/{realm}/
|
|
127
|
-
POST /admin/realms/{realm}/
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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** (
|
|
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}/
|
|
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}/
|
|
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.
|
|
667
|
+
### `@tideorg/js` Export Reference (v0.13.33)
|
|
665
668
|
|
|
666
|
-
`@tideorg/js` exports
|
|
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**:
|
|
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
|
-
|
|
744
|
-
|
|
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**:
|
|
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
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
311
|
-
const
|
|
312
|
-
const
|
|
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
|
|
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
|