@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.
Files changed (133) hide show
  1. package/dist/__tests__/api-keys.test.d.ts +2 -0
  2. package/dist/__tests__/api-keys.test.d.ts.map +1 -0
  3. package/dist/__tests__/api-keys.test.js +240 -0
  4. package/dist/__tests__/api-keys.test.js.map +1 -0
  5. package/dist/__tests__/auth.test.d.ts +2 -0
  6. package/dist/__tests__/auth.test.d.ts.map +1 -0
  7. package/dist/__tests__/auth.test.js +227 -0
  8. package/dist/__tests__/auth.test.js.map +1 -0
  9. package/dist/__tests__/config.test.d.ts +2 -0
  10. package/dist/__tests__/config.test.d.ts.map +1 -0
  11. package/dist/__tests__/config.test.js +201 -0
  12. package/dist/__tests__/config.test.js.map +1 -0
  13. package/dist/__tests__/tenants.test.d.ts +2 -0
  14. package/dist/__tests__/tenants.test.d.ts.map +1 -0
  15. package/dist/__tests__/tenants.test.js +214 -0
  16. package/dist/__tests__/tenants.test.js.map +1 -0
  17. package/dist/__tests__/test-helpers.d.ts +71 -0
  18. package/dist/__tests__/test-helpers.d.ts.map +1 -0
  19. package/dist/__tests__/test-helpers.js +172 -0
  20. package/dist/__tests__/test-helpers.js.map +1 -0
  21. package/dist/app.d.ts +3 -0
  22. package/dist/app.d.ts.map +1 -0
  23. package/dist/app.js +114 -0
  24. package/dist/app.js.map +1 -0
  25. package/dist/config.d.ts +11 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +26 -0
  28. package/dist/config.js.map +1 -0
  29. package/dist/db/connection.d.ts +4 -0
  30. package/dist/db/connection.d.ts.map +1 -0
  31. package/dist/db/connection.js +30 -0
  32. package/dist/db/connection.js.map +1 -0
  33. package/dist/db/migrate.d.ts +3 -0
  34. package/dist/db/migrate.d.ts.map +1 -0
  35. package/dist/db/migrate.js +56 -0
  36. package/dist/db/migrate.js.map +1 -0
  37. package/dist/db/regional-pool-router.d.ts +9 -0
  38. package/dist/db/regional-pool-router.d.ts.map +1 -0
  39. package/dist/db/regional-pool-router.js +29 -0
  40. package/dist/db/regional-pool-router.js.map +1 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +36 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/middleware/auth.d.ts +21 -0
  46. package/dist/middleware/auth.d.ts.map +1 -0
  47. package/dist/middleware/auth.js +70 -0
  48. package/dist/middleware/auth.js.map +1 -0
  49. package/dist/middleware/authorize.d.ts +3 -0
  50. package/dist/middleware/authorize.d.ts.map +1 -0
  51. package/dist/middleware/authorize.js +54 -0
  52. package/dist/middleware/authorize.js.map +1 -0
  53. package/dist/middleware/error-handler.d.ts +3 -0
  54. package/dist/middleware/error-handler.d.ts.map +1 -0
  55. package/dist/middleware/error-handler.js +48 -0
  56. package/dist/middleware/error-handler.js.map +1 -0
  57. package/dist/middleware/per-key-rate-limit.d.ts +3 -0
  58. package/dist/middleware/per-key-rate-limit.d.ts.map +1 -0
  59. package/dist/middleware/per-key-rate-limit.js +62 -0
  60. package/dist/middleware/per-key-rate-limit.js.map +1 -0
  61. package/dist/middleware/rate-limit-redis.d.ts +16 -0
  62. package/dist/middleware/rate-limit-redis.d.ts.map +1 -0
  63. package/dist/middleware/rate-limit-redis.js +118 -0
  64. package/dist/middleware/rate-limit-redis.js.map +1 -0
  65. package/dist/middleware/telemetry.d.ts +14 -0
  66. package/dist/middleware/telemetry.d.ts.map +1 -0
  67. package/dist/middleware/telemetry.js +81 -0
  68. package/dist/middleware/telemetry.js.map +1 -0
  69. package/dist/middleware/tenant-scope.d.ts +20 -0
  70. package/dist/middleware/tenant-scope.d.ts.map +1 -0
  71. package/dist/middleware/tenant-scope.js +60 -0
  72. package/dist/middleware/tenant-scope.js.map +1 -0
  73. package/dist/openapi.d.ts +3 -0
  74. package/dist/openapi.d.ts.map +1 -0
  75. package/dist/openapi.js +44 -0
  76. package/dist/openapi.js.map +1 -0
  77. package/dist/redis.d.ts +16 -0
  78. package/dist/redis.d.ts.map +1 -0
  79. package/dist/redis.js +71 -0
  80. package/dist/redis.js.map +1 -0
  81. package/dist/routes/api-keys.d.ts +4 -0
  82. package/dist/routes/api-keys.d.ts.map +1 -0
  83. package/dist/routes/api-keys.js +78 -0
  84. package/dist/routes/api-keys.js.map +1 -0
  85. package/dist/routes/audit-logs.d.ts +6 -0
  86. package/dist/routes/audit-logs.d.ts.map +1 -0
  87. package/dist/routes/audit-logs.js +43 -0
  88. package/dist/routes/audit-logs.js.map +1 -0
  89. package/dist/routes/config-diff.d.ts +4 -0
  90. package/dist/routes/config-diff.d.ts.map +1 -0
  91. package/dist/routes/config-diff.js +33 -0
  92. package/dist/routes/config-diff.js.map +1 -0
  93. package/dist/routes/config.d.ts +4 -0
  94. package/dist/routes/config.d.ts.map +1 -0
  95. package/dist/routes/config.js +54 -0
  96. package/dist/routes/config.js.map +1 -0
  97. package/dist/routes/consent.d.ts +4 -0
  98. package/dist/routes/consent.d.ts.map +1 -0
  99. package/dist/routes/consent.js +45 -0
  100. package/dist/routes/consent.js.map +1 -0
  101. package/dist/routes/health.d.ts +9 -0
  102. package/dist/routes/health.d.ts.map +1 -0
  103. package/dist/routes/health.js +32 -0
  104. package/dist/routes/health.js.map +1 -0
  105. package/dist/routes/maintenance.d.ts +4 -0
  106. package/dist/routes/maintenance.d.ts.map +1 -0
  107. package/dist/routes/maintenance.js +35 -0
  108. package/dist/routes/maintenance.js.map +1 -0
  109. package/dist/routes/permissions.d.ts +4 -0
  110. package/dist/routes/permissions.d.ts.map +1 -0
  111. package/dist/routes/permissions.js +36 -0
  112. package/dist/routes/permissions.js.map +1 -0
  113. package/dist/routes/regions.d.ts +4 -0
  114. package/dist/routes/regions.d.ts.map +1 -0
  115. package/dist/routes/regions.js +38 -0
  116. package/dist/routes/regions.js.map +1 -0
  117. package/dist/routes/roles.d.ts +4 -0
  118. package/dist/routes/roles.d.ts.map +1 -0
  119. package/dist/routes/roles.js +95 -0
  120. package/dist/routes/roles.js.map +1 -0
  121. package/dist/routes/tenants.d.ts +4 -0
  122. package/dist/routes/tenants.d.ts.map +1 -0
  123. package/dist/routes/tenants.js +112 -0
  124. package/dist/routes/tenants.js.map +1 -0
  125. package/dist/routes/webhooks.d.ts +4 -0
  126. package/dist/routes/webhooks.d.ts.map +1 -0
  127. package/dist/routes/webhooks.js +116 -0
  128. package/dist/routes/webhooks.js.map +1 -0
  129. package/dist/services/isolation-service.d.ts +22 -0
  130. package/dist/services/isolation-service.d.ts.map +1 -0
  131. package/dist/services/isolation-service.js +94 -0
  132. package/dist/services/isolation-service.js.map +1 -0
  133. 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,4 @@
