@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
@@ -0,0 +1,155 @@
1
+ # Performance and Memory Optimization
2
+
3
+ This guide explains why Keystone may feel heavy during local development and how to run it efficiently in production.
4
+
5
+ ---
6
+
7
+ ## Why it feels heavy during development
8
+
9
+ ### 1. Development tools use more memory
10
+
11
+ When you run `./start.sh`, the backend starts with:
12
+
13
+ ```bash
14
+ npx tsx watch src/index.ts
15
+ ```
16
+
17
+ `tsx watch` keeps a TypeScript compiler in memory and recompiles files on every change. This is convenient but uses more RAM than a compiled build.
18
+
19
+ The frontend runs:
20
+
21
+ ```bash
22
+ npm run dev # Vite dev server
23
+ ```
24
+
25
+ Vite also keeps modules in memory for fast hot reload.
26
+
27
+ **Typical dev memory usage:**
28
+
29
+ | Process | Memory |
30
+ |---------|--------|
31
+ | Backend (`tsx watch`) | ~200–400 MB |
32
+ | Frontend (`vite dev`) | ~150–300 MB |
33
+ | PostgreSQL container | ~100–200 MB |
34
+ | Redis container | ~10–50 MB |
35
+ | Browsers | 500 MB–2 GB |
36
+
37
+ ### 2. Stale processes accumulate
38
+
39
+ If `./start.sh` is run multiple times or crashes, old `tsx watch` and `vite` processes may stay alive and continue using memory and ports.
40
+
41
+ ### 3. `node_modules` is large
42
+
43
+ Node projects naturally have large dependency trees:
44
+
45
+ ```
46
+ 413 MB total node_modules across backend + frontend + SDK
47
+ ```
48
+
49
+ This is disk space, not RAM, but it makes the project feel heavy.
50
+
51
+ ---
52
+
53
+ ## Quick fixes
54
+
55
+ ### Kill stale processes
56
+
57
+ The latest `start.sh` does this automatically. If you still see old processes:
58
+
59
+ ```bash
60
+ ./scripts/kill-keystone.sh
61
+ ```
62
+
63
+ Or manually:
64
+
65
+ ```bash
66
+ pkill -f "tsx watch src/index.ts"
67
+ pkill -f "tsx watch src/setup-server.ts"
68
+ pkill -f "frontend/node_modules/.bin/vite"
69
+ ```
70
+
71
+ ### Run in production mode
72
+
73
+ After building:
74
+
75
+ ```bash
76
+ npm run build:all
77
+ ./start.sh --production
78
+ ```
79
+
80
+ This uses `node dist/index.js` instead of `tsx watch`, which uses significantly less memory.
81
+
82
+ ### Stop the frontend if you only need the API
83
+
84
+ If you are integrating an external app and do not need the admin UI:
85
+
86
+ ```bash
87
+ npm start # backend only
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Production deployment optimizations
93
+
94
+ 1. **Use `npm start` or `node dist/index.js`** — never `tsx watch` in production.
95
+ 2. **Run with `NODE_ENV=production`** — this disables development logging and tracing overhead.
96
+ 3. **Disable OpenTelemetry if not needed** — remove `OTEL_EXPORTER_OTLP_ENDPOINT` from `.env`.
97
+ 4. **Use managed PostgreSQL and Redis** — reduces local container overhead.
98
+ 5. **Use a process manager** like systemd, PM2, or Docker Compose with restart policies.
99
+ 6. **Build the frontend once** and serve the `dist/` folder with Nginx/Caddy instead of `vite dev`.
100
+
101
+ ### Example systemd service
102
+
103
+ ```ini
104
+ [Unit]
105
+ Description=Hilbras Keystone
106
+ After=network.target
107
+
108
+ [Service]
109
+ Type=simple
110
+ User=keystone
111
+ WorkingDirectory=/opt/keystone
112
+ ExecStart=/usr/bin/node dist/index.js
113
+ Restart=always
114
+ RestartSec=5
115
+ Environment=NODE_ENV=production
116
+ EnvironmentFile=/opt/keystone/.env
117
+
118
+ [Install]
119
+ WantedBy=multi-user.target
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Reducing bundle size
125
+
126
+ The admin frontend bundle is currently:
127
+
128
+ ```
129
+ 252 KB index.js (gzipped)
130
+ 24 KB index.css (gzipped)
131
+ ```
132
+
133
+ If you need to reduce it further:
134
+
135
+ - Use route-based code splitting.
136
+ - Lazy-load heavy panels (organizations, audit logs, billing).
137
+ - Remove unused Lucide icons by importing only the ones used.
138
+
139
+ ---
140
+
141
+ ## Monitoring memory
142
+
143
+ Check Keystone memory usage:
144
+
145
+ ```bash
146
+ ps -eo pid,ppid,%mem,rss,args | grep "node dist/index.js" | grep -v grep
147
+ ```
148
+
149
+ Check Docker container resources:
150
+
151
+ ```bash
152
+ docker stats --no-stream
153
+ ```
154
+
155
+ If memory keeps growing after running for hours, it may be a leak. Enable heap snapshots or contact the maintainers.
package/docs/RBAC.md ADDED
@@ -0,0 +1,100 @@
1
+ # Keystone RBAC and Authorization Boundaries
2
+
3
+ ## Role namespaces
4
+
5
+ Keystone maintains two independent role namespaces.
6
+
7
+ | Namespace | Stored in | Values | Scope |
8
+ | --- | --- | --- | --- |
9
+ | Platform role | `users.role` | `owner`, `user` | The entire Keystone installation |
10
+ | Organization role | `org_memberships.role` | `owner`, `admin`, `member` | One organization membership |
11
+
12
+ An organization `owner` is not a platform `owner`. Organization membership roles are never copied into `users.role`, and organization APIs cannot modify platform roles.
13
+
14
+ ## Platform administration
15
+
16
+ Only a platform owner can:
17
+
18
+ - Change a platform role through `PATCH /v1/admin/platform/users/:userId/role`.
19
+ - Administer platform users, applications, webhooks, configuration, and global audit logs.
20
+ - Change role-permission mappings.
21
+
22
+ The platform role endpoint accepts only:
23
+
24
+ ```json
25
+ { "role": "owner" }
26
+ ```
27
+
28
+ or:
29
+
30
+ ```json
31
+ { "role": "user" }
32
+ ```
33
+
34
+ The last platform owner cannot be demoted. Generic user profile updates do not accept a `role` field.
35
+
36
+ ## Organization membership
37
+
38
+ Organization membership is always resolved by the pair `(organizationId, userId)`.
39
+
40
+ - `owner` can manage memberships and grant organization-owner access.
41
+ - `admin` can manage members and administrators, but cannot grant or modify `owner` access.
42
+ - `member` cannot manage memberships.
43
+ - The sole organization owner cannot be demoted or removed.
44
+ - Membership reads and writes never grant platform privileges.
45
+
46
+ Use the membership endpoints for organization roles:
47
+
48
+ ```text
49
+ PATCH /v1/admin/organizations/:organizationId/members/:userId
50
+ DELETE /v1/admin/organizations/:organizationId/members/:userId
51
+ ```
52
+
53
+ The legacy organization user PATCH/DELETE routes do not mutate global accounts. Use the platform user administration route for account-wide changes.
54
+
55
+ ## Authorization evaluation
56
+
57
+ Organization permission checks resolve the authenticated actor and route organization explicitly. The internal authorization SDK also requires `userId` and `organizationId`; a bare role string is not a valid cross-tenant authorization decision. Client-supplied application or origin context is not sufficient to establish membership.
58
+
59
+ For `/v1/authz/check`, provide the organization being evaluated:
60
+
61
+ ```json
62
+ {
63
+ "organizationId": "00000000-0000-0000-0000-000000000000",
64
+ "resource": "application",
65
+ "action": "read"
66
+ }
67
+ ```
68
+
69
+ A user who is not a member of that organization receives `403`; an absent or invalid organization context is not treated as authorization. OAuth/OIDC client context never grants organization claims unless the authenticated user is a member of the application's organization.
70
+
71
+ ## Tenant workflows
72
+
73
+ Organization workflows may use safe notification and email steps. They cannot:
74
+
75
+ - Assign a platform or organization role.
76
+ - Add a user to an arbitrary organization.
77
+ - Add a user through an arbitrary application client ID.
78
+ - Send tenant data to arbitrary webhook URLs.
79
+
80
+ Unsafe or malformed persisted workflow definitions fail closed and are recorded as blocked runs.
81
+
82
+ ## Audit records
83
+
84
+ Security-relevant transitions emit versioned events with structured metadata, including:
85
+
86
+ - Actor and target user.
87
+ - Organization and application context.
88
+ - Previous and new state.
89
+ - Request ID, IP address, and user agent where available.
90
+
91
+ Role, membership, permission, and denied-authorization changes are retained in the audit log. Audit records are asynchronous and should be monitored for subscriber failures in production.
92
+
93
+ ## Migration from v1.6.x
94
+
95
+ 1. Replace organization user PATCH calls that intended to change a platform role with the dedicated platform role endpoint.
96
+ 2. Replace organization user deactivation calls with a platform-owner workflow.
97
+ 3. Use `/members/:userId` for organization role changes.
98
+ 4. Remove workflow definitions containing `assign_role`, `add_membership`, or `add_app_membership` before deployment.
99
+ 5. Include `organizationId` in authorization-check requests.
100
+ 6. Review existing organization user clients for reliance on password hashes, TOTP secrets, or other internal user fields; those fields are no longer returned.
@@ -0,0 +1,72 @@
1
+ # v3.0.0 re-audit matrix
2
+
3
+ <!-- Generated by scripts/render-reaudit-matrix.mjs. Do not edit by hand. -->
4
+
5
+ Each cell is verified against the repository when this file is generated, not asserted. `Regression Test` is **Yes** only if the named file exists and contains a test; `Fixed` only if the fix site still exists. Deleting a test turns the cell red on the next run rather than leaving a stale assurance behind.
6
+
7
+ | Finding | Original severity | Findings | Fixed | Regression test | Documentation | Entries |
8
+ | --- | --- | --- | --- | --- | --- | --- |
9
+ | Privilege escalation | Critical | 8 | Yes | Yes | Yes | SEC-001, SEC-002, SEC-003, SEC-007, SEC-023, SEC-025, SEC-028, SEC-032 |
10
+ | MFA bypass | Critical | 4 | Yes | Yes | Yes | SEC-004, SEC-005, SEC-015, SEC-034 |
11
+ | SCIM isolation | Critical | 1 | Yes | Yes | Yes | SEC-006 |
12
+ | mTLS trust | Critical | 3 | Yes | Yes | Yes | SEC-007, SEC-009, SEC-010 |
13
+ | Fastify vulnerability | High | 1 | Yes | Yes | Yes | SEC-011 |
14
+ | fast-uri vulnerabilities | High | 1 | Yes | Yes | Yes | SEC-011 |
15
+ | Rate limiting | High | 5 | Yes | Yes | Yes | SEC-008, SEC-033, SEC-034, SEC-035, SEC-039 |
16
+ | Token races | High | 3 | Yes | Yes | Yes | SEC-012, SEC-013, SEC-038 |
17
+ | Session invalidation | High | 3 | Yes | Yes | Yes | SEC-014, SEC-015, SEC-038 |
18
+ | OAuth hardening | High | 5 | Yes | Yes | Yes | SEC-016, SEC-017, SEC-018, SEC-019, SEC-020 |
19
+ | SAML hardening | Medium | 2 | Yes | Yes | Yes | SEC-021, SEC-022 |
20
+ | Secrets | High | 4 | Yes | Yes | Yes | SEC-026, SEC-029, SEC-030, SEC-031 |
21
+ | CORS | High | 2 | Yes | Yes | Yes | SEC-027, SEC-028 |
22
+ | CI security | High | 6 | Yes | Yes | Yes | SEC-011, SEC-040, SEC-041, SEC-042, SEC-045, SEC-046 |
23
+ | Abuse prevention | High | 5 | Yes | Yes | Yes | SEC-033, SEC-035, SEC-036, SEC-037, SEC-039 |
24
+ | Audit integrity | High | 4 | Yes | Yes | Yes | SEC-036, SEC-037, SEC-045, SEC-046 |
25
+ | Supply chain | High | 1 | Yes | Yes | Yes | SEC-011 |
26
+ | Test integrity | High | 3 | Yes | Yes | Yes | SEC-040, SEC-041, SEC-042 |
27
+
28
+ ## API security review
29
+
30
+ Every route enumerated from source and checked for a guard. Result in
31
+ [`docs/API-REVIEW.md`](./API-REVIEW.md).
32
+
33
+ **69 routes. Zero with no authentication guard.** 27 carry no route-level
34
+ authorization and each was traced: SCIM’s bearer token *is* the authorization,
35
+ `GET /me` returns the caller’s own profile, the OAuth authorize endpoint
36
+ authenticates in the request, and `workflows.ts` checks membership in the handler.
37
+
38
+ One structural weakness found: `workflows.ts` does its organization check inside
39
+ each of five handlers rather than in a guard, so a sixth route added later would
40
+ have no reason to include it. Not a live vulnerability — the checks are correct —
41
+ but the only module in the codebase that does it this way.
42
+
43
+ ## Unresolved Critical findings
44
+
45
+ None. Every finding the plan classifies as Critical is fixed, has a regression test, and is documented.
46
+
47
+ ## Withdrawn entries
48
+
49
+ Recorded rather than renumbered, so every id already cited elsewhere keeps its meaning.
50
+
51
+ ### SEC-043 — withdrawn 2026-09-27
52
+
53
+ Written during the 2.9.0 release with a fix at a path that does not exist and an issue description the test does not cover. src/tests/security/saml/saml-validator.test.ts exercises the *valid* signed-response path; it does not test a missing audience or issuer requirement. There is no evidenced defect behind this entry, so it was not a finding. The suite is recorded under `coverage` instead.
54
+
55
+ ### SEC-044 — withdrawn 2026-09-27
56
+
57
+ Written during the 2.9.0 release with a fix at a path that does not exist and an issue description the test does not cover. src/tests/security/saml/sso-endpoint.test.ts tests isPrivateAddress, a guard on operator-supplied endpoint addresses; it does not test an unregistered host alias. No defect was evidenced behind this entry. The suite is recorded under `coverage` instead.
58
+
59
+ ## Coverage without a numbered finding
60
+
61
+ Suites that assert a property but do not correspond to a defect we can evidence. Recording these as findings to satisfy a completeness check is how a registry starts asserting things nobody checked.
62
+
63
+ - `src/tests/security/saml/saml-validator.test.ts` — A valid signed SAML response is accepted by the registered schema validator, and validateSamlSemantics accepts it.
64
+ Positive-path coverage for the SAML validator. Not tied to a numbered finding: it asserts that a correct response is accepted, not that a previously-missing check was added.
65
+ - `src/tests/security/saml/sso-endpoint.test.ts` — isPrivateAddress classifies loopback, private, link-local, carrier-grade NAT, benchmarking and both spellings of IPv4-mapped IPv6 as non-public.
66
+ Coverage for the endpoint address policy in src/services/ssoEndpointPolicy.ts, which keeps operator-supplied SSO endpoint URLs from pointing at internal addresses. No defect was evidenced behind it, so it is not a numbered finding.
67
+
68
+ ## Coverage
69
+
70
+ - 44 findings recorded in `docs/security/registry.json`.
71
+ - 44 of 44 verified to have a regression test present right now.
72
+ - 6 critical, 25 high, 12 medium, 1 low.
package/docs/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Hilbras Keystone Documentation
2
+
3
+ Complete documentation for the Hilbras Keystone identity platform.
4
+
5
+ ## Getting started
6
+
7
+ | Document | Purpose |
8
+ | --- | --- |
9
+ | [README](../README.md) | Feature overview, quick start, environment reference |
10
+ | [Installation & deployment](DEPLOYMENT.md) | Docker Compose, Kubernetes, systemd, production hardening |
11
+ | [Integration guide](INTEGRATION.md) | Connect your apps: tokens, sessions, OAuth2/OIDC, drop-in widget |
12
+
13
+ ## Reference
14
+
15
+ | Document | Purpose |
16
+ | --- | --- |
17
+ | [HTTP API reference](API.md) | Every endpoint, grouped by area, with auth requirements |
18
+ | [Architecture](ARCHITECTURE.md) | Layered design, services, repositories, events, plugins |
19
+ | [Performance tuning](PERFORMANCE.md) | Connection pools, caching, indexes, load-testing results |
20
+ | [Security model](SECURITY.md) | Threat model, token lifecycle, storage, hardening checklist |
21
+ | [RBAC and authorization](RBAC.md) | Platform/organization role boundaries, policy evaluation, and migration |
22
+ | [v1.7 migration guide](MIGRATION-1.7.md) | Upgrade steps and breaking authorization changes |
23
+ | [v1.8 migration guide](MIGRATION-1.8.md) | MFA enforcement: new endpoints, response changes, and upgrade steps |
24
+ | [v1.9 migration guide](MIGRATION-1.9.md) | SCIM tenancy: per-organization credentials, deprovisioning semantics, and upgrade steps |
25
+ | [v1.7 release checklist](RELEASE-1.7.md) | Local gates, dependency/lint blockers, and publication steps |
26
+
27
+ ## Contributing
28
+
29
+ | Document | Purpose |
30
+ | --- | --- |
31
+ | [Contributing guide](CONTRIBUTING.md) | Dev environment, branch and commit conventions, PR process |
32
+ | [Code of Conduct](../CODE_OF_CONDUCT.md) | Community standards and enforcement |
33
+ | [Changelog](../CHANGELOG.md) | Release history (Keep a Changelog format) |
34
+ | [Security policy](../.github/SECURITY.md) | How to report vulnerabilities |
35
+ | [Roadmap](ROADMAP.md) | Planned work and priorities |
36
+
37
+ ## Architecture decision records
38
+
39
+ | ADR | Decision |
40
+ | --- | --- |
41
+ | [001 — Identity connectors as adapters](adrs/001-identity-connectors-as-adapters.md) | Provider-agnostic connector interface |
42
+ | [002 — Versioned event bus](adrs/002-versioned-event-bus.md) | Event envelope versioning strategy |
43
+ | [003 — BullMQ for background work](adrs/003-bullmq-for-background-work.md) | Queue technology choice |
44
+ | [004 — argon2id password hashing](adrs/004-argon2id-password-hashing.md) | Password hashing parameters |
45
+
46
+ ## Guides by example
47
+
48
+ | Example | Demonstrates |
49
+ | --- | --- |
50
+ | [Drop-in login widget](../examples/drop-in-login/README.md) | Zero-build HTML integration via `/sdk/keystone-dropin.js` |
51
+ | [HTML drop-in](../examples/html-dropin/README.md) | Plain HTML pages with hosted UI |
52
+ | [React SPA](../examples/react-spa/README.md) | Token-based SPA with refresh rotation |
53
+ | [Login form (React)](../examples/login-form-react/README.md) | Custom-branded login against `/auth/*` |
54
+ | [SDK package](../packages/keystone-sdk/README.md) | `@hilbras/keystone-sdk` builds and SRI usage |
@@ -0,0 +1,53 @@
1
+ # v1.7.0 Release Checklist
2
+
3
+ This checklist is for the local release candidate. Tagging, GitHub Release creation, and npm publication still require explicit approval.
4
+
5
+ ## Local verification
6
+
7
+ - [x] `npm ci` (2026-09-25 local run; release CI repeats the install)
8
+ - [x] `npm run lint`
9
+ - [x] `npm run typecheck`
10
+ - [x] `npm run build`
11
+ - [x] `npm test` with PostgreSQL and Redis (97 passed, 1 skipped)
12
+ - [x] `npm run test:security` (47 passed)
13
+ - [x] `cd frontend && npm run build`
14
+ - [x] `npm pack --dry-run`
15
+ - [x] `git diff --check`
16
+
17
+ ## Security verification
18
+
19
+ - [x] Organization-to-platform role escalation regression coverage
20
+ - [x] Organization membership rank and last-owner coverage
21
+ - [x] Workflow allowlist, malformed-definition, and tenant-scope coverage
22
+ - [x] SAML/OIDC organization-scope, schema validation, ID-token verification, and one-time RelayState transaction coverage
23
+ - [x] Existing-user enterprise SSO membership/identity-link and platform-owner isolation coverage
24
+ - [x] Generic OAuth verified-identity and no-email-autolink coverage
25
+ - [x] Scoped SCIM credential, organization isolation, review-state, platform-owner isolation, service attribution, and last-owner coverage
26
+ - [x] OIDC endpoint SSRF policy coverage, including private, carrier-grade, dotted/hex mapped, and DNS-pinned address ranges
27
+ - [x] User/application/API-key/configuration secret projection coverage
28
+ - [x] Account deactivation, refresh-token/API-key revocation, and cross-client token-scope coverage
29
+ - [x] Non-burning wrong-client refresh rejection, OAuth2 refresh success/failure audit coverage, and execution-time workflow authorization coverage
30
+ - [x] Fail-closed legacy migration and SAML audience/destination/recipient semantic coverage
31
+ - [x] Authorization audit metadata, top-level tenant attribution, and denied-attempt coverage
32
+ - [x] Runtime role constraints, owner-required organization creation, and legacy-value normalization migration
33
+
34
+ ## Unresolved release blockers
35
+
36
+ ### Dependency audit
37
+
38
+ The release workflow runs `npm audit --omit=dev --audit-level=high` as a blocking gate. The 2026-09-25 run passes with **0 High** findings after safe transitive overrides for `@xmldom/xmldom`, `fast-uri`, and `find-my-way`. Four Moderate findings remain outside the High-severity release gate and should continue to be tracked.
39
+
40
+ ### Lint
41
+
42
+ The repository now has a real `oxlint` gate (`npm run lint`) with warnings denied, and CI runs it as a blocking step. Typecheck remains a separate gate.
43
+
44
+ ### npm publication
45
+
46
+ The release workflow now builds the package, runs `npm pack --dry-run`, installs the packed tarball in a clean temporary prefix, and smoke-tests the CLI before publication. The job still requires the `NPM_TOKEN` GitHub secret, which is not currently configured. Verify trusted publishing or configure the secret before release.
47
+
48
+ ### External actions
49
+
50
+ - [ ] User approves tag and remote publication
51
+ - [ ] Tag `v1.7.0` is pushed
52
+ - [ ] GitHub Release is verified
53
+ - [ ] npm package is installed and smoke-tested
@@ -0,0 +1,41 @@
1
+ # Keystone Roadmap
2
+
3
+ This roadmap captures completed milestones and planned work. It is ordered by priority and dependency.
4
+
5
+ ## Completed
6
+
7
+ 1. **v1.7 authorization boundary hardening** — separated platform and organization roles, closed tenant workflow escalation paths, scoped SSO lookups, and added security regression coverage.
8
+ 2. **Local password reset** — token-based password reset independent of Zitadel, with email queued for delivery.
9
+ 2. **BullMQ background queue** — durable Redis-backed queue for email, webhooks, and workflows.
10
+ 3. **Sanitized OAuth/Federation errors** — centralized error helper that exposes only opaque public codes.
11
+ 4. **Distributed Redis rate limiting** — atomic sliding-window rate limiter with `Retry-After`.
12
+ 5. **JWT key rotation grace period** — JWKS publishes active + recently rotated keys for 24 hours.
13
+ 6. **Argon2id password hashing** — new hashes use argon2id; legacy scrypt hashes remain verifiable.
14
+ 7. **Real email/SMS providers** — SMTP, SendGrid, Mailgun, and Twilio.
15
+ 8. **Full SAML signature validation** — SAML responses validated through `samlify`.
16
+ 9. **Integration tests + CI** — GitHub Actions job with Postgres/Redis services and a BullMQ integration test.
17
+ 10. **CLI enhancements** — `user:create`, `keys:list`, `config:validate`, `org:create`.
18
+ 11. **Backend setup endpoints** — `/setup/status` and `/setup/init` for fresh installations.
19
+ 12. **Standalone setup frontend** — React + Vite wizard with Playwright E2E tests.
20
+
21
+ ## Near term
22
+
23
+ - **Audit log UI and export** — paginated admin API, CSV export, SIEM-friendly streaming.
24
+ - **Organization-level SSO** — self-service OIDC and SAML connection configuration.
25
+ - **Branding and email templates** — customizable email HTML and sender identity per organization.
26
+ - **Session management** — list and revoke active sessions per user.
27
+ - **Webhook reliability** — retries, signatures, and delivery logs.
28
+
29
+ ## Mid term
30
+
31
+ - **Multi-region replication** — read replicas, geo-distributed signing keys.
32
+ - **Advanced authorization** — attribute-based access control (ABAC) and custom policy plugins.
33
+ - **User impersonation** — secure admin impersonation with full audit trail.
34
+ - **SCIM provisioning** — inbound/outbound user and group provisioning.
35
+ - **Billing integration hooks** — seat-count events and plan enforcement.
36
+
37
+ ## Long term
38
+
39
+ - **FIDO2/WebAuthn passkeys as primary auth** — passwordless-first flows.
40
+ - **Device trust and risk-based authentication** — risk scoring from anomaly detection.
41
+ - **Keystone Operator for Kubernetes** — automated deployments, certificate rotation, and backups.
@@ -0,0 +1,143 @@
1
+ # Keystone Security
2
+
3
+ This document outlines the security model and operational practices for Hilbras Keystone.
4
+
5
+ ## Authentication
6
+
7
+ - **Passwords** are hashed with **argon2id** (OWASP-recommended parameters). Legacy deployments that used scrypt can still verify existing hashes; new hashes always use argon2id.
8
+ - **Multi-factor authentication** is mandatory for any user with TOTP enabled. Password authentication stops at `requires_mfa` and issues no access token, refresh token, or session; only `POST /auth/mfa/verify` completes the transition to `authenticated`.
9
+ - MFA challenges are opaque, stored only as a hash, short-lived (`MFA_CHALLENGE_TTL_SECONDS`, default 300), and single-use. Each state change is a conditional database update, so concurrent verification has exactly one winner. Starting a new password step supersedes any outstanding challenge.
10
+ - TOTP codes are verified against the user's own decrypted secret. Each time-step is accepted once — a captured code is rejected even against a freshly issued challenge.
11
+ - Backup codes carry 80 bits of entropy, are stored as a keyed (peppered) hash, expire after `TOTP_BACKUP_CODE_TTL_SECONDS` (default 90 days), and are consumed by a conditional update so a code can never be used twice.
12
+ - TOTP secrets are encrypted with AES-256-GCM. Values written by earlier versions used AES-256-CBC and remain readable so enrolled authenticators survive the upgrade.
13
+ - Token issuance is guarded at a single chokepoint: a token cannot be minted for an MFA-enabled user without a recorded factor, so no login path can bypass MFA by omission. WebAuthn assertions satisfy the requirement on their own; magic links refuse to downgrade a TOTP-protected account, and SAML, enterprise OIDC, federation, and OAuth2 report a typed `mfa_required` error.
14
+ - Sessions record the factor that satisfied MFA. Refresh rotation refuses sessions with no recorded factor, and enabling MFA revokes every existing refresh token and session for the account.
15
+ - A verified WebAuthn assertion also satisfies the MFA requirement, so a passkey sign-in does not additionally require a TOTP code. A passkey registered *after* TOTP was enabled does not: it is a single factor, and sign-in is refused. Registering a passkey on a TOTP-protected account requires the account password.
16
+ - Changing how an account proves its identity — enrolling, confirming, regenerating backup codes for, or disabling TOTP — requires step-up: the account password in addition to the session. A stolen access token must not be enough to take over an account's second factor.
17
+ - Failed MFA factor attempts count toward the account lockout, so a stolen password cannot be brute-forced through the slower MFA step.
18
+ - **Social and enterprise login** is handled by connectors that normalize profile data. Keystone acts as the identity broker and issues its own tokens.
19
+ - **Enterprise SSO** uses OIDC or SAML. SAML responses are schema-validated with the configured `xmllint` validator, signature-verified, and checked for audience, destination, and subject recipient. OIDC ID tokens are verified against the connection JWKS and issuer/audience. Existing enterprise users require an explicit `(connection, subject)` identity link and organization membership; platform owners cannot authenticate through tenant SSO.
20
+ - Generic OAuth requires a provider-verified email and never treats email equality alone as an identity link.
21
+
22
+ ## Tokens
23
+
24
+ - **Access tokens** are short-lived RS256-signed JWTs. Sessions that completed MFA carry `mfa_verified`, `mfa_factor`, and an `amr` list so relying parties can assert the authentication method used.
25
+ - **Refresh tokens** are opaque, rotated on use, and stored as SHA-256 hashes. Rotation validates client, application, membership, and account state before an atomic conditional claim, so a wrong-client request cannot burn a valid token.
26
+ - **API keys** are opaque, prefix-searchable, and hashed at rest.
27
+ - **JWT signing keys** are rotatable. The JWKS endpoint publishes the active key plus recently rotated keys for a 24-hour grace period.
28
+ - **Cookies** use `HttpOnly`, `Secure` (configurable), and `SameSite=lax`.
29
+ - SCIM credentials are per-organization. Every connection belongs to exactly one organization, at most one connection is live per organization, and every user and group read and write is filtered by that organization. A cross-tenant target returns `404`, not `403`, so the endpoint is not a tenant oracle.
30
+ - SCIM bearer tokens are stored only as a SHA-256 digest and resolved by that digest, so a database dump yields no usable token and the comparison carries no timing signal. Tokens can expire, be rotated, and be revoked. Rotation invalidates the old token immediately unless an explicit grace window is requested — a grace window is a continuity aid, not a revocation.
31
+ - Issuing, rotating, and revoking a SCIM credential is owner-only; a SCIM token can provision and deactivate tenant users, so a mere admin or member must not be able to mint one.
32
+ - A user row is global, so deprovisioning removes the organization's membership first and deactivates the account only once no membership remains. SCIM refuses to change global attributes of a user who also belongs to another organization, and refuses to reactivate a shared account.
33
+ - SCIM cannot remove the last owner of an organization, and never modifies platform owners or accounts pending platform review.
34
+ - SCIM audit rows record the connection id and organization for every mutation, and credential lifecycle transitions emit `scim_connection_created`, `scim_connection_rotated`, and `scim_connection_revoked`. Rejected requests emit `scim_access_denied` and `scim_authentication_failed`.
35
+ - `SCIM_BEARER_TOKEN` and `SCIM_ORG_ID` are deprecated. They are adopted once into a connection at startup and then ignored, so removing them does not affect an adopted credential; revoke through the connection API instead.
36
+ - Configuration and profile responses redact secret-like values before leaving the API; raw database URLs, credentials, signing keys, provider secrets, SCIM tokens, Vault tokens, and generic `*_CLIENT_SECRET` values are not returned.
37
+ - SAML RelayState is bound to a short-lived, one-time Redis transaction and initiating browser cookie; production requires a high-entropy `KEYSTONE_INTERNAL_API_KEY`.
38
+ - Deactivated users are rejected by password, token, API-key, refresh, magic-link, WebAuthn, OAuth, SAML, and OIDC authentication; deactivation revokes refresh tokens, sessions, and user API keys. Ambiguous legacy unverified accounts are quarantined with `account_review_required` during migration.
39
+
40
+ ## Authorization
41
+
42
+ - Platform roles are exactly `owner` and `user`; organization roles are exactly `owner`, `admin`, and `member`.
43
+ - The namespaces are independent. Organization membership never grants platform-owner access.
44
+ - Platform role changes use the dedicated owner-only endpoint `PATCH /v1/admin/platform/users/:userId/role`.
45
+ - Organization member APIs may change only `organizationMembership.role`; global account writes and deactivation are rejected.
46
+ - Organization permission checks resolve the authenticated actor and route organization explicitly. Client-controlled application/origin context is not an authorization decision.
47
+ - `/v1/authz/check` requires an explicit `organizationId` and fails closed when the actor is not a member.
48
+ - OIDC/SAML endpoint configuration rejects local/private targets by default, including hex-form IPv4-mapped IPv6; private enterprise endpoints require an explicit deployment opt-in and outbound fetches pin DNS answers and reject redirects.
49
+ - The last platform owner and the last organization owner cannot be demoted.
50
+ - Tenant workflows cannot assign roles or add memberships across organizations, cannot be registered or executed by actors lacking current workflow-management permission, and are blocked when inactive. Out-of-scope events do not create durable workflow runs.
51
+ - See [RBAC.md](RBAC.md) for the role matrix and migration guidance.
52
+
53
+ ## Secrets
54
+
55
+ The secrets provider abstraction stores:
56
+
57
+ - JWT signing and encryption keys
58
+ - API keys and client secrets
59
+ - Password hashes
60
+
61
+ Default provider stores secrets in PostgreSQL. Production deployments should use `EnvironmentSecretsProvider` or an enterprise backend (AWS KMS, HashiCorp Vault, Azure Key Vault) via plugin.
62
+
63
+ ## User data exposure
64
+
65
+ Administrative and organization user responses use a redacted public projection. Password hashes, TOTP secrets, setup tokens, and sensitive metadata are never returned by user-management endpoints. Self-service profile responses may return the authenticated user's own metadata, but API-key validation and cross-principal projections never do. Treat any client that depends on administrative metadata fields as requiring a separate, explicitly authorized migration.
66
+
67
+ ## Sessions and credential revocation
68
+
69
+ All revocation goes through `src/services/sessionRevocation.ts`. Revocation that is open-coded per call site is revocation that eventually omits the important case — which is how completing a password reset came to change the password while leaving every session and refresh token working, defeating the point of a reset.
70
+
71
+ A successful password reset invalidates:
72
+
73
+ - every authentication session,
74
+ - every refresh token, so no further access token can be minted,
75
+ - every outstanding recovery credential, so a reset email captured earlier cannot be completed after the user has already recovered.
76
+
77
+ Enabling MFA applies the same rule, through the same function.
78
+
79
+ **API keys are not revoked by a password reset.** They are separately issued, long-lived credentials belonging to integrations rather than to the person, and silently invalidating them on a password reset breaks deployments. The residual gap is that a key minted by an attacker who already held the password survives the recovery. That is a key-lifetime problem, and the fix is expiry and rotation, not coupling key lifetime to a human's password. If your threat model requires otherwise, revoke the keys explicitly through the key API.
80
+
81
+ ## Single-use credentials
82
+
83
+ Magic links, password reset tokens, SMS OTP codes, MFA challenges, TOTP backup codes, OAuth2 authorization codes, and refresh tokens are each consumable exactly once, enforced by the database rather than by application logic.
84
+
85
+ The claim is a single conditional write:
86
+
87
+ ```sql
88
+ UPDATE <table> SET used_at = now()
89
+ WHERE <hash> = ? AND expires_at > now() AND used_at IS NULL
90
+ RETURNING *
91
+ ```
92
+
93
+ Exactly one transaction can match that predicate, so concurrency cannot redeem a credential twice. Every consumption goes through `src/services/singleUse.ts`; a credential that reads and then writes inline can be raced.
94
+
95
+ Presenting an already-spent credential is reported as a **replay** and emits an audit event (`magic_link_replayed`, `sms_otp_replayed`, `password_reset_token_replayed`). An expired credential is not a replay and is not reported as one. A token that never existed is reported only as invalid, so the endpoint does not become an oracle for which tokens once existed.
96
+
97
+ ## Enterprise federation
98
+
99
+ SAML assertions and OIDC ID tokens are both validated in full: signature, issuer, audience, algorithm, lifetime, subject, and — for OIDC — a nonce binding the token to the authorization request that started it. See [enterprise-sso.md](security/enterprise-sso.md) for every check, the certificate-rotation procedure, and the endpoint SSRF policy.
100
+
101
+ Two properties are worth stating here because they are easy to get wrong:
102
+
103
+ - **The response-level SAML `Issuer` is unsigned**, so it is compared to the registered IdP entity ID directly. The assertion's own issuer is inside the signed region and is covered by the signature.
104
+ - **A connector that overrides `exchangeCode` must forward its options.** Doing otherwise silently disabled nonce validation for that provider, which is what happened to Google until 2.5.0.
105
+
106
+ ## Trust boundaries
107
+
108
+ Keystone trusts exactly two things: the socket peer address, and credentials it can verify cryptographically. Everything arriving in a header is attacker-controlled unless the peer is a configured trusted proxy.
109
+
110
+ `KEYSTONE_TRUSTED_PROXIES` (comma-separated IPs, IPv4 CIDRs, or IPv6 prefixes) names the proxies permitted to set client-identity headers. It is **empty by default**, which means nothing is trusted: forwarded headers are stripped in an `onRequest` hook before routing, and rate limiting keys on the peer address.
111
+
112
+ A service account authenticates only with a client certificate whose SHA-256 fingerprint is bound to it in `service_accounts.cert_fingerprint`, or with an authenticated credential (API key, session, bearer token). No header establishes identity — before v2.0.0, `x-service-account-id` alone authenticated as any named service account.
113
+
114
+ Details: [trust-boundaries.md](security/trust-boundaries.md), [proxy-security.md](security/proxy-security.md), [mtls.md](security/mtls.md). Upgrade notes: [MIGRATION-2.0.md](MIGRATION-2.0.md).
115
+
116
+ ## Rate limiting
117
+
118
+ A Redis-backed sliding-window rate limiter protects authentication and public endpoints. It returns `429 Too Many Requests` with a `Retry-After` header. It fails open if Redis is unreachable.
119
+
120
+ Rate-limit keys come from the peer address unless the request came from a configured trusted proxy, in which case the forwarded client address is used. Before v2.0.0 the limiter read `x-forwarded-for` unconditionally, so any client could present a fresh address per request and never be limited.
121
+
122
+ SCIM carries two budgets: one keyed on the authenticated credential, so one noisy identity provider cannot exhaust every other tenant's allowance, and an address-keyed budget in front of the authentication hook, because that hook runs before the credential limiter and emits audit and webhook events on failure.
123
+
124
+ MFA verification does not rely on that limiter alone: every challenge carries its own attempt budget (`MFA_MAX_ATTEMPTS`, default 5) enforced in the database, so attempts are counted even when Redis is unavailable. Factor management endpoints (`/auth/totp/*`) have dedicated budgets separate from login.
125
+
126
+ ## Audit and monitoring
127
+
128
+ Every security-relevant action emits a versioned event:
129
+
130
+ - `user_registered`, `user_login`, `user_login_failed`
131
+ - `mfa_challenge_created`, `mfa_challenge_failed`, `mfa_challenge_expired`, `mfa_challenge_rejected`, `mfa_verified`, `mfa_bypass_blocked`, `mfa_backup_code_regenerated`
132
+ - `scim_connection_created`, `scim_connection_rotated`, `scim_connection_revoked`, `scim_access_denied`, `scim_authentication_failed`, `scim_group_created`, `scim_group_updated`, `scim_group_deleted`
133
+ - `oauth_callback`, `saml_sso_login`, `oidc_enterprise_login`
134
+ - `api_key_created`, `api_key_revoked`
135
+ - `authz_check`, `password_reset_requested`, `password_reset_completed`
136
+ - `platform_role_changed`, `organization_member_role_updated`, `organization_member_invited`, `organization_member_removed`
137
+ - `permission_role_updated`, `workflow_blocked`, `unauthorized_access`, `api_key_used`, `oauth2_refresh`, `oauth2_refresh_failed`; refresh, OAuth, SCIM, and API-key mutations carry server-derived actor and tenant attribution.
138
+
139
+ Events are written to the audit log, exported to webhooks, and consumed by anomaly detection. Authorization transitions include actor, target, organization, previous/new state, request ID, IP address, and user agent where available. Audit persistence is asynchronous; production deployments should monitor subscriber failures.
140
+
141
+ ## Reporting vulnerabilities
142
+
143
+ If you discover a security issue, please report it privately to the Hilbras security team. Do not open a public issue until a fix is released.
@@ -0,0 +1,27 @@
1
+ # ADR 001: Identity Connectors as Provider Adapters
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Context
8
+
9
+ Keystone needs to support many external identity providers (Google, GitHub, Azure AD, Okta, Keycloak, Zitadel) without becoming a wrapper around any single one. Early drafts coupled user creation and token issuance directly inside connector code, making connectors hard to test and reuse.
10
+
11
+ ## Decision
12
+
13
+ Introduce a dedicated **Identity Connectors** layer. Each connector is a pure adapter responsible only for:
14
+
15
+ - Building authorization URLs
16
+ - Exchanging codes for tokens
17
+ - Validating identity tokens
18
+ - Retrieving and normalizing profile data
19
+
20
+ User creation, identity linking, Keystone token issuance, and session management remain in higher-level domain services (`AuthenticationDomainService`, `IdentityDomainService`).
21
+
22
+ ## Consequences
23
+
24
+ - Connectors are small, stateless, and easy to unit test.
25
+ - New providers can be added without changing core business logic.
26
+ - The platform remains provider-agnostic.
27
+ - A small amount of orchestration code is duplicated across domain services, which is acceptable for clarity.