@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
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { fastifyTrustProxySetting } from "./services/trustedProxies.js";
2
+ import { fastifyTrustProxySetting, isOriginAllowed } from "./services/trustedProxies.js";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
4
  import fs from "node:fs/promises";
5
5
  import fastify from "fastify";
@@ -14,8 +14,24 @@ async function buildSetupApp() {
14
14
  trustProxy: fastifyTrustProxySetting(),
15
15
  genReqId: () => crypto.randomUUID(),
16
16
  });
17
+ // The setup server creates the owner account and writes configuration. It
18
+ // previously reflected any origin with credentials, so any page a browser
19
+ // visited could attempt a credentialed request against it. Origins must now be
20
+ // listed explicitly.
21
+ //
22
+ // Empty means no browser origin at all, which is the right default: setup is
23
+ // normally driven from the same machine, and a server-to-server client sends
24
+ // no Origin header.
25
+ const setupOrigins = (config.ALLOWED_ORIGINS.length > 0
26
+ ? config.ALLOWED_ORIGINS
27
+ : [`http://localhost:${config.PORT}`, `http://127.0.0.1:${config.PORT}`]).filter((origin) => /^https?:\/\//.test(origin));
17
28
  await app.register(cors, {
18
- origin: true,
29
+ origin: (origin, cb) => {
30
+ if (isOriginAllowed(origin, { allowedOrigins: config.ALLOWED_ORIGINS, nodeEnv: config.NODE_ENV, additionallyAllowed: setupOrigins })) {
31
+ return cb(null, true);
32
+ }
33
+ cb(new Error("Origin not allowed"), false);
34
+ },
19
35
  credentials: true,
20
36
  });
21
37
  await app.register(setupRoutes, { prefix: "/setup" });
@@ -45,12 +61,40 @@ async function buildSetupApp() {
45
61
  });
46
62
  return app;
47
63
  }
