@stratum-hq/control-plane 0.1.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/dist/__tests__/api-keys.test.d.ts +2 -0
- package/dist/__tests__/api-keys.test.d.ts.map +1 -0
- package/dist/__tests__/api-keys.test.js +240 -0
- package/dist/__tests__/api-keys.test.js.map +1 -0
- package/dist/__tests__/auth.test.d.ts +2 -0
- package/dist/__tests__/auth.test.d.ts.map +1 -0
- package/dist/__tests__/auth.test.js +227 -0
- package/dist/__tests__/auth.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +201 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/tenants.test.d.ts +2 -0
- package/dist/__tests__/tenants.test.d.ts.map +1 -0
- package/dist/__tests__/tenants.test.js +214 -0
- package/dist/__tests__/tenants.test.js.map +1 -0
- package/dist/__tests__/test-helpers.d.ts +71 -0
- package/dist/__tests__/test-helpers.d.ts.map +1 -0
- package/dist/__tests__/test-helpers.js +172 -0
- package/dist/__tests__/test-helpers.js.map +1 -0
- package/dist/app.d.ts +3 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +114 -0
- package/dist/app.js.map +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +26 -0
- package/dist/config.js.map +1 -0
- package/dist/db/connection.d.ts +4 -0
- package/dist/db/connection.d.ts.map +1 -0
- package/dist/db/connection.js +30 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/migrate.d.ts +3 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +56 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/regional-pool-router.d.ts +9 -0
- package/dist/db/regional-pool-router.d.ts.map +1 -0
- package/dist/db/regional-pool-router.js +29 -0
- package/dist/db/regional-pool-router.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +21 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +70 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/authorize.d.ts +3 -0
- package/dist/middleware/authorize.d.ts.map +1 -0
- package/dist/middleware/authorize.js +54 -0
- package/dist/middleware/authorize.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +3 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +48 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/per-key-rate-limit.d.ts +3 -0
- package/dist/middleware/per-key-rate-limit.d.ts.map +1 -0
- package/dist/middleware/per-key-rate-limit.js +62 -0
- package/dist/middleware/per-key-rate-limit.js.map +1 -0
- package/dist/middleware/rate-limit-redis.d.ts +16 -0
- package/dist/middleware/rate-limit-redis.d.ts.map +1 -0
- package/dist/middleware/rate-limit-redis.js +118 -0
- package/dist/middleware/rate-limit-redis.js.map +1 -0
- package/dist/middleware/telemetry.d.ts +14 -0
- package/dist/middleware/telemetry.d.ts.map +1 -0
- package/dist/middleware/telemetry.js +81 -0
- package/dist/middleware/telemetry.js.map +1 -0
- package/dist/middleware/tenant-scope.d.ts +20 -0
- package/dist/middleware/tenant-scope.d.ts.map +1 -0
- package/dist/middleware/tenant-scope.js +60 -0
- package/dist/middleware/tenant-scope.js.map +1 -0
- package/dist/openapi.d.ts +3 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +44 -0
- package/dist/openapi.js.map +1 -0
- package/dist/redis.d.ts +16 -0
- package/dist/redis.d.ts.map +1 -0
- package/dist/redis.js +71 -0
- package/dist/redis.js.map +1 -0
- package/dist/routes/api-keys.d.ts +4 -0
- package/dist/routes/api-keys.d.ts.map +1 -0
- package/dist/routes/api-keys.js +78 -0
- package/dist/routes/api-keys.js.map +1 -0
- package/dist/routes/audit-logs.d.ts +6 -0
- package/dist/routes/audit-logs.d.ts.map +1 -0
- package/dist/routes/audit-logs.js +43 -0
- package/dist/routes/audit-logs.js.map +1 -0
- package/dist/routes/config-diff.d.ts +4 -0
- package/dist/routes/config-diff.d.ts.map +1 -0
- package/dist/routes/config-diff.js +33 -0
- package/dist/routes/config-diff.js.map +1 -0
- package/dist/routes/config.d.ts +4 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +54 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/consent.d.ts +4 -0
- package/dist/routes/consent.d.ts.map +1 -0
- package/dist/routes/consent.js +45 -0
- package/dist/routes/consent.js.map +1 -0
- package/dist/routes/health.d.ts +9 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +32 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/maintenance.d.ts +4 -0
- package/dist/routes/maintenance.d.ts.map +1 -0
- package/dist/routes/maintenance.js +35 -0
- package/dist/routes/maintenance.js.map +1 -0
- package/dist/routes/permissions.d.ts +4 -0
- package/dist/routes/permissions.d.ts.map +1 -0
- package/dist/routes/permissions.js +36 -0
- package/dist/routes/permissions.js.map +1 -0
- package/dist/routes/regions.d.ts +4 -0
- package/dist/routes/regions.d.ts.map +1 -0
- package/dist/routes/regions.js +38 -0
- package/dist/routes/regions.js.map +1 -0
- package/dist/routes/roles.d.ts +4 -0
- package/dist/routes/roles.d.ts.map +1 -0
- package/dist/routes/roles.js +95 -0
- package/dist/routes/roles.js.map +1 -0
- package/dist/routes/tenants.d.ts +4 -0
- package/dist/routes/tenants.d.ts.map +1 -0
- package/dist/routes/tenants.js +112 -0
- package/dist/routes/tenants.js.map +1 -0
- package/dist/routes/webhooks.d.ts +4 -0
- package/dist/routes/webhooks.d.ts.map +1 -0
- package/dist/routes/webhooks.js +116 -0
- package/dist/routes/webhooks.js.map +1 -0
- package/dist/services/isolation-service.d.ts +22 -0
- package/dist/services/isolation-service.d.ts.map +1 -0
- package/dist/services/isolation-service.js +94 -0
- package/dist/services/isolation-service.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createAuthMiddleware = void 0;
|
|
7
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
8
|
+
const config_js_1 = require("../config.js");
|
|
9
|
+
const core_1 = require("@stratum-hq/core");
|
|
10
|
+
function createAuthMiddleware(stratum) {
|
|
11
|
+
return async function authMiddleware(request, reply) {
|
|
12
|
+
// Skip auth for health and documentation endpoints
|
|
13
|
+
if (request.url === "/api/v1/health" ||
|
|
14
|
+
request.url.startsWith("/api/v1/health?") ||
|
|
15
|
+
request.url.startsWith("/api/docs")) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
// Try API key first
|
|
19
|
+
const apiKeyHeader = request.headers["x-api-key"];
|
|
20
|
+
if (apiKeyHeader && typeof apiKeyHeader === "string") {
|
|
21
|
+
const result = await stratum.validateApiKey(apiKeyHeader);
|
|
22
|
+
if (!result) {
|
|
23
|
+
throw new core_1.UnauthorizedError("Invalid or revoked API key");
|
|
24
|
+
}
|
|
25
|
+
request.apiKey = {
|
|
26
|
+
id: result.key_id,
|
|
27
|
+
tenant_id: result.tenant_id,
|
|
28
|
+
key_hash: "",
|
|
29
|
+
name: "",
|
|
30
|
+
created_at: new Date(),
|
|
31
|
+
scopes: result.scopes,
|
|
32
|
+
rate_limit_max: result.rate_limit_max,
|
|
33
|
+
rate_limit_window: result.rate_limit_window,
|
|
34
|
+
};
|
|
35
|
+
request.authMethod = "api_key";
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Try JWT Bearer token
|
|
39
|
+
const authHeader = request.headers["authorization"];
|
|
40
|
+
if (authHeader && typeof authHeader === "string" && authHeader.startsWith("Bearer ")) {
|
|
41
|
+
const token = authHeader.slice(7);
|
|
42
|
+
try {
|
|
43
|
+
const payload = jsonwebtoken_1.default.verify(token, config_js_1.config.jwtSecret, { algorithms: ["HS256"] });
|
|
44
|
+
// Store a synthetic api key record from JWT claims
|
|
45
|
+
// Extract scopes from JWT claims; default to full access for backward compat
|
|
46
|
+
const jwtScopes = Array.isArray(payload.scopes)
|
|
47
|
+
? payload.scopes.filter((s) => typeof s === "string" && ["read", "write", "admin"].includes(s))
|
|
48
|
+
: ["read"];
|
|
49
|
+
request.apiKey = {
|
|
50
|
+
id: payload.sub ?? "",
|
|
51
|
+
tenant_id: payload.tenant_id ?? null,
|
|
52
|
+
key_hash: "",
|
|
53
|
+
name: payload.name ?? "jwt",
|
|
54
|
+
created_at: new Date(),
|
|
55
|
+
scopes: jwtScopes,
|
|
56
|
+
rate_limit_max: null,
|
|
57
|
+
rate_limit_window: null,
|
|
58
|
+
};
|
|
59
|
+
request.authMethod = "jwt";
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new core_1.UnauthorizedError("Invalid or expired token");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw new core_1.UnauthorizedError("Authentication required. Provide X-API-Key or Authorization: Bearer header");
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
exports.createAuthMiddleware = createAuthMiddleware;
|
|
70
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":";;;;;;AACA,gEAA+B;AAC/B,4CAAsC;AACtC,2CAAqD;AAqBrD,SAAgB,oBAAoB,CAAC,OAAgB;IACnD,OAAO,KAAK,UAAU,cAAc,CAClC,OAAuB,EACvB,KAAmB;QAEnB,mDAAmD;QACnD,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,oBAAoB;QACpB,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QAClD,IAAI,YAAY,IAAI,OAAO,YAAY,KAAK,QAAQ,EAAE,CAAC;YACrD,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;YAC1D,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,wBAAiB,CAAC,4BAA4B,CAAC,CAAC;YAC5D,CAAC;YACD,OAAO,CAAC,MAAM,GAAG;gBACf,EAAE,EAAE,MAAM,CAAC,MAAM;gBACjB,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,QAAQ,EAAE,EAAE;gBACZ,IAAI,EAAE,EAAE;gBACR,UAAU,EAAE,IAAI,IAAI,EAAE;gBACtB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,cAAc,EAAE,MAAM,CAAC,cAAc;gBACrC,iBAAiB,EAAE,MAAM,CAAC,iBAAiB;aAC5C,CAAC;YACF,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;YAC/B,OAAO;QACT,CAAC;QAED,uBAAuB;QACvB,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QACpD,IAAI,UAAU,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACrF,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAClC,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,kBAAM,CAAC,SAAS,EAAE,EAAE,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,CAAmB,CAAC;gBACjG,mDAAmD;gBACnD,6EAA6E;gBAC7E,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC;oBAC7C,CAAC,CAAE,OAAO,CAAC,MAAoB,CAAC,MAAM,CAClC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CACpF;oBACH,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;gBACb,OAAO,CAAC,MAAM,GAAG;oBACf,EAAE,EAAE,OAAO,CAAC,GAAG,IAAI,EAAE;oBACrB,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI;oBACpC,QAAQ,EAAE,EAAE;oBACZ,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,KAAK;oBAC3B,UAAU,EAAE,IAAI,IAAI,EAAE;oBACtB,MAAM,EAAE,SAAS;oBACjB,cAAc,EAAE,IAAI;oBACpB,iBAAiB,EAAE,IAAI;iBACxB,CAAC;gBACF,OAAO,CAAC,UAAU,GAAG,KAAK,CAAC;gBAC3B,OAAO;YACT,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,IAAI,wBAAiB,CAAC,0BAA0B,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;QAED,MAAM,IAAI,wBAAiB,CAAC,4EAA4E,CAAC,CAAC;IAC5G,CAAC,CAAC;AACJ,CAAC;AAnED,oDAmEC"}
|
|
@@ -0,0 +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;AAqCvD,wBAAgB,yBAAyB,cAE5B,cAAc,SAChB,YAAY,KAClB,QAAQ,IAAI,CAAC,CAsBjB"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAuthorizeMiddleware = void 0;
|
|
4
|
+
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
|
+
}
|
|
14
|
+
}
|
|
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
|
+
function getRequiredScope(method, url) {
|
|
27
|
+
for (const pattern of ADMIN_ROUTES) {
|
|
28
|
+
if (pattern.test(url)) {
|
|
29
|
+
return "admin";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return methodToScope(method);
|
|
33
|
+
}
|
|
34
|
+
function createAuthorizeMiddleware() {
|
|
35
|
+
return async function authorizeMiddleware(request, reply) {
|
|
36
|
+
// Skip for health and documentation endpoints
|
|
37
|
+
if (request.url === "/api/v1/health" ||
|
|
38
|
+
request.url.startsWith("/api/v1/health?") ||
|
|
39
|
+
request.url.startsWith("/api/docs")) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// If no apiKey, auth middleware should have rejected — fail closed
|
|
43
|
+
if (!request.apiKey) {
|
|
44
|
+
throw new core_1.UnauthorizedError("Authentication required");
|
|
45
|
+
}
|
|
46
|
+
const requiredScope = getRequiredScope(request.method, request.url);
|
|
47
|
+
const scopes = request.apiKey.scopes ?? ["read"];
|
|
48
|
+
if (!scopes.includes(requiredScope)) {
|
|
49
|
+
throw new core_1.ForbiddenError("Insufficient permissions for this operation");
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
exports.createAuthorizeMiddleware = createAuthorizeMiddleware;
|
|
54
|
+
//# sourceMappingURL=authorize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../src/middleware/authorize.ts"],"names":[],"mappings":";;;AACA,2CAAqE;AAIrE,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,SAAS,gBAAgB,CAAC,MAAc,EAAE,GAAW;IACnD,KAAK,MAAM,OAAO,IAAI,YAAY,EAAE,CAAC;QACnC,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,OAAO,OAAO,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED,SAAgB,yBAAyB;IACvC,OAAO,KAAK,UAAU,mBAAmB,CACvC,OAAuB,EACvB,KAAmB;QAEnB,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,MAAM,aAAa,GAAG,gBAAgB,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QACpE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;QAEjD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,qBAAc,CAAC,6CAA6C,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AA1BD,8DA0BC"}
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.errorHandler = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const core_1 = require("@stratum-hq/core");
|
|
6
|
+
const config_js_1 = require("../config.js");
|
|
7
|
+
function errorHandler(error, request, reply) {
|
|
8
|
+
if (config_js_1.config.nodeEnv === "development") {
|
|
9
|
+
console.error("[error]", error);
|
|
10
|
+
}
|
|
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({
|
|
17
|
+
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
|
+
})),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Fastify validation errors (e.g. schema validation)
|
|
30
|
+
const fastifyError = error;
|
|
31
|
+
if (fastifyError.statusCode && fastifyError.statusCode < 500) {
|
|
32
|
+
reply.status(fastifyError.statusCode).send({
|
|
33
|
+
error: {
|
|
34
|
+
code: "VALIDATION_ERROR",
|
|
35
|
+
message: error.message,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
reply.status(500).send({
|
|
41
|
+
error: {
|
|
42
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
43
|
+
message: "An unexpected error occurred",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
exports.errorHandler = errorHandler;
|
|
48
|
+
//# sourceMappingURL=error-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-handler.js","sourceRoot":"","sources":["../../src/middleware/error-handler.ts"],"names":[],"mappings":";;;AACA,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;AA/CD,oCA+CC"}
|
|
@@ -0,0 +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;AAqCvD,wBAAgB,+BAA+B,cAKlC,cAAc,SAChB,YAAY,KAClB,QAAQ,IAAI,CAAC,CA2CjB"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPerKeyRateLimitMiddleware = void 0;
|
|
4
|
+
const config_js_1 = require("../config.js");
|
|
5
|
+
const keyLimits = new Map();
|
|
6
|
+
/** Parse a duration string like "1 minute", "30 seconds", "1 hour" into milliseconds. */
|
|
7
|
+
function parseWindow(window) {
|
|
8
|
+
const match = window.match(/^(\d+)\s*(second|minute|hour|day)s?$/i);
|
|
9
|
+
if (!match)
|
|
10
|
+
return 60_000; // default 1 minute
|
|
11
|
+
const value = parseInt(match[1], 10);
|
|
12
|
+
switch (match[2].toLowerCase()) {
|
|
13
|
+
case "second": return value * 1_000;
|
|
14
|
+
case "minute": return value * 60_000;
|
|
15
|
+
case "hour": return value * 3_600_000;
|
|
16
|
+
case "day": return value * 86_400_000;
|
|
17
|
+
default: return 60_000;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function createPerKeyRateLimitMiddleware() {
|
|
21
|
+
const globalMax = config_js_1.config.rateLimitMax;
|
|
22
|
+
const globalWindowMs = parseWindow(config_js_1.config.rateLimitWindow);
|
|
23
|
+
return async function perKeyRateLimitMiddleware(request, reply) {
|
|
24
|
+
// Skip for unauthenticated requests (health endpoint)
|
|
25
|
+
if (!request.apiKey)
|
|
26
|
+
return;
|
|
27
|
+
const keyId = request.apiKey.id;
|
|
28
|
+
const max = request.apiKey.rate_limit_max ?? globalMax;
|
|
29
|
+
const windowMs = request.apiKey.rate_limit_window
|
|
30
|
+
? parseWindow(request.apiKey.rate_limit_window)
|
|
31
|
+
: globalWindowMs;
|
|
32
|
+
let entry = keyLimits.get(keyId);
|
|
33
|
+
if (!entry || entry.max !== max || entry.windowMs !== windowMs) {
|
|
34
|
+
entry = { timestamps: [], windowMs, max };
|
|
35
|
+
keyLimits.set(keyId, entry);
|
|
36
|
+
}
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
const cutoff = now - windowMs;
|
|
39
|
+
// Prune expired timestamps
|
|
40
|
+
entry.timestamps = entry.timestamps.filter((t) => t > cutoff);
|
|
41
|
+
if (entry.timestamps.length >= max) {
|
|
42
|
+
const retryAfter = Math.ceil((entry.timestamps[0] + windowMs - now) / 1000);
|
|
43
|
+
reply.header("Retry-After", String(retryAfter));
|
|
44
|
+
reply.header("X-RateLimit-Limit", String(max));
|
|
45
|
+
reply.header("X-RateLimit-Remaining", "0");
|
|
46
|
+
reply.header("X-RateLimit-Reset", new Date(entry.timestamps[0] + windowMs).toISOString());
|
|
47
|
+
reply.status(429).send({
|
|
48
|
+
error: {
|
|
49
|
+
code: "RATE_LIMITED",
|
|
50
|
+
message: `Rate limit exceeded. Try again in ${retryAfter} seconds.`,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
entry.timestamps.push(now);
|
|
56
|
+
// Set rate limit headers on all responses
|
|
57
|
+
reply.header("X-RateLimit-Limit", String(max));
|
|
58
|
+
reply.header("X-RateLimit-Remaining", String(max - entry.timestamps.length));
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
exports.createPerKeyRateLimitMiddleware = createPerKeyRateLimitMiddleware;
|
|
62
|
+
//# sourceMappingURL=per-key-rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"per-key-rate-limit.js","sourceRoot":"","sources":["../../src/middleware/per-key-rate-limit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FastifyRequest, FastifyReply } from "fastify";
|
|
2
|
+
import type Redis from "ioredis";
|
|
3
|
+
export interface RedisRateLimitConfig {
|
|
4
|
+
/** Maximum number of requests allowed in the window. */
|
|
5
|
+
maxRequests: number;
|
|
6
|
+
/** Window duration in milliseconds. */
|
|
7
|
+
windowMs: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Create a Redis-backed per-key rate limiter.
|
|
11
|
+
*
|
|
12
|
+
* Returns a Fastify preHandler hook. If Redis is unavailable or slow (>500ms)
|
|
13
|
+
* the request is allowed through (fail-open).
|
|
14
|
+
*/
|
|
15
|
+
export declare function createRedisRateLimiter(redis: Redis, defaults: RedisRateLimitConfig): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
16
|
+
//# sourceMappingURL=rate-limit-redis.d.ts.map
|
|
@@ -0,0 +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;AAEjC,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAwBD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,oBAAoB,aAcnB,cAAc,SAChB,YAAY,KAClB,QAAQ,IAAI,CAAC,CA+CjB"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRedisRateLimiter = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Lua script implementing a sliding-window counter.
|
|
6
|
+
*
|
|
7
|
+
* Keys[1] = the rate-limit key
|
|
8
|
+
* ARGV[1] = window TTL in seconds
|
|
9
|
+
* ARGV[2] = current timestamp in seconds (integer)
|
|
10
|
+
*
|
|
11
|
+
* The key is of the form stratum:ratelimit:{api_key_id}:{window_start}
|
|
12
|
+
* where window_start is computed on the caller side so the key naturally
|
|
13
|
+
* rotates every window.
|
|
14
|
+
*
|
|
15
|
+
* The script INCRs the key, sets a TTL on first creation, and returns the
|
|
16
|
+
* current count.
|
|
17
|
+
*/
|
|
18
|
+
const SLIDING_WINDOW_LUA = `
|
|
19
|
+
local current = redis.call('INCR', KEYS[1])
|
|
20
|
+
if current == 1 then
|
|
21
|
+
redis.call('EXPIRE', KEYS[1], ARGV[1])
|
|
22
|
+
end
|
|
23
|
+
return current
|
|
24
|
+
`;
|
|
25
|
+
/**
|
|
26
|
+
* Create a Redis-backed per-key rate limiter.
|
|
27
|
+
*
|
|
28
|
+
* Returns a Fastify preHandler hook. If Redis is unavailable or slow (>500ms)
|
|
29
|
+
* the request is allowed through (fail-open).
|
|
30
|
+
*/
|
|
31
|
+
function createRedisRateLimiter(redis, defaults) {
|
|
32
|
+
// Pre-load the Lua script so subsequent calls use EVALSHA
|
|
33
|
+
let scriptSha = null;
|
|
34
|
+
redis
|
|
35
|
+
.script("LOAD", SLIDING_WINDOW_LUA)
|
|
36
|
+
.then((sha) => {
|
|
37
|
+
scriptSha = sha;
|
|
38
|
+
})
|
|
39
|
+
.catch(() => {
|
|
40
|
+
// Will fall back to EVAL on each call
|
|
41
|
+
});
|
|
42
|
+
return async function redisRateLimitHook(request, reply) {
|
|
43
|
+
// Skip for unauthenticated requests (health, docs)
|
|
44
|
+
if (!request.apiKey)
|
|
45
|
+
return;
|
|
46
|
+
const keyId = request.apiKey.id;
|
|
47
|
+
const max = request.apiKey.rate_limit_max ?? defaults.maxRequests;
|
|
48
|
+
const windowMs = request.apiKey.rate_limit_window
|
|
49
|
+
? parseWindowStr(request.apiKey.rate_limit_window)
|
|
50
|
+
: defaults.windowMs;
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const windowStart = Math.floor(now / windowMs) * windowMs;
|
|
53
|
+
const windowTtlSec = Math.ceil(windowMs / 1000);
|
|
54
|
+
const redisKey = `stratum:ratelimit:${keyId}:${windowStart}`;
|
|
55
|
+
// The window resets at windowStart + windowMs
|
|
56
|
+
const resetAt = new Date(windowStart + windowMs);
|
|
57
|
+
let currentCount;
|
|
58
|
+
try {
|
|
59
|
+
currentCount = await withTimeout(evalScript(redis, scriptSha, redisKey, windowTtlSec), 500);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Redis unavailable or slow — fail-open
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Set rate-limit headers on every response
|
|
66
|
+
const remaining = Math.max(0, max - currentCount);
|
|
67
|
+
reply.header("X-RateLimit-Limit", String(max));
|
|
68
|
+
reply.header("X-RateLimit-Remaining", String(remaining));
|
|
69
|
+
reply.header("X-RateLimit-Reset", resetAt.toISOString());
|
|
70
|
+
if (currentCount > max) {
|
|
71
|
+
const retryAfterSec = Math.ceil((windowStart + windowMs - now) / 1000);
|
|
72
|
+
reply.header("Retry-After", String(retryAfterSec));
|
|
73
|
+
reply.status(429).send({
|
|
74
|
+
error: {
|
|
75
|
+
code: "RATE_LIMITED",
|
|
76
|
+
message: `Rate limit exceeded. Try again in ${retryAfterSec} seconds.`,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
exports.createRedisRateLimiter = createRedisRateLimiter;
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Helpers
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
async function evalScript(redis, sha, key, ttlSec) {
|
|
87
|
+
const now = Math.floor(Date.now() / 1000);
|
|
88
|
+
try {
|
|
89
|
+
if (sha) {
|
|
90
|
+
return (await redis.evalsha(sha, 1, key, String(ttlSec), String(now)));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// NOSCRIPT or other error — fall through to EVAL
|
|
95
|
+
}
|
|
96
|
+
return (await redis.eval(SLIDING_WINDOW_LUA, 1, key, String(ttlSec), String(now)));
|
|
97
|
+
}
|
|
98
|
+
function withTimeout(promise, ms) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const timer = setTimeout(() => reject(new Error("Redis timeout")), ms);
|
|
101
|
+
promise.then((val) => { clearTimeout(timer); resolve(val); }, (err) => { clearTimeout(timer); reject(err); });
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** Parse a duration string like "1 minute" into milliseconds. */
|
|
105
|
+
function parseWindowStr(window) {
|
|
106
|
+
const match = window.match(/^(\d+)\s*(second|minute|hour|day)s?$/i);
|
|
107
|
+
if (!match)
|
|
108
|
+
return 60_000;
|
|
109
|
+
const value = parseInt(match[1], 10);
|
|
110
|
+
switch (match[2].toLowerCase()) {
|
|
111
|
+
case "second": return value * 1_000;
|
|
112
|
+
case "minute": return value * 60_000;
|
|
113
|
+
case "hour": return value * 3_600_000;
|
|
114
|
+
case "day": return value * 86_400_000;
|
|
115
|
+
default: return 60_000;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=rate-limit-redis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit-redis.js","sourceRoot":"","sources":["../../src/middleware/rate-limit-redis.ts"],"names":[],"mappings":";;;AAUA;;;;;;;;;;;;;GAaG;AACH,MAAM,kBAAkB,GAAG;;;;;;CAM1B,CAAC;AAEF;;;;;GAKG;AACH,SAAgB,sBAAsB,CACpC,KAAY,EACZ,QAA8B;IAE9B,0DAA0D;IAC1D,IAAI,SAAS,GAAkB,IAAI,CAAC;IACpC,KAAK;SACF,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC;SAClC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE;QACZ,SAAS,GAAG,GAAa,CAAC;IAC5B,CAAC,CAAC;SACD,KAAK,CAAC,GAAG,EAAE;QACV,sCAAsC;IACxC,CAAC,CAAC,CAAC;IAEL,OAAO,KAAK,UAAU,kBAAkB,CACtC,OAAuB,EACvB,KAAmB;QAEnB,mDAAmD;QACnD,IAAI,CAAC,OAAO,CAAC,MAAM;YAAE,OAAO;QAE5B,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,cAAc,IAAI,QAAQ,CAAC,WAAW,CAAC;QAClE,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,iBAAiB;YAC/C,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC;YAClD,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAEtB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,QAAQ,CAAC,GAAG,QAAQ,CAAC;QAC1D,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;QAChD,MAAM,QAAQ,GAAG,qBAAqB,KAAK,IAAI,WAAW,EAAE,CAAC;QAE7D,8CAA8C;QAC9C,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,WAAW,GAAG,QAAQ,CAAC,CAAC;QAEjD,IAAI,YAAoB,CAAC;QAEzB,IAAI,CAAC;YACH,YAAY,GAAG,MAAM,WAAW,CAC9B,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,CAAC,EACpD,GAAG,CACJ,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,wCAAwC;YACxC,OAAO;QACT,CAAC;QAED,2CAA2C;QAC3C,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,YAAY,CAAC,CAAC;QAClD,KAAK,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/C,KAAK,CAAC,MAAM,CAAC,uBAAuB,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;QACzD,KAAK,CAAC,MAAM,CAAC,mBAAmB,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;QAEzD,IAAI,YAAY,GAAG,GAAG,EAAE,CAAC;YACvB,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,WAAW,GAAG,QAAQ,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;YACvE,KAAK,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;YACnD,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACrB,KAAK,EAAE;oBACL,IAAI,EAAE,cAAc;oBACpB,OAAO,EAAE,qCAAqC,aAAa,WAAW;iBACvE;aACF,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAjED,wDAiEC;AAED,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,KAAK,UAAU,UAAU,CACvB,KAAY,EACZ,GAAkB,EAClB,GAAW,EACX,MAAc;IAEd,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC;QACH,IAAI,GAAG,EAAE,CAAC;YACR,OAAO,CAAC,MAAM,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAW,CAAC;QACnF,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,iDAAiD;IACnD,CAAC;IACD,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAW,CAAC;AAC/F,CAAC;AAED,SAAS,WAAW,CAAI,OAAmB,EAAE,EAAU;IACrD,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACxC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACvE,OAAO,CAAC,IAAI,CACV,CAAC,GAAG,EAAE,EAAE,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAC/C,CAAC,GAAG,EAAE,EAAE,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAC/C,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,iEAAiE;AACjE,SAAS,cAAc,CAAC,MAAc;IACpC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;IACpE,IAAI,CAAC,KAAK;QAAE,OAAO,MAAM,CAAC;IAC1B,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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry Fastify middleware for the Stratum control plane.
|
|
3
|
+
*
|
|
4
|
+
* Creates a span per HTTP request and records standard HTTP + Stratum
|
|
5
|
+
* attributes. Gracefully no-ops when @opentelemetry/api is not installed.
|
|
6
|
+
*/
|
|
7
|
+
import type { FastifyInstance } from "fastify";
|
|
8
|
+
/**
|
|
9
|
+
* Register the telemetry onRequest / onResponse hooks on a Fastify instance.
|
|
10
|
+
*
|
|
11
|
+
* If @opentelemetry/api is not installed, this is a no-op.
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerTelemetryHooks(app: FastifyInstance): void;
|
|
14
|
+
//# sourceMappingURL=telemetry.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OpenTelemetry Fastify middleware for the Stratum control plane.
|
|
4
|
+
*
|
|
5
|
+
* Creates a span per HTTP request and records standard HTTP + Stratum
|
|
6
|
+
* attributes. Gracefully no-ops when @opentelemetry/api is not installed.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.registerTelemetryHooks = void 0;
|
|
10
|
+
// Lazy-loaded OTel API — stays `null` when the package is absent.
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
let otel = null;
|
|
13
|
+
try {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
15
|
+
otel = require("@opentelemetry/api");
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// @opentelemetry/api is not installed — telemetry is a no-op.
|
|
19
|
+
}
|
|
20
|
+
const TRACER_NAME = "@stratum-hq/control-plane";
|
|
21
|
+
/**
|
|
22
|
+
* Register the telemetry onRequest / onResponse hooks on a Fastify instance.
|
|
23
|
+
*
|
|
24
|
+
* If @opentelemetry/api is not installed, this is a no-op.
|
|
25
|
+
*/
|
|
26
|
+
function registerTelemetryHooks(app) {
|
|
27
|
+
if (!otel)
|
|
28
|
+
return;
|
|
29
|
+
const tracer = otel.trace.getTracer(TRACER_NAME);
|
|
30
|
+
const { SpanStatusCode, SpanKind } = otel;
|
|
31
|
+
// Store spans keyed by request id so the onResponse hook can close them.
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
const inflightSpans = new Map();
|
|
34
|
+
app.addHook("onRequest", async (request, _reply) => {
|
|
35
|
+
const span = tracer.startSpan(`HTTP ${request.method} ${request.routeOptions?.url ?? request.url}`, {
|
|
36
|
+
kind: SpanKind.SERVER,
|
|
37
|
+
attributes: {
|
|
38
|
+
"http.method": request.method,
|
|
39
|
+
"http.url": request.url,
|
|
40
|
+
"http.route": request.routeOptions?.url ?? request.url,
|
|
41
|
+
"http.request_id": request.id,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
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
|
+
inflightSpans.set(request.id, span);
|
|
49
|
+
});
|
|
50
|
+
app.addHook("onResponse", async (request, reply) => {
|
|
51
|
+
const span = inflightSpans.get(request.id);
|
|
52
|
+
if (!span)
|
|
53
|
+
return;
|
|
54
|
+
inflightSpans.delete(request.id);
|
|
55
|
+
span.setAttribute("http.status_code", reply.statusCode);
|
|
56
|
+
if (reply.statusCode >= 400) {
|
|
57
|
+
span.setStatus({
|
|
58
|
+
code: SpanStatusCode.ERROR,
|
|
59
|
+
message: `HTTP ${reply.statusCode}`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
64
|
+
}
|
|
65
|
+
span.end();
|
|
66
|
+
});
|
|
67
|
+
// Safety net: if the request errors out before onResponse fires
|
|
68
|
+
app.addHook("onError", async (request, _reply, error) => {
|
|
69
|
+
const span = inflightSpans.get(request.id);
|
|
70
|
+
if (!span)
|
|
71
|
+
return;
|
|
72
|
+
span.setStatus({
|
|
73
|
+
code: SpanStatusCode.ERROR,
|
|
74
|
+
message: error.message,
|
|
75
|
+
});
|
|
76
|
+
span.recordException(error);
|
|
77
|
+
// Don't end here — onResponse will still fire and end the span.
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
exports.registerTelemetryHooks = registerTelemetryHooks;
|
|
81
|
+
//# sourceMappingURL=telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../../src/middleware/telemetry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FastifyRequest, FastifyReply } from "fastify";
|
|
2
|
+
import { Stratum } from "@stratum-hq/lib";
|
|
3
|
+
/**
|
|
4
|
+
* Tenant-scope enforcement middleware.
|
|
5
|
+
*
|
|
6
|
+
* Ensures that tenant-scoped API keys can only access data belonging
|
|
7
|
+
* to their own tenant or its descendants in the hierarchy.
|
|
8
|
+
*
|
|
9
|
+
* Global keys (tenant_id === null) have unrestricted access.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createTenantScopeGuard(stratum: Stratum, extractTenantId: (req: FastifyRequest) => string | null): (request: FastifyRequest, _reply: FastifyReply) => Promise<void>;
|
|
12
|
+
/** Extract :id param (tenant routes, config routes, permission routes) */
|
|
13
|
+
export declare function fromParamId(req: FastifyRequest): string | null;
|
|
14
|
+
/** Extract :tenantId param (consent routes) */
|
|
15
|
+
export declare function fromParamTenantId(req: FastifyRequest): string | null;
|
|
16
|
+
/** Extract tenant_id from query string */
|
|
17
|
+
export declare function fromQueryTenantId(req: FastifyRequest): string | null;
|
|
18
|
+
/** Extract tenant_id from request body */
|
|
19
|
+
export declare function fromBodyTenantId(req: FastifyRequest): string | null;
|
|
20
|
+
//# sourceMappingURL=tenant-scope.d.ts.map
|
|
@@ -0,0 +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,MAAM,SAAS,CAAC;AAEvD,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE1C;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,eAAe,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,MAAM,GAAG,IAAI,aAG5C,cAAc,UACf,YAAY,KACnB,QAAQ,IAAI,CAAC,CA0BjB;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,0CAA0C;AAC1C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEnE"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fromBodyTenantId = exports.fromQueryTenantId = exports.fromParamTenantId = exports.fromParamId = exports.createTenantScopeGuard = void 0;
|
|
4
|
+
const core_1 = require("@stratum-hq/core");
|
|
5
|
+
/**
|
|
6
|
+
* Tenant-scope enforcement middleware.
|
|
7
|
+
*
|
|
8
|
+
* Ensures that tenant-scoped API keys can only access data belonging
|
|
9
|
+
* to their own tenant or its descendants in the hierarchy.
|
|
10
|
+
*
|
|
11
|
+
* Global keys (tenant_id === null) have unrestricted access.
|
|
12
|
+
*/
|
|
13
|
+
function createTenantScopeGuard(stratum, extractTenantId) {
|
|
14
|
+
return async function tenantScopeGuard(request, _reply) {
|
|
15
|
+
const apiKey = request.apiKey;
|
|
16
|
+
if (!apiKey || apiKey.tenant_id === null)
|
|
17
|
+
return;
|
|
18
|
+
const targetTenantId = extractTenantId(request);
|
|
19
|
+
if (!targetTenantId)
|
|
20
|
+
return;
|
|
21
|
+
// Fast path: exact match
|
|
22
|
+
if (apiKey.tenant_id === targetTenantId)
|
|
23
|
+
return;
|
|
24
|
+
// Hierarchy check: target must be a descendant of the key's tenant
|
|
25
|
+
try {
|
|
26
|
+
const target = await stratum.getTenant(targetTenantId);
|
|
27
|
+
const ancestorIds = target.ancestry_path.split("/").filter(Boolean);
|
|
28
|
+
if (ancestorIds.includes(apiKey.tenant_id))
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Tenant not found — fail closed for scoped keys
|
|
33
|
+
throw new core_1.ForbiddenError("API key tenant scope does not grant access to this tenant");
|
|
34
|
+
}
|
|
35
|
+
throw new core_1.ForbiddenError("API key tenant scope does not grant access to this tenant");
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
exports.createTenantScopeGuard = createTenantScopeGuard;
|
|
39
|
+
// --- Extractors ---
|
|
40
|
+
/** Extract :id param (tenant routes, config routes, permission routes) */
|
|
41
|
+
function fromParamId(req) {
|
|
42
|
+
return req.params.id ?? null;
|
|
43
|
+
}
|
|
44
|
+
exports.fromParamId = fromParamId;
|
|
45
|
+
/** Extract :tenantId param (consent routes) */
|
|
46
|
+
function fromParamTenantId(req) {
|
|
47
|
+
return req.params.tenantId ?? null;
|
|
48
|
+
}
|
|
49
|
+
exports.fromParamTenantId = fromParamTenantId;
|
|
50
|
+
/** Extract tenant_id from query string */
|
|
51
|
+
function fromQueryTenantId(req) {
|
|
52
|
+
return req.query.tenant_id ?? null;
|
|
53
|
+
}
|
|
54
|
+
exports.fromQueryTenantId = fromQueryTenantId;
|
|
55
|
+
/** Extract tenant_id from request body */
|
|
56
|
+
function fromBodyTenantId(req) {
|
|
57
|
+
return req.body?.tenant_id ?? null;
|
|
58
|
+
}
|
|
59
|
+
exports.fromBodyTenantId = fromBodyTenantId;
|
|
60
|
+
//# sourceMappingURL=tenant-scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-scope.js","sourceRoot":"","sources":["../../src/middleware/tenant-scope.ts"],"names":[],"mappings":";;;AACA,2CAAkD;AAGlD;;;;;;;GAOG;AACH,SAAgB,sBAAsB,CACpC,OAAgB,EAChB,eAAuD;IAEvD,OAAO,KAAK,UAAU,gBAAgB,CACpC,OAAuB,EACvB,MAAoB;QAEpB,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC9B,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,SAAS,KAAK,IAAI;YAAE,OAAO;QAEjD,MAAM,cAAc,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;QAChD,IAAI,CAAC,cAAc;YAAE,OAAO;QAE5B,yBAAyB;QACzB,IAAI,MAAM,CAAC,SAAS,KAAK,cAAc;YAAE,OAAO;QAEhD,mEAAmE;QACnE,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;YACvD,MAAM,WAAW,GAAG,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACpE,IAAI,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC;gBAAE,OAAO;QACrD,CAAC;QAAC,MAAM,CAAC;YACP,iDAAiD;YACjD,MAAM,IAAI,qBAAc,CACtB,2DAA2D,CAC5D,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,qBAAc,CACtB,2DAA2D,CAC5D,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAjCD,wDAiCC;AAED,qBAAqB;AAErB,0EAA0E;AAC1E,SAAgB,WAAW,CAAC,GAAmB;IAC7C,OAAQ,GAAG,CAAC,MAAiC,CAAC,EAAE,IAAI,IAAI,CAAC;AAC3D,CAAC;AAFD,kCAEC;AAED,+CAA+C;AAC/C,SAAgB,iBAAiB,CAAC,GAAmB;IACnD,OAAQ,GAAG,CAAC,MAAiC,CAAC,QAAQ,IAAI,IAAI,CAAC;AACjE,CAAC;AAFD,8CAEC;AAED,0CAA0C;AAC1C,SAAgB,iBAAiB,CAAC,GAAmB;IACnD,OAAQ,GAAG,CAAC,KAAgC,CAAC,SAAS,IAAI,IAAI,CAAC;AACjE,CAAC;AAFD,8CAEC;AAED,0CAA0C;AAC1C,SAAgB,gBAAgB,CAAC,GAAmB;IAClD,OAAQ,GAAG,CAAC,IAAsC,EAAE,SAAS,IAAI,IAAI,CAAC;AACxE,CAAC;AAFD,4CAEC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI1C,wBAAsB,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAkCzE"}
|