@hilbras/keystone 2.6.0 → 3.0.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 (122) hide show
  1. package/CHANGELOG.md +350 -0
  2. package/README.md +72 -1
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +7 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +15 -11
  8. package/dist/index.js.map +1 -1
  9. package/dist/plugins/rateLimit.d.ts +10 -7
  10. package/dist/plugins/rateLimit.d.ts.map +1 -1
  11. package/dist/plugins/rateLimit.js +75 -36
  12. package/dist/plugins/rateLimit.js.map +1 -1
  13. package/dist/routes/admin/organizations.d.ts.map +1 -1
  14. package/dist/routes/admin/organizations.js +2 -0
  15. package/dist/routes/admin/organizations.js.map +1 -1
  16. package/dist/routes/admin/platform.d.ts.map +1 -1
  17. package/dist/routes/admin/platform.js +14 -1
  18. package/dist/routes/admin/platform.js.map +1 -1
  19. package/dist/routes/apiKeys.d.ts.map +1 -1
  20. package/dist/routes/apiKeys.js +15 -1
  21. package/dist/routes/apiKeys.js.map +1 -1
  22. package/dist/routes/auth.d.ts.map +1 -1
  23. package/dist/routes/auth.js +104 -3
  24. package/dist/routes/auth.js.map +1 -1
  25. package/dist/routes/emailVerification.d.ts.map +1 -1
  26. package/dist/routes/emailVerification.js +2 -0
  27. package/dist/routes/emailVerification.js.map +1 -1
  28. package/dist/routes/magicLinks.d.ts.map +1 -1
  29. package/dist/routes/magicLinks.js +2 -0
  30. package/dist/routes/magicLinks.js.map +1 -1
  31. package/dist/routes/oauth2.d.ts.map +1 -1
  32. package/dist/routes/oauth2.js +16 -2
  33. package/dist/routes/oauth2.js.map +1 -1
  34. package/dist/routes/password.d.ts.map +1 -1
  35. package/dist/routes/password.js +2 -0
  36. package/dist/routes/password.js.map +1 -1
  37. package/dist/routes/scim.d.ts.map +1 -1
  38. package/dist/routes/scim.js +2 -0
  39. package/dist/routes/scim.js.map +1 -1
  40. package/dist/routes/smsOtp.d.ts.map +1 -1
  41. package/dist/routes/smsOtp.js +4 -0
  42. package/dist/routes/smsOtp.js.map +1 -1
  43. package/dist/routes/totp.d.ts.map +1 -1
  44. package/dist/routes/totp.js +18 -1
  45. package/dist/routes/totp.js.map +1 -1
  46. package/dist/services/configuration/profiles.d.ts +26 -0
  47. package/dist/services/configuration/profiles.d.ts.map +1 -1
  48. package/dist/services/configuration/profiles.js +80 -1
  49. package/dist/services/configuration/profiles.js.map +1 -1
  50. package/dist/services/events/subscribers/auditLog.d.ts.map +1 -1
  51. package/dist/services/events/subscribers/auditLog.js +38 -3
  52. package/dist/services/events/subscribers/auditLog.js.map +1 -1
  53. package/dist/services/events/types.d.ts +3 -1
  54. package/dist/services/events/types.d.ts.map +1 -1
  55. package/dist/services/events/validate.d.ts +1 -0
  56. package/dist/services/events/validate.d.ts.map +1 -1
  57. package/dist/services/events/validate.js +5 -1
  58. package/dist/services/events/validate.js.map +1 -1
  59. package/dist/services/localRateLimit.d.ts +44 -0
  60. package/dist/services/localRateLimit.d.ts.map +1 -0
  61. package/dist/services/localRateLimit.js +86 -0
  62. package/dist/services/localRateLimit.js.map +1 -0
  63. package/dist/services/refreshTokenState.d.ts +5 -0
  64. package/dist/services/refreshTokenState.d.ts.map +1 -0
  65. package/dist/services/refreshTokenState.js +30 -0
  66. package/dist/services/refreshTokenState.js.map +1 -0
  67. package/dist/services/setup/token.d.ts +12 -0
  68. package/dist/services/setup/token.d.ts.map +1 -1
  69. package/dist/services/setup/token.js +27 -3
  70. package/dist/services/setup/token.js.map +1 -1
  71. package/dist/services/tokens.d.ts +1 -0
  72. package/dist/services/tokens.d.ts.map +1 -1
  73. package/dist/services/tokens.js +1 -1
  74. package/dist/services/tokens.js.map +1 -1
  75. package/dist/services/trustedProxies.d.ts +24 -0
  76. package/dist/services/trustedProxies.d.ts.map +1 -1
  77. package/dist/services/trustedProxies.js +19 -0
  78. package/dist/services/trustedProxies.js.map +1 -1
  79. package/dist/services/webhooks.d.ts +16 -0
  80. package/dist/services/webhooks.d.ts.map +1 -1
  81. package/dist/services/webhooks.js +48 -3
  82. package/dist/services/webhooks.js.map +1 -1
  83. package/dist/setup-server.js +47 -3
  84. package/dist/setup-server.js.map +1 -1
  85. package/docs/API-REVIEW.md +121 -0
  86. package/docs/API.md +457 -0
  87. package/docs/ARCHITECTURE.md +142 -0
  88. package/docs/CONTRIBUTING.md +61 -0
  89. package/docs/DEPLOYMENT.md +257 -0
  90. package/docs/INTEGRATION.md +336 -0
  91. package/docs/LOGIN_FORM_INTEGRATION.md +306 -0
  92. package/docs/MIGRATION-1.7.md +70 -0
  93. package/docs/MIGRATION-1.8.md +183 -0
  94. package/docs/MIGRATION-1.9.md +200 -0
  95. package/docs/MIGRATION-2.0.md +203 -0
  96. package/docs/MIGRATION-2.4.md +185 -0
  97. package/docs/PERFORMANCE.md +155 -0
  98. package/docs/RBAC.md +100 -0
  99. package/docs/RE-AUDIT.md +72 -0
  100. package/docs/README.md +54 -0
  101. package/docs/RELEASE-1.7.md +53 -0
  102. package/docs/ROADMAP.md +41 -0
  103. package/docs/SECURITY.md +143 -0
  104. package/docs/adrs/001-identity-connectors-as-adapters.md +27 -0
  105. package/docs/adrs/002-versioned-event-bus.md +30 -0
  106. package/docs/adrs/003-bullmq-for-background-work.md +20 -0
  107. package/docs/adrs/004-argon2id-password-hashing.md +19 -0
  108. package/docs/plans/KEYSTONE_IMPROVEMENT_PLAN.md +334 -0
  109. package/docs/plans/UI_SIMPLIFICATION_IMPROVEMENT_PLAN.md +271 -0
  110. package/docs/security/audit.md +82 -0
  111. package/docs/security/configuration.md +60 -0
  112. package/docs/security/enterprise-sso.md +193 -0
  113. package/docs/security/mtls.md +132 -0
  114. package/docs/security/proxy-security.md +128 -0
  115. package/docs/security/rate-limiting.md +79 -0
  116. package/docs/security/registry-exceptions.md +34 -0
  117. package/docs/security/registry.json +649 -0
  118. package/docs/security/registry.md +657 -0
  119. package/docs/security/scopes.md +45 -0
  120. package/docs/security/supply-chain.md +49 -0
  121. package/docs/security/trust-boundaries.md +111 -0
  122. package/package.json +15 -6