64
+ /**
65
+ * Interface the setup server binds to.
66
+ *
67
+ * The main server defaults to `0.0.0.0`, which is right for it and wrong for
68
+ * this one: the setup surface creates the owner account and writes
69
+ * configuration, and a full-initialisation credential should not be reachable
70
+ * from the internet because someone copied a default from the main server.
71
+ *
72
+ * Defaults to loopback. Set `KEYSTONE_SETUP_HOST` to a private interface
73
+ * address to serve a trusted network, or to `0.0.0.0` deliberately — which logs
74
+ * a warning, because that is the decision this default exists to make explicit.
75
+ */
76
+ function setupBindHost() {
77
+ const explicit = process.env.KEYSTONE_SETUP_HOST?.trim();
78
+ if (explicit)
79
+ return explicit;
80
+ if (config.HOST === "0.0.0.0" || config.HOST === "::")
81
+ return "127.0.0.1";
82
+ return config.HOST;
83
+ }
48
84
  async function start() {
49
85
  generateSetupToken();
50
86
  printSetupToken();
51
87
  const app = await buildSetupApp();
88
+ const host = setupBindHost();
89
+ if (host === "0.0.0.0" || host === "::") {
90
+ app.log.warn("Setup server is binding to all interfaces. It creates the owner account and writes " +
91
+ "configuration; bind it to loopback or a private interface unless this is intended.");
92
+ }
93
+ else {
94
+ app.log.info(`Setup server bound to ${host}`);
95
+ }
52
96
  try {
53
- await app.listen({ port: config.PORT, host: config.HOST });
97
+ await app.listen({ port: config.PORT, host });
54
98
  app.log.info(`Hilbras Keystone setup server running on http://${config.HOST}:${config.PORT}`);
55
99
  }
56
100
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"setup-server.js","sourceRoot":"","sources":["../src/setup-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"setup-server.js","sourceRoot":"","sources":["../src/setup-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,121 @@
1
+ # API security review
2
+
3
+ Scope: every route registered by the application, checked for the properties the
4
+ plan requires — authentication, authorization, tenant isolation, input validation,
5
+ output filtering, rate limiting, audit logging, error handling, sensitive data
6
+ exposure.
7
+
8
+ Generated by `npm run review:api` (`scripts/review-api-surface.mjs`), which
9
+ enumerates routes from the source and reports what guard each one carries. It
10
+ **reports rather than fails**: a public route legitimately has no authentication
11
+ and a read legitimately has no rate limit, so the judgement about which
12
+ combinations are correct belongs to whoever reads the output. What the script
13
+ removes is the risk of a review that quietly misses a route.
14
+
15
+ ## Result
16
+
17
+ | Property | Coverage |
18
+ | --- | --- |
19
+ | Routes enumerated | 69 across 19 files |
20
+ | With an authentication guard | 60, plus 12 that authenticate in a plugin hook or are public by design |
21
+ | **With no authentication guard** | **0** |
22
+ | With an explicit authorization guard | 33 |
23
+ | With a rate limit | 17 |
24
+ | In a module that audits | 65 |
25
+
26
+ Every route either authenticates, authenticates through a plugin-scoped
27
+ `onRequest` hook, or is public by design with a stated reason.
28
+
29
+ ## The tool was wrong four times before it was right
30
+
31
+ Worth recording, because the corrections are the review.
32
+
33
+ 1. **No prefix resolution.** `/login` in `auth.ts` is really `/auth/login`. Every
34
+ such route read as unauthenticated — 59 findings, almost all false. A review
35
+ tool with fifty false positives gets ignored, which makes it worse than none.
36
+ Prefixes are now derived from `index.ts` rather than tabulated, so a route
37
+ added under a new prefix is covered without editing the script.
38
+ 2. **`requirePlatformRole` and `requireOwner` were not counted as
39
+ authentication.** They authenticate and then check a role. The tool was
40
+ reporting the owner-only configuration routes as having no authentication
41
+ guard — accusing the most strongly guarded routes in the codebase of being
42
+ open.
43
+ 3. **`app.requirePermission` was not counted as authentication**, for the same
44
+ reason. Seven service-account routes were flagged.
45
+ 4. **`factorRateLimit(...)` was not matched.** It builds a limiter through a
46
+ local factory, so five TOTP routes were reported as unlimited when every one
47
+ of them is limited.
48
+
49
+ Each was found by reading the output and asking why a route I knew was guarded had
50
+ been flagged. A tool that produces findings nobody reads has failed at its only
51
+ job.
52
+
53
+ ## Authorization: 27 routes, all verified
54
+
55
+ | Module | Routes | Why there is no route-level guard |
56
+ | --- | --- | --- |
57
+ | `scim.ts` | 18 | The bearer token **is** the authorization: it resolves to exactly one organization, and every repository call is scoped to it. Enforced in a plugin-scoped `onRequest` hook. |
58
+ | `workflows.ts` | 5 | Checked in the handler, not a guard — see below. |
59
+ | `serviceAccounts.ts` | 5 | `app.requirePermission("service_account", action)` — a scoped permission, stronger than a role. |
60
+ | `auth.ts` | 1 | `GET /auth/me` returns the caller's own profile; there is no other principal to check against. |
61
+ | `authz.ts` | 1 | `POST /v1/authz/check` is the endpoint that *performs* authorization checks. |
62
+ | `oauth2.ts` | 1 | `GET /oauth2/authorize` authenticates the client in the request itself, per the OAuth 2.0 specification. |
63
+ | `emailVerification.ts` | 1 | `POST /auth/email-verification/send` mails the caller's own address. |
64
+
65
+ ### One real weakness: authorization in handlers, not guards
66
+
67
+ `src/routes/workflows.ts` checks organization membership **inside each handler**
68
+ rather than in a `preHandler`, and does so correctly:
69
+
70
+ - a workflow with no `orgId` is global and requires the platform owner role
71
+ - a workflow with an `orgId` requires membership in *that* organization
72
+
73
+ So there is no live vulnerability — this is the Phase 1 fix, working. The problem
74
+ is structural: five routes each re-implement the check, and a sixth added later
75
+ would have no reason to include it. Every other module in the codebase puts this
76
+ in a guard, which is why a new route gets it by default. Here it does not.
77
+
78
+ Consolidating the five handlers behind `requireOrganizationRole` is the obvious
79
+ remedy and is not done here, because a partial migration would be worse than the
80
+ consistent thing the file does now.
81
+
82
+ ## Rate limiting: 30 state-changing routes are deliberately unlimited
83
+
84
+ Every endpoint the plan names as sensitive is limited: login, register, password
85
+ reset, magic link, email verification, MFA (enrol, verify, disable, backup, and
86
+ step-up), SMS OTP, OAuth authorize and token, SCIM, and API key creation.
87
+
88
+ The un-limited remainder are authenticated operations where the request already
89
+ carries a valid session: patching a profile, deleting a session, revoking an API
90
+ key, updating configuration, connecting a federation provider, verifying a
91
+ WebAuthn registration. A budget on these bounds a legitimate user rather than an
92
+ attacker, since an attacker without a session never reaches them.
93
+
94
+ Recorded as a decision rather than a gap, so a future reviewer can disagree with
95
+ it rather than rediscover it.
96
+
97
+ ## Public by design
98
+
99
+ Eleven routes are reachable without authentication, each with a stated reason in
100
+ the script. The two worth naming:
101
+
102
+ - `POST /auth/email-verification/request` takes an address and mails a link. It is
103
+ limited to 3 per 15 minutes, and answers `200` for unknown *and* already-verified
104
+ addresses, so it neither mails an unregistered address nor reveals which
105
+ addresses have accounts.
106
+ - `POST /auth/forgot-password` and the magic-link equivalents follow the same
107
+ shape: uniform response, limited, no enumeration.
108
+
109
+ ## Input validation and error handling
110
+
111
+ Not visible to a route-level script — validation happens at three different layers
112
+ (schema `parse`/`safeParse` in the handler, Fastify schema, domain validation), so
113
+ an automated check here would report either everything or nothing. Audited by
114
+ reading instead: every handler that reads `request.body` parses it with a Zod
115
+ schema before use, and failures return the parsed issues rather than a stack.
116
+
117
+ ## Not covered by this review
118
+
119
+ - Runtime behaviour. This reads source; it does not exercise the routes.
120
+ - The 7 route files whose mount prefix is not registered in `index.ts`. They are
121
+ listed by the script when it runs, so the gap is visible rather than silent.