1
+ import { FastifyInstance } from "fastify";
2
+ import { Stratum } from "@stratum-hq/lib";
3
+ export declare function createRegionRoutes(stratum: Stratum): (app: FastifyInstance) => Promise<void>;
4
+ //# sourceMappingURL=regions.d.ts.map
@@ -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,4 @@
1
+ import { FastifyInstance } from "fastify";
2
+ import { Stratum } from "@stratum-hq/lib";
3
+ export declare function createRoleRoutes(stratum: Stratum): (app: FastifyInstance) => Promise<void>;
4
+ //# sourceMappingURL=roles.d.ts.map
@@ -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,4 @@
1
+ import { FastifyInstance } from "fastify";
2
+ import { Stratum } from "@stratum-hq/lib";
3
+ export declare function createTenantRoutes(stratum: Stratum): (app: FastifyInstance) => Promise<void>;
4
+ //# sourceMappingURL=tenants.d.ts.map
@@ -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,4 @@
1
+ import { FastifyInstance } from "fastify";
2
+ import { Stratum } from "@stratum-hq/lib";
3
+ export declare function createWebhookRoutes(stratum: Stratum): (app: FastifyInstance) => Promise<void>;
4
+ //# sourceMappingURL=webhooks.d.ts.map
@@ -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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"}