@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.
- package/CHANGELOG.md +81 -0
- package/GAP_REGISTER.md +18 -8
- package/PRIVACY.md +41 -0
- package/README.md +204 -197
- package/adapters/AGENTS.md +3 -2
- package/adapters/CLAUDE.md +1 -1
- 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/security-gap-mapping.md +506 -0
- package/canon/security-runtime-probes.md +177 -0
- package/canon/tidecloak-bootstrap.md +21 -16
- package/canon/tidecloak-endpoints.md +60 -98
- package/canon/troubleshooting.md +115 -53
- package/canon/version-policy.md +8 -12
- package/mcp-server/dist/server.js +221 -19
- 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/deploy-tidecloak-docker.md +31 -28
- package/playbooks/diagnose-missing-roles-or-claims.md +2 -2
- package/playbooks/initialize-admin-and-link-account.md +22 -19
- 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/verify-jwt-server-side.md +20 -1
- package/prompts/build-private-customer-portal.md +1 -1
- package/prompts/security-gap-analysis.md +55 -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/scenario.md +2 -2
- package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +8 -8
- 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/role-policy-matrix.md +12 -13
- package/reference-apps/iga-admin-governance/scenario.md +15 -13
- package/reference-apps/organisation-password-manager/bootstrap-sequence.md +5 -6
- package/reference-apps/policy-governed-signing/bootstrap-sequence.md +9 -9
- package/skills/tide-diagnostics/SKILL.md +1 -1
- package/skills/tide-integration/SKILL.md +9 -18
- 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-security-analyst/SKILL.md +182 -0
- package/skills/tide-setup/SKILL.md +1 -1
- package/canon/delegation.md +0 -195
- package/playbooks/setup-server-delegation.md +0 -142
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# IGA / QEA Change-Requests API (current surface)
|
|
2
|
+
|
|
3
|
+
The authoritative REST surface for IGA (QEA — Quorum Enforced Authorization) change requests. **VERIFIED** (source: `tide-foundation/tidecloak-iga-extensions` → `docs/qea-iga-api.md`, iga-core; confirmed by Tide 2026-07-07, GAP-065).
|
|
4
|
+
|
|
5
|
+
**This `/iga/change-requests/...` surface REPLACES the legacy `/tide-admin/change-set/...` surface.** The old surface (documented in the repo-root `CHANGE_REQUESTS_API.md`) is stale — do not use it for new work. Enabling IGA is unchanged: `POST /tide-admin/toggle-iga` (realm attribute `isIGAEnabled="true"`).
|
|
6
|
+
|
|
7
|
+
For the security/trust implications and the Tide-vs-Tideless mode split, see `canon/feature-mapping.md` (IGA Quorum Approval), `canon/invariants.md` (I-10), and `canon/security-gap-mapping.md` (IGA-model note).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Base path & auth
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
{keycloak-url}/admin/realms/{realm}/iga/...
|
|
15
|
+
{keycloak-url}/admin/realms/{realm}/iga-tve/... # TVE diagnostic bundles
|
|
16
|
+
{keycloak-url}/admin/realms/{realm}/tide-admin/... # toggle-iga, legacy
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Header: `Authorization: Bearer $TOKEN` — requires `manage-realm` (some reads are lighter, noted below).
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Change-request lifecycle
|
|
24
|
+
|
|
25
|
+
| Step | Method + path | Body | Success | Notable failures |
|
|
26
|
+
|------|---------------|------|---------|------------------|
|
|
27
|
+
| A governed admin write is captured | (the original admin call, e.g. assign role) | — | **202 Accepted** + `Location` header → the new CR | — |
|
|
28
|
+
| List pending | `GET /iga/change-requests?status=PENDING` | — | 200, array of CR objects | — |
|
|
29
|
+
| Get one | `GET /iga/change-requests/{id}` | — | 200 | 404 |
|
|
30
|
+
| Authorize (sign) — simple lane | `POST /iga/change-requests/{id}/authorize` | `{}` (optional) | 200, updated CR (**does not commit**) | 403 (missing approver role), 409 (not PENDING / already signed / conflicting CR / a MultiAdmin CR) |
|
|
31
|
+
| Approve — MultiAdmin enclave lane | `POST /iga/change-requests/{id}/approve` | enclave approval payload | 200, updated CR (**auto-commits once quorum met**) | 403, 409, 412 |
|
|
32
|
+
| Commit | `POST /iga/change-requests/{id}/commit` | — | 200, CR now APPROVED | 412 (under threshold or unmet dependency) |
|
|
33
|
+
| Edit pending CR | `PUT /iga/change-requests/{id}` | `{"rows":[...]}` | 200, updated CR | 404, 409 (not PENDING) |
|
|
34
|
+
| Deny | `POST /iga/change-requests/{id}/deny` | — | 204 | 404 |
|
|
35
|
+
| Bulk authorize | `POST /iga/change-requests/bulk-authorize` | `{"actionTypeIn":["CREATE","DELETE"],"limit":100}` | 200, `{results:[...], summary:{...}}` | 429 (a bulk run is already in progress — per-realm mutex) |
|
|
36
|
+
|
|
37
|
+
**`status`** values: `PENDING`, `APPROVED`, `DENIED`, `CANCELLED`.
|
|
38
|
+
|
|
39
|
+
### `authorize` vs `approve` vs `commit` — do not conflate
|
|
40
|
+
|
|
41
|
+
Three distinct server operations (source: `IgaAdminResource` on origin/main). The button label in the admin console is **not** the same as the REST verb:
|
|
42
|
+
|
|
43
|
+
- **`POST .../{id}/authorize`** (`IgaAdminResource:529`) — the **simple lane** (FirstAdmin / Tideless). Records the caller's approval and **NEVER commits**: even when `authCount >= threshold` the server does *not* call `combineFinal()`; commit is a separate explicit step. **Refuses tide-MultiAdmin CRs.**
|
|
44
|
+
- **`POST .../{id}/approve`** (`IgaAdminResource:1096`) — the **MultiAdmin enclave lane**. Consumes the enclave approval and **approves AND auto-commits once quorum is met**. This is what the deployed admin console's **"Authorize" button** calls (button label ≠ REST `/authorize`).
|
|
45
|
+
- **`POST .../{id}/commit`** — explicit **apply-only** step (`412` if still sub-quorum / dependency unmet). Used after a simple-lane `authorize` reaches threshold.
|
|
46
|
+
|
|
47
|
+
**`PUT /iga/change-requests/{id}`** (`IgaAdminResource:1848`, `updateChangeRequest` → `service.updateRows()`) edits a still-`PENDING` CR's `rows` payload; body `{"rows":[...]}`.
|
|
48
|
+
|
|
49
|
+
**Bulk-authorize authorizes; it does not commit.** After bulk-authorize, commit the CRs that became `readyToCommit` (see the bootstrap pattern below). ("Bulk Commit" in the admin UI is a client-side loop, not a server endpoint.)
|
|
50
|
+
|
|
51
|
+
### Two-phase MultiAdmin approval (Tide mode enclave)
|
|
52
|
+
|
|
53
|
+
In Tide mode (`iga.attestor=tide`) with MultiAdmin, the simple `authorize` lane refuses the CR; approval instead goes through `approve`, which requires an enclave signature obtained via the approval-model exchange:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Phase 1: GET /iga/change-requests/{id}/approval-model
|
|
57
|
+
→ 200 {"changeRequestId":"...","requestModel":"<base64>"}
|
|
58
|
+
(decode requestModel, present to the Tide enclave, admin signs → base64 doken)
|
|
59
|
+
|
|
60
|
+
Phase 2: POST /iga/change-requests/{id}/approve (MultiAdmin enclave lane — approves and auto-commits at quorum)
|
|
61
|
+
Body: the enclave approval / {"requestModel":"<base64 doken>"}
|
|
62
|
+
→ 200 {"recorded":true,"authCount":N,"threshold":M}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**This is the automation boundary.** In FirstAdmin mode (fresh realm before any `tide-realm-admin`) and in Tideless mode, the simple-lane `authorize` signs server-side with no enclave popup (then `commit` applies), so bootstrap is fully automatable. In Tide MultiAdmin mode a human must sign the enclave challenge for `approve`.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Change-request object (response fields)
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
id, realmId, entityType, entityId, actionType,
|
|
73
|
+
status, # PENDING | APPROVED | DENIED | CANCELLED
|
|
74
|
+
requestedBy, createdAt, resolvedAt, resolvedBy,
|
|
75
|
+
rows, # payload replayed on commit
|
|
76
|
+
authorizationCount, authorizers[], threshold,
|
|
77
|
+
readyToCommit, # boolean — safe to commit now
|
|
78
|
+
requiredApproverRoles, scopeMode, # scopeMode: "any" | "all"
|
|
79
|
+
dependsOn, blocked, blockedReason # dependency gating
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Use `id` for all per-CR operations** (this replaces the legacy `draftRecordId`). `entityType`/`actionType` replace the legacy `changeSetType`/`changeSetType`+`actionType` fields, and there is no `{changeSets:[...]}` request envelope anymore — operations are per-`id`.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Comments
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
GET /iga/change-requests/{id}/comments → 200 array
|
|
90
|
+
POST /iga/change-requests/{id}/comments {"comment":"text"} (≤2000 chars) → 201
|
|
91
|
+
PUT /iga/change-requests/{id}/comments/{commentId} → 200 (author only)
|
|
92
|
+
DELETE /iga/change-requests/{id}/comments/{commentId} → 204 (author or realm admin)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Manual ADOPT
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
POST /iga/adopt {"entityType":"USER","entityId":"..."} → 201 | 409 (ALREADY_ATTESTED)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Role policies (M0 admin policy)
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
GET /iga/role-policies # read: authenticated, no manage-realm
|
|
105
|
+
GET /iga/role-policies/{id}
|
|
106
|
+
GET /iga/role-policies/name/tide-realm-admin
|
|
107
|
+
POST /iga/role-policies {"name":"...","policy":"<base64>","policySig":"..."} # 403 if name="tide-realm-admin"
|
|
108
|
+
DELETE /iga/role-policies/{id}
|
|
109
|
+
DELETE /iga/role-policies/name/{name}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Diagnostics
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
GET /iga/change-requests/{id}/diagnostic-bundle
|
|
116
|
+
→ {"diag_kind":"iga_cr_bundle","schema_version":1,"cr":{...},"authorizations":[...],"threshold":N,"approver_role":"..."}
|
|
117
|
+
POST /iga-tve/tve-bundle {"mode":"synthesize|pasted","clientId":"...","userId":"..."} (Accept: application/json | application/cbor)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Status codes (summary)
|
|
123
|
+
|
|
124
|
+
| Code | Meaning |
|
|
125
|
+
|------|---------|
|
|
126
|
+
| 202 | Governed action captured into a CR (`Location` → the CR) |
|
|
127
|
+
| 400 | Malformed body / missing field |
|
|
128
|
+
| 403 | Missing approver role |
|
|
129
|
+
| 404 | Unknown CR or the entity vanished |
|
|
130
|
+
| 409 | CR not PENDING, already signed, or a conflicting CR exists (four-eyes) |
|
|
131
|
+
| 412 | Under threshold, or a dependency is unmet |
|
|
132
|
+
| 429 | `bulk-authorize` already running (per-realm mutex) |
|
|
133
|
+
|
|
134
|
+
## Threshold & approver resolution
|
|
135
|
+
|
|
136
|
+
- `ADOPT_*` CRs short-circuit to threshold 1 (no approver-role check).
|
|
137
|
+
- Otherwise the entity-level `iga.threshold` applies **only if that entity also sets `iga.approverRole`** (the coupling rule); else the realm `iga.threshold`; else 1 (clamped `max(1, …)` — the gate can't be disabled).
|
|
138
|
+
- **FirstAdmin** mode (fresh realm, before any `tide-realm-admin`): threshold 1, any `manage-realm` admin signs, server-side VRK, no enclave.
|
|
139
|
+
- **MultiAdmin** (Tide): dynamic floor `max(1, floor(0.7 × active tide-realm-admins))` unless overridden; enclave signature required.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Bootstrap pattern (FirstAdmin / Tideless — fully automatable)
|
|
144
|
+
|
|
145
|
+
During bootstrap the realm is in FirstAdmin (or Tideless) mode, so `authorize` signs server-side and the whole loop is scriptable. Replace the legacy per-type `sign`+`commit` calls with: **authorize all pending → commit the ready ones, in dependency passes.**
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# $TC = base url, $REALM = realm, get_token() mints a fresh admin token.
|
|
149
|
+
approve_all_pending() {
|
|
150
|
+
local TOKEN
|
|
151
|
+
# 1. Authorize every pending CREATE/DELETE change request in one call.
|
|
152
|
+
# FirstAdmin/Tideless: server signs with VRK / records username — no enclave.
|
|
153
|
+
TOKEN="$(get_token)"
|
|
154
|
+
curl -s -X POST "$TC/admin/realms/$REALM/iga/change-requests/bulk-authorize" \
|
|
155
|
+
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
156
|
+
-d '{"actionTypeIn":["CREATE","DELETE"],"limit":100}' > /dev/null
|
|
157
|
+
|
|
158
|
+
# 2. Commit everything that is ready. Loop a few passes so dependent CRs
|
|
159
|
+
# (e.g. a role must exist before its assignment) become ready and commit.
|
|
160
|
+
for pass in 1 2 3 4 5; do
|
|
161
|
+
TOKEN="$(get_token)"
|
|
162
|
+
local ready
|
|
163
|
+
ready=$(curl -s "$TC/admin/realms/$REALM/iga/change-requests?status=PENDING" \
|
|
164
|
+
-H "Authorization: Bearer $TOKEN" | jq -r '.[] | select(.readyToCommit==true) | .id')
|
|
165
|
+
[ -z "$ready" ] && break
|
|
166
|
+
for id in $ready; do
|
|
167
|
+
TOKEN="$(get_token)"
|
|
168
|
+
curl -s -X POST "$TC/admin/realms/$REALM/iga/change-requests/$id/commit" \
|
|
169
|
+
-H "Authorization: Bearer $TOKEN" > /dev/null
|
|
170
|
+
done
|
|
171
|
+
done
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Call `approve_all_pending` at each stage that produces CRs (after client creation, after user creation, after role assignment) — the same stage points as the legacy script, but it now lists **all** pending CRs at once rather than per type.
|
|
176
|
+
|
|
177
|
+
**Per-id alternative** (if you prefer not to use bulk-authorize):
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
TOKEN="$(get_token)"
|
|
181
|
+
for id in $(curl -s "$TC/admin/realms/$REALM/iga/change-requests?status=PENDING" \
|
|
182
|
+
-H "Authorization: Bearer $TOKEN" | jq -r '.[].id'); do
|
|
183
|
+
TOKEN="$(get_token)"
|
|
184
|
+
curl -s -X POST "$TC/admin/realms/$REALM/iga/change-requests/$id/authorize" \
|
|
185
|
+
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}' > /dev/null
|
|
186
|
+
done
|
|
187
|
+
# then the same commit-in-passes loop as above
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Status of the bootstrap pattern: VERIFIED against the API spec; REQUIRES_RUNTIME_VALIDATION against a live iga-core instance.** The endpoint paths, payloads, and status codes are from the canonical `qea-iga-api.md`; the exact loop/ordering has not yet been run end-to-end here. Validate on a live instance and adjust the pass count / dependency handling if a CR stays `blocked`.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Old → new migration map
|
|
195
|
+
|
|
196
|
+
| Legacy (`/tide-admin/change-set/...`) | Current (`/iga/change-requests/...`) |
|
|
197
|
+
|----------------------------------------|--------------------------------------|
|
|
198
|
+
| `GET .../change-set/{type}/requests` | `GET /iga/change-requests?status=PENDING` |
|
|
199
|
+
| `GET .../change-set/counts` | (derive from the list, or use diagnostics) |
|
|
200
|
+
| `POST .../change-set/sign/batch` `{changeSets:[{changeSetId,changeSetType,actionType}]}` | `POST /iga/change-requests/{id}/authorize` `{}` (per id); batch: `POST /iga/change-requests/bulk-authorize` |
|
|
201
|
+
| `POST .../change-set/commit/batch` `{changeSets:[...]}` | `POST /iga/change-requests/{id}/commit` |
|
|
202
|
+
| `POST .../change-set/cancel/batch` | `POST /iga/change-requests/{id}/deny` |
|
|
203
|
+
| `POST .../tideAdminResources/add-review` (enclave) | `POST /iga/change-requests/{id}/approval-model` |
|
|
204
|
+
| field `draftRecordId` | field `id` |
|
|
205
|
+
| fields `changeSetType` + `actionType` | fields `entityType` + `actionType` |
|
|
206
|
+
| toggle `POST /tide-admin/toggle-iga` | **unchanged** |
|
|
207
|
+
|
|
208
|
+
## Status Legend
|
|
209
|
+
|
|
210
|
+
- **VERIFIED** — from the canonical `qea-iga-api.md` (iga-core)
|
|
211
|
+
- **REQUIRES_RUNTIME_VALIDATION** — bootstrap loop not yet exercised end-to-end against a live instance
|
package/canon/invariants.md
CHANGED
|
@@ -136,7 +136,7 @@ grep -r "hasRole\|checkRole" server/routes/ | grep -v "after verifyJWT"
|
|
|
136
136
|
**Rule**: JWT verification must use embedded JWKS from adapter JSON. Do not use `createRemoteJWKSet`. Do not fetch keys from the remote OIDC certs endpoint. If the `jwk` field is missing from `tidecloak.json`, this is a setup/bootstrap failure — stop and fix the export, do not fall back to remote key fetching.
|
|
137
137
|
|
|
138
138
|
**What this means**:
|
|
139
|
-
- Adapter JSON contains `jwk: { keys: JWK[] }` field (EdDSA public key
|
|
139
|
+
- Adapter JSON contains `jwk: { keys: JWK[] }` field (EdDSA public key). It is emitted whenever the realm has a Tide vendor key; it is NOT gated on IGA being enabled.
|
|
140
140
|
- Use `createLocalJWKSet(config.jwk)` from `jose`
|
|
141
141
|
- If `config.jwk` is missing, throw an error. The fix is to re-export the adapter from TideCloak with IGA enabled, not to fetch keys remotely.
|
|
142
142
|
- Also validate `azp` matches `config.resource`
|
|
@@ -145,6 +145,8 @@ grep -r "hasRole\|checkRole" server/routes/ | grep -v "after verifyJWT"
|
|
|
145
145
|
|
|
146
146
|
**Forbidden**: `createRemoteJWKSet` must not be used in Tide verification flows. Do not add it as a fallback. Do not import it. If verification fails with `ERR_JWKS_NO_MATCHING_KEY`, the adapter JSON is stale — re-export it from TideCloak.
|
|
147
147
|
|
|
148
|
+
**Note on the shipped `@tidecloak/verify`**: the packaged `verifyTideCloakToken` DOES fall back to `createRemoteJWKSet` when `config.jwk` is absent (`tidecloak-js: packages/tidecloak-verify/src/TideJWT.js`). That fallback is exactly the network path this invariant forbids. Therefore the operative rule for Tide apps is: **always pass `config.jwk` so verification uses the local JWKS; never let it fall through to the remote path.** A missing `jwk` is a setup failure to fix at the source, not a code path to rely on. This is intentional security doctrine — the pack is deliberately stricter than the SDK default.
|
|
149
|
+
|
|
148
150
|
**Required pattern**:
|
|
149
151
|
```typescript
|
|
150
152
|
import { createLocalJWKSet, jwtVerify } from 'jose';
|
|
@@ -186,7 +188,7 @@ grep -r "createRemoteJWKSet" lib/auth/ src/lib/auth/
|
|
|
186
188
|
|
|
187
189
|
**Related invariants**: I-03 (Server-Side Verification), I-05 (Adapter JSON Required)
|
|
188
190
|
|
|
189
|
-
**Note**: The `jwk` field is
|
|
191
|
+
**Note**: The `jwk` field is present whenever the realm has a Tide vendor key (it is not gated on IGA). Validate at startup. If missing, the fix is TideCloak bootstrap / re-exporting the adapter, not remote JWKS.
|
|
190
192
|
|
|
191
193
|
---
|
|
192
194
|
|
|
@@ -197,7 +199,7 @@ grep -r "createRemoteJWKSet" lib/auth/ src/lib/auth/
|
|
|
197
199
|
**Canonical filename**: `tidecloak.json`. Do not use `adapter.json`, `keycloak.json`, or other names.
|
|
198
200
|
|
|
199
201
|
**What this means**:
|
|
200
|
-
- Export adapter JSON from TideCloak
|
|
202
|
+
- Export adapter JSON from TideCloak via the realm-level endpoint `GET /admin/realms/{realm}/vendorResources/get-installations-provider?clientId={clientId}&providerId=keycloak-oidc-keycloak-json`. The client is a **query parameter** (`clientId=`), NOT a `/clients/{id}/` path segment. `keycloak-oidc-keycloak-json` is the only valid provider ID (`tidecloak-oidc-keycloak-json` does not exist).
|
|
201
203
|
- Store at the correct location for the framework (see below)
|
|
202
204
|
- Load at startup
|
|
203
205
|
- Validate presence of Tide extensions
|
|
@@ -429,12 +431,14 @@ grep -r "if.*admin.*skip.*verify" server/
|
|
|
429
431
|
|
|
430
432
|
**Rule**: Admin changes to roles, users, clients require quorum approval sealed by VVK threshold signatures. No single admin can act unilaterally. **VERIFIED** (IGA.mdx, SetupIGA.md, keylessh `init-tidecloak.sh`)
|
|
431
433
|
|
|
432
|
-
**Quorum formula**: `max(1, floor(TotalAdmins * 0.7))` **VERIFIED** (SetupIGA.md)
|
|
434
|
+
**Quorum formula**: `max(1, floor(TotalAdmins * 0.7))` **VERIFIED** (SetupIGA.md). This is the cryptographic quorum in **Tide mode** (`iga.attestor=tide`). In **Tideless mode** (`iga.attestor=simple`/unset) the same quorum count is enforced by TideCloak server logic with a username attestation and no cryptography — see `canon/security-gap-mapping.md` IGA-model note.
|
|
435
|
+
|
|
436
|
+
> **⚠️ API SURFACE MIGRATION — confirmed by Tide 2026-07-07 (GAP-065).** The change-request endpoints have moved to **`/admin/realms/{realm}/iga/change-requests/...`** (iga-core), which **replaces** the legacy `/tide-admin/change-set/...` paths below. New surface: approve `POST .../iga/change-requests/{id}/authorize` (409 = four-eyes re-sign), commit `POST .../iga/change-requests/{id}/commit` (412 = quorum unmet), reject `POST .../{id}/deny`, batch `POST /iga/change-requests/bulk-authorize`, list `GET /iga/change-requests?status=PENDING`, Tide-mode enclave step `POST .../{id}/approval-model`. Enabling IGA is unchanged (`POST /tide-admin/toggle-iga`). **Full endpoint spec, payloads, and the bootstrap approve/commit loop: `canon/iga-change-requests-api.md`.** Bootstrap scripts and the examples below now use the new surface; verify against the live instance (bootstrap loop is REQUIRES_RUNTIME_VALIDATION).
|
|
433
437
|
|
|
434
|
-
**Change-
|
|
438
|
+
**Change-request workflow** (legacy surface — see migration banner) **VERIFIED for legacy** (CHANGE_REQUEST_API.md):
|
|
435
439
|
1. Create: Admin action triggers draft creation automatically
|
|
436
|
-
2. Approve: `POST
|
|
437
|
-
3. Commit: `POST
|
|
440
|
+
2. Approve: legacy `POST .../tide-admin/change-set/sign/batch` → new `POST .../iga/change-requests/{id}/authorize` (MultiAdmin/Tide mode returns enclave challenge)
|
|
441
|
+
3. Commit: legacy `POST .../tide-admin/change-set/commit/batch` → new `POST .../iga/change-requests/{id}/commit`
|
|
438
442
|
|
|
439
443
|
**What this means**:
|
|
440
444
|
- Single admin cannot create, modify, or delete users
|
|
@@ -459,19 +463,19 @@ curl "${TIDECLOAK_URL}/admin/realms/${REALM}" | jq '.igaEnabled'
|
|
|
459
463
|
|
|
460
464
|
# Attempt admin change without approval
|
|
461
465
|
curl -X POST "${TIDECLOAK_URL}/admin/realms/${REALM}/users" -d '{...}'
|
|
462
|
-
# Should
|
|
466
|
+
# Should return 202 (captured as a change request), not immediate user creation
|
|
463
467
|
|
|
464
|
-
# Check pending change-
|
|
465
|
-
curl "${TIDECLOAK_URL}/admin/realms/${REALM}/
|
|
466
|
-
-H "Authorization: Bearer ${ADMIN_TOKEN}"
|
|
467
|
-
# Should list pending change
|
|
468
|
+
# Check pending change requests (current /iga/ surface — see canon/iga-change-requests-api.md)
|
|
469
|
+
curl "${TIDECLOAK_URL}/admin/realms/${REALM}/iga/change-requests?status=PENDING" \
|
|
470
|
+
-H "Authorization: Bearer ${ADMIN_TOKEN}" | jq '.[].id'
|
|
471
|
+
# Should list pending change request ids
|
|
468
472
|
```
|
|
469
473
|
|
|
470
474
|
**Consequence of violation**: Single compromised admin can create backdoor accounts, grant unauthorized roles, bypass all governance. IGA enforcement collapses to single-admin trust.
|
|
471
475
|
|
|
472
476
|
**Related invariants**: I-09 (No Single Bypass)
|
|
473
477
|
|
|
474
|
-
Change requests
|
|
478
|
+
Change requests carry a cryptographic expiry enforced by the Tide enclave; after the window, signing is rejected, and there is no automatic DB cleanup (stale drafts persist until cancelled). Note: `2628000s` (~1 month) is the VRK rotation period (`vrkRotationPeriodSeconds`, `ork` TaskConstants), NOT a per-change-request expiry constant — do not conflate the two. The exact CR expiry is enclave-side and was not confirmed as a hardcoded value in the extension repos on main.
|
|
475
479
|
|
|
476
480
|
---
|
|
477
481
|
|
|
@@ -509,15 +513,13 @@ No application-level max payload size **VERIFIED** (vendor confirmation, GAP-013
|
|
|
509
513
|
|
|
510
514
|
## I-12: DPoP Token Binding (Bidirectional Lockstep)
|
|
511
515
|
|
|
512
|
-
**Rule**: DPoP
|
|
516
|
+
**Rule**: DPoP is **ENABLED and ENFORCED by default** in the TideCloak SDK. `useDPoP` defaults to `{ mode: 'strict' }` in `IAMService` — it is opt-OUT, not opt-in. Disabling it requires setting `useDPoP: false` (or `{ mode: 'auto' }`) explicitly, and in strict mode init fails if the realm does not advertise DPoP support. **VERIFIED** (`tidecloak-js: packages/tidecloak-js/src/IAMService.js` — "DPoP is enabled and ENFORCED by default across all TideCloak SDKs").
|
|
513
517
|
|
|
514
|
-
**
|
|
515
|
-
- **Server-side** (realm template): `"dpop.bound.access.tokens": "true"` on the OIDC client
|
|
516
|
-
- **Client-side** (provider config): `useDPoP: { mode: 'strict', alg: 'ES256' }` inside the config object
|
|
517
|
-
-
|
|
518
|
-
- Server on, client
|
|
519
|
-
- Client on, server off → proofs generated but ignored by TideCloak.
|
|
520
|
-
- `useDPoP` goes inside the config object, NOT as a JSX prop on `TideCloakProvider`. VERIFIED (session-002).
|
|
518
|
+
**Server/client must agree (lockstep)**:
|
|
519
|
+
- **Server-side** (realm template): `"dpop.bound.access.tokens": "true"` on the OIDC client, and the realm must advertise `dpop_signing_alg_values_supported`. Because the client defaults to strict, a realm that does not advertise DPoP makes SDK init fail.
|
|
520
|
+
- **Client-side** (provider config): the SDK default is `useDPoP: { mode: 'strict', alg: 'ES256' }`. To weaken/disable, set `useDPoP` explicitly. `useDPoP` goes inside the config object, NOT as a JSX prop on the provider. VERIFIED (session-002).
|
|
521
|
+
- Server off, client left at default strict → init fails ("server does not advertise DPoP support"). Set `useDPoP: false` or enable DPoP on the realm.
|
|
522
|
+
- Server on, client set to `false` → proofs not generated; token endpoint returns 400 "DPoP proof is missing".
|
|
521
523
|
|
|
522
524
|
**`tide_dpop_auth.html` required** (when DPoP is enabled):
|
|
523
525
|
- Copy `tide_dpop_auth.html` to `public/`. This file is loaded by the Tide enclave during login to prove DPoP key possession to the ORKs. **Do not modify it** — its content is integrity-checked.
|
|
@@ -572,10 +574,10 @@ DPoP is required for Tide's full security guarantees **VERIFIED** (vendor confir
|
|
|
572
574
|
**Rule**: Adapter JSON must include Tide extensions (`jwk`, `vendorId`, `homeOrkUrl`). Generic Keycloak adapter is insufficient. **VERIFIED** (keylessh `tidecloakConfig.ts`)
|
|
573
575
|
|
|
574
576
|
**What this means**:
|
|
575
|
-
- Export adapter from TideCloak via `vendorResources/get-installations-provider?providerId=keycloak-oidc-keycloak-json` (the only valid provider ID)
|
|
577
|
+
- Export adapter from TideCloak via the realm-level `GET /admin/realms/{realm}/vendorResources/get-installations-provider?clientId={clientId}&providerId=keycloak-oidc-keycloak-json` (client is a query param; `keycloak-oidc-keycloak-json` is the only valid provider ID)
|
|
576
578
|
- Do not use generic Keycloak adapter export
|
|
577
579
|
- Do not manually construct adapter JSON without Tide extensions
|
|
578
|
-
- `jwk` field is
|
|
580
|
+
- `jwk` field is emitted whenever the realm has a Tide vendor key (NOT gated on IGA)
|
|
579
581
|
|
|
580
582
|
**Required Tide extensions**:
|
|
581
583
|
- `jwk: { keys: JWK[] }` - Embedded JWKS for local JWT verification
|
|
@@ -597,7 +599,7 @@ jq '.jwk.keys | length' data/tidecloak.json
|
|
|
597
599
|
|
|
598
600
|
**Related invariants**: I-04 (Embedded JWKS), I-05 (Adapter JSON Required)
|
|
599
601
|
|
|
600
|
-
**Note**: The `jwk` field is
|
|
602
|
+
**Note**: The `jwk` field is present whenever the realm has a Tide vendor key (not gated on IGA). Validate at startup.
|
|
601
603
|
|
|
602
604
|
---
|
|
603
605
|
|
|
@@ -651,17 +653,12 @@ Vendor guidance: universally required. Declare in realm.json template.
|
|
|
651
653
|
- Compromised ORK cannot override policy
|
|
652
654
|
- No client-side or server-side policy bypass
|
|
653
655
|
|
|
654
|
-
**Contract submission
|
|
656
|
+
**Contract submission**: `tc.createTideRequest` takes a single **pre-encoded `Uint8Array`** (a `BaseTideRequest` you build and `.encode()`), NOT a `{ contract, modelName, authFlow, ... }` object. Build the request first, then submit its encoding (see AP-53). The C# contract source is carried inside the request's draft.
|
|
655
657
|
```typescript
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
authorizer: tc.doken,
|
|
661
|
-
challengeData: { ... }
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
const result = await tc.executeSignRequest(signRequest);
|
|
658
|
+
// Build a BaseTideRequest (name, version, authFlow, draft), then encode it.
|
|
659
|
+
const request = new BaseTideRequest(name, version, "Policy:1", draft);
|
|
660
|
+
const initializedBytes = await tc.createTideRequest(request.encode());
|
|
661
|
+
const result = await tc.executeSignRequest(initializedBytes);
|
|
665
662
|
```
|
|
666
663
|
|
|
667
664
|
**Verification**:
|
|
@@ -679,7 +676,7 @@ grep -r "bypassPolicy\|skipPolicy" src/ server/
|
|
|
679
676
|
|
|
680
677
|
**Related invariants**: I-02 (Threshold), I-09 (No Single Bypass)
|
|
681
678
|
|
|
682
|
-
**Sandbox model** **VERIFIED** (vendor confirmation, GAP-008 resolved): Five-layer security (Roslyn compilation, IL vetting with namespace block-list, AssemblyLoadContext isolation, VmHost process isolation, gas metering at 50,000 gas default). Block-list: `System.IO`, `System.Net`, `System.Diagnostics`, `System.Threading`, `System.Reflection`, `System.Runtime.InteropServices`, `System.Reflection.Emit`, `Microsoft.Win32`, plus `System.Console` and `System.Runtime.CompilerServices.Unsafe`. Non-deterministic calls blocked by default. Gas metering: `Claim(key)` = 5 gas, `Log(message)` = 25
|
|
679
|
+
**Sandbox model** **VERIFIED** (vendor confirmation, GAP-008 resolved): Five-layer security (Roslyn compilation, IL vetting with namespace block-list, AssemblyLoadContext isolation, VmHost process isolation, gas metering at 50,000 gas default). Block-list: `System.IO`, `System.Net`, `System.Diagnostics`, `System.Threading`, `System.Reflection`, `System.Runtime.InteropServices`, `System.Reflection.Emit`, `Microsoft.Win32`, plus `System.Console` and `System.Runtime.CompilerServices.Unsafe`. Non-deterministic calls blocked by default. Gas metering: `Claim(key)` = 5 gas, `Log(message)` = 25 gas, `OutOfGasException` when exhausted.
|
|
683
680
|
|
|
684
681
|
**Remaining open questions**: Contract debugging opaque **PARTIALLY_RESOLVED** (GAP-018).
|
|
685
682
|
|
|
@@ -785,6 +782,7 @@ curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/auth/redirect
|
|
|
785
782
|
|--------|----------|----------|------------------------|
|
|
786
783
|
| App state | Fresh app (no auth) | Existing app (has auth) | Does the project have an existing auth system? |
|
|
787
784
|
| Infrastructure | Generated app (has init script) | Manual / BYO TideCloak | Does `package.json` have an `"init"` script? Is TideCloak already running? |
|
|
785
|
+
| TideCloak hosting | Self-hosted (Docker/K8s) | Partner-hosted (Skycloak, managed) | Does the team want to run auth infra, or use a managed instance? See `canon/hosting-options.md`. Does not change app wiring; changes bootstrap. |
|
|
788
786
|
| Encryption model | Self-encryption (private, user-bound) | Policy-governed VVK (shared, cross-user) | Do other users need to decrypt the same ciphertext? |
|
|
789
787
|
| App scope | Single-user private app | Organisation / team / shared app | Are there multiple users who share data or resources? |
|
|
790
788
|
| Task type | Diagnosis (something is broken) | Setup (something needs building) | Is the app partially working, or not started? |
|