@stratum-hq/control-plane 1.1.0 → 1.3.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 (62) hide show
  1. package/README.md +5 -3
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +8 -5
  4. package/dist/config.js.map +1 -1
  5. package/dist/db/migrate.d.ts.map +1 -1
  6. package/dist/db/migrate.js +5 -1
  7. package/dist/db/migrate.js.map +1 -1
  8. package/dist/middleware/authorize.d.ts +31 -1
  9. package/dist/middleware/authorize.d.ts.map +1 -1
  10. package/dist/middleware/authorize.js +47 -41
  11. package/dist/middleware/authorize.js.map +1 -1
  12. package/dist/middleware/error-handler.d.ts +39 -0
  13. package/dist/middleware/error-handler.d.ts.map +1 -1
  14. package/dist/middleware/error-handler.js +106 -16
  15. package/dist/middleware/error-handler.js.map +1 -1
  16. package/dist/middleware/telemetry.d.ts +1 -1
  17. package/dist/middleware/telemetry.d.ts.map +1 -1
  18. package/dist/middleware/telemetry.js +27 -8
  19. package/dist/middleware/telemetry.js.map +1 -1
  20. package/dist/middleware/tenant-scope.d.ts +10 -1
  21. package/dist/middleware/tenant-scope.d.ts.map +1 -1
  22. package/dist/middleware/tenant-scope.js +16 -6
  23. package/dist/middleware/tenant-scope.js.map +1 -1
  24. package/dist/routes/abac.d.ts.map +1 -1
  25. package/dist/routes/abac.js +2 -0
  26. package/dist/routes/abac.js.map +1 -1
  27. package/dist/routes/api-keys.d.ts.map +1 -1
  28. package/dist/routes/api-keys.js +4 -1
  29. package/dist/routes/api-keys.js.map +1 -1
  30. package/dist/routes/audit-logs.d.ts.map +1 -1
  31. package/dist/routes/audit-logs.js +2 -0
  32. package/dist/routes/audit-logs.js.map +1 -1
  33. package/dist/routes/config-diff.d.ts.map +1 -1
  34. package/dist/routes/config-diff.js +2 -0
  35. package/dist/routes/config-diff.js.map +1 -1
  36. package/dist/routes/config.d.ts.map +1 -1
  37. package/dist/routes/config.js +2 -0
  38. package/dist/routes/config.js.map +1 -1
  39. package/dist/routes/consent.d.ts.map +1 -1
  40. package/dist/routes/consent.js +2 -0
  41. package/dist/routes/consent.js.map +1 -1
  42. package/dist/routes/maintenance.d.ts.map +1 -1
  43. package/dist/routes/maintenance.js +2 -0
  44. package/dist/routes/maintenance.js.map +1 -1
  45. package/dist/routes/permissions.d.ts.map +1 -1
  46. package/dist/routes/permissions.js +2 -0
  47. package/dist/routes/permissions.js.map +1 -1
  48. package/dist/routes/regions.d.ts.map +1 -1
  49. package/dist/routes/regions.js +2 -0
  50. package/dist/routes/regions.js.map +1 -1
  51. package/dist/routes/roles.d.ts.map +1 -1
  52. package/dist/routes/roles.js +5 -2
  53. package/dist/routes/roles.js.map +1 -1
  54. package/dist/routes/tenants.d.ts.map +1 -1
  55. package/dist/routes/tenants.js +52 -8
  56. package/dist/routes/tenants.js.map +1 -1
  57. package/dist/routes/webhooks.d.ts.map +1 -1
  58. package/dist/routes/webhooks.js +2 -0
  59. package/dist/routes/webhooks.js.map +1 -1
  60. package/dist/services/isolation-service.js +1 -1
  61. package/dist/services/isolation-service.js.map +1 -1
  62. package/package.json +3 -3