package/docs/API.md ADDED
@@ -0,0 +1,457 @@
1
+ # Hilbras Keystone — HTTP API Reference
2
+
3
+ Version 1.7.0
4
+
5
+ Every route also ships an interactive OpenAPI description served by the API itself:
6
+
7
+ - **Swagger UI:** `GET /documentation`
8
+ - **OpenAPI JSON:** `GET /documentation/json`
9
+
10
+ Unless stated otherwise, endpoints are served by the main API process
11
+ (`KEYSTONE_SETUP_MODE` unset). The first-run **setup server** exposes its own
12
+ smaller surface under `/setup/*` and is documented at the end.
13
+
14
+ ## Authentication schemes
15
+
16
+ | Scheme | How | Used for |
17
+ | --- | --- | --- |
18
+ | Session cookie | Set by `POST /auth/login`, `register`, `token-login` | Browser flows |
19
+ | Bearer access token | `Authorization: Bearer <access_token>` (RS256 JWT) | APIs, SPAs |
20
+ | API key | `Authorization: Bearer <api_key>` or `x-api-key: <api_key>` | Machine-to-machine |
21
+
22
+ Endpoints marked **auth** accept any authenticated principal (session or bearer).
23
+ Endpoints marked **owner** additionally require the platform-owner role.
24
+
25
+ ## Conventions
26
+
27
+ - All bodies and responses are JSON.
28
+ - Errors return `{ "error": string, ...details }` with an appropriate HTTP status
29
+ (`400` validation, `401` unauthenticated, `403` forbidden, `404` missing,
30
+ `409` conflict, `429` rate-limited).
31
+ - Refresh tokens rotate atomically on every use and are bound to their client/application; replay or client mismatch is rejected.
32
+ - Application-bound OAuth2 refresh requests must include the original `client_id` and `client_secret`; the refresh route authenticates the client before rotation.
33
+
34
+ ---
35
+
36
+ ## Discovery & operations
37
+
38
+ | Method | Path | Auth | Description |
39
+ | --- | --- | --- | --- |
40
+ | GET | `/.well-known/jwks.json` | public | RSA public keys for verifying access tokens (JWKS) |
41
+ | GET | `/documentation` | public | Swagger UI |
42
+ | GET | `/documentation/json` | public | OpenAPI document |
43
+ | GET | `/metrics` | public* | Prometheus metrics (*restrict via reverse proxy in production) |
44
+
45
+ ---
46
+
47
+ ## Core authentication — `/auth`
48
+
49
+ | Method | Path | Auth | Description |
50
+ | --- | --- | --- | --- |
51
+ | POST | `/auth/register` | public | Create an account; sets session cookies |
52
+ | POST | `/auth/login` | public | Password login; sets cookies, returns tokens + user |
53
+ | POST | `/auth/token-login` | public | Exchange a one-time token (e.g. magic link) for a session |
54
+ | GET | `/auth/me` | auth | Current user profile with roles/memberships |
55
+ | POST | `/auth/refresh` | refresh cookie/body | Rotate refresh token, issue new access token |
56
+ | POST | `/auth/logout` | refresh cookie/body | Revoke refresh token, clear cookies |
57
+
58
+ ### Sessions
59
+
60
+ | Method | Path | Auth | Description |
61
+ | --- | --- | --- | --- |
62
+ | GET | `/auth/sessions` | auth | List active sessions for current user |
63
+ | DELETE | `/auth/sessions/:id` | auth | Revoke one session |
64
+ | POST | `/auth/sessions/revoke-all` | auth | Revoke every session of current user |
65
+
66
+ ### Profile
67
+
68
+ | Method | Path | Auth | Description |
69
+ | --- | --- | --- | --- |
70
+ | GET | `/auth/profile` | auth | Full profile of current user |
71
+ | PATCH | `/auth/profile` | auth | Update name, username, avatar, password change, etc. |
72
+
73
+ ### Email verification
74
+
75
+ | Method | Path | Auth | Description |
76
+ | --- | --- | --- | --- |
77
+ | POST | `/auth/email-verification/send` | auth | Send verification email to signed-in address |
78
+ | POST | `/auth/email-verification/request` | public | Request verification email by email address |
79
+ | GET | `/auth/email-verification/verify` | public | Verify with `token` query parameter |
80
+
81
+ ### Password recovery
82
+
83
+ | Method | Path | Auth | Description |
84
+ | --- | --- | --- | --- |
85
+ | POST | `/auth/forgot-password` | public | Send password-reset email |
86
+ | POST | `/auth/reset-password` | public | Reset password with `token` + new password |
87
+
88
+ ### Magic links
89
+
90
+ | Method | Path | Auth | Description |
91
+ | --- | --- | --- | --- |
92
+ | POST | `/auth/magic-link/send` | public | Email a sign-in link |
93
+ | GET | `/auth/magic-link/verify` | public | Consume link token, establish session |
94
+
95
+ ### Multi-factor authentication
96
+
97
+ When a user has TOTP enabled, the password step never returns a token. Instead
98
+ `/auth/login` and `/auth/token-login` respond with `401` and:
99
+
100
+ ```json
101
+ {
102
+ "error": "Multi-factor authentication required",
103
+ "code": "MFA_REQUIRED",
104
+ "mfaRequired": true,
105
+ "challenge": "<opaque single-use challenge>",
106
+ "expiresAt": "2026-01-01T00:05:00.000Z",
107
+ "methods": ["totp", "backup_code"]
108
+ }
109
+ ```
110
+
111
+ The challenge is short-lived, single-use, and stored only as a hash. Exchange it
112
+ once for tokens:
113
+
114
+ ```http
115
+ POST /auth/mfa/verify
116
+ { "challenge": "<challenge>", "code": "123456" }
117
+ ```
118
+
119
+ `factor` may be sent as `"totp"` or `"backup_code"`; when omitted it is inferred
120
+ from the code shape (six digits = TOTP).
121
+
122
+ | Method | Path | Auth | Description |
123
+ | --- | --- | --- | --- |
124
+ | POST | `/auth/mfa/verify` | challenge | Complete the second factor and receive tokens |
125
+
126
+ Error codes returned by `/auth/mfa/verify`:
127
+
128
+ | Code | Meaning |
129
+ | --- | --- |
130
+ | `MFA_CHALLENGE_INVALID` | Unknown challenge |
131
+ | `MFA_CHALLENGE_EXPIRED` | Challenge passed its expiry |
132
+ | `MFA_CHALLENGE_REPLAYED` | Challenge was already consumed |
133
+ | `MFA_CHALLENGE_LOCKED` | Attempt budget exhausted |
134
+ | `MFA_INVALID_CODE` | Factor did not match |
135
+ | `MFA_NOT_REQUIRED` | The factor was disabled while the challenge was open |
136
+
137
+ A TOTP time-step is accepted once. Replaying the same code — even against a
138
+ freshly created challenge — is rejected. Backup codes are likewise single-use
139
+ and expire after `TOTP_BACKUP_CODE_TTL_SECONDS` (default 90 days).
140
+
141
+ Enabling MFA revokes every existing refresh token and session for the account,
142
+ so credentials issued before enrollment cannot be used to obtain a new session.
143
+
144
+ ### TOTP (authenticator apps)
145
+
146
+ All factor-management endpoints require **step-up**: the account password must be
147
+ supplied in the request body in addition to the session. A stolen access token
148
+ alone must never be enough to change how an account proves its identity.
149
+
150
+ | Method | Path | Body | Description |
151
+ | --- | --- | --- | --- |
152
+ | POST | `/auth/totp/enroll` | `{ password }` | Begin enrollment; returns otpauth URI, QR secret, and backup codes |
153
+ | POST | `/auth/totp/verify` | `{ password, code }` | Confirm enrollment; enables MFA and revokes existing sessions |
154
+ | POST | `/auth/totp/backup` | `{ password, code }` | Regenerate backup codes |
155
+ | POST | `/auth/totp/backup/verify` | `{ code }` | Consume a backup code; never establishes a session |
156
+ | POST | `/auth/totp/disable` | `{ password, code }` | Disable TOTP and destroy its backup codes |
157
+
158
+ Step-up failures return `401 STEP_UP_REQUIRED` (no password supplied) or
159
+ `401 INVALID_CREDENTIALS` (wrong password). Failed step-up attempts count toward
160
+ the account lockout.
161
+
162
+ ### Passkeys and MFA
163
+
164
+ `POST /auth/webauthn/register/verify` also requires `{ password }` when the
165
+ account has TOTP enabled. A passkey satisfies the MFA requirement on its own,
166
+ **except** when it was registered after TOTP was enabled: such a credential is
167
+ treated as a single factor and sign-in is refused with `403 MFA_REQUIRED`. This
168
+ prevents a leaked session token from being traded for a permanent bypass.
169
+
170
+ ### WebAuthn / passkeys
171
+
172
+ | Method | Path | Auth | Description |
173
+ | --- | --- | --- | --- |
174
+ | GET | `/auth/webauthn/register/options` | auth | PublicKeyCredentialCreationOptions |
175
+ | POST | `/auth/webauthn/register/verify` | auth | Attestation verification, stores credential |
176
+ | POST | `/auth/webauthn/authenticate/options` | public | Assertion options for a username/discoverable flow |
177
+ | POST | `/auth/webauthn/authenticate/verify` | public | Verify assertion, establish session |
178
+
179
+ ### SMS OTP
180
+
181
+ | Method | Path | Auth | Description |
182
+ | --- | --- | --- | --- |
183
+ | POST | `/auth/sms-otp/send` | public | Send one-time code to phone number |
184
+ | POST | `/auth/sms-otp/verify` | public | Verify code, establish session |
185
+
186
+ ### Social OAuth
187
+
188
+ | Method | Path | Auth | Description |
189
+ | --- | --- | --- | --- |
190
+ | GET | `/auth/oauth/:provider` | public | Redirect to provider (google, github, …) |
191
+ | GET | `/auth/callback/:provider` | public | Provider callback; links/creates account, sets session |
192
+
193
+ ### API keys
194
+
195
+ | Method | Path | Auth | Description |
196
+ | --- | --- | --- | --- |
197
+ | POST | `/auth/api-keys` | auth | Create API key (returned once, hashed at rest) |
198
+ | GET | `/auth/api-keys` | auth | List caller's API keys (metadata only) |
199
+ | DELETE | `/auth/api-keys/:id` | auth | Revoke an API key |
200
+ | GET | `/auth/validate` | auth or API key | Validate current credentials, returns principal info |
201
+
202
+ ---
203
+
204
+ ## OAuth 2.0 provider — `/oauth2`
205
+
206
+ Hilbras Keystone acts as an authorization server for first-party and third-party apps.
207
+
208
+ | Method | Path | Auth | Description |
209
+ | --- | --- | --- | --- |
210
+ | GET | `/oauth2/authorize` | session | Authorization endpoint (`response_type=code`, PKCE supported) |
211
+ | POST | `/oauth2/token` | client creds | Token exchange: `authorization_code`, `refresh_token`, `client_credentials` |
212
+ | GET | `/oauth2/userinfo` | auth | OpenID Connect userinfo claims |
213
+ | POST | `/oauth2/revoke` | public | RFC 7009 token revocation |
214
+ | POST | `/oauth2/consent` | auth | Grant or withdraw consent for an application's scopes |
215
+
216
+ ---
217
+
218
+ ## Authorization check
219
+
220
+ | Method | Path | Auth | Description |
221
+ | --- | --- | --- | --- |
222
+ | POST | `/v1/authz/check` | auth | Evaluate organization RBAC: `{ organizationId, action, resource }` → `{ allowed }` |
223
+
224
+ `organizationId` is required. Keystone resolves the authenticated user's membership for that organization and returns `403` when the actor is not a member.
225
+
226
+ ---
227
+
228
+ ## Federation (social identity connectors) — `/federation`
229
+
230
+ | Method | Path | Auth | Description |
231
+ | --- | --- | --- | --- |
232
+ | GET | `/federation/providers` | public | Enabled connectors for the requesting app |
233
+ | GET | `/federation/:provider/start` | public | Begin federated sign-in for a connector |
234
+ | GET | `/federation/:provider/callback` | public | Connector callback |
235
+ | GET | `/federation/identities` | auth | External identities linked to current user |
236
+
237
+ ---
238
+
239
+ ## Enterprise SSO — `/sso`
240
+
241
+ | Method | Path | Auth | Description |
242
+ | --- | --- | --- | --- |
243
+ | GET | `/sso/saml/:connectionId?orgId=:organizationId` | public | Start SAML login for a connection (organization-scoped) |
244
+ | POST | `/sso/saml/acs` | public | SAML Assertion Consumer Service; signed RelayState binds the organization |
245
+ | GET | `/sso/saml/:connectionId/metadata?orgId=:organizationId` | public | Organization-scoped SAML metadata XML |
246
+ | GET | `/sso/sso/oidc/:connectionId?orgId=:organizationId` | public | Start organization-scoped enterprise OIDC login; connection requires JWKS URI |
247
+ | GET | `/sso/sso/oidc/:connectionId/callback?orgId=:organizationId` | public | Enterprise OIDC callback; state binds the organization |
248
+
249
+ > ⚠️ Note the doubled `/sso/sso/oidc` segment — the OIDC enterprise routes declare
250
+ > `/sso/oidc/...` paths *and* are mounted under the `/sso` prefix. This is slated
251
+ > for normalization in a future minor release.
252
+
253
+ ## SCIM 2.0 provisioning — `/scim/v2`
254
+
255
+ Each request is authorized by a **per-organization SCIM connection**, and every
256
+ read and write is scoped to that connection's organization. There is no global
257
+ SCIM configuration. A target that belongs to another organization is reported as
258
+ `404`, so the endpoint is not a tenant oracle.
259
+
260
+ Create and manage the bearer token through
261
+ [the SCIM connection API](#scim-connections--v1admin).
262
+
263
+ | Method | Path | Description |
264
+ | --- | --- | --- |
265
+ | GET | `/scim/v2/Users` | List users. `filter=userName eq "…"`, `startIndex`, `count` |
266
+ | GET | `/scim/v2/Users/:userId` | Fetch user |
267
+ | POST | `/scim/v2/Users` | Provision or update a user (create-or-update) |
268
+ | PUT | `/scim/v2/Users/:userId` | Replace user |
269
+ | PATCH | `/scim/v2/Users/:userId` | Partial update (`active`, `name.*`, `userName`) |
270
+ | DELETE | `/scim/v2/Users/:userId` | Deprovision user |
271
+ | POST | `/scim/v2/Users/.search` | Search users (POST form of the list endpoint) |
272
+ | GET | `/scim/v2/Groups` | List groups. `filter=displayName\|externalId eq "…"` |
273
+ | GET | `/scim/v2/Groups/:groupId` | Fetch group with members |
274
+ | POST | `/scim/v2/Groups` | Create group |
275
+ | PUT | `/scim/v2/Groups/:groupId` | Replace group and its membership |
276
+ | PATCH | `/scim/v2/Groups/:groupId` | Partial update |
277
+ | DELETE | `/scim/v2/Groups/:groupId` | Delete group and its memberships |
278
+ | GET | `/scim/v2/Groups/:groupId/members` | List group members |
279
+ | POST | `/scim/v2/Groups/:groupId/members` | Add member(s) |
280
+ | DELETE | `/scim/v2/Groups/:groupId/members/:userId` | Remove a member |
281
+ | GET | `/scim/v2/ServiceProviderConfig` | Supported features |
282
+ | GET | `/scim/v2/ResourceTypes` | Resource type schema URIs |
283
+
284
+ ### Deprovisioning and shared users
285
+
286
+ A user row is global, so a blanket deactivation would revoke that person's access
287
+ to *every* organization they belong to — which this credential is not authorized
288
+ to do. Deprovisioning therefore removes the organization's membership, and
289
+ deactivates the account only once no membership remains anywhere.
290
+
291
+ For the same reason SCIM refuses to change the global attributes of a user who
292
+ also belongs to another organization. It returns `409` with
293
+ `scimType: "mutability"`; remove the membership from this organization instead.
294
+
295
+ | Response | Meaning |
296
+ | --- | --- |
297
+ | `404` | Not found, or belongs to another organization |
298
+ | `409` `uniqueness` | A user with that `userName` already exists |
299
+ | `409` `mutability` | Platform owner, review-required account, shared user, or last organization owner |
300
+ | `401` | Missing, unknown, revoked, or expired credential |
301
+
302
+ Platform owners and accounts pending platform review are never modified through
303
+ SCIM.
304
+
305
+ ---
306
+
307
+ ## SCIM connections — `/v1/admin`
308
+
309
+ Every SCIM connection belongs to exactly one organization. Issuing, rotating, and
310
+ revoking a token is **owner-only**: a SCIM token provisions and deactivates
311
+ tenant users, so it must not be mintable by a mere admin or member.
312
+
313
+ | Method | Path | Auth | Description |
314
+ | --- | --- | --- | --- |
315
+ | GET | `/organizations/:id/scim-config` | sso read | Status, base URL, and the active connection |
316
+ | GET | `/organizations/:id/scim-connections` | sso read | All connections, including revoked |
317
+ | POST | `/organizations/:id/scim-connections` | **owner** | Create a connection; returns the token once |
318
+ | POST | `/organizations/:id/scim-connections/:connectionId/rotate` | **owner** | Issue a new token |
319
+ | DELETE | `/organizations/:id/scim-connections/:connectionId` | **owner** | Revoke immediately |
320
+
321
+ ```bash
322
+ curl -X POST https://keystone.example.com/v1/admin/organizations/$ORG/scim-connections \
323
+ -H "Authorization: Bearer $ADMIN_TOKEN" \
324
+ -H "Content-Type: application/json" \
325
+ -d '{"name":"Okta","expiresInDays":365}'
326
+ ```
327
+
328
+ The bearer token is returned **only** in the create and rotate responses. It is
329
+ stored as a SHA-256 digest and is not recoverable afterwards; listings show a
330
+ four-character hint instead.
331
+
332
+ Rotation invalidates the old token immediately by default. Pass
333
+ `rotationGraceSeconds` to keep the previous token valid for a window, which
334
+ avoids dropping in-flight provisioning — but it is not a revocation mechanism,
335
+ so do not use a grace window when rotating in response to a leak.
336
+
337
+ Set `expiresInDays` on creation to require rotation on a schedule.
338
+
339
+ ---
340
+
341
+ ## Admin API — `/v1/admin`
342
+
343
+ Most platform endpoints require the **owner** role. Organization and role reads
344
+ are available to any authenticated member.
345
+
346
+ ### Platform
347
+
348
+ | Method | Path | Auth | Description |
349
+ | --- | --- | --- | --- |
350
+ | GET | `/v1/admin/platform/users` | owner | Redacted public users across orgs |
351
+ | PATCH | `/v1/admin/platform/users/:id` | owner | Update non-role platform user fields |
352
+ | PATCH | `/v1/admin/platform/users/:id/role` | owner | Change platform role (`owner` or `user`) |
353
+ | POST | `/v1/admin/platform/users/:id/account-review` | owner | Resolve a quarantined legacy account (`{ "active": true/false }`) |
354
+ | DELETE | `/v1/admin/platform/users/:id` | owner | Deactivate account and revoke sessions/tokens/API keys |
355
+ | GET | `/v1/admin/platform/organizations` | owner | All organizations |
356
+ | GET | `/v1/admin/platform/applications` | owner | All applications |
357
+ | GET | `/v1/admin/platform/audit-logs` | owner | Query audit trail |
358
+ | GET | `/v1/admin/platform/audit-logs/export` | owner | Export audit logs (CSV) |
359
+ | GET | `/v1/admin/platform/metrics/usage` | owner | Usage metrics |
360
+ | GET | `/v1/admin/platform/security-summary` | owner | Security posture snapshot |
361
+ | GET | `/v1/admin/platform/queue` | owner | Background queue stats |
362
+ | GET | `/v1/admin/platform/queue/failed` | owner | Failed jobs |
363
+ | POST | `/v1/admin/platform/queue/failed/:id/retry` | owner | Retry failed job |
364
+ | POST | `/v1/admin/platform/queue/retry-all` | owner | Retry all failed jobs |
365
+ | GET | `/v1/admin/platform/webhooks` | owner | List webhooks |
366
+ | POST | `/v1/admin/platform/webhooks` | owner | Create webhook |
367
+ | PATCH | `/v1/admin/platform/webhooks/:id` | owner | Update webhook |
368
+ | DELETE | `/v1/admin/platform/webhooks/:id` | owner | Delete webhook |
369
+ | POST | `/v1/admin/platform/webhooks/:id/rotate-secret` | owner | Rotate signing secret |
370
+ | GET | `/v1/admin/platform/webhooks/:id/deliveries` | owner | Delivery history |
371
+ | POST | `/v1/admin/platform/webhook-deliveries/:id/retry` | owner | Redeliver webhook |
372
+ | GET | `/v1/admin/platform/keys` | owner | Signing key metadata |
373
+ | POST | `/v1/admin/platform/keys/rotate` | owner | Rotate JWT signing keys |
374
+ | GET | `/v1/admin/platform/plugins` | owner | Installed plugins |
375
+ | GET | `/v1/admin/platform/plugins/extensions` | owner | Plugin extension points |
376
+ | DELETE | `/v1/admin/platform/plugins/:name` | owner | Uninstall plugin |
377
+ | GET | `/v1/admin/platform/feature-flags` | owner | Feature flags |
378
+ | GET | `/v1/admin/platform/feature-flags/:key` | owner | Single flag |
379
+ | DELETE | `/v1/admin/platform/feature-flags/:key` | owner | Delete flag |
380
+ | GET | `/v1/admin/platform/configuration-profiles` | owner | Saved configuration profiles |
381
+ | GET | `/v1/admin/platform/configuration-profiles/:id` | owner | Profile detail |
382
+
383
+ ### Organizations, roles, service accounts
384
+
385
+ | Method | Path | Auth | Description |
386
+ | --- | --- | --- | --- |
387
+ | GET | `/v1/admin/organizations` | auth | Organizations of current user |
388
+ | GET | `/v1/admin/organizations/:id` | auth | Org detail (members, apps) |
389
+ | POST | `/v1/admin/organizations/:id/invites` | org owner/admin | Invite a member with an organization role |
390
+ | GET | `/v1/admin/organizations/:id/members` | org member | Redacted members with `membershipRole` and separate `platformRole` fields |
391
+ | PATCH/DELETE | `/v1/admin/organizations/:id/members/:userId` | org owner/admin | Change/remove an organization membership |
392
+ | GET | `/v1/admin/organizations/:id/users` | org member | Redacted organization users |
393
+ | GET | `/v1/admin/organizations/:id/users/:userId` | org member | Redacted organization user |
394
+ | GET | `/v1/admin/permissions` | owner | Effective permission catalog |
395
+ | POST | `/v1/admin/permissions` | owner | Create custom permission |
396
+ | DELETE | `/v1/admin/permissions/:id` | owner | Delete custom permission |
397
+ | GET | `/v1/admin/roles` | owner | Built-in organization role catalog |
398
+ | GET | `/v1/admin/roles/:role/permissions` | owner | Permissions mapped to an organization role |
399
+ | POST | `/v1/admin/organizations/:id/service-accounts` | org permission | Create service account |
400
+ | GET | `/v1/admin/organizations/:id/service-accounts` | org permission | List service accounts |
401
+ | GET | `/v1/admin/organizations/:id/service-accounts/:accountId` | org permission | Service account detail |
402
+ | PATCH | `/v1/admin/organizations/:id/service-accounts/:accountId` | org permission | Update service account |
403
+ | PUT | `/v1/admin/organizations/:id/service-accounts/:accountId/certificate` | org permission (`service_account:update`) | Bind or clear a client-certificate SHA-256 fingerprint. `409` if already bound to another account |
404
+ | POST | `/v1/admin/organizations/:id/service-accounts/:accountId/revoke` | org permission (`service_account:update`) | Revoke a service account. `409` if already revoked |
405
+ | POST | `/v1/admin/organizations/:id/service-accounts/:accountId/api-keys` | org permission | Issue API key for service account |
406
+
407
+ Organization user PATCH/DELETE routes are retained only as explicit migration tombstones (`410`) for clients that used them to mutate global accounts. Use platform user administration for account-wide changes and `/members/:userId` for organization roles.
408
+
409
+ ### Workflows
410
+
411
+ | Method | Path | Auth | Description |
412
+ | --- | --- | --- | --- |
413
+ | GET | `/v1/admin/workflows?orgId=:organizationId` | org member | List organization workflows |
414
+ | POST | `/v1/admin/workflows` | org owner/admin | Create workflow with safe email steps and an `orgId` |
415
+ | GET | `/v1/admin/workflows/:id` | org member / platform owner for global | Workflow detail |
416
+ | DELETE | `/v1/admin/workflows/:id` | org owner/admin / platform owner for global | Remove workflow |
417
+ | GET | `/v1/admin/workflows/:id/runs` | org member / platform owner for global | Execution history |
418
+
419
+ ### Billing & runtime configuration
420
+
421
+ | Method | Path | Auth | Description |
422
+ | --- | --- | --- | --- |
423
+ | GET | `/v1/admin/billing/plans` | auth | Available plans |
424
+ | GET | `/v1/admin/config` | owner | Redacted runtime configuration view |
425
+ | PUT | `/v1/admin/config` | owner | Update configuration |
426
+ | POST | `/v1/admin/config/restart` | owner | Graceful restart of services |
427
+
428
+ ---
429
+
430
+ ## SDK serving — `/sdk`
431
+
432
+ | Method | Path | Auth | Description |
433
+ | --- | --- | --- | --- |
434
+ | GET | `/sdk/keystone-dropin.js` | public | Drop-in browser widget (self-hosted build) |
435
+ | GET | `/sdk/keystone-dropin.js.sri` | public | Subresource-integrity hash for the drop-in |
436
+ | GET | `/sdk/branding/:clientId` | public | Per-application branding payload |
437
+ | POST | `/sdk/connect` | owner | Handshake used by embedded SDK components |
438
+
439
+ ---
440
+
441
+ ## Setup server (first-run wizard)
442
+
443
+ Enabled by starting with `KEYSTONE_SETUP_MODE=true` (see `docs/DEPLOYMENT.md`).
444
+
445
+ | Method | Path | Auth | Description |
446
+ | --- | --- | --- | --- |
447
+ | GET | `/setup/status` | public | Whether setup is completed |
448
+ | GET | `/setup/diagnostics` | public | Environment diagnostics |
449
+ | POST | `/setup/config/dry-run` | public | Validate config payload without persisting |
450
+ | POST | `/setup/validate/db` | public | Test PostgreSQL connectivity |
451
+ | POST | `/setup/validate/redis` | public | Test Redis connectivity |
452
+ | POST | `/setup/validate/email` | public | Send test email via SMTP settings |
453
+ | POST | `/setup/validate/sms` | public | Send test SMS via provider settings |
454
+ | POST | `/setup/config` | public | Persist configuration |
455
+ | POST | `/setup/migrate` | public | Run database migrations |
456
+ | POST | `/setup/restart` | public | Restart into normal mode |
457
+ | POST | `/setup/init` | public | Bootstrap owner account + organization |
@@ -0,0 +1,142 @@
1
+ # Keystone Architecture
2
+
3
+ This document describes the high-level architecture of Hilbras Keystone and the conventions used across the codebase.
4
+
5
+ ## Goals
6
+
7
+ - Be a standalone identity platform, not a wrapper around another IdP.
8
+ - Keep business logic independent of HTTP so the same code can be used by routes, CLI, workers, and future transports.
9
+ - Treat every provider as optional and replaceable.
10
+ - Emit auditable, versioned events for every security-relevant action.
11
+ - Support horizontal scaling through Redis-backed state and background queues.
12
+
13
+ ## Layered architecture
14
+
15
+ ```
16
+ Routes / CLI / Workers / Webhooks
17
+ │
18
+ ▼
19
+ Application Services (use cases, orchestration)
20
+ │
21
+ ▼
22
+ Domain Services (business rules)
23
+ │
24
+ ▼
25
+ Repositories (persistence abstraction)
26
+ │
27
+ ▼
28
+ PostgreSQL / Redis / External APIs
29
+ ```
30
+
31
+ ### Routes
32
+
33
+ Fastify routes are thin. They validate input, call application services or the internal SDK, and format responses. They contain no business logic.
34
+
35
+ ### Application services
36
+
37
+ Application services coordinate domain services for a specific use case. Examples:
38
+
39
+ - `AuthenticationApplicationService` — login, register, refresh, logout flows.
40
+ - `OrganizationApplicationService` — create org, invite members, manage apps.
41
+
42
+ ### Domain services
43
+
44
+ Domain services encode business rules and depend on repository interfaces:
45
+
46
+ - `AuthenticationDomainService`
47
+ - `AuthorizationDomainService`
48
+ - `IdentityDomainService`
49
+ - `OrganizationDomainService`
50
+
51
+ ### Repositories
52
+
53
+ Repository interfaces (`UserRepository`, `OrganizationRepository`, etc.) hide persistence details. Drizzle ORM implementations live in `src/repositories/`.
54
+
55
+ ## Authorization boundaries
56
+
57
+ Authorization is evaluated in two independent namespaces:
58
+
59
+ - **Platform:** `users.role` is limited to `owner` and `user`. Only the explicit platform-role use case and owner-only route may change it.
60
+ - **Organization:** `org_memberships.role` is limited to `owner`, `admin`, and `member`, and is always queried by `(organizationId, userId)`.
61
+
62
+ Route pre-handlers authenticate the actor and resolve organization membership from trusted database state. Application/domain services enforce actor/target role transitions and last-owner invariants. Client-controlled application or origin headers are context hints only and never establish membership or permission.
63
+
64
+ The internal identity update contract separates ordinary profile updates from platform-role changes. User-management HTTP responses use a redacted public projection. Role, membership, permission, and denied-authorization transitions emit versioned audit events.
65
+
66
+ ## Dependency injection
67
+
68
+ `src/di.ts` wires the container. Services receive dependencies through constructors, making unit tests with mocked repositories easy. The container is exposed to Fastify as `app.container` and can be retrieved globally via `getContainer()`.
69
+
70
+ ## Internal SDK
71
+
72
+ The SDK (`src/sdk/`) provides stable TypeScript contracts for the rest of the application:
73
+
74
+ ```ts
75
+ const sdk = getSdk();
76
+ await sdk.authentication.login({ email, password });
77
+ await sdk.identity.findUser(userId);
78
+ await sdk.organization.createOrganization({ name: "Acme" });
79
+ ```
80
+
81
+ Routes, CLI commands, workers, and workflows all consume the same SDK.
82
+
83
+ ## Identity connectors
84
+
85
+ Connectors live in `src/services/connectors/` and are pure provider adapters. They only:
86
+
87
+ - Build authorization URLs.
88
+ - Exchange codes for tokens.
89
+ - Validate identity tokens.
90
+ - Retrieve and normalize profile data.
91
+
92
+ They do **not** create users, link identities, issue Keystone tokens, or manage sessions. Those responsibilities live in domain services.
93
+
94
+ Supported connectors include Google, GitHub, Azure AD, Okta, Keycloak, and Zitadel. Enterprise SAML connections are handled through `samlify` with signature validation.
95
+
96
+ ## Event bus
97
+
98
+ Every security-relevant action emits a versioned event:
99
+
100
+ ```json
101
+ {
102
+ "type": "user.login",
103
+ "version": 1,
104
+ "timestamp": "2026-07-13T00:00:00Z",
105
+ "payload": { "userId": "...", "ip": "..." }
106
+ }
107
+ ```
108
+
109
+ Subscribers consume events for audit logging, webhooks, anomaly detection, analytics, and workflow triggers. Long-running work is dispatched to the background queue.
110
+
111
+ ## Background queue
112
+
113
+ Keystone supports an in-process queue for local development and a BullMQ-backed queue for production. Set `KEYSTONE_QUEUE_PROVIDER=bullmq` or leave it empty when Redis is available. The queue runs email delivery, webhooks, and workflow actions asynchronously.
114
+
115
+ ## Secrets management
116
+
117
+ The secrets provider abstraction supports multiple backends:
118
+
119
+ - `DatabaseSecretsProvider` (default)
120
+ - `EnvironmentSecretsProvider`
121
+ - Enterprise backends via plugins or future built-in providers
122
+
123
+ It handles JWT signing keys, encryption keys, password hashes, API keys, and client secrets. JWT signing keys are rotated with a 24-hour grace period; the JWKS endpoint publishes both the active and the expiring key so existing tokens remain valid.
124
+
125
+ ## Rate limiting
126
+
127
+ A Redis-backed sliding-window rate limiter protects public endpoints. It uses an atomic Lua script and returns `429 Too Many Requests` with a `Retry-After` header. If Redis is unavailable it fails open so the service remains functional.
128
+
129
+ ## Plugins
130
+
131
+ The plugin registry (`src/services/plugins/`) lets third-party code extend Keystone without modifying core:
132
+
133
+ - Identity providers and authentication methods
134
+ - Email and SMS providers
135
+ - Workflow steps
136
+ - Custom authorization policies
137
+
138
+ Load plugins at startup via `KEYSTONE_PLUGINS=./plugins/my-plugin.js`.
139
+
140
+ ## Configuration and feature flags
141
+
142
+ `ConfigurationService` centralizes loading, defaults, validation, and environment-specific overrides. Feature flags can be toggled at runtime through `KEYSTONE_FEATURE_FLAGS`.
@@ -0,0 +1,61 @@
1
+ # Contributing to Keystone
2
+
3
+ Thank you for contributing to Hilbras Keystone. This guide covers the development workflow, conventions, and how to submit changes.
4
+
5
+ ## Development setup
6
+
7
+ 1. Start Postgres and Redis:
8
+ ```bash
9
+ docker compose -f docker-compose.test.yml up -d
10
+ ```
11
+ 2. Copy `.env.example` to `.env` and set at least `DATABASE_URL`.
12
+ 3. Install dependencies:
13
+ ```bash
14
+ npm install
15
+ cd frontend && npm install
16
+ ```
17
+ 4. Run migrations:
18
+ ```bash
19
+ npm run db:migrate
20
+ ```
21
+ 5. Start the dev server:
22
+ ```bash
23
+ npm run dev
24
+ ```
25
+
26
+ ## Testing
27
+
28
+ - `npm run typecheck` — TypeScript type checking.
29
+ - `npm run build` — Compile and copy migration files.
30
+ - `npm test` — Run the test suite.
31
+ - `npm run test:e2e` (from `frontend/`) — Run Playwright E2E tests.
32
+
33
+ Integration tests require Postgres and Redis. They skip gracefully when services are unavailable, but CI runs them against real services.
34
+
35
+ ## Code conventions
36
+
37
+ - Write TypeScript with `strict: true`.
38
+ - Keep routes thin; business logic belongs in application and domain services.
39
+ - Prefer repository interfaces over direct SQL in domain services.
40
+ - Return `Result<T>` from internal services instead of throwing for expected failures.
41
+ - Add tests for new behavior.
42
+ - Update relevant documentation (`README.md`, `docs/ARCHITECTURE.md`, `docs/ROADMAP.md`) when architecture changes.
43
+
44
+ ## Architecture decisions
45
+
46
+ Significant design changes should be recorded as an ADR in `docs/adrs/`. Use the format `NNNN-short-title.md` and include:
47
+
48
+ - Context
49
+ - Decision
50
+ - Consequences
51
+
52
+ ## Submitting changes
53
+
54
+ 1. Open a pull request against `main`.
55
+ 2. Ensure CI passes (`typecheck`, `build`, `test`).
56
+ 3. Request review from a maintainer.
57
+ 4. Squash commits if requested.
58
+
59
+ ## Code of conduct
60
+
61
+ Be respectful, constructive, and inclusive. All contributions are subject to the project's license.