@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,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPermissionRoutes = void 0;
|
|
4
|
+
const core_1 = require("@stratum-hq/core");
|
|
5
|
+
const audit_logs_js_1 = require("./audit-logs.js");
|
|
6
|
+
const tenant_scope_js_1 = require("../middleware/tenant-scope.js");
|
|
7
|
+
function createPermissionRoutes(stratum) {
|
|
8
|
+
return async function permissionRoutes(app) {
|
|
9
|
+
// Tenant-scoped keys can only access permissions for their own tenant subtree
|
|
10
|
+
app.addHook("preHandler", (0, tenant_scope_js_1.createTenantScopeGuard)(stratum, tenant_scope_js_1.fromParamId));
|
|
11
|
+
// GET /api/v1/tenants/:id/permissions — Get resolved permissions
|
|
12
|
+
app.get("/", async (request, reply) => {
|
|
13
|
+
const resolved = await stratum.resolvePermissions(request.params.id);
|
|
14
|
+
reply.status(200).send(resolved);
|
|
15
|
+
});
|
|
16
|
+
// POST /api/v1/tenants/:id/permissions — Create permission policy
|
|
17
|
+
app.post("/", async (request, reply) => {
|
|
18
|
+
const input = core_1.CreatePermissionInputSchema.parse(request.body);
|
|
19
|
+
const policy = await stratum.createPermission(request.params.id, input, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
20
|
+
reply.status(201).send(policy);
|
|
21
|
+
});
|
|
22
|
+
// PATCH /api/v1/tenants/:id/permissions/:policyId — Update permission policy
|
|
23
|
+
app.patch("/:policyId", async (request, reply) => {
|
|
24
|
+
const input = core_1.UpdatePermissionInputSchema.parse(request.body);
|
|
25
|
+
const policy = await stratum.updatePermission(request.params.id, request.params.policyId, input, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
26
|
+
reply.status(200).send(policy);
|
|
27
|
+
});
|
|
28
|
+
// DELETE /api/v1/tenants/:id/permissions/:policyId — Delete/revoke permission policy
|
|
29
|
+
app.delete("/:policyId", async (request, reply) => {
|
|
30
|
+
await stratum.deletePermission(request.params.id, request.params.policyId, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
31
|
+
reply.status(204).send();
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
exports.createPermissionRoutes = createPermissionRoutes;
|
|
36
|
+
//# sourceMappingURL=permissions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../src/routes/permissions.ts"],"names":[],"mappings":";;;AACA,2CAA4F;AAE5F,mDAAoD;AACpD,mEAAoF;AAEpF,SAAgB,sBAAsB,CAAC,OAAgB;IACrD,OAAO,KAAK,UAAU,gBAAgB,CAAC,GAAoB;QACzD,8EAA8E;QAC9E,GAAG,CAAC,OAAO,CAAC,YAAY,EAAE,IAAA,wCAAsB,EAAC,OAAO,EAAE,6BAAW,CAAC,CAAC,CAAC;QACxE,iEAAiE;QACjE,GAAG,CAAC,GAAG,CAA6B,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;YAChE,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,kBAAkB,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACrE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;QAEH,kEAAkE;QAClE,GAAG,CAAC,IAAI,CAA6B,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;YACjE,MAAM,KAAK,GAAG,kCAA2B,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC9D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,gBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,IAAA,iCAAiB,EAAC,OAAO,CAAC,CAAC,CAAC;YACpG,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,6EAA6E;QAC7E,GAAG,CAAC,KAAK,CAA+C,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;YAC7F,MAAM,KAAK,GAAG,kCAA2B,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC9D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,gBAAgB,CAC3C,OAAO,CAAC,MAAM,CAAC,EAAE,EACjB,OAAO,CAAC,MAAM,CAAC,QAAQ,EACvB,KAAK,EACL,IAAA,iCAAiB,EAAC,OAAO,CAAC,CAC3B,CAAC;YACF,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,qFAAqF;QACrF,GAAG,CAAC,MAAM,CAA+C,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;YAC9F,MAAM,OAAO,CAAC,gBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAA,iCAAiB,EAAC,OAAO,CAAC,CAAC,CAAC;YACvG,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QAC3B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC;AAnCD,wDAmCC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regions.d.ts","sourceRoot":"","sources":["../../src/routes/regions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1C,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAG1C,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,SACT,eAAe,KAAG,QAAQ,IAAI,CAAC,CAiCxE"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRegionRoutes = void 0;
|
|
4
|
+
const core_1 = require("@stratum-hq/core");
|
|
5
|
+
const audit_logs_js_1 = require("./audit-logs.js");
|
|
6
|
+
function createRegionRoutes(stratum) {
|
|
7
|
+
return async function regionRoutes(app) {
|
|
8
|
+
// POST /api/v1/regions — Create region
|
|
9
|
+
app.post("/", async (request, reply) => {
|
|
10
|
+
const input = core_1.CreateRegionInputSchema.parse(request.body);
|
|
11
|
+
const region = await stratum.createRegion(input, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
12
|
+
reply.status(201).send(region);
|
|
13
|
+
});
|
|
14
|
+
// GET /api/v1/regions — List regions
|
|
15
|
+
app.get("/", async (_request, reply) => {
|
|
16
|
+
const regions = await stratum.listRegions();
|
|
17
|
+
reply.status(200).send(regions);
|
|
18
|
+
});
|
|
19
|
+
// GET /api/v1/regions/:id — Get region by ID
|
|
20
|
+
app.get("/:id", async (request, reply) => {
|
|
21
|
+
const region = await stratum.getRegion(request.params.id);
|
|
22
|
+
reply.status(200).send(region);
|
|
23
|
+
});
|
|
24
|
+
// PATCH /api/v1/regions/:id — Update region
|
|
25
|
+
app.patch("/:id", async (request, reply) => {
|
|
26
|
+
const input = core_1.UpdateRegionInputSchema.parse(request.body);
|
|
27
|
+
const region = await stratum.updateRegion(request.params.id, input, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
28
|
+
reply.status(200).send(region);
|
|
29
|
+
});
|
|
30
|
+
// DELETE /api/v1/regions/:id — Delete region
|
|
31
|
+
app.delete("/:id", async (request, reply) => {
|
|
32
|
+
await stratum.deleteRegion(request.params.id, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
33
|
+
reply.status(204).send();
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
exports.createRegionRoutes = createRegionRoutes;
|
|
38
|
+
//# sourceMappingURL=regions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regions.js","sourceRoot":"","sources":["../../src/routes/regions.ts"],"names":[],"mappings":";;;AACA,2CAAoF;AAEpF,mDAAoD;AAEpD,SAAgB,kBAAkB,CAAC,OAAgB;IACjD,OAAO,KAAK,UAAU,YAAY,CAAC,GAAoB;QACrD,uCAAuC;QACvC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;YACrC,MAAM,KAAK,GAAG,8BAAuB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,KAAK,EAAE,IAAA,iCAAiB,EAAC,OAAO,CAAC,CAAC,CAAC;YAC7E,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,qCAAqC;QACrC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;YACrC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC;YAC5C,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAClC,CAAC,CAAC,CAAC;QAEH,6CAA6C;QAC7C,GAAG,CAAC,GAAG,CAA6B,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;YACnE,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC1D,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,4CAA4C;QAC5C,GAAG,CAAC,KAAK,CAA6B,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;YACrE,MAAM,KAAK,GAAG,8BAAuB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,IAAA,iCAAiB,EAAC,OAAO,CAAC,CAAC,CAAC;YAChG,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,6CAA6C;QAC7C,GAAG,CAAC,MAAM,CAA6B,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;YACtE,MAAM,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,IAAA,iCAAiB,EAAC,OAAO,CAAC,CAAC,CAAC;YAC1E,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QAC3B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC;AAlCD,gDAkCC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../../src/routes/roles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAqB1C,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,SACT,eAAe,KAAG,QAAQ,IAAI,CAAC,CA8EtE"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRoleRoutes = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const audit_logs_js_1 = require("./audit-logs.js");
|
|
6
|
+
const createRoleSchema = zod_1.z.object({
|
|
7
|
+
name: zod_1.z.string().min(1).max(100),
|
|
8
|
+
description: zod_1.z.string().max(500).optional(),
|
|
9
|
+
scopes: zod_1.z.array(zod_1.z.enum(["read", "write", "admin"])).min(1),
|
|
10
|
+
tenant_id: zod_1.z.string().uuid().optional().nullable(),
|
|
11
|
+
});
|
|
12
|
+
const updateRoleSchema = zod_1.z.object({
|
|
13
|
+
name: zod_1.z.string().min(1).max(100).optional(),
|
|
14
|
+
description: zod_1.z.string().max(500).optional(),
|
|
15
|
+
scopes: zod_1.z.array(zod_1.z.enum(["read", "write", "admin"])).min(1).optional(),
|
|
16
|
+
});
|
|
17
|
+
const assignRoleSchema = zod_1.z.object({
|
|
18
|
+
role_id: zod_1.z.string().uuid(),
|
|
19
|
+
});
|
|
20
|
+
function createRoleRoutes(stratum) {
|
|
21
|
+
return async function roleRoutes(app) {
|
|
22
|
+
// POST /api/v1/roles — Create a role
|
|
23
|
+
app.post("/", async (request, reply) => {
|
|
24
|
+
const parsed = createRoleSchema.safeParse(request.body);
|
|
25
|
+
if (!parsed.success) {
|
|
26
|
+
reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: "Invalid request body" }, details: parsed.error.issues });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const role = await stratum.createRole(parsed.data, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
30
|
+
reply.status(201).send(role);
|
|
31
|
+
});
|
|
32
|
+
// GET /api/v1/roles — List roles (optional ?tenant_id=)
|
|
33
|
+
app.get("/", async (request, reply) => {
|
|
34
|
+
const roles = await stratum.listRoles(request.query.tenant_id);
|
|
35
|
+
reply.status(200).send(roles);
|
|
36
|
+
});
|
|
37
|
+
// GET /api/v1/roles/:id — Get role
|
|
38
|
+
app.get("/:id", async (request, reply) => {
|
|
39
|
+
const role = await stratum.getRole(request.params.id);
|
|
40
|
+
if (!role) {
|
|
41
|
+
reply.status(404).send({ error: { code: "NOT_FOUND", message: "Role not found" } });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
reply.status(200).send(role);
|
|
45
|
+
});
|
|
46
|
+
// PATCH /api/v1/roles/:id — Update role
|
|
47
|
+
app.patch("/:id", async (request, reply) => {
|
|
48
|
+
const parsed = updateRoleSchema.safeParse(request.body);
|
|
49
|
+
if (!parsed.success) {
|
|
50
|
+
reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: "Invalid request body" }, details: parsed.error.issues });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const role = await stratum.updateRole(request.params.id, parsed.data, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
54
|
+
if (!role) {
|
|
55
|
+
reply.status(404).send({ error: { code: "NOT_FOUND", message: "Role not found" } });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
reply.status(200).send(role);
|
|
59
|
+
});
|
|
60
|
+
// DELETE /api/v1/roles/:id — Delete role
|
|
61
|
+
app.delete("/:id", async (request, reply) => {
|
|
62
|
+
const deleted = await stratum.deleteRole(request.params.id, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
63
|
+
if (!deleted) {
|
|
64
|
+
reply.status(404).send({ error: { code: "NOT_FOUND", message: "Role not found" } });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
reply.status(204).send();
|
|
68
|
+
});
|
|
69
|
+
// POST /api/v1/roles/assign/:keyId — Assign role to API key
|
|
70
|
+
app.post("/assign/:keyId", async (request, reply) => {
|
|
71
|
+
const parsed = assignRoleSchema.safeParse(request.body);
|
|
72
|
+
if (!parsed.success) {
|
|
73
|
+
reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: "role_id is required" } });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const success = await stratum.assignRoleToKey(request.params.keyId, parsed.data.role_id);
|
|
77
|
+
if (!success) {
|
|
78
|
+
reply.status(404).send({ error: { code: "NOT_FOUND", message: "API key not found or already revoked" } });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
reply.status(200).send({ success: true });
|
|
82
|
+
});
|
|
83
|
+
// DELETE /api/v1/roles/assign/:keyId — Remove role from API key
|
|
84
|
+
app.delete("/assign/:keyId", async (request, reply) => {
|
|
85
|
+
const success = await stratum.removeRoleFromKey(request.params.keyId);
|
|
86
|
+
if (!success) {
|
|
87
|
+
reply.status(404).send({ error: { code: "NOT_FOUND", message: "API key not found or already revoked" } });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
reply.status(200).send({ success: true });
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
exports.createRoleRoutes = createRoleRoutes;
|
|
95
|
+
//# sourceMappingURL=roles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roles.js","sourceRoot":"","sources":["../../src/routes/roles.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenants.d.ts","sourceRoot":"","sources":["../../src/routes/tenants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAU1C,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAQ1C,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,SACT,eAAe,KAAG,QAAQ,IAAI,CAAC,CAqHxE"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createTenantRoutes = void 0;
|
|
4
|
+
const core_1 = require("@stratum-hq/core");
|
|
5
|
+
const isolation_service_js_1 = require("../services/isolation-service.js");
|
|
6
|
+
const audit_logs_js_1 = require("./audit-logs.js");
|
|
7
|
+
const tenant_scope_js_1 = require("../middleware/tenant-scope.js");
|
|
8
|
+
function createTenantRoutes(stratum) {
|
|
9
|
+
return async function tenantRoutes(app) {
|
|
10
|
+
// Tenant-scoped keys can only access their own tenant subtree
|
|
11
|
+
app.addHook("preHandler", (0, tenant_scope_js_1.createTenantScopeGuard)(stratum, tenant_scope_js_1.fromParamId));
|
|
12
|
+
// GET /api/v1/tenants — List tenants (with cursor pagination)
|
|
13
|
+
app.get("/", async (request, reply) => {
|
|
14
|
+
const query = core_1.PaginationSchema.parse(request.query);
|
|
15
|
+
const result = await stratum.listTenants(query);
|
|
16
|
+
reply.status(200).send(result);
|
|
17
|
+
});
|
|
18
|
+
// POST /api/v1/tenants — Create tenant
|
|
19
|
+
app.post("/", async (request, reply) => {
|
|
20
|
+
const input = core_1.CreateTenantInputSchema.parse(request.body);
|
|
21
|
+
// Reject unsupported isolation strategies (SHARED_RLS and SCHEMA_PER_TENANT are supported)
|
|
22
|
+
if (input.isolation_strategy && !(0, core_1.isSupportedIsolationStrategy)(input.isolation_strategy)) {
|
|
23
|
+
throw new core_1.IsolationStrategyUnsupportedError(input.isolation_strategy);
|
|
24
|
+
}
|
|
25
|
+
const tenant = await stratum.createTenant(input, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
26
|
+
// Provision isolation resources based on strategy
|
|
27
|
+
const strategy = tenant.isolation_strategy ?? "SHARED_RLS";
|
|
28
|
+
if (strategy === "SCHEMA_PER_TENANT") {
|
|
29
|
+
await (0, isolation_service_js_1.setupSchemaForTenant)(tenant.slug);
|
|
30
|
+
}
|
|
31
|
+
else if (strategy === "DB_PER_TENANT") {
|
|
32
|
+
await (0, isolation_service_js_1.setupDatabaseForTenant)(tenant.slug);
|
|
33
|
+
}
|
|
34
|
+
reply.status(201).send(tenant);
|
|
35
|
+
});
|
|
36
|
+
// POST /api/v1/tenants/batch — Create multiple tenants atomically
|
|
37
|
+
app.post("/batch", async (request, reply) => {
|
|
38
|
+
const body = request.body;
|
|
39
|
+
if (!Array.isArray(body?.tenants) || body.tenants.length === 0) {
|
|
40
|
+
reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: "Body must contain a non-empty 'tenants' array" } });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (body.tenants.length > 100) {
|
|
44
|
+
reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: "Batch limited to 100 tenants" } });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const inputs = body.tenants.map((t) => core_1.CreateTenantInputSchema.parse(t));
|
|
48
|
+
const result = await stratum.batchCreateTenants(inputs, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
49
|
+
reply.status(201).send(result);
|
|
50
|
+
});
|
|
51
|
+
// GET /api/v1/tenants/:id — Get tenant
|
|
52
|
+
app.get("/:id", async (request, reply) => {
|
|
53
|
+
const tenant = await stratum.getTenant(request.params.id);
|
|
54
|
+
reply.status(200).send(tenant);
|
|
55
|
+
});
|
|
56
|
+
// PATCH /api/v1/tenants/:id — Update tenant
|
|
57
|
+
app.patch("/:id", async (request, reply) => {
|
|
58
|
+
const patch = core_1.UpdateTenantInputSchema.parse(request.body);
|
|
59
|
+
const tenant = await stratum.updateTenant(request.params.id, patch, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
60
|
+
reply.status(200).send(tenant);
|
|
61
|
+
});
|
|
62
|
+
// DELETE /api/v1/tenants/:id — Soft-delete (archive) tenant
|
|
63
|
+
app.delete("/:id", async (request, reply) => {
|
|
64
|
+
await stratum.deleteTenant(request.params.id, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
65
|
+
reply.status(204).send();
|
|
66
|
+
});
|
|
67
|
+
// POST /api/v1/tenants/:id/move — Move tenant
|
|
68
|
+
app.post("/:id/move", async (request, reply) => {
|
|
69
|
+
const input = core_1.MoveTenantInputSchema.parse(request.body);
|
|
70
|
+
const tenant = await stratum.moveTenant(request.params.id, input.new_parent_id, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
71
|
+
reply.status(200).send(tenant);
|
|
72
|
+
});
|
|
73
|
+
// GET /api/v1/tenants/:id/ancestors — Get ancestors
|
|
74
|
+
app.get("/:id/ancestors", async (request, reply) => {
|
|
75
|
+
const ancestors = await stratum.getAncestors(request.params.id);
|
|
76
|
+
reply.status(200).send(ancestors);
|
|
77
|
+
});
|
|
78
|
+
// GET /api/v1/tenants/:id/descendants — Get descendants
|
|
79
|
+
app.get("/:id/descendants", async (request, reply) => {
|
|
80
|
+
const descendants = await stratum.getDescendants(request.params.id);
|
|
81
|
+
reply.status(200).send(descendants);
|
|
82
|
+
});
|
|
83
|
+
// GET /api/v1/tenants/:id/children — Get direct children
|
|
84
|
+
app.get("/:id/children", async (request, reply) => {
|
|
85
|
+
const children = await stratum.getChildren(request.params.id);
|
|
86
|
+
reply.status(200).send(children);
|
|
87
|
+
});
|
|
88
|
+
// POST /api/v1/tenants/:id/migrate-region — Migrate tenant to a new region
|
|
89
|
+
app.post("/:id/migrate-region", async (request, reply) => {
|
|
90
|
+
const { region_id } = core_1.MigrateRegionInputSchema.parse(request.body);
|
|
91
|
+
await stratum.migrateRegion(request.params.id, region_id, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
92
|
+
reply.status(200).send({ success: true });
|
|
93
|
+
});
|
|
94
|
+
// POST /api/v1/tenants/:id/purge — GDPR Article 17: hard-delete all tenant data
|
|
95
|
+
app.post("/:id/purge", async (request, reply) => {
|
|
96
|
+
await stratum.purgeTenant(request.params.id, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
97
|
+
reply.status(204).send();
|
|
98
|
+
});
|
|
99
|
+
// GET /api/v1/tenants/:id/export — GDPR Article 20: export all tenant data
|
|
100
|
+
app.get("/:id/export", async (request, reply) => {
|
|
101
|
+
const data = await stratum.exportTenantData(request.params.id);
|
|
102
|
+
reply.status(200).send(data);
|
|
103
|
+
});
|
|
104
|
+
// GET /api/v1/tenants/:id/context — Resolve full tenant impersonation context (admin scope)
|
|
105
|
+
app.get("/:id/context", async (request, reply) => {
|
|
106
|
+
const context = await stratum.getTenantContext(request.params.id);
|
|
107
|
+
reply.status(200).send(context);
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
exports.createTenantRoutes = createTenantRoutes;
|
|
112
|
+
//# sourceMappingURL=tenants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenants.js","sourceRoot":"","sources":["../../src/routes/tenants.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../src/routes/webhooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAkB,MAAM,SAAS,CAAC;AAE1D,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAiB1C,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,SACT,eAAe,KAAG,QAAQ,IAAI,CAAC,CAwGzE"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWebhookRoutes = void 0;
|
|
4
|
+
const core_1 = require("@stratum-hq/core");
|
|
5
|
+
const audit_logs_js_1 = require("./audit-logs.js");
|
|
6
|
+
/**
|
|
7
|
+
* Post-fetch tenant access check for webhook routes.
|
|
8
|
+
* Used after loading a webhook by ID to verify the caller's key
|
|
9
|
+
* has access to the webhook's tenant.
|
|
10
|
+
* Global keys (tenant_id = null) have unrestricted access.
|
|
11
|
+
*/
|
|
12
|
+
function assertTenantAccess(request, webhookTenantId) {
|
|
13
|
+
const apiKey = request.apiKey;
|
|
14
|
+
if (!apiKey || apiKey.tenant_id === null)
|
|
15
|
+
return;
|
|
16
|
+
if (webhookTenantId !== null && apiKey.tenant_id !== webhookTenantId) {
|
|
17
|
+
throw new core_1.ForbiddenError("API key does not have access to this webhook's tenant");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function createWebhookRoutes(stratum) {
|
|
21
|
+
return async function webhookRoutes(app) {
|
|
22
|
+
// POST /api/v1/webhooks — Create webhook
|
|
23
|
+
app.post("/", async (request, reply) => {
|
|
24
|
+
const input = core_1.CreateWebhookInputSchema.parse(request.body);
|
|
25
|
+
// Tenant-scoped keys can only create webhooks for their own tenant
|
|
26
|
+
if (request.apiKey?.tenant_id !== null && input.tenant_id !== null) {
|
|
27
|
+
assertTenantAccess(request, input.tenant_id);
|
|
28
|
+
}
|
|
29
|
+
const webhook = await stratum.createWebhook(input, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
30
|
+
reply.status(201).send(webhook);
|
|
31
|
+
});
|
|
32
|
+
// GET /api/v1/webhooks — List webhooks (optional ?tenant_id=)
|
|
33
|
+
app.get("/", async (request, reply) => {
|
|
34
|
+
// Tenant-scoped keys can only list their own tenant's webhooks
|
|
35
|
+
const tenantId = request.apiKey?.tenant_id ?? request.query.tenant_id;
|
|
36
|
+
const webhooks = await stratum.listWebhooks(tenantId);
|
|
37
|
+
reply.status(200).send(webhooks);
|
|
38
|
+
});
|
|
39
|
+
// GET /api/v1/webhooks/:id — Get webhook
|
|
40
|
+
app.get("/:id", async (request, reply) => {
|
|
41
|
+
const webhook = await stratum.getWebhook(request.params.id);
|
|
42
|
+
assertTenantAccess(request, webhook.tenant_id);
|
|
43
|
+
reply.status(200).send(webhook);
|
|
44
|
+
});
|
|
45
|
+
// PATCH /api/v1/webhooks/:id — Update webhook
|
|
46
|
+
app.patch("/:id", async (request, reply) => {
|
|
47
|
+
const existing = await stratum.getWebhook(request.params.id);
|
|
48
|
+
assertTenantAccess(request, existing.tenant_id);
|
|
49
|
+
const input = core_1.UpdateWebhookInputSchema.parse(request.body);
|
|
50
|
+
const webhook = await stratum.updateWebhook(request.params.id, input, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
51
|
+
reply.status(200).send(webhook);
|
|
52
|
+
});
|
|
53
|
+
// DELETE /api/v1/webhooks/:id — Delete webhook
|
|
54
|
+
app.delete("/:id", async (request, reply) => {
|
|
55
|
+
const existing = await stratum.getWebhook(request.params.id);
|
|
56
|
+
assertTenantAccess(request, existing.tenant_id);
|
|
57
|
+
await stratum.deleteWebhook(request.params.id, (0, audit_logs_js_1.buildAuditContext)(request));
|
|
58
|
+
reply.status(204).send();
|
|
59
|
+
});
|
|
60
|
+
// GET /api/v1/webhooks/:id/deliveries — List deliveries for a webhook
|
|
61
|
+
app.get("/:id/deliveries", async (request, reply) => {
|
|
62
|
+
const webhook = await stratum.getWebhook(request.params.id);
|
|
63
|
+
assertTenantAccess(request, webhook.tenant_id);
|
|
64
|
+
const deliveries = await stratum.listWebhookDeliveries(request.params.id);
|
|
65
|
+
reply.status(200).send(deliveries);
|
|
66
|
+
});
|
|
67
|
+
// POST /api/v1/webhooks/:id/test — Send test event
|
|
68
|
+
app.post("/:id/test", async (request, reply) => {
|
|
69
|
+
const webhook = await stratum.getWebhook(request.params.id);
|
|
70
|
+
assertTenantAccess(request, webhook.tenant_id);
|
|
71
|
+
const result = await stratum.testWebhook(request.params.id);
|
|
72
|
+
reply.status(200).send(result);
|
|
73
|
+
});
|
|
74
|
+
// --- Dead-Letter Queue / Delivery Dashboard ---
|
|
75
|
+
// GET /api/v1/webhooks/deliveries/stats — Delivery statistics
|
|
76
|
+
app.get("/deliveries/stats", async (request, reply) => {
|
|
77
|
+
// Scoped keys: restrict to their tenant's deliveries
|
|
78
|
+
const tenantId = request.apiKey?.tenant_id ?? undefined;
|
|
79
|
+
const stats = await stratum.getDeliveryStats(tenantId);
|
|
80
|
+
reply.status(200).send(stats);
|
|
81
|
+
});
|
|
82
|
+
// GET /api/v1/webhooks/deliveries/failed — List failed deliveries (DLQ)
|
|
83
|
+
app.get("/deliveries/failed", async (request, reply) => {
|
|
84
|
+
const rawLimit = request.query.limit ? parseInt(request.query.limit, 10) : 100;
|
|
85
|
+
const limit = Number.isNaN(rawLimit) || rawLimit < 1 ? 100 : Math.min(rawLimit, 500);
|
|
86
|
+
const tenantId = request.apiKey?.tenant_id ?? undefined;
|
|
87
|
+
const failed = await stratum.listFailedDeliveries(limit, tenantId);
|
|
88
|
+
reply.status(200).send(failed);
|
|
89
|
+
});
|
|
90
|
+
// POST /api/v1/webhooks/deliveries/retry-all — Retry all failed deliveries
|
|
91
|
+
app.post("/deliveries/retry-all", async (request, reply) => {
|
|
92
|
+
const tenantId = request.apiKey?.tenant_id ?? undefined;
|
|
93
|
+
const count = await stratum.retryFailedDeliveries(tenantId);
|
|
94
|
+
reply.status(200).send({ retried: count });
|
|
95
|
+
});
|
|
96
|
+
// POST /api/v1/webhooks/deliveries/:deliveryId/retry — Retry single delivery
|
|
97
|
+
app.post("/deliveries/:deliveryId/retry", async (request, reply) => {
|
|
98
|
+
// Verify tenant access for scoped keys
|
|
99
|
+
if (request.apiKey?.tenant_id) {
|
|
100
|
+
const failed = await stratum.listFailedDeliveries(1000, request.apiKey.tenant_id);
|
|
101
|
+
if (!failed.some((d) => d.id === request.params.deliveryId)) {
|
|
102
|
+
reply.status(404).send({ error: { code: "NOT_FOUND", message: "Delivery not found or not in failed state" } });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const success = await stratum.retryDelivery(request.params.deliveryId);
|
|
107
|
+
if (!success) {
|
|
108
|
+
reply.status(404).send({ error: { code: "NOT_FOUND", message: "Delivery not found or not in failed state" } });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
reply.status(200).send({ success: true });
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
exports.createWebhookRoutes = createWebhookRoutes;
|
|
116
|
+
//# sourceMappingURL=webhooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../../src/routes/webhooks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare function registerTable(tableName: string): void;
|
|
2
|
+
export declare function setupRLSForTable(tableName: string): Promise<void>;
|
|
3
|
+
export declare function teardownRLSForTable(tableName: string): Promise<void>;
|
|
4
|
+
export declare function setupAllRLS(): Promise<void>;
|
|
5
|
+
export declare function setupSchemaForTenant(tenantSlug: string, tables?: string[]): Promise<void>;
|
|
6
|
+
export declare function teardownSchemaForTenant(tenantSlug: string): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Creates the dedicated database for a DB_PER_TENANT tenant.
|
|
9
|
+
*
|
|
10
|
+
* CREATE DATABASE cannot run inside a transaction, so this function uses a
|
|
11
|
+
* standalone client from the pool (no BEGIN/COMMIT wrapping).
|
|
12
|
+
*/
|
|
13
|
+
export declare function setupDatabaseForTenant(tenantSlug: string, templateDb?: string): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Tears down the DB_PER_TENANT isolation for a tenant.
|
|
16
|
+
*
|
|
17
|
+
* This closes any open connection pools for the tenant database but does NOT
|
|
18
|
+
* drop the database — data preservation is left to the operator.
|
|
19
|
+
* To actually drop the database, use dropDatabase() from @stratum-hq/db-adapters directly.
|
|
20
|
+
*/
|
|
21
|
+
export declare function teardownDatabaseForTenant(_tenantSlug: string): Promise<void>;
|
|
22
|
+
//# sourceMappingURL=isolation-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isolation-service.d.ts","sourceRoot":"","sources":["../../src/services/isolation-service.ts"],"names":[],"mappings":"AAqBA,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAErD;AAED,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcvE;AAED,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU1E;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAIjD;AAED,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,wBAAsB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI/E;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIlF"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.teardownDatabaseForTenant = exports.setupDatabaseForTenant = exports.teardownSchemaForTenant = exports.setupSchemaForTenant = exports.setupAllRLS = exports.teardownRLSForTable = exports.setupRLSForTable = exports.registerTable = void 0;
|
|
4
|
+
const lib_1 = require("@stratum-hq/lib");
|
|
5
|
+
const db_adapters_1 = require("@stratum-hq/db-adapters");
|
|
6
|
+
const connection_js_1 = require("../db/connection.js");
|
|
7
|
+
const registeredTables = new Set();
|
|
8
|
+
// Validate table name to prevent SQL injection (only allows alphanumeric + underscores)
|
|
9
|
+
function validateTableName(name) {
|
|
10
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
11
|
+
throw new Error(`Invalid table name: ${name}`);
|
|
12
|
+
}
|
|
13
|
+
return name;
|
|
14
|
+
}
|
|
15
|
+
function registerTable(tableName) {
|
|
16
|
+
registeredTables.add(validateTableName(tableName));
|
|
17
|
+
}
|
|
18
|
+
exports.registerTable = registerTable;
|
|
19
|
+
async function setupRLSForTable(tableName) {
|
|
20
|
+
const safe = validateTableName(tableName);
|
|
21
|
+
return (0, lib_1.withTransaction)((0, connection_js_1.getPool)(), async (client) => {
|
|
22
|
+
await client.query(`ALTER TABLE ${safe} ENABLE ROW LEVEL SECURITY`);
|
|
23
|
+
await client.query(`ALTER TABLE ${safe} FORCE ROW LEVEL SECURITY`);
|
|
24
|
+
await client.query(`CREATE POLICY tenant_isolation ON ${safe}
|
|
25
|
+
USING (tenant_id = current_setting('app.current_tenant_id')::uuid)`);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
exports.setupRLSForTable = setupRLSForTable;
|
|
29
|
+
async function teardownRLSForTable(tableName) {
|
|
30
|
+
const safe = validateTableName(tableName);
|
|
31
|
+
return (0, lib_1.withTransaction)((0, connection_js_1.getPool)(), async (client) => {
|
|
32
|
+
await client.query(`DROP POLICY IF EXISTS tenant_isolation ON ${safe}`);
|
|
33
|
+
await client.query(`ALTER TABLE ${safe} DISABLE ROW LEVEL SECURITY`);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
exports.teardownRLSForTable = teardownRLSForTable;
|
|
37
|
+
async function setupAllRLS() {
|
|
38
|
+
for (const tableName of registeredTables) {
|
|
39
|
+
await setupRLSForTable(tableName);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.setupAllRLS = setupAllRLS;
|
|
43
|
+
async function setupSchemaForTenant(tenantSlug, tables) {
|
|
44
|
+
return (0, lib_1.withTransaction)((0, connection_js_1.getPool)(), async (client) => {
|
|
45
|
+
await (0, db_adapters_1.createSchema)(client, tenantSlug);
|
|
46
|
+
if (tables && tables.length > 0) {
|
|
47
|
+
const schemaName = (0, db_adapters_1.tenantSchemaName)(tenantSlug);
|
|
48
|
+
for (const tableName of tables) {
|
|
49
|
+
await (0, db_adapters_1.replicateTableToSchema)(client, validateTableName(tableName), schemaName);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
exports.setupSchemaForTenant = setupSchemaForTenant;
|
|
55
|
+
async function teardownSchemaForTenant(tenantSlug) {
|
|
56
|
+
return (0, lib_1.withTransaction)((0, connection_js_1.getPool)(), async (client) => {
|
|
57
|
+
await (0, db_adapters_1.dropSchema)(client, tenantSlug);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
exports.teardownSchemaForTenant = teardownSchemaForTenant;
|
|
61
|
+
/**
|
|
62
|
+
* Creates the dedicated database for a DB_PER_TENANT tenant.
|
|
63
|
+
*
|
|
64
|
+
* CREATE DATABASE cannot run inside a transaction, so this function uses a
|
|
65
|
+
* standalone client from the pool (no BEGIN/COMMIT wrapping).
|
|
66
|
+
*/
|
|
67
|
+
async function setupDatabaseForTenant(tenantSlug, templateDb) {
|
|
68
|
+
const pool = (0, connection_js_1.getPool)();
|
|
69
|
+
const client = await pool.connect();
|
|
70
|
+
try {
|
|
71
|
+
const exists = await (0, db_adapters_1.databaseExists)(client, tenantSlug);
|
|
72
|
+
if (!exists) {
|
|
73
|
+
await (0, db_adapters_1.createDatabase)(client, tenantSlug, templateDb);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
client.release();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.setupDatabaseForTenant = setupDatabaseForTenant;
|
|
81
|
+
/**
|
|
82
|
+
* Tears down the DB_PER_TENANT isolation for a tenant.
|
|
83
|
+
*
|
|
84
|
+
* This closes any open connection pools for the tenant database but does NOT
|
|
85
|
+
* drop the database — data preservation is left to the operator.
|
|
86
|
+
* To actually drop the database, use dropDatabase() from @stratum-hq/db-adapters directly.
|
|
87
|
+
*/
|
|
88
|
+
async function teardownDatabaseForTenant(_tenantSlug) {
|
|
89
|
+
// Pool cleanup is managed externally via DatabasePoolManager.closePool().
|
|
90
|
+
// This function is intentionally a no-op at the isolation-service level
|
|
91
|
+
// so that callers can decide whether to drop the database independently.
|
|
92
|
+
}
|
|
93
|
+
exports.teardownDatabaseForTenant = teardownDatabaseForTenant;
|
|
94
|
+
//# sourceMappingURL=isolation-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isolation-service.js","sourceRoot":"","sources":["../../src/services/isolation-service.ts"],"names":[],"mappings":"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"}
|