package/README.md CHANGED
@@ -26,14 +26,16 @@ On startup the server runs database migrations, then listens on `PORT` (default
26
26
  |----------|---------|-------------|
27
27
  | `PORT` | `3001` | Listen port |
28
28
  | `DATABASE_URL` | `postgres://stratum:stratum_dev@localhost:5432/stratum` | PostgreSQL connection string |
29
- | `JWT_SECRET` | dev fallback | JWT signing secret — **required** in production/staging (server refuses to start without it). In production it must be at least 32 bytes and not a placeholder such as `change-me-in-production` |
30
- | `JWT_AUDIENCE` | — | Optional; when set, Bearer tokens must carry this `aud` claim (for example `stratum-control-plane`). Recommended whenever `JWT_SECRET` is shared with another application |
29
+ | `JWT_SECRET` | dev fallback | JWT signing secret — **required** whenever `NODE_ENV` is not `development` or `test` (server refuses to start without it). There it must also be at least 32 bytes and not a placeholder such as `change-me-in-production` |
30
+ | `JWT_AUDIENCE` | — | Optional; when set, Bearer tokens must carry this `aud` claim (for example `stratum-control-plane`). Recommended whenever `JWT_SECRET` is shared with another application; the server warns at startup when it is unset and `NODE_ENV` is not `development` or `test` |
31
31
  | `JWT_ISSUER` | — | Optional; when set, Bearer tokens must carry this `iss` claim |
32
32
  | `ALLOWED_ORIGINS` | `localhost:3000,3300` | Comma-separated CORS allowlist |
33
33
  | `RATE_LIMIT_MAX` | `100` | Requests per window |
34
34
  | `RATE_LIMIT_WINDOW` | `1 minute` | Rate-limit window |
35
35
  | `REDIS_URL` | — | Optional; enables distributed per-key rate limiting. While Redis is unreachable the client keeps reconnecting and per-key limits are counted in memory per process |
36
- | `NODE_ENV` | `development` | Environment |
36
+ | `STRATUM_ENCRYPTION_KEY` | dev fallback | Field-level encryption key — **required** whenever `NODE_ENV` is not `development` or `test` |
37
+ | `STRATUM_HKDF_SALT` | dev fallback | Hex-encoded HKDF salt — **required** whenever `NODE_ENV` is not `development` or `test` |
38
+ | `NODE_ENV` | `development` | Environment. Any value other than `development` or `test` (staging and preview included) enforces the `JWT_SECRET` checks, requires real encryption key material, and runs migrations with RLS enforcement (refusing a `BYPASSRLS` role). Unset counts as `development` |
37
39
 
38
40
  Security middleware (`@fastify/helmet`, CORS, rate limiting) is enabled by default.
39
41
 
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAyCA,eAAO,MAAM,MAAM;;;;;;;;;;;CAalB,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AA4CA,eAAO,MAAM,MAAM;;;;;;;;;;;CAalB,CAAC"}
package/dist/config.js CHANGED
@@ -7,9 +7,12 @@ exports.config = void 0;
7
7
  const node_crypto_1 = __importDefault(require("node:crypto"));
8
8
  const jwtSecretEnv = process.env.JWT_SECRET;
9
9
  const nodeEnv = process.env.NODE_ENV || "development";
10
+ // Secret checks apply everywhere except local development and test runs, so a
11
+ // staging or preview deployment cannot start with a weak or published secret.
12
+ const enforceSecretHygiene = nodeEnv !== "development" && nodeEnv !== "test";
10
13
  if (!jwtSecretEnv) {
11
- if (nodeEnv === "production" || nodeEnv === "staging") {
12
- throw new Error("FATAL: JWT_SECRET must be set in production. Refusing to start.");
14
+ if (enforceSecretHygiene) {
15
+ throw new Error(`FATAL: JWT_SECRET must be set (NODE_ENV=${nodeEnv}). Refusing to start.`);
13
16
  }
14
17
  else {
15
18
  console.warn("[stratum] JWT_SECRET not set — using dev fallback. Set JWT_SECRET before deploying to production.");
@@ -22,12 +25,12 @@ const PLACEHOLDER_JWT_SECRETS = new Set([
22
25
  "your-jwt-secret-change-in-production",
23
26
  ]);
24
27
  const MIN_JWT_SECRET_BYTES = 32;
25
- if (jwtSecretEnv && nodeEnv === "production") {
28
+ if (jwtSecretEnv && enforceSecretHygiene) {
26
29
  if (PLACEHOLDER_JWT_SECRETS.has(jwtSecretEnv)) {
27
30
  throw new Error("FATAL: JWT_SECRET is a published placeholder value. Set a random secret. Refusing to start.");
28
31
  }
29
32
  if (Buffer.byteLength(jwtSecretEnv, "utf8") < MIN_JWT_SECRET_BYTES) {
30
- throw new Error(`FATAL: JWT_SECRET must be at least ${MIN_JWT_SECRET_BYTES} bytes in production. Refusing to start.`);
33
+ throw new Error(`FATAL: JWT_SECRET must be at least ${MIN_JWT_SECRET_BYTES} bytes outside development and test. Refusing to start.`);
31
34
  }
32
35
  }
33
36
  // Optional JWT audience / issuer binding. When set, a Bearer token is accepted
@@ -36,7 +39,7 @@ if (jwtSecretEnv && nodeEnv === "production") {
36
39
  // behavior (no aud / iss check) for deployments whose tokens do not carry them.
37
40
  const jwtAudienceEnv = process.env.JWT_AUDIENCE || undefined;
38
41
  const jwtIssuerEnv = process.env.JWT_ISSUER || undefined;
39
- if (!jwtAudienceEnv && (nodeEnv === "production" || nodeEnv === "staging")) {
42
+ if (!jwtAudienceEnv && enforceSecretHygiene) {
40
43
  console.warn("[stratum] JWT_AUDIENCE not set — Bearer tokens are not bound to the control plane. Set JWT_AUDIENCE (for example stratum-control-plane).");
41
44
  }
42
45
  exports.config = {
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;;;;AAAA,8DAAiC;AAEjC,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;AAC5C,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa,CAAC;AAEtD,IAAI,CAAC,YAAY,EAAE,CAAC;IAClB,IAAI,OAAO,KAAK,YAAY,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;IACrF,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,IAAI,CAAC,mGAAmG,CAAC,CAAC;IACpH,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC;IACtC,yBAAyB;IACzB,8CAA8C;IAC9C,sCAAsC;CACvC,CAAC,CAAC;AACH,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC,IAAI,YAAY,IAAI,OAAO,KAAK,YAAY,EAAE,CAAC;IAC7C,IAAI,uBAAuB,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,6FAA6F,CAAC,CAAC;IACjH,CAAC;IACD,IAAI,MAAM,CAAC,UAAU,CAAC,YAAY,EAAE,MAAM,CAAC,GAAG,oBAAoB,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CAAC,sCAAsC,oBAAoB,0CAA0C,CAAC,CAAC;IACxH,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,4EAA4E;AAC5E,2EAA2E;AAC3E,gFAAgF;AAChF,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,SAAS,CAAC;AAC7D,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,SAAS,CAAC;AAEzD,IAAI,CAAC,cAAc,IAAI,CAAC,OAAO,KAAK,YAAY,IAAI,OAAO,KAAK,SAAS,CAAC,EAAE,CAAC;IAC3E,OAAO,CAAC,IAAI,CAAC,0IAA0I,CAAC,CAAC;AAC3J,CAAC;AAEY,QAAA,MAAM,GAAG;IACpB,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM,CAAC;IAC1C,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,uDAAuD;IAChG,OAAO;IACP,SAAS,EAAE,YAAY,IAAI,qBAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;IACjE,WAAW,EAAE,cAAc;IAC3B,SAAS,EAAE,YAAY;IACvB,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe;QACzC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7D,CAAC,CAAC,CAAC,uBAAuB,EAAE,uBAAuB,CAAC;IACtD,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,KAAK,CAAC;IAC3D,eAAe,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,UAAU;IAC5D,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,SAAS;CAC7C,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;;;;AAAA,8DAAiC;AAEjC,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;AAC5C,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa,CAAC;AACtD,8EAA8E;AAC9E,8EAA8E;AAC9E,MAAM,oBAAoB,GAAG,OAAO,KAAK,aAAa,IAAI,OAAO,KAAK,MAAM,CAAC;AAE7E,IAAI,CAAC,YAAY,EAAE,CAAC;IAClB,IAAI,oBAAoB,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,2CAA2C,OAAO,uBAAuB,CAAC,CAAC;IAC7F,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,IAAI,CAAC,mGAAmG,CAAC,CAAC;IACpH,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC;IACtC,yBAAyB;IACzB,8CAA8C;IAC9C,sCAAsC;CACvC,CAAC,CAAC;AACH,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC,IAAI,YAAY,IAAI,oBAAoB,EAAE,CAAC;IACzC,IAAI,uBAAuB,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,6FAA6F,CAAC,CAAC;IACjH,CAAC;IACD,IAAI,MAAM,CAAC,UAAU,CAAC,YAAY,EAAE,MAAM,CAAC,GAAG,oBAAoB,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CAAC,sCAAsC,oBAAoB,yDAAyD,CAAC,CAAC;IACvI,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,4EAA4E;AAC5E,2EAA2E;AAC3E,gFAAgF;AAChF,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,SAAS,CAAC;AAC7D,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,SAAS,CAAC;AAEzD,IAAI,CAAC,cAAc,IAAI,oBAAoB,EAAE,CAAC;IAC5C,OAAO,CAAC,IAAI,CAAC,0IAA0I,CAAC,CAAC;AAC3J,CAAC;AAEY,QAAA,MAAM,GAAG;IACpB,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM,CAAC;IAC1C,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,uDAAuD;IAChG,OAAO;IACP,SAAS,EAAE,YAAY,IAAI,qBAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;IACjE,WAAW,EAAE,cAAc;IAC3B,SAAS,EAAE,YAAY;IACvB,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe;QACzC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7D,CAAC,CAAC,CAAC,uBAAuB,EAAE,uBAAuB,CAAC;IACtD,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,KAAK,CAAC;IAC3D,eAAe,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,UAAU;IAC5D,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,SAAS;CAC7C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/db/migrate.ts"],"names":[],"mappings":"AAGA,iBAAe,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAKtC;AAED,OAAO,EAAE,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/db/migrate.ts"],"names":[],"mappings":"AAGA,iBAAe,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAStC;AAED,OAAO,EAAE,OAAO,EAAE,CAAC"}
@@ -6,7 +6,11 @@ const connection_js_1 = require("./connection.js");
6
6
  async function migrate() {
7
7
  const pool = (0, connection_js_1.getPool)();
8
8
  console.log("Running migrations...");
9
- await (0, lib_1.migrate)({ pool, enforceRls: process.env.NODE_ENV === "production" });
9
+ // RLS is enforced everywhere except local development and test runs (an
10
+ // unset NODE_ENV counts as development), matching the JWT_SECRET checks.
11
+ const nodeEnv = process.env.NODE_ENV || "development";
12
+ const enforceRls = nodeEnv !== "development" && nodeEnv !== "test";
13
+ await (0, lib_1.migrate)({ pool, enforceRls });
10
14
  console.log("Migrations complete.");
11
15
  }
12
16
  //# sourceMappingURL=migrate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../src/db/migrate.ts"],"names":[],"mappings":";;AAUS,0BAAO;AAVhB,yCAA2D;AAC3D,mDAA0C;AAE1C,KAAK,UAAU,OAAO;IACpB,MAAM,IAAI,GAAG,IAAA,uBAAO,GAAE,CAAC;IACvB,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;IACrC,MAAM,IAAA,aAAa,EAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC,CAAC;IACjF,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;AACtC,CAAC"}
1
+ {"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../src/db/migrate.ts"],"names":[],"mappings":";;AAcS,0BAAO;AAdhB,yCAA2D;AAC3D,mDAA0C;AAE1C,KAAK,UAAU,OAAO;IACpB,MAAM,IAAI,GAAG,IAAA,uBAAO,GAAE,CAAC;IACvB,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;IACrC,wEAAwE;IACxE,yEAAyE;IACzE,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa,CAAC;IACtD,MAAM,UAAU,GAAG,OAAO,KAAK,aAAa,IAAI,OAAO,KAAK,MAAM,CAAC;IACnE,MAAM,IAAA,aAAa,EAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;IAC1C,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;AACtC,CAAC"}
@@ -1,3 +1,33 @@
1
- import { FastifyRequest, FastifyReply } from "fastify";
1
+ import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
2
+ /**
3
+ * The scope a route requires:
4
+ * - "read" / "write" / "admin": the caller's key must hold that scope (admin
5
+ * implies write implies read).
6
+ * - "operator": admin scope held by a global operator API key
7
+ * (tenant_id === null). Tenant-scoped callers are refused.
8
+ */
9
+ export type RequiredScope = "read" | "write" | "admin" | "operator";
10
+ declare module "fastify" {
11
+ interface FastifyContextConfig {
12
+ requiredScope?: RequiredScope;
13
+ }
14
+ }
15
+ /**
16
+ * A plugin-wide required-scope declaration: one scope for every route, or one
17
+ * scope for safe methods (GET, HEAD, OPTIONS) and another for all others.
18
+ */
19
+ export type RequiredScopeDeclaration = RequiredScope | {
20
+ read: RequiredScope;
21
+ write: RequiredScope;
22
+ };
23
+ /**
24
+ * Declare the required scope for every route registered in the current plugin.
25
+ *
26
+ * Stamps the scope onto each route's config when the route is registered. A
27
+ * route may still set its own `config.requiredScope`, which wins. A route that
28
+ * registers several methods at once must set its own `config.requiredScope`
29
+ * unless the declaration is a single scope.
30
+ */
31
+ export declare function declareRequiredScope(app: FastifyInstance, declaration: RequiredScopeDeclaration): void;
2
32
  export declare function createAuthorizeMiddleware(): (request: FastifyRequest, _reply: FastifyReply) => Promise<void>;
3
33
  //# sourceMappingURL=authorize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../src/middleware/authorize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AA+CvD,wBAAgB,yBAAyB,KAErC,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CA4BjB"}
1
+ {"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../src/middleware/authorize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAIxE;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,CAAC;AAEpE,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,oBAAoB;QAC5B,aAAa,CAAC,EAAE,aAAa,CAAC;KAC/B;CACF;AAED;;;GAGG;AACH,MAAM,MAAM,wBAAwB,GAChC,aAAa,GACb;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,CAAC;AAMlD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,eAAe,EACpB,WAAW,EAAE,wBAAwB,GACpC,IAAI,CAkBN;AAED,wBAAgB,yBAAyB,KAErC,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CAoCjB"}
@@ -1,44 +1,42 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.declareRequiredScope = declareRequiredScope;
3
4
  exports.createAuthorizeMiddleware = createAuthorizeMiddleware;
4
5
  const core_1 = require("@stratum-hq/core");
5
- function methodToScope(method) {
6
- switch (method.toUpperCase()) {
7
- case "GET":
8
- case "HEAD":
9
- case "OPTIONS":
10
- return "read";
11
- default:
12
- return "write";
13
- }
6
+ const tenant_scope_js_1 = require("./tenant-scope.js");
7
+ function isSafeMethod(method) {
8
+ return ["GET", "HEAD", "OPTIONS"].includes(method.toUpperCase());
14
9
  }
15
- // Route patterns that require admin scope for ALL methods
16
- const ADMIN_ROUTES = [
17
- /^\/api\/v1\/api-keys/,
18
- /^\/api\/v1\/audit-logs/,
19
- /^\/api\/v1\/maintenance/,
20
- /^\/api\/v1\/regions/,
21
- /^\/api\/v1\/tenants\/[^/]+\/purge$/,
22
- /^\/api\/v1\/tenants\/[^/]+\/export$/,
23
- /^\/api\/v1\/tenants\/[^/]+\/migrate-region$/,
24
- /^\/api\/v1\/tenants\/[^/]+\/context$/,
25
- ];
26
- // Route patterns whose mutations require admin scope. Managing roles changes
27
- // the scopes that API keys resolve to, so it is an admin operation.
28
- const ADMIN_MUTATION_ROUTES = [
29
- /^\/api\/v1\/roles/,
30
- ];
31
- function getRequiredScope(method, path) {
32
- for (const pattern of ADMIN_ROUTES) {
33
- if (pattern.test(path)) {
34
- return "admin";
10
+ /**
11
+ * Declare the required scope for every route registered in the current plugin.
12
+ *
13
+ * Stamps the scope onto each route's config when the route is registered. A
14
+ * route may still set its own `config.requiredScope`, which wins. A route that
15
+ * registers several methods at once must set its own `config.requiredScope`
16
+ * unless the declaration is a single scope.
17
+ */
18
+ function declareRequiredScope(app, declaration) {
19
+ app.addHook("onRoute", (routeOptions) => {
20
+ if (routeOptions.config?.requiredScope !== undefined)
21
+ return;
22
+ let scope;
23
+ if (typeof declaration === "string") {
24
+ scope = declaration;
25
+ }
26
+ else {
27
+ const methods = Array.isArray(routeOptions.method)
28
+ ? routeOptions.method
29
+ : [routeOptions.method];
30
+ const safe = methods.every(isSafeMethod);
31
+ const unsafe = methods.every((method) => !isSafeMethod(method));
32
+ // A mixed-method route stays undeclared, so it is refused.
33
+ if (safe)
34
+ scope = declaration.read;
35
+ else if (unsafe)
36
+ scope = declaration.write;
35
37
  }
36
- }
37
- const scope = methodToScope(method);
38
- if (scope === "write" && ADMIN_MUTATION_ROUTES.some((pattern) => pattern.test(path))) {
39
- return "admin";
40
- }
41
- return scope;
38
+ routeOptions.config = { ...routeOptions.config, requiredScope: scope };
39
+ });
42
40
  }
43
41
  function createAuthorizeMiddleware() {
44
42
  return async function authorizeMiddleware(request, _reply) {
@@ -52,17 +50,25 @@ function createAuthorizeMiddleware() {
52
50
  if (!request.apiKey) {
53
51
  throw new core_1.UnauthorizedError("Authentication required");
54
52
  }
55
- // Decide the scope from the route pattern that was matched (for example
56
- // "/api/v1/tenants/:id/purge"), so it is a property of the route rather than
57
- // of the request URL. With no matched route there is no handler to reach.
58
- const routePath = request.routeOptions?.url ?? request.url.split("?", 1)[0];
59
- const requiredScope = getRequiredScope(request.method, routePath);
53
+ // A request that matched no route reaches only the not-found handler, so
54
+ // it is answered with 404 once the caller is authenticated.
55
+ if (request.is404)
56
+ return;
57
+ // The required scope is a property of the matched route, declared in its
58
+ // config (see declareRequiredScope). A route that declares none is refused.
59
+ const requiredScope = request.routeOptions?.config?.requiredScope;
60
+ if (requiredScope === undefined) {
61
+ throw new core_1.ForbiddenError("Route has no required-scope declaration");
62
+ }
60
63
  const scopes = request.apiKey.scopes ?? ["read"];
61
64
  // Hierarchical scopes: admin implies write implies read. A granted scope
62
65
  // satisfies any required scope of equal-or-lower rank.
63
- if (!(0, core_1.scopeSatisfies)(scopes, requiredScope)) {
66
+ if (!(0, core_1.scopeSatisfies)(scopes, requiredScope === "operator" ? "admin" : requiredScope)) {
64
67
  throw new core_1.ForbiddenError("Insufficient permissions for this operation");
65
68
  }
69
+ if (requiredScope === "operator") {
70
+ (0, tenant_scope_js_1.assertOperator)(request);
71
+ }
66
72
  };
67
73
  }
68
74
  //# sourceMappingURL=authorize.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../src/middleware/authorize.ts"],"names":[],"mappings":";;AA+CA,8DAgCC;AA9ED,2CAAqF;AAIrF,SAAS,aAAa,CAAC,MAAc;IACnC,QAAQ,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC;QAC7B,KAAK,KAAK,CAAC;QACX,KAAK,MAAM,CAAC;QACZ,KAAK,SAAS;YACZ,OAAO,MAAM,CAAC;QAChB;YACE,OAAO,OAAO,CAAC;IACnB,CAAC;AACH,CAAC;AAED,0DAA0D;AAC1D,MAAM,YAAY,GAAG;IACnB,sBAAsB;IACtB,wBAAwB;IACxB,yBAAyB;IACzB,qBAAqB;IACrB,oCAAoC;IACpC,qCAAqC;IACrC,6CAA6C;IAC7C,sCAAsC;CACvC,CAAC;AAEF,6EAA6E;AAC7E,oEAAoE;AACpE,MAAM,qBAAqB,GAAG;IAC5B,mBAAmB;CACpB,CAAC;AAEF,SAAS,gBAAgB,CAAC,MAAc,EAAE,IAAY;IACpD,KAAK,MAAM,OAAO,IAAI,YAAY,EAAE,CAAC;QACnC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACvB,OAAO,OAAO,CAAC;QACjB,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,KAAK,KAAK,OAAO,IAAI,qBAAqB,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QACrF,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAgB,yBAAyB;IACvC,OAAO,KAAK,UAAU,mBAAmB,CACvC,OAAuB,EACvB,MAAoB;QAEpB,8CAA8C;QAC9C,IACE,OAAO,CAAC,GAAG,KAAK,gBAAgB;YAChC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,iBAAiB,CAAC;YACzC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC,EACnC,CAAC;YACD,OAAO;QACT,CAAC;QAED,mEAAmE;QACnE,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,IAAI,wBAAiB,CAAC,yBAAyB,CAAC,CAAC;QACzD,CAAC;QAED,wEAAwE;QACxE,6EAA6E;QAC7E,0EAA0E;QAC1E,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5E,MAAM,aAAa,GAAG,gBAAgB,CAAC,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAClE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;QAEjD,yEAAyE;QACzE,uDAAuD;QACvD,IAAI,CAAC,IAAA,qBAAc,EAAC,MAAM,EAAE,aAAa,CAAC,EAAE,CAAC;YAC3C,MAAM,IAAI,qBAAc,CAAC,6CAA6C,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../src/middleware/authorize.ts"],"names":[],"mappings":";;AAuCA,oDAqBC;AAED,8DAwCC;AArGD,2CAAqF;AACrF,uDAAmD;AAyBnD,SAAS,YAAY,CAAC,MAAc;IAClC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;AACnE,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,oBAAoB,CAClC,GAAoB,EACpB,WAAqC;IAErC,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,YAAY,EAAE,EAAE;QACtC,IAAI,YAAY,CAAC,MAAM,EAAE,aAAa,KAAK,SAAS;YAAE,OAAO;QAC7D,IAAI,KAAgC,CAAC;QACrC,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;YACpC,KAAK,GAAG,WAAW,CAAC;QACtB,CAAC;aAAM,CAAC;YACN,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC;gBAChD,CAAC,CAAC,YAAY,CAAC,MAAM;gBACrB,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YAC1B,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YACzC,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;YAChE,2DAA2D;YAC3D,IAAI,IAAI;gBAAE,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC;iBAC9B,IAAI,MAAM;gBAAE,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC;QAC7C,CAAC;QACD,YAAY,CAAC,MAAM,GAAG,EAAE,GAAG,YAAY,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC;IACzE,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAgB,yBAAyB;IACvC,OAAO,KAAK,UAAU,mBAAmB,CACvC,OAAuB,EACvB,MAAoB;QAEpB,8CAA8C;QAC9C,IACE,OAAO,CAAC,GAAG,KAAK,gBAAgB;YAChC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,iBAAiB,CAAC;YACzC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC,EACnC,CAAC;YACD,OAAO;QACT,CAAC;QAED,mEAAmE;QACnE,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,IAAI,wBAAiB,CAAC,yBAAyB,CAAC,CAAC;QACzD,CAAC;QAED,yEAAyE;QACzE,4DAA4D;QAC5D,IAAI,OAAO,CAAC,KAAK;YAAE,OAAO;QAE1B,yEAAyE;QACzE,4EAA4E;QAC5E,MAAM,aAAa,GAAG,OAAO,CAAC,YAAY,EAAE,MAAM,EAAE,aAAa,CAAC;QAClE,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;YAChC,MAAM,IAAI,qBAAc,CAAC,yCAAyC,CAAC,CAAC;QACtE,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;QAEjD,yEAAyE;QACzE,uDAAuD;QACvD,IAAI,CAAC,IAAA,qBAAc,EAAC,MAAM,EAAE,aAAa,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC;YACpF,MAAM,IAAI,qBAAc,CAAC,6CAA6C,CAAC,CAAC;QAC1E,CAAC;QACD,IAAI,aAAa,KAAK,UAAU,EAAE,CAAC;YACjC,IAAA,gCAAc,EAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
@@ -1,3 +1,42 @@
1
1
  import { FastifyRequest, FastifyReply, FastifyError } from "fastify";
2
+ import { ErrorCode } from "@stratum-hq/core";
3
+ /** One failed field of a request, in the shape every validation response uses. */
4
+ export interface ValidationIssue {
5
+ path: (string | number)[];
6
+ message: string;
7
+ code: string;
8
+ }
9
+ /**
10
+ * Returns the body of a 400 VALIDATION_ERROR response.
11
+ * The issues go in `error.details.issues`, next to any other details.
12
+ * `error.issues` is a deprecated copy for older clients.
13
+ * It goes away in the next major release.
14
+ */
15
+ export declare function validationErrorBody(message: string, issues: readonly ValidationIssue[], details?: Record<string, unknown>): {
16
+ error: {
17
+ code: ErrorCode;
18
+ message: string;
19
+ details: {
20
+ issues: ValidationIssue[];
21
+ };
22
+ issues: ValidationIssue[];
23
+ };
24
+ };
25
+ /**
26
+ * Returns the body of a 400 VALIDATION_ERROR response from the key and role routes.
27
+ * The top-level `details` is a deprecated copy of the issues for older clients.
28
+ * It goes away in the next major release.
29
+ */
30
+ export declare function legacyValidationErrorBody(message: string, issues: readonly ValidationIssue[]): {
31
+ details: ValidationIssue[];
32
+ error: {
33
+ code: ErrorCode;
34
+ message: string;
35
+ details: {
36
+ issues: ValidationIssue[];
37
+ };
38
+ issues: ValidationIssue[];
39
+ };
40
+ };
2
41
  export declare function errorHandler(error: FastifyError | Error, request: FastifyRequest, reply: FastifyReply): void;
3
42
  //# sourceMappingURL=error-handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../../src/middleware/error-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAKrE,wBAAgB,YAAY,CAC1B,KAAK,EAAE,YAAY,GAAG,KAAK,EAC3B,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,GAClB,IAAI,CA2CN"}
1
+ {"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../../src/middleware/error-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAErE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAG7C,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,SAAS,eAAe,EAAE,EAClC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;;;EAYlC;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,eAAe,EAAE;;;;;;;;;;EAG5F;AAmFD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,YAAY,GAAG,KAAK,EAC3B,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,GAClB,IAAI,CA2CN"}
@@ -1,37 +1,127 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validationErrorBody = validationErrorBody;
4
+ exports.legacyValidationErrorBody = legacyValidationErrorBody;
3
5
  exports.errorHandler = errorHandler;
4
- const zod_1 = require("zod");
5
6
  const core_1 = require("@stratum-hq/core");
6
7
  const config_js_1 = require("../config.js");
8
+ /**
9
+ * Returns the body of a 400 VALIDATION_ERROR response.
10
+ * The issues go in `error.details.issues`, next to any other details.
11
+ * `error.issues` is a deprecated copy for older clients.
12
+ * It goes away in the next major release.
13
+ */
14
+ function validationErrorBody(message, issues, details) {
15
+ // A zod issue has more fields than these three, and they differ per issue code.
16
+ const normalized = issues.map(({ path, message, code }) => ({ path, message, code }));
17
+ return {
18
+ error: {
19
+ code: core_1.ErrorCode.VALIDATION_ERROR,
20
+ message,
21
+ details: { ...details, issues: normalized },
22
+ issues: normalized,
23
+ },
24
+ };
25
+ }
26
+ /**
27
+ * Returns the body of a 400 VALIDATION_ERROR response from the key and role routes.
28
+ * The top-level `details` is a deprecated copy of the issues for older clients.
29
+ * It goes away in the next major release.
30
+ */
31
+ function legacyValidationErrorBody(message, issues) {
32
+ const body = validationErrorBody(message, issues);
33
+ return { ...body, details: body.error.details.issues };
34
+ }
35
+ /**
36
+ * Returns true when the error has the shape of a ZodError.
37
+ * The check is structural because a process can load more than one copy of zod.
38
+ * An instanceof check fails for an error from a copy other than this one.
39
+ */
40
+ function isZodError(error) {
41
+ return (error instanceof Error &&
42
+ error.name === "ZodError" &&
43
+ Array.isArray(error.issues));
44
+ }
45
+ const ERROR_CODES = new Set(Object.values(core_1.ErrorCode));
46
+ /**
47
+ * Returns true when the error has the shape of a StratumError.
48
+ * The check is structural for the same reason as isZodError: a process can load more
49
+ * than one copy of @stratum-hq/core. A known error code tells it apart from a Fastify
50
+ * error, which also has a code and a status code.
51
+ */
52
+ function isStratumError(error) {
53
+ if (!(error instanceof Error))
54
+ return false;
55
+ const { code, statusCode } = error;
56
+ return (typeof code === "string" &&
57
+ ERROR_CODES.has(code) &&
58
+ typeof statusCode === "number" &&
59
+ Number.isInteger(statusCode) &&
60
+ statusCode >= 400 &&
61
+ statusCode <= 599);
62
+ }
63
+ /** Returns true when every item has the three fields of a ValidationIssue. */
64
+ function isIssueList(value) {
65
+ return (Array.isArray(value) &&
66
+ value.every((issue) => typeof issue === "object" &&
67
+ issue !== null &&
68
+ Array.isArray(issue.path) &&
69
+ typeof issue.message === "string" &&
70
+ typeof issue.code === "string"));
71
+ }
72
+ /**
73
+ * The code for an error below 500 that is not a StratumError, by status.
74
+ * The codes match the codes that the routes and rate limiters send themselves.
75
+ */
76
+ const CLIENT_ERROR_CODES = {
77
+ 400: "BAD_REQUEST",
78
+ 401: core_1.ErrorCode.UNAUTHORIZED,
79
+ 403: core_1.ErrorCode.FORBIDDEN,
80
+ 404: "NOT_FOUND",
81
+ 409: "CONFLICT",
82
+ 413: "PAYLOAD_TOO_LARGE",
83
+ 415: "UNSUPPORTED_MEDIA_TYPE",
84
+ 429: "RATE_LIMITED",
85
+ };
86
+ /**
87
+ * Returns the code for an error below 500 that is not a StratumError or a ZodError.
88
+ * Only a Fastify schema validation error is a VALIDATION_ERROR.
89
+ * Any other error gets a neutral code for its status, so a client does not read a
90
+ * conflict or a missing resource as a bad field.
91
+ */
92
+ function clientErrorCode(error) {
93
+ if (error.validation || error.code === "FST_ERR_VALIDATION")
94
+ return core_1.ErrorCode.VALIDATION_ERROR;
95
+ return CLIENT_ERROR_CODES[error.statusCode] ?? "BAD_REQUEST";
96
+ }
7
97
  function errorHandler(error, request, reply) {
8
98
  if (config_js_1.config.nodeEnv === "development") {
9
99
  console.error("[error]", error);
10
100
  }
11
- if (error instanceof core_1.StratumError) {
12
- reply.status(error.statusCode).send(error.toJSON());
13
- return;
14
- }
15
- if (error instanceof zod_1.ZodError) {
16
- reply.status(400).send({
101
+ if (isStratumError(error)) {
102
+ const issues = error.details?.issues;
103
+ if (error.code === core_1.ErrorCode.VALIDATION_ERROR && isIssueList(issues)) {
104
+ reply.status(error.statusCode).send(validationErrorBody(error.message, issues, error.details));
105
+ return;
106
+ }
107
+ reply.status(error.statusCode).send({
17
108
  error: {
18
- code: "VALIDATION_ERROR",
19
- message: "Validation failed",
20
- issues: error.issues.map((issue) => ({
21
- path: issue.path,
22
- message: issue.message,
23
- code: issue.code,
24
- })),
109
+ code: error.code,
110
+ message: error.message,
111
+ ...(error.details ? { details: error.details } : {}),
25
112
  },
26
113
  });
27
114
  return;
28
115
  }
29
- // Fastify validation errors (e.g. schema validation)
116
+ if (isZodError(error)) {
117
+ reply.status(400).send(validationErrorBody("Validation failed", error.issues));
118
+ return;
119
+ }
30
120
  const fastifyError = error;
31
121
  if (fastifyError.statusCode && fastifyError.statusCode < 500) {
32
122
  reply.status(fastifyError.statusCode).send({
33
123
  error: {
34
- code: "VALIDATION_ERROR",
124
+ code: clientErrorCode(fastifyError),
35
125
  message: error.message,
36
126
  },
37
127
  });
@@ -1 +1 @@
1
- {"version":3,"file":"error-handler.js","sourceRoot":"","sources":["../../src/middleware/error-handler.ts"],"names":[],"mappings":";;AAKA,oCA+CC;AAnDD,6BAA+B;AAC/B,2CAAgD;AAChD,4CAAsC;AAEtC,SAAgB,YAAY,CAC1B,KAA2B,EAC3B,OAAuB,EACvB,KAAmB;IAEnB,IAAI,kBAAM,CAAC,OAAO,KAAK,aAAa,EAAE,CAAC;QACrC,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IAClC,CAAC;IAED,IAAI,KAAK,YAAY,mBAAY,EAAE,CAAC;QAClC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;QACpD,OAAO;IACT,CAAC;IAED,IAAI,KAAK,YAAY,cAAQ,EAAE,CAAC;QAC9B,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACrB,KAAK,EAAE;gBACL,IAAI,EAAE,kBAAkB;gBACxB,OAAO,EAAE,mBAAmB;gBAC5B,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;oBACnC,IAAI,EAAE,KAAK,CAAC,IAAI;oBAChB,OAAO,EAAE,KAAK,CAAC,OAAO;oBACtB,IAAI,EAAE,KAAK,CAAC,IAAI;iBACjB,CAAC,CAAC;aACJ;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,qDAAqD;IACrD,MAAM,YAAY,GAAG,KAAqB,CAAC;IAC3C,IAAI,YAAY,CAAC,UAAU,IAAI,YAAY,CAAC,UAAU,GAAG,GAAG,EAAE,CAAC;QAC7D,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;YACzC,KAAK,EAAE;gBACL,IAAI,EAAE,kBAAkB;gBACxB,OAAO,EAAE,KAAK,CAAC,OAAO;aACvB;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QACrB,KAAK,EAAE;YACL,IAAI,EAAE,uBAAuB;YAC7B,OAAO,EAAE,8BAA8B;SACxC;KACF,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"error-handler.js","sourceRoot":"","sources":["../../src/middleware/error-handler.ts"],"names":[],"mappings":"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"}
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { FastifyInstance } from "fastify";
8
8
  /**
9
- * Register the telemetry onRequest / onResponse hooks on a Fastify instance.
9
+ * Register the telemetry onRequest, onResponse, onRequestAbort, and onError hooks on a Fastify instance.
10
10
  *
11
11
  * If @opentelemetry/api is not installed, this is a no-op.
12
12
  */
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../../src/middleware/telemetry.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAe7E;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CA4DjE"}
1
+ {"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../../src/middleware/telemetry.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAe7E;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CA+EjE"}
@@ -19,7 +19,7 @@ catch {
19
19
  }
20
20
  const TRACER_NAME = "@stratum-hq/control-plane";
21
21
  /**
22
- * Register the telemetry onRequest / onResponse hooks on a Fastify instance.
22
+ * Register the telemetry onRequest, onResponse, onRequestAbort, and onError hooks on a Fastify instance.
23
23
  *
24
24
  * If @opentelemetry/api is not installed, this is a no-op.
25
25
  */
@@ -32,19 +32,18 @@ function registerTelemetryHooks(app) {
32
32
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
33
  const inflightSpans = new Map();
34
34
  app.addHook("onRequest", async (request, _reply) => {
35
- const span = tracer.startSpan(`HTTP ${request.method} ${request.routeOptions?.url ?? request.url}`, {
35
+ // A query string can carry cursors or filter values, so spans record the path only.
36
+ const path = request.url.split("?", 1)[0];
37
+ const route = request.routeOptions?.url ?? path;
38
+ const span = tracer.startSpan(`HTTP ${request.method} ${route}`, {
36
39
  kind: SpanKind.SERVER,
37
40
  attributes: {
38
41
  "http.method": request.method,
39
- "http.url": request.url,
40
- "http.route": request.routeOptions?.url ?? request.url,
42
+ "http.url": path,
43
+ "http.route": route,
41
44
  "http.request_id": request.id,
42
45
  },
43
46
  });
44
- // Attach tenant_id from the authenticated key if available
45
- if (request.apiKey?.tenant_id) {
46
- span.setAttribute("stratum.tenant_id", request.apiKey.tenant_id);
47
- }
48
47
  inflightSpans.set(request.id, span);
49
48
  });
50
49
  app.addHook("onResponse", async (request, reply) => {
@@ -52,6 +51,10 @@ function registerTelemetryHooks(app) {
52
51
  if (!span)
53
52
  return;
54
53
  inflightSpans.delete(request.id);
54
+ // Authentication runs as a preHandler, after onRequest, so the tenant is known only here.
55
+ if (request.apiKey?.tenant_id) {
56
+ span.setAttribute("stratum.tenant_id", request.apiKey.tenant_id);
57
+ }
55
58
  span.setAttribute("http.status_code", reply.statusCode);
56
59
  if (reply.statusCode >= 400) {
57
60
  span.setStatus({
@@ -64,6 +67,22 @@ function registerTelemetryHooks(app) {
64
67
  }
65
68
  span.end();
66
69
  });
70
+ // A client that disconnects early can prevent onResponse, so this hook ends the span instead.
71
+ // Each hook removes the span from the map first, so only one of them can end it.
72
+ app.addHook("onRequestAbort", async (request) => {
73
+ const span = inflightSpans.get(request.id);
74
+ if (!span)
75
+ return;
76
+ inflightSpans.delete(request.id);
77
+ if (request.apiKey?.tenant_id) {
78
+ span.setAttribute("stratum.tenant_id", request.apiKey.tenant_id);
79
+ }
80
+ span.setStatus({
81
+ code: SpanStatusCode.ERROR,
82
+ message: "Client closed the connection before the response",
83
+ });
84
+ span.end();
85
+ });
67
86
  // Safety net: if the request errors out before onResponse fires
68
87
  app.addHook("onError", async (request, _reply, error) => {
69
88
  const span = inflightSpans.get(request.id);
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../../src/middleware/telemetry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../../src/middleware/telemetry.ts"],"names":[],"mappings":"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"}
@@ -14,6 +14,12 @@ export type TenantScopeDeclaration = TenantIdExtractor | "global" | "operator";
14
14
  declare module "fastify" {
15
15
  interface FastifyContextConfig {
16
16
  tenantScope?: TenantScopeDeclaration;
17
+ /**
18
+ * Also accept a pending target in the tenant-scope check. A tenant whose
19
+ * storage provisioning failed stays pending. The key that created it must
20
+ * be able to remove it. Archived and suspended targets stay refused.
21
+ */
22
+ tenantScopeIncludesPending?: boolean;
17
23
  }
18
24
  }
19
25
  /**
@@ -26,8 +32,11 @@ declare module "fastify" {
26
32
  *
27
33
  * Exported for routes whose target tenant is not present in the request and
28
34
  * must be resolved first (for example a role's owning tenant, looked up by id).
35
+ *
36
+ * @param includePending - also accept a pending target. An active target is
37
+ * always accepted. Archived and suspended targets are always refused.
29
38
  */
30
- export declare function assertTenantInScope(stratum: Stratum, request: FastifyRequest, tenantId: string | null): Promise<void>;
39
+ export declare function assertTenantInScope(stratum: Stratum, request: FastifyRequest, tenantId: string | null, includePending?: boolean): Promise<void>;
31
40
  /** True when the caller is a global operator API key (no tenant scope). */
32
41
  export declare function isOperator(request: FastifyRequest): boolean;
33
42
  /** Refuse the request unless the caller is a global operator API key. */
@@ -1 +1 @@
1
- {"version":3,"file":"tenant-scope.d.ts","sourceRoot":"","sources":["../../src/middleware/tenant-scope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAExE,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE1C,sFAAsF;AACtF,MAAM,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,cAAc,KAAK,MAAM,GAAG,IAAI,CAAC;AAEvE;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG,iBAAiB,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE/E,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,oBAAoB;QAC5B,WAAW,CAAC,EAAE,sBAAsB,CAAC;KACtC;CACF;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,OAAO,CAAC,IAAI,CAAC,CAgCf;AAED,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAE3D;AAED,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAI5D;AAaD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,eAAe,EAAE,iBAAiB,IAGhC,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CAGjB;AA6BD;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,IAEpD,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CAIjB;AAKD;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,IAEzD,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CAcjB;AAYD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,eAAe,EACpB,WAAW,EAAE,sBAAsB,GAClC,IAAI,CAON;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,IAEtD,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CAejB;AAID,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAE9D;AAED,+CAA+C;AAC/C,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED,0CAA0C;AAC1C,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEnE;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEnE;AAED,0CAA0C;AAC1C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEtE"}
1
+ {"version":3,"file":"tenant-scope.d.ts","sourceRoot":"","sources":["../../src/middleware/tenant-scope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAExE,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE1C,sFAAsF;AACtF,MAAM,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,cAAc,KAAK,MAAM,GAAG,IAAI,CAAC;AAEvE;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG,iBAAiB,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE/E,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,oBAAoB;QAC5B,WAAW,CAAC,EAAE,sBAAsB,CAAC;QACrC;;;;WAIG;QACH,0BAA0B,CAAC,EAAE,OAAO,CAAC;KACtC;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,cAAc,UAAQ,GACrB,OAAO,CAAC,IAAI,CAAC,CAoCf;AAED,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAE3D;AAED,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAI5D;AAcD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,eAAe,EAAE,iBAAiB,IAGhC,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CAGjB;AA6BD;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,IAEpD,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CAIjB;AAKD;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,IAEzD,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CAcjB;AAYD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,eAAe,EACpB,WAAW,EAAE,sBAAsB,GAClC,IAAI,CAON;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,IAEtD,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CAkBjB;AAID,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAE9D;AAED,+CAA+C;AAC/C,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED,0CAA0C;AAC1C,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEnE;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEnE;AAED,0CAA0C;AAC1C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEtE"}