@stratum-hq/control-plane 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +26 -0
- package/dist/config.js.map +1 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +6 -1
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/authorize.d.ts.map +1 -1
- package/dist/middleware/authorize.js +16 -6
- package/dist/middleware/authorize.js.map +1 -1
- package/dist/middleware/error-handler.d.ts +39 -0
- package/dist/middleware/error-handler.d.ts.map +1 -1
- package/dist/middleware/error-handler.js +80 -14
- package/dist/middleware/error-handler.js.map +1 -1
- package/dist/middleware/per-key-rate-limit.d.ts +11 -1
- package/dist/middleware/per-key-rate-limit.d.ts.map +1 -1
- package/dist/middleware/per-key-rate-limit.js +14 -4
- package/dist/middleware/per-key-rate-limit.js.map +1 -1
- package/dist/middleware/rate-limit-redis.d.ts +2 -1
- package/dist/middleware/rate-limit-redis.d.ts.map +1 -1
- package/dist/middleware/rate-limit-redis.js +7 -4
- package/dist/middleware/rate-limit-redis.js.map +1 -1
- package/dist/middleware/telemetry.d.ts +1 -1
- package/dist/middleware/telemetry.d.ts.map +1 -1
- package/dist/middleware/telemetry.js +27 -8
- package/dist/middleware/telemetry.js.map +1 -1
- package/dist/middleware/tenant-scope.d.ts +19 -4
- package/dist/middleware/tenant-scope.d.ts.map +1 -1
- package/dist/middleware/tenant-scope.js +39 -9
- package/dist/middleware/tenant-scope.js.map +1 -1
- package/dist/redis.d.ts +3 -1
- package/dist/redis.d.ts.map +1 -1
- package/dist/redis.js +5 -6
- package/dist/redis.js.map +1 -1
- package/dist/routes/abac.d.ts.map +1 -1
- package/dist/routes/abac.js +3 -2
- package/dist/routes/abac.js.map +1 -1
- package/dist/routes/api-keys.d.ts.map +1 -1
- package/dist/routes/api-keys.js +20 -5
- package/dist/routes/api-keys.js.map +1 -1
- package/dist/routes/maintenance.js +3 -3
- package/dist/routes/maintenance.js.map +1 -1
- package/dist/routes/regions.d.ts.map +1 -1
- package/dist/routes/regions.js +5 -4
- package/dist/routes/regions.js.map +1 -1
- package/dist/routes/roles.d.ts.map +1 -1
- package/dist/routes/roles.js +18 -5
- package/dist/routes/roles.js.map +1 -1
- package/dist/routes/tenants.d.ts.map +1 -1
- package/dist/routes/tenants.js +97 -13
- package/dist/routes/tenants.js.map +1 -1
- package/dist/services/isolation-service.d.ts +6 -5
- package/dist/services/isolation-service.d.ts.map +1 -1
- package/dist/services/isolation-service.js +18 -12
- package/dist/services/isolation-service.js.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -26,11 +26,13 @@ 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) |
|
|
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 |
|
|
31
|
+
| `JWT_ISSUER` | — | Optional; when set, Bearer tokens must carry this `iss` claim |
|
|
30
32
|
| `ALLOWED_ORIGINS` | `localhost:3000,3300` | Comma-separated CORS allowlist |
|
|
31
33
|
| `RATE_LIMIT_MAX` | `100` | Requests per window |
|
|
32
34
|
| `RATE_LIMIT_WINDOW` | `1 minute` | Rate-limit window |
|
|
33
|
-
| `REDIS_URL` | — | Optional; enables distributed rate limiting |
|
|
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 |
|
|
34
36
|
| `NODE_ENV` | `development` | Environment |
|
|
35
37
|
|
|
36
38
|
Security middleware (`@fastify/helmet`, CORS, rate limiting) is enabled by default.
|
package/dist/config.d.ts
CHANGED
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAyCA,eAAO,MAAM,MAAM;;;;;;;;;;;CAalB,CAAC"}
|
package/dist/config.js
CHANGED
|
@@ -15,11 +15,37 @@ if (!jwtSecretEnv) {
|
|
|
15
15
|
console.warn("[stratum] JWT_SECRET not set — using dev fallback. Set JWT_SECRET before deploying to production.");
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
// Placeholder secrets published in this repository's examples and scaffolds.
|
|
19
|
+
const PLACEHOLDER_JWT_SECRETS = new Set([
|
|
20
|
+
"change-me-in-production",
|
|
21
|
+
"stratum-demo-secret-do-not-use-in-production",
|
|
22
|
+
"your-jwt-secret-change-in-production",
|
|
23
|
+
]);
|
|
24
|
+
const MIN_JWT_SECRET_BYTES = 32;
|
|
25
|
+
if (jwtSecretEnv && nodeEnv === "production") {
|
|
26
|
+
if (PLACEHOLDER_JWT_SECRETS.has(jwtSecretEnv)) {
|
|
27
|
+
throw new Error("FATAL: JWT_SECRET is a published placeholder value. Set a random secret. Refusing to start.");
|
|
28
|
+
}
|
|
29
|
+
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.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Optional JWT audience / issuer binding. When set, a Bearer token is accepted
|
|
34
|
+
// only if its `aud` (and `iss`) claim matches, so tokens minted for another
|
|
35
|
+
// application that shares JWT_SECRET are refused. Unset keeps the previous
|
|
36
|
+
// behavior (no aud / iss check) for deployments whose tokens do not carry them.
|
|
37
|
+
const jwtAudienceEnv = process.env.JWT_AUDIENCE || undefined;
|
|
38
|
+
const jwtIssuerEnv = process.env.JWT_ISSUER || undefined;
|
|
39
|
+
if (!jwtAudienceEnv && (nodeEnv === "production" || nodeEnv === "staging")) {
|
|
40
|
+
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
|
+
}
|
|
18
42
|
exports.config = {
|
|
19
43
|
port: parseInt(process.env.PORT || "3001"),
|
|
20
44
|
databaseUrl: process.env.DATABASE_URL || "postgres://stratum:stratum_dev@localhost:5432/stratum",
|
|
21
45
|
nodeEnv,
|
|
22
46
|
jwtSecret: jwtSecretEnv || node_crypto_1.default.randomBytes(32).toString("hex"),
|
|
47
|
+
jwtAudience: jwtAudienceEnv,
|
|
48
|
+
jwtIssuer: jwtIssuerEnv,
|
|
23
49
|
allowedOrigins: process.env.ALLOWED_ORIGINS
|
|
24
50
|
? process.env.ALLOWED_ORIGINS.split(",").map((o) => o.trim())
|
|
25
51
|
: ["http://localhost:3000", "http://localhost:3300"],
|
package/dist/config.js.map
CHANGED
|
@@ -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;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,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;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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAIvD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE1C,UAAU,YAAY;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,IAAI,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,MAAM,CAAC,EAAE,YAAY,CAAC;QACtB,UAAU,CAAC,EAAE,SAAS,GAAG,KAAK,CAAC;KAChC;CACF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,IAEjD,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAIvD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE1C,UAAU,YAAY;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,IAAI,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,MAAM,CAAC,EAAE,YAAY,CAAC;QACtB,UAAU,CAAC,EAAE,SAAS,GAAG,KAAK,CAAC;KAChC;CACF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,IAEjD,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CAmFjB"}
|
package/dist/middleware/auth.js
CHANGED
|
@@ -40,7 +40,12 @@ function createAuthMiddleware(stratum) {
|
|
|
40
40
|
if (authHeader && typeof authHeader === "string" && authHeader.startsWith("Bearer ")) {
|
|
41
41
|
const token = authHeader.slice(7);
|
|
42
42
|
try {
|
|
43
|
-
const payload = jsonwebtoken_1.default.verify(token, config_js_1.config.jwtSecret, {
|
|
43
|
+
const payload = jsonwebtoken_1.default.verify(token, config_js_1.config.jwtSecret, {
|
|
44
|
+
algorithms: ["HS256"],
|
|
45
|
+
// Enforced only when configured (JWT_AUDIENCE / JWT_ISSUER).
|
|
46
|
+
...(config_js_1.config.jwtAudience ? { audience: config_js_1.config.jwtAudience } : {}),
|
|
47
|
+
...(config_js_1.config.jwtIssuer ? { issuer: config_js_1.config.jwtIssuer } : {}),
|
|
48
|
+
});
|
|
44
49
|
// JWT must contain a tenant_id claim — JWTs without one are not allowed
|
|
45
50
|
// global access (only API keys may have null tenant_id).
|
|
46
51
|
if (!payload.tenant_id || typeof payload.tenant_id !== "string") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":";;;;;AAwBA,
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -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;
|
|
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"}
|
|
@@ -23,16 +23,22 @@ const ADMIN_ROUTES = [
|
|
|
23
23
|
/^\/api\/v1\/tenants\/[^/]+\/migrate-region$/,
|
|
24
24
|
/^\/api\/v1\/tenants\/[^/]+\/context$/,
|
|
25
25
|
];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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) {
|
|
30
32
|
for (const pattern of ADMIN_ROUTES) {
|
|
31
33
|
if (pattern.test(path)) {
|
|
32
34
|
return "admin";
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
|
-
|
|
37
|
+
const scope = methodToScope(method);
|
|
38
|
+
if (scope === "write" && ADMIN_MUTATION_ROUTES.some((pattern) => pattern.test(path))) {
|
|
39
|
+
return "admin";
|
|
40
|
+
}
|
|
41
|
+
return scope;
|
|
36
42
|
}
|
|
37
43
|
function createAuthorizeMiddleware() {
|
|
38
44
|
return async function authorizeMiddleware(request, _reply) {
|
|
@@ -46,7 +52,11 @@ function createAuthorizeMiddleware() {
|
|
|
46
52
|
if (!request.apiKey) {
|
|
47
53
|
throw new core_1.UnauthorizedError("Authentication required");
|
|
48
54
|
}
|
|
49
|
-
|
|
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);
|
|
50
60
|
const scopes = request.apiKey.scopes ?? ["read"];
|
|
51
61
|
// Hierarchical scopes: admin implies write implies read. A granted scope
|
|
52
62
|
// satisfies any required scope of equal-or-lower rank.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../src/middleware/authorize.ts"],"names":[],"mappings":";;
|
|
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,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;
|
|
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;AAyDD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,YAAY,GAAG,KAAK,EAC3B,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,GAClB,IAAI,CA4CN"}
|
|
@@ -1,31 +1,97 @@
|
|
|
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
|
+
}
|
|
7
72
|
function errorHandler(error, request, reply) {
|
|
8
73
|
if (config_js_1.config.nodeEnv === "development") {
|
|
9
74
|
console.error("[error]", error);
|
|
10
75
|
}
|
|
11
|
-
if (error
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
76
|
+
if (isStratumError(error)) {
|
|
77
|
+
const issues = error.details?.issues;
|
|
78
|
+
if (error.code === core_1.ErrorCode.VALIDATION_ERROR && isIssueList(issues)) {
|
|
79
|
+
reply.status(error.statusCode).send(validationErrorBody(error.message, issues, error.details));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
reply.status(error.statusCode).send({
|
|
17
83
|
error: {
|
|
18
|
-
code:
|
|
19
|
-
message:
|
|
20
|
-
|
|
21
|
-
path: issue.path,
|
|
22
|
-
message: issue.message,
|
|
23
|
-
code: issue.code,
|
|
24
|
-
})),
|
|
84
|
+
code: error.code,
|
|
85
|
+
message: error.message,
|
|
86
|
+
...(error.details ? { details: error.details } : {}),
|
|
25
87
|
},
|
|
26
88
|
});
|
|
27
89
|
return;
|
|
28
90
|
}
|
|
91
|
+
if (isZodError(error)) {
|
|
92
|
+
reply.status(400).send(validationErrorBody("Validation failed", error.issues));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
29
95
|
// Fastify validation errors (e.g. schema validation)
|
|
30
96
|
const fastifyError = error;
|
|
31
97
|
if (fastifyError.statusCode && fastifyError.statusCode < 500) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-handler.js","sourceRoot":"","sources":["../../src/middleware/error-handler.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"error-handler.js","sourceRoot":"","sources":["../../src/middleware/error-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
import { FastifyRequest, FastifyReply } from "fastify";
|
|
2
|
-
|
|
2
|
+
/** Parse a duration string like "1 minute", "30 seconds", "1 hour" into milliseconds. */
|
|
3
|
+
export declare function parseWindow(window: string): number;
|
|
4
|
+
/**
|
|
5
|
+
* The bucket a request is counted in. API keys count per key id. A JWT `sub`
|
|
6
|
+
* is only unique within its tenant, so JWT callers count per tenant and subject.
|
|
7
|
+
*/
|
|
8
|
+
export declare function rateLimitBucket(request: FastifyRequest): string;
|
|
9
|
+
export declare function createPerKeyRateLimitMiddleware(defaults?: {
|
|
10
|
+
maxRequests: number;
|
|
11
|
+
windowMs: number;
|
|
12
|
+
}): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
3
13
|
//# sourceMappingURL=per-key-rate-limit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"per-key-rate-limit.d.ts","sourceRoot":"","sources":["../../src/middleware/per-key-rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"per-key-rate-limit.d.ts","sourceRoot":"","sources":["../../src/middleware/per-key-rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAuBvD,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAWlD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAG/D;AAED,wBAAgB,+BAA+B,CAAC,QAAQ,CAAC,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,IAKhG,SAAS,cAAc,EACvB,OAAO,YAAY,KAClB,OAAO,CAAC,IAAI,CAAC,CA2CjB"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseWindow = parseWindow;
|
|
4
|
+
exports.rateLimitBucket = rateLimitBucket;
|
|
3
5
|
exports.createPerKeyRateLimitMiddleware = createPerKeyRateLimitMiddleware;
|
|
4
6
|
const config_js_1 = require("../config.js");
|
|
5
7
|
const keyLimits = new Map();
|
|
@@ -17,14 +19,22 @@ function parseWindow(window) {
|
|
|
17
19
|
default: return 60_000;
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* The bucket a request is counted in. API keys count per key id. A JWT `sub`
|
|
24
|
+
* is only unique within its tenant, so JWT callers count per tenant and subject.
|
|
25
|
+
*/
|
|
26
|
+
function rateLimitBucket(request) {
|
|
27
|
+
const apiKey = request.apiKey;
|
|
28
|
+
return request.authMethod === "jwt" ? `jwt:${apiKey.tenant_id}:${apiKey.id}` : apiKey.id;
|
|
29
|
+
}
|
|
30
|
+
function createPerKeyRateLimitMiddleware(defaults) {
|
|
31
|
+
const globalMax = defaults?.maxRequests ?? config_js_1.config.rateLimitMax;
|
|
32
|
+
const globalWindowMs = defaults?.windowMs ?? parseWindow(config_js_1.config.rateLimitWindow);
|
|
23
33
|
return async function perKeyRateLimitMiddleware(request, reply) {
|
|
24
34
|
// Skip for unauthenticated requests (health endpoint)
|
|
25
35
|
if (!request.apiKey)
|
|
26
36
|
return;
|
|
27
|
-
const keyId = request
|
|
37
|
+
const keyId = rateLimitBucket(request);
|
|
28
38
|
const max = request.apiKey.rate_limit_max ?? globalMax;
|
|
29
39
|
const windowMs = request.apiKey.rate_limit_window
|
|
30
40
|
? parseWindow(request.apiKey.rate_limit_window)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"per-key-rate-limit.js","sourceRoot":"","sources":["../../src/middleware/per-key-rate-limit.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"per-key-rate-limit.js","sourceRoot":"","sources":["../../src/middleware/per-key-rate-limit.ts"],"names":[],"mappings":";;AAwBA,kCAWC;AAMD,0CAGC;AAED,0EAkDC;AA/FD,4CAAsC;AAoBtC,MAAM,SAAS,GAAG,IAAI,GAAG,EAA0B,CAAC;AAEpD,yFAAyF;AACzF,SAAgB,WAAW,CAAC,MAAc;IACxC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;IACpE,IAAI,CAAC,KAAK;QAAE,OAAO,MAAM,CAAC,CAAC,mBAAmB;IAC9C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACrC,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;QAC/B,KAAK,QAAQ,CAAC,CAAC,OAAO,KAAK,GAAG,KAAK,CAAC;QACpC,KAAK,QAAQ,CAAC,CAAC,OAAO,KAAK,GAAG,MAAM,CAAC;QACrC,KAAK,MAAM,CAAC,CAAG,OAAO,KAAK,GAAG,SAAS,CAAC;QACxC,KAAK,KAAK,CAAC,CAAI,OAAO,KAAK,GAAG,UAAU,CAAC;QACzC,OAAO,CAAC,CAAO,OAAO,MAAM,CAAC;IAC/B,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAgB,eAAe,CAAC,OAAuB;IACrD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAO,CAAC;IAC/B,OAAO,OAAO,CAAC,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC,OAAO,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;AAC3F,CAAC;AAED,SAAgB,+BAA+B,CAAC,QAAoD;IAClG,MAAM,SAAS,GAAG,QAAQ,EAAE,WAAW,IAAI,kBAAM,CAAC,YAAY,CAAC;IAC/D,MAAM,cAAc,GAAG,QAAQ,EAAE,QAAQ,IAAI,WAAW,CAAC,kBAAM,CAAC,eAAe,CAAC,CAAC;IAEjF,OAAO,KAAK,UAAU,yBAAyB,CAC7C,OAAuB,EACvB,KAAmB;QAEnB,sDAAsD;QACtD,IAAI,CAAC,OAAO,CAAC,MAAM;YAAE,OAAO;QAE5B,MAAM,KAAK,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;QACvC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,cAAc,IAAI,SAAS,CAAC;QACvD,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,iBAAiB;YAC/C,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC;YAC/C,CAAC,CAAC,cAAc,CAAC;QAEnB,IAAI,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACjC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,GAAG,KAAK,GAAG,IAAI,KAAK,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC/D,KAAK,GAAG,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;YAC1C,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC9B,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,GAAG,GAAG,QAAQ,CAAC;QAE9B,2BAA2B;QAC3B,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC;QAE9D,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;YACnC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,QAAQ,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;YAC5E,KAAK,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;YAChD,KAAK,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YAC/C,KAAK,CAAC,MAAM,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;YAC3C,KAAK,CAAC,MAAM,CAAC,mBAAmB,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;YAC1F,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACrB,KAAK,EAAE;oBACL,IAAI,EAAE,cAAc;oBACpB,OAAO,EAAE,qCAAqC,UAAU,WAAW;iBACpE;aACF,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAE3B,0CAA0C;QAC1C,KAAK,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/C,KAAK,CAAC,MAAM,CAAC,uBAAuB,EAAE,MAAM,CAAC,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/E,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -10,7 +10,8 @@ export interface RedisRateLimitConfig {
|
|
|
10
10
|
* Create a Redis-backed per-key rate limiter.
|
|
11
11
|
*
|
|
12
12
|
* Returns a Fastify preHandler hook. If Redis is unavailable or slow (>500ms)
|
|
13
|
-
* the request is
|
|
13
|
+
* the request is counted by the in-memory per-key limiter instead, so per-key
|
|
14
|
+
* limits stay enforced (per process) until Redis is reachable again.
|
|
14
15
|
*/
|
|
15
16
|
export declare function createRedisRateLimiter(redis: Redis, defaults: RedisRateLimitConfig): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
16
17
|
//# sourceMappingURL=rate-limit-redis.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit-redis.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit-redis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvD,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"rate-limit-redis.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit-redis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvD,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AAGjC,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAwBD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,oBAAoB,IAgB5B,SAAS,cAAc,EACvB,OAAO,YAAY,KAClB,OAAO,CAAC,IAAI,CAAC,CA+CjB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createRedisRateLimiter = createRedisRateLimiter;
|
|
4
|
+
const per_key_rate_limit_js_1 = require("./per-key-rate-limit.js");
|
|
4
5
|
/**
|
|
5
6
|
* Lua script implementing a sliding-window counter.
|
|
6
7
|
*
|
|
@@ -26,9 +27,11 @@ return current
|
|
|
26
27
|
* Create a Redis-backed per-key rate limiter.
|
|
27
28
|
*
|
|
28
29
|
* Returns a Fastify preHandler hook. If Redis is unavailable or slow (>500ms)
|
|
29
|
-
* the request is
|
|
30
|
+
* the request is counted by the in-memory per-key limiter instead, so per-key
|
|
31
|
+
* limits stay enforced (per process) until Redis is reachable again.
|
|
30
32
|
*/
|
|
31
33
|
function createRedisRateLimiter(redis, defaults) {
|
|
34
|
+
const fallback = (0, per_key_rate_limit_js_1.createPerKeyRateLimitMiddleware)(defaults);
|
|
32
35
|
// Pre-load the Lua script so subsequent calls use EVALSHA
|
|
33
36
|
let scriptSha = null;
|
|
34
37
|
redis
|
|
@@ -43,7 +46,7 @@ function createRedisRateLimiter(redis, defaults) {
|
|
|
43
46
|
// Skip for unauthenticated requests (health, docs)
|
|
44
47
|
if (!request.apiKey)
|
|
45
48
|
return;
|
|
46
|
-
const keyId = request
|
|
49
|
+
const keyId = (0, per_key_rate_limit_js_1.rateLimitBucket)(request);
|
|
47
50
|
const max = request.apiKey.rate_limit_max ?? defaults.maxRequests;
|
|
48
51
|
const windowMs = request.apiKey.rate_limit_window
|
|
49
52
|
? parseWindowStr(request.apiKey.rate_limit_window)
|
|
@@ -59,8 +62,8 @@ function createRedisRateLimiter(redis, defaults) {
|
|
|
59
62
|
currentCount = await withTimeout(evalScript(redis, scriptSha, redisKey, windowTtlSec), 500);
|
|
60
63
|
}
|
|
61
64
|
catch {
|
|
62
|
-
// Redis unavailable or slow
|
|
63
|
-
return;
|
|
65
|
+
// Redis unavailable or slow: count this request in memory instead
|
|
66
|
+
return fallback(request, reply);
|
|
64
67
|
}
|
|
65
68
|
// Set rate-limit headers on every response
|
|
66
69
|
const remaining = Math.max(0, max - currentCount);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit-redis.js","sourceRoot":"","sources":["../../src/middleware/rate-limit-redis.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"rate-limit-redis.js","sourceRoot":"","sources":["../../src/middleware/rate-limit-redis.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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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":
|
|
40
|
-
"http.route":
|
|
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":";AAAA;;;;;GAKG;;AAsBH,
|
|
1
|
+
{"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../../src/middleware/telemetry.ts"],"names":[],"mappings":"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"}
|