@stratum-hq/lib 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 (109) hide show
  1. package/dist/__tests__/crypto.test.d.ts +2 -0
  2. package/dist/__tests__/crypto.test.d.ts.map +1 -0
  3. package/dist/__tests__/crypto.test.js +41 -0
  4. package/dist/__tests__/crypto.test.js.map +1 -0
  5. package/dist/crypto.d.ts +7 -0
  6. package/dist/crypto.d.ts.map +1 -0
  7. package/dist/crypto.js +74 -0
  8. package/dist/crypto.js.map +1 -0
  9. package/dist/index.d.ts +11 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +13 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/pool-helpers.d.ts +4 -0
  14. package/dist/pool-helpers.d.ts.map +1 -0
  15. package/dist/pool-helpers.js +31 -0
  16. package/dist/pool-helpers.js.map +1 -0
  17. package/dist/services/__tests__/audit-service.test.d.ts +2 -0
  18. package/dist/services/__tests__/audit-service.test.d.ts.map +1 -0
  19. package/dist/services/__tests__/audit-service.test.js +220 -0
  20. package/dist/services/__tests__/audit-service.test.js.map +1 -0
  21. package/dist/services/__tests__/config-service.test.d.ts +2 -0
  22. package/dist/services/__tests__/config-service.test.d.ts.map +1 -0
  23. package/dist/services/__tests__/config-service.test.js +555 -0
  24. package/dist/services/__tests__/config-service.test.js.map +1 -0
  25. package/dist/services/__tests__/consent-service.test.d.ts +2 -0
  26. package/dist/services/__tests__/consent-service.test.d.ts.map +1 -0
  27. package/dist/services/__tests__/consent-service.test.js +159 -0
  28. package/dist/services/__tests__/consent-service.test.js.map +1 -0
  29. package/dist/services/__tests__/event-service.test.d.ts +2 -0
  30. package/dist/services/__tests__/event-service.test.d.ts.map +1 -0
  31. package/dist/services/__tests__/event-service.test.js +151 -0
  32. package/dist/services/__tests__/event-service.test.js.map +1 -0
  33. package/dist/services/__tests__/permission-service.test.d.ts +2 -0
  34. package/dist/services/__tests__/permission-service.test.d.ts.map +1 -0
  35. package/dist/services/__tests__/permission-service.test.js +470 -0
  36. package/dist/services/__tests__/permission-service.test.js.map +1 -0
  37. package/dist/services/__tests__/retention-service.test.d.ts +2 -0
  38. package/dist/services/__tests__/retention-service.test.d.ts.map +1 -0
  39. package/dist/services/__tests__/retention-service.test.js +126 -0
  40. package/dist/services/__tests__/retention-service.test.js.map +1 -0
  41. package/dist/services/__tests__/tenant-service.test.d.ts +2 -0
  42. package/dist/services/__tests__/tenant-service.test.d.ts.map +1 -0
  43. package/dist/services/__tests__/tenant-service.test.js +451 -0
  44. package/dist/services/__tests__/tenant-service.test.js.map +1 -0
  45. package/dist/services/__tests__/test-helpers.d.ts +9 -0
  46. package/dist/services/__tests__/test-helpers.d.ts.map +1 -0
  47. package/dist/services/__tests__/test-helpers.js +20 -0
  48. package/dist/services/__tests__/test-helpers.js.map +1 -0
  49. package/dist/services/__tests__/webhook-service.test.d.ts +2 -0
  50. package/dist/services/__tests__/webhook-service.test.d.ts.map +1 -0
  51. package/dist/services/__tests__/webhook-service.test.js +174 -0
  52. package/dist/services/__tests__/webhook-service.test.js.map +1 -0
  53. package/dist/services/api-key-service.d.ts +61 -0
  54. package/dist/services/api-key-service.d.ts.map +1 -0
  55. package/dist/services/api-key-service.js +164 -0
  56. package/dist/services/api-key-service.js.map +1 -0
  57. package/dist/services/audit-service.d.ts +6 -0
  58. package/dist/services/audit-service.d.ts.map +1 -0
  59. package/dist/services/audit-service.js +85 -0
  60. package/dist/services/audit-service.js.map +1 -0
  61. package/dist/services/config-service.d.ts +33 -0
  62. package/dist/services/config-service.d.ts.map +1 -0
  63. package/dist/services/config-service.js +271 -0
  64. package/dist/services/config-service.js.map +1 -0
  65. package/dist/services/consent-service.d.ts +7 -0
  66. package/dist/services/consent-service.d.ts.map +1 -0
  67. package/dist/services/consent-service.js +71 -0
  68. package/dist/services/consent-service.js.map +1 -0
  69. package/dist/services/event-service.d.ts +39 -0
  70. package/dist/services/event-service.d.ts.map +1 -0
  71. package/dist/services/event-service.js +331 -0
  72. package/dist/services/event-service.js.map +1 -0
  73. package/dist/services/key-rotation-service.d.ts +15 -0
  74. package/dist/services/key-rotation-service.d.ts.map +1 -0
  75. package/dist/services/key-rotation-service.js +41 -0
  76. package/dist/services/key-rotation-service.js.map +1 -0
  77. package/dist/services/permission-service.d.ts +21 -0
  78. package/dist/services/permission-service.d.ts.map +1 -0
  79. package/dist/services/permission-service.js +213 -0
  80. package/dist/services/permission-service.js.map +1 -0
  81. package/dist/services/region-service.d.ts +9 -0
  82. package/dist/services/region-service.d.ts.map +1 -0
  83. package/dist/services/region-service.js +105 -0
  84. package/dist/services/region-service.js.map +1 -0
  85. package/dist/services/retention-service.d.ts +19 -0
  86. package/dist/services/retention-service.d.ts.map +1 -0
  87. package/dist/services/retention-service.js +88 -0
  88. package/dist/services/retention-service.js.map +1 -0
  89. package/dist/services/role-service.d.ts +35 -0
  90. package/dist/services/role-service.d.ts.map +1 -0
  91. package/dist/services/role-service.js +101 -0
  92. package/dist/services/role-service.js.map +1 -0
  93. package/dist/services/tenant-service.d.ts +25 -0
  94. package/dist/services/tenant-service.d.ts.map +1 -0
  95. package/dist/services/tenant-service.js +335 -0
  96. package/dist/services/tenant-service.js.map +1 -0
  97. package/dist/services/webhook-service.d.ts +12 -0
  98. package/dist/services/webhook-service.d.ts.map +1 -0
  99. package/dist/services/webhook-service.js +129 -0
  100. package/dist/services/webhook-service.js.map +1 -0
  101. package/dist/stratum.d.ts +107 -0
  102. package/dist/stratum.d.ts.map +1 -0
  103. package/dist/stratum.js +531 -0
  104. package/dist/stratum.js.map +1 -0
  105. package/dist/telemetry.d.ts +27 -0
  106. package/dist/telemetry.d.ts.map +1 -0
  107. package/dist/telemetry.js +94 -0
  108. package/dist/telemetry.js.map +1 -0
  109. package/package.json +46 -0
@@ -0,0 +1,331 @@
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.listFailedDeliveries = exports.getDeliveryStats = exports.retryDelivery = exports.retryFailedDeliveries = exports.processDeliveries = exports.deliverWebhook = exports.emitEvent = void 0;
7
+ const node_crypto_1 = __importDefault(require("node:crypto"));
8
+ const promises_1 = __importDefault(require("node:dns/promises"));
9
+ const pool_helpers_js_1 = require("../pool-helpers.js");
10
+ const webhook_service_js_1 = require("./webhook-service.js");
11
+ const MAX_ATTEMPTS = 5;
12
+ /** Blocked IP ranges for SSRF protection */
13
+ const BLOCKED_IP_PATTERNS = [
14
+ /^127\./, // loopback
15
+ /^10\./, // RFC 1918
16
+ /^172\.(1[6-9]|2\d|3[01])\./, // RFC 1918
17
+ /^192\.168\./, // RFC 1918
18
+ /^169\.254\./, // link-local
19
+ /^0\./, // current network
20
+ /^::1$/, // IPv6 loopback
21
+ /^fc00:/, // IPv6 unique local
22
+ /^fe80:/, // IPv6 link-local
23
+ /^fd00:ec2:/, // AWS IMDSv2 IPv6
24
+ ];
25
+ const BLOCKED_HOSTNAMES = new Set([
26
+ "localhost",
27
+ "localhost.localdomain",
28
+ "metadata.google.internal", // GCP metadata
29
+ "metadata.goog", // GCP alternate
30
+ "169.254.169.254", // AWS/Azure metadata
31
+ ]);
32
+ /** Check if an IP matches any blocked private/reserved range. */
33
+ function isBlockedIp(ip) {
34
+ return BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(ip));
35
+ }
36
+ /** Validates that a webhook URL does not target internal/private networks. */
37
+ function validateWebhookUrl(url) {
38
+ let parsed;
39
+ try {
40
+ parsed = new URL(url);
41
+ }
42
+ catch {
43
+ throw new Error(`Invalid webhook URL: ${url}`);
44
+ }
45
+ // Only allow http/https
46
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
47
+ throw new Error(`Webhook URL must use http or https: ${url}`);
48
+ }
49
+ const hostname = parsed.hostname.toLowerCase();
50
+ // Block known internal hostnames
51
+ if (BLOCKED_HOSTNAMES.has(hostname)) {
52
+ throw new Error(`Webhook URL targets a blocked host: ${hostname}`);
53
+ }
54
+ // Block private IP ranges
55
+ if (isBlockedIp(hostname)) {
56
+ throw new Error(`Webhook URL targets a private/reserved IP range: ${hostname}`);
57
+ }
58
+ }
59
+ /**
60
+ * DNS-rebinding-safe validation: resolve hostname and check all resolved IPs
61
+ * against blocked ranges. Call this at delivery time, not just registration.
62
+ */
63
+ async function validateWebhookUrlWithDns(url) {
64
+ // First run the synchronous checks
65
+ validateWebhookUrl(url);
66
+ const parsed = new URL(url);
67
+ const hostname = parsed.hostname.toLowerCase();
68
+ // If hostname is already an IP literal, synchronous check is sufficient
69
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname) || hostname.startsWith("[")) {
70
+ return;
71
+ }
72
+ // Resolve DNS (both A and AAAA records) and validate all returned IPs
73
+ let addresses;
74
+ try {
75
+ const [v4Result, v6Result] = await Promise.allSettled([
76
+ promises_1.default.resolve4(hostname),
77
+ promises_1.default.resolve6(hostname),
78
+ ]);
79
+ addresses = [
80
+ ...(v4Result.status === "fulfilled" ? v4Result.value : []),
81
+ ...(v6Result.status === "fulfilled" ? v6Result.value : []),
82
+ ];
83
+ if (addresses.length === 0) {
84
+ throw new Error("No DNS records found");
85
+ }
86
+ }
87
+ catch {
88
+ // DNS resolution failed — fail closed to prevent SSRF via DNS rebinding
89
+ throw new Error(`DNS resolution failed for webhook host: ${hostname}`);
90
+ }
91
+ for (const ip of addresses) {
92
+ if (isBlockedIp(ip)) {
93
+ throw new Error(`Webhook URL hostname ${hostname} resolves to blocked IP ${ip}`);
94
+ }
95
+ }
96
+ }
97
+ function signPayload(secret, payload) {
98
+ return ("sha256=" +
99
+ node_crypto_1.default.createHmac("sha256", secret).update(payload).digest("hex"));
100
+ }
101
+ function retryDelayMs(attempts) {
102
+ // attempts^2 * 5000ms: 5s, 20s, 45s, 80s, 125s
103
+ return Math.pow(attempts, 2) * 5000;
104
+ }
105
+ async function emitEvent(pool, type, tenantId, data) {
106
+ // Insert event record
107
+ const eventRow = await (0, pool_helpers_js_1.withClient)(pool, async (client) => {
108
+ const res = await client.query(`INSERT INTO webhook_events (type, tenant_id, data)
109
+ VALUES ($1, $2, $3)
110
+ RETURNING *`, [type, tenantId, JSON.stringify(data)]);
111
+ return res.rows[0];
112
+ });
113
+ // Find matching webhooks
114
+ const webhooks = await (0, webhook_service_js_1.getWebhooksForEvent)(pool, type, tenantId);
115
+ if (webhooks.length === 0) {
116
+ return;
117
+ }
118
+ // Create delivery records for each matching webhook
119
+ await (0, pool_helpers_js_1.withClient)(pool, async (client) => {
120
+ for (const webhook of webhooks) {
121
+ await client.query(`INSERT INTO webhook_deliveries (webhook_id, event_id, status, attempts)
122
+ VALUES ($1, $2, 'pending', 0)`, [webhook.id, eventRow.id]);
123
+ }
124
+ });
125
+ // Fire-and-forget delivery — do not await
126
+ processDeliveries(pool).catch(() => {
127
+ // Non-critical: delivery failures are tracked in webhook_deliveries
128
+ });
129
+ }
130
+ exports.emitEvent = emitEvent;
131
+ async function deliverWebhook(webhook, event, deliveryId) {
132
+ const payload = JSON.stringify({
133
+ id: event.id,
134
+ type: event.type,
135
+ tenant_id: event.tenant_id,
136
+ data: event.data,
137
+ created_at: event.created_at,
138
+ });
139
+ // SSRF protection: validate URL with DNS rebinding check before making request
140
+ await validateWebhookUrlWithDns(webhook.url);
141
+ const rawSecret = (0, webhook_service_js_1.decryptSecret)(webhook.secret_hash);
142
+ const signature = signPayload(rawSecret, payload);
143
+ const timestamp = new Date().toISOString();
144
+ try {
145
+ const response = await globalThis.fetch(webhook.url, {
146
+ method: "POST",
147
+ headers: {
148
+ "Content-Type": "application/json",
149
+ "X-Stratum-Event": event.type,
150
+ "X-Stratum-Signature": signature,
151
+ "X-Stratum-Delivery-ID": deliveryId,
152
+ "X-Stratum-Timestamp": timestamp,
153
+ },
154
+ body: payload,
155
+ signal: AbortSignal.timeout(10_000),
156
+ });
157
+ if (response.ok) {
158
+ return { success: true, responseCode: response.status, error: null };
159
+ }
160
+ return {
161
+ success: false,
162
+ responseCode: response.status,
163
+ error: `HTTP ${response.status}`,
164
+ };
165
+ }
166
+ catch (err) {
167
+ const message = err instanceof Error ? err.message : String(err);
168
+ return { success: false, responseCode: null, error: message };
169
+ }
170
+ }
171
+ exports.deliverWebhook = deliverWebhook;
172
+ async function processDeliveries(pool) {
173
+ // Find pending deliveries due for delivery
174
+ const deliveries = await (0, pool_helpers_js_1.withClient)(pool, async (client) => {
175
+ const res = await client.query(`SELECT * FROM webhook_deliveries
176
+ WHERE status = 'pending'
177
+ AND (next_retry_at IS NULL OR next_retry_at <= now())
178
+ ORDER BY created_at ASC
179
+ LIMIT 100`);
180
+ return res.rows;
181
+ });
182
+ for (const delivery of deliveries) {
183
+ await (0, pool_helpers_js_1.withTransaction)(pool, async (client) => {
184
+ // Re-fetch delivery with row lock
185
+ const lockRes = await client.query(`SELECT * FROM webhook_deliveries WHERE id = $1 AND status = 'pending' FOR UPDATE SKIP LOCKED`, [delivery.id]);
186
+ if (lockRes.rows.length === 0) {
187
+ return; // Already processed by another worker
188
+ }
189
+ const locked = lockRes.rows[0];
190
+ // Fetch the webhook and event
191
+ const webhookRes = await client.query(`SELECT * FROM webhooks WHERE id = $1`, [locked.webhook_id]);
192
+ const eventRes = await client.query(`SELECT * FROM webhook_events WHERE id = $1`, [locked.event_id]);
193
+ if (webhookRes.rows.length === 0 || eventRes.rows.length === 0) {
194
+ // Webhook or event was deleted; mark failed
195
+ await client.query(`UPDATE webhook_deliveries
196
+ SET status = 'failed', last_error = 'Webhook or event not found', completed_at = now()
197
+ WHERE id = $1`, [locked.id]);
198
+ return;
199
+ }
200
+ const webhook = webhookRes.rows[0];
201
+ const event = eventRes.rows[0];
202
+ const attempts = locked.attempts + 1;
203
+ const result = await deliverWebhook(webhook, event, locked.id);
204
+ if (result.success) {
205
+ await client.query(`UPDATE webhook_deliveries
206
+ SET status = 'success', attempts = $1, response_code = $2,
207
+ last_error = NULL, completed_at = now()
208
+ WHERE id = $3`, [attempts, result.responseCode, locked.id]);
209
+ }
210
+ else if (attempts >= MAX_ATTEMPTS) {
211
+ await client.query(`UPDATE webhook_deliveries
212
+ SET status = 'failed', attempts = $1, response_code = $2,
213
+ last_error = $3, completed_at = now()
214
+ WHERE id = $4`, [attempts, result.responseCode, result.error, locked.id]);
215
+ }
216
+ else {
217
+ const nextRetryMs = retryDelayMs(attempts);
218
+ await client.query(`UPDATE webhook_deliveries
219
+ SET attempts = $1, response_code = $2, last_error = $3,
220
+ next_retry_at = now() + ($4 || ' milliseconds')::interval
221
+ WHERE id = $5`, [attempts, result.responseCode, result.error, nextRetryMs, locked.id]);
222
+ }
223
+ }).catch(() => {
224
+ // Per-delivery failures are non-fatal; continue processing others
225
+ });
226
+ }
227
+ }
228
+ exports.processDeliveries = processDeliveries;
229
+ async function retryFailedDeliveries(pool, tenantId) {
230
+ const result = await (0, pool_helpers_js_1.withClient)(pool, async (client) => {
231
+ let query;
232
+ const params = [];
233
+ if (tenantId) {
234
+ query = `WITH updated AS (
235
+ UPDATE webhook_deliveries
236
+ SET status = 'pending', next_retry_at = NULL, attempts = 0
237
+ WHERE status = 'failed' AND webhook_id IN (SELECT id FROM webhooks WHERE tenant_id = $1)
238
+ RETURNING id
239
+ ) SELECT COUNT(*) as count FROM updated`;
240
+ params.push(tenantId);
241
+ }
242
+ else {
243
+ query = `WITH updated AS (
244
+ UPDATE webhook_deliveries
245
+ SET status = 'pending', next_retry_at = NULL, attempts = 0
246
+ WHERE status = 'failed'
247
+ RETURNING id
248
+ ) SELECT COUNT(*) as count FROM updated`;
249
+ }
250
+ const res = await client.query(query, params);
251
+ return parseInt(res.rows[0].count, 10);
252
+ });
253
+ if (result > 0) {
254
+ processDeliveries(pool).catch(() => { });
255
+ }
256
+ return result;
257
+ }
258
+ exports.retryFailedDeliveries = retryFailedDeliveries;
259
+ async function retryDelivery(pool, deliveryId) {
260
+ const updated = await (0, pool_helpers_js_1.withClient)(pool, async (client) => {
261
+ const res = await client.query(`UPDATE webhook_deliveries
262
+ SET status = 'pending', next_retry_at = NULL, attempts = 0
263
+ WHERE id = $1 AND status = 'failed'
264
+ RETURNING id`, [deliveryId]);
265
+ return res.rows.length > 0;
266
+ });
267
+ if (updated) {
268
+ processDeliveries(pool).catch(() => { });
269
+ }
270
+ return updated;
271
+ }
272
+ exports.retryDelivery = retryDelivery;
273
+ async function getDeliveryStats(pool, tenantId) {
274
+ return (0, pool_helpers_js_1.withClient)(pool, async (client) => {
275
+ let query;
276
+ const params = [];
277
+ if (tenantId) {
278
+ query = `SELECT wd.status, COUNT(*) as count FROM webhook_deliveries wd
279
+ JOIN webhooks w ON w.id = wd.webhook_id
280
+ WHERE w.tenant_id = $1 GROUP BY wd.status`;
281
+ params.push(tenantId);
282
+ }
283
+ else {
284
+ query = `SELECT status, COUNT(*) as count FROM webhook_deliveries GROUP BY status`;
285
+ }
286
+ const res = await client.query(query, params);
287
+ const counts = {};
288
+ let total = 0;
289
+ for (const row of res.rows) {
290
+ counts[row.status] = parseInt(row.count, 10);
291
+ total += counts[row.status];
292
+ }
293
+ return {
294
+ total,
295
+ pending: counts["pending"] ?? 0,
296
+ success: counts["success"] ?? 0,
297
+ failed: counts["failed"] ?? 0,
298
+ };
299
+ });
300
+ }
301
+ exports.getDeliveryStats = getDeliveryStats;
302
+ async function listFailedDeliveries(pool, limit = 100, tenantId) {
303
+ return (0, pool_helpers_js_1.withClient)(pool, async (client) => {
304
+ let query;
305
+ const params = [];
306
+ if (tenantId) {
307
+ query = `SELECT wd.*, we.type as event_type, we.tenant_id as event_tenant_id, w.url as webhook_url
308
+ FROM webhook_deliveries wd
309
+ JOIN webhook_events we ON we.id = wd.event_id
310
+ JOIN webhooks w ON w.id = wd.webhook_id
311
+ WHERE wd.status = 'failed' AND w.tenant_id = $1
312
+ ORDER BY wd.completed_at DESC
313
+ LIMIT $2`;
314
+ params.push(tenantId, limit);
315
+ }
316
+ else {
317
+ query = `SELECT wd.*, we.type as event_type, we.tenant_id as event_tenant_id, w.url as webhook_url
318
+ FROM webhook_deliveries wd
319
+ JOIN webhook_events we ON we.id = wd.event_id
320
+ JOIN webhooks w ON w.id = wd.webhook_id
321
+ WHERE wd.status = 'failed'
322
+ ORDER BY wd.completed_at DESC
323
+ LIMIT $1`;
324
+ params.push(limit);
325
+ }
326
+ const res = await client.query(query, params);
327
+ return res.rows;
328
+ });
329
+ }
330
+ exports.listFailedDeliveries = listFailedDeliveries;
331
+ //# sourceMappingURL=event-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-service.js","sourceRoot":"","sources":["../../src/services/event-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import pg from "pg";
2
+ export interface KeyRotationResult {
3
+ config_entries_rotated: number;
4
+ webhooks_rotated: number;
5
+ }
6
+ /**
7
+ * Re-encrypts all sensitive data (config entries and webhook secrets)
8
+ * from oldKey to newKey in a single transaction.
9
+ *
10
+ * This is a zero-downtime operation: the transaction ensures either
11
+ * all values are rotated or none are. After rotation, update the
12
+ * STRATUM_ENCRYPTION_KEY environment variable to the new key.
13
+ */
14
+ export declare function rotateEncryptionKey(pool: pg.Pool, oldKeyMaterial: string, newKeyMaterial: string): Promise<KeyRotationResult>;
15
+ //# sourceMappingURL=key-rotation-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-rotation-service.d.ts","sourceRoot":"","sources":["../../src/services/key-rotation-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,MAAM,WAAW,iBAAiB;IAChC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,iBAAiB,CAAC,CAwC5B"}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rotateEncryptionKey = void 0;
4
+ const pool_helpers_js_1 = require("../pool-helpers.js");
5
+ const crypto_js_1 = require("../crypto.js");
6
+ /**
7
+ * Re-encrypts all sensitive data (config entries and webhook secrets)
8
+ * from oldKey to newKey in a single transaction.
9
+ *
10
+ * This is a zero-downtime operation: the transaction ensures either
11
+ * all values are rotated or none are. After rotation, update the
12
+ * STRATUM_ENCRYPTION_KEY environment variable to the new key.
13
+ */
14
+ async function rotateEncryptionKey(pool, oldKeyMaterial, newKeyMaterial) {
15
+ return (0, pool_helpers_js_1.withTransaction)(pool, async (client) => {
16
+ let configCount = 0;
17
+ let webhookCount = 0;
18
+ // 1. Re-encrypt sensitive config entries
19
+ const configRes = await client.query(`SELECT id, value FROM config_entries WHERE sensitive = true`);
20
+ for (const row of configRes.rows) {
21
+ // value is stored as JSON string containing the encrypted blob
22
+ const encryptedBlob = JSON.parse(row.value);
23
+ const reEncrypted = (0, crypto_js_1.reEncrypt)(encryptedBlob, oldKeyMaterial, newKeyMaterial);
24
+ await client.query(`UPDATE config_entries SET value = $1, updated_at = now() WHERE id = $2`, [JSON.stringify(reEncrypted), row.id]);
25
+ configCount++;
26
+ }
27
+ // 2. Re-encrypt webhook secrets
28
+ const webhookRes = await client.query(`SELECT id, secret_hash FROM webhooks WHERE secret_hash IS NOT NULL`);
29
+ for (const row of webhookRes.rows) {
30
+ const reEncrypted = (0, crypto_js_1.reEncrypt)(row.secret_hash, oldKeyMaterial, newKeyMaterial);
31
+ await client.query(`UPDATE webhooks SET secret_hash = $1 WHERE id = $2`, [reEncrypted, row.id]);
32
+ webhookCount++;
33
+ }
34
+ return {
35
+ config_entries_rotated: configCount,
36
+ webhooks_rotated: webhookCount,
37
+ };
38
+ });
39
+ }
40
+ exports.rotateEncryptionKey = rotateEncryptionKey;
41
+ //# sourceMappingURL=key-rotation-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-rotation-service.js","sourceRoot":"","sources":["../../src/services/key-rotation-service.ts"],"names":[],"mappings":";;;AACA,wDAAqD;AACrD,4CAAyC;AAOzC;;;;;;;GAOG;AACI,KAAK,UAAU,mBAAmB,CACvC,IAAa,EACb,cAAsB,EACtB,cAAsB;IAEtB,OAAO,IAAA,iCAAe,EAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QAC5C,IAAI,WAAW,GAAG,CAAC,CAAC;QACpB,IAAI,YAAY,GAAG,CAAC,CAAC;QAErB,yCAAyC;QACzC,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,KAAK,CAClC,6DAA6D,CAC9D,CAAC;QAEF,KAAK,MAAM,GAAG,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC;YACjC,+DAA+D;YAC/D,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAW,CAAC;YACtD,MAAM,WAAW,GAAG,IAAA,qBAAS,EAAC,aAAa,EAAE,cAAc,EAAE,cAAc,CAAC,CAAC;YAC7E,MAAM,MAAM,CAAC,KAAK,CAChB,wEAAwE,EACxE,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CACtC,CAAC;YACF,WAAW,EAAE,CAAC;QAChB,CAAC;QAED,gCAAgC;QAChC,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,KAAK,CACnC,oEAAoE,CACrE,CAAC;QAEF,KAAK,MAAM,GAAG,IAAI,UAAU,CAAC,IAAI,EAAE,CAAC;YAClC,MAAM,WAAW,GAAG,IAAA,qBAAS,EAAC,GAAG,CAAC,WAAW,EAAE,cAAc,EAAE,cAAc,CAAC,CAAC;YAC/E,MAAM,MAAM,CAAC,KAAK,CAChB,oDAAoD,EACpD,CAAC,WAAW,EAAE,GAAG,CAAC,EAAE,CAAC,CACtB,CAAC;YACF,YAAY,EAAE,CAAC;QACjB,CAAC;QAED,OAAO;YACL,sBAAsB,EAAE,WAAW;YACnC,gBAAgB,EAAE,YAAY;SAC/B,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AA5CD,kDA4CC"}
@@ -0,0 +1,21 @@
1
+ import pg from "pg";
2
+ import { type PermissionPolicy, type CreatePermissionInput, type UpdatePermissionInput, type ResolvedPermission } from "@stratum-hq/core";
3
+ /**
4
+ * Resolve effective permissions for a tenant by batch-loading all ancestor
5
+ * permission policies in a single query and walking root→leaf.
6
+ */
7
+ export declare function resolvePermissions(pool: pg.Pool, tenantId: string): Promise<Record<string, ResolvedPermission>>;
8
+ /**
9
+ * Create a new permission policy for a tenant.
10
+ * Rejects if the key is already LOCKED by an ancestor.
11
+ */
12
+ export declare function createPermission(pool: pg.Pool, tenantId: string, input: CreatePermissionInput): Promise<PermissionPolicy>;
13
+ /**
14
+ * Update a permission policy, enforcing LOCKED constraints.
15
+ */
16
+ export declare function updatePermission(pool: pg.Pool, tenantId: string, policyId: string, input: UpdatePermissionInput): Promise<PermissionPolicy>;
17
+ /**
18
+ * Delete a permission policy, handling CASCADE / SOFT / PERMANENT revocation modes.
19
+ */
20
+ export declare function deletePermission(pool: pg.Pool, tenantId: string, policyId: string): Promise<void>;
21
+ //# sourceMappingURL=permission-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-service.d.ts","sourceRoot":"","sources":["../../src/services/permission-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EAQxB,MAAM,kBAAkB,CAAC;AAE1B;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CA2F7C;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CA0C3B;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAiE3B;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAuDf"}
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deletePermission = exports.updatePermission = exports.createPermission = exports.resolvePermissions = void 0;
4
+ const pool_helpers_js_1 = require("../pool-helpers.js");
5
+ const core_1 = require("@stratum-hq/core");
6
+ /**
7
+ * Resolve effective permissions for a tenant by batch-loading all ancestor
8
+ * permission policies in a single query and walking root→leaf.
9
+ */
10
+ async function resolvePermissions(pool, tenantId) {
11
+ return (0, pool_helpers_js_1.withClient)(pool, async (client) => {
12
+ // Load the tenant's ancestry path
13
+ const tenantRes = await client.query(`SELECT ancestry_path FROM tenants WHERE id = $1`, [tenantId]);
14
+ if (tenantRes.rows.length === 0) {
15
+ throw new core_1.TenantNotFoundError(tenantId);
16
+ }
17
+ const ancestryPath = tenantRes.rows[0].ancestry_path;
18
+ const ancestorIds = (0, core_1.parseAncestryPath)(ancestryPath);
19
+ // Include self
20
+ const allIds = [...ancestorIds, tenantId];
21
+ // Single query: batch-load all policies for the entire ancestor chain
22
+ const policiesRes = await client.query(`SELECT * FROM permission_policies WHERE tenant_id = ANY($1)`, [allIds]);
23
+ // Group by tenant_id, maintaining insertion order
24
+ const byTenant = new Map();
25
+ for (const id of allIds) {
26
+ byTenant.set(id, []);
27
+ }
28
+ for (const policy of policiesRes.rows) {
29
+ const list = byTenant.get(policy.tenant_id);
30
+ if (list) {
31
+ list.push(policy);
32
+ }
33
+ }
34
+ // Walk root→leaf applying mode semantics
35
+ const resolved = new Map();
36
+ for (const currentTenantId of allIds) {
37
+ const policies = byTenant.get(currentTenantId) ?? [];
38
+ for (const policy of policies) {
39
+ const existing = resolved.get(policy.key);
40
+ if (existing?.locked) {
41
+ // Key is LOCKED by an ancestor — descendants cannot override it
42
+ continue;
43
+ }
44
+ switch (policy.mode) {
45
+ case core_1.PermissionMode.LOCKED:
46
+ resolved.set(policy.key, {
47
+ policy_id: policy.id,
48
+ key: policy.key,
49
+ value: policy.value,
50
+ mode: policy.mode,
51
+ source_tenant_id: currentTenantId,
52
+ locked: true,
53
+ delegated: false,
54
+ });
55
+ break;
56
+ case core_1.PermissionMode.DELEGATED:
57
+ resolved.set(policy.key, {
58
+ policy_id: policy.id,
59
+ key: policy.key,
60
+ value: policy.value,
61
+ mode: policy.mode,
62
+ source_tenant_id: currentTenantId,
63
+ locked: false,
64
+ delegated: true,
65
+ });
66
+ break;
67
+ case core_1.PermissionMode.INHERITED:
68
+ default:
69
+ resolved.set(policy.key, {
70
+ policy_id: policy.id,
71
+ key: policy.key,
72
+ value: policy.value,
73
+ mode: policy.mode,
74
+ source_tenant_id: currentTenantId,
75
+ locked: false,
76
+ delegated: false,
77
+ });
78
+ break;
79
+ }
80
+ }
81
+ }
82
+ return Object.fromEntries(resolved);
83
+ });
84
+ }
85
+ exports.resolvePermissions = resolvePermissions;
86
+ /**
87
+ * Create a new permission policy for a tenant.
88
+ * Rejects if the key is already LOCKED by an ancestor.
89
+ */
90
+ async function createPermission(pool, tenantId, input) {
91
+ return (0, pool_helpers_js_1.withTransaction)(pool, async (client) => {
92
+ // Load ancestry to check for locked ancestor permissions
93
+ const tenantRes = await client.query(`SELECT ancestry_path FROM tenants WHERE id = $1`, [tenantId]);
94
+ if (tenantRes.rows.length === 0) {
95
+ throw new core_1.TenantNotFoundError(tenantId);
96
+ }
97
+ const ancestorIds = (0, core_1.parseAncestryPath)(tenantRes.rows[0].ancestry_path);
98
+ // Exclude self (ancestry_path does not include self)
99
+ if (ancestorIds.length > 0) {
100
+ const lockedRes = await client.query(`SELECT * FROM permission_policies
101
+ WHERE tenant_id = ANY($1)
102
+ AND key = $2
103
+ AND mode = $3`, [ancestorIds, input.key, core_1.PermissionMode.LOCKED]);
104
+ if (lockedRes.rows.length > 0) {
105
+ const locker = lockedRes.rows[0];
106
+ throw new core_1.PermissionLockedError(input.key, locker.source_tenant_id);
107
+ }
108
+ }
109
+ const res = await client.query(`INSERT INTO permission_policies (tenant_id, key, value, mode, revocation_mode, source_tenant_id)
110
+ VALUES ($1, $2, $3, $4, $5, $1)
111
+ RETURNING *`, [
112
+ tenantId,
113
+ input.key,
114
+ JSON.stringify(input.value ?? true),
115
+ input.mode ?? core_1.PermissionMode.INHERITED,
116
+ input.revocation_mode ?? core_1.RevocationMode.CASCADE,
117
+ ]);
118
+ return res.rows[0];
119
+ });
120
+ }
121
+ exports.createPermission = createPermission;
122
+ /**
123
+ * Update a permission policy, enforcing LOCKED constraints.
124
+ */
125
+ async function updatePermission(pool, tenantId, policyId, input) {
126
+ return (0, pool_helpers_js_1.withTransaction)(pool, async (client) => {
127
+ const existing = await client.query(`SELECT * FROM permission_policies WHERE id = $1 AND tenant_id = $2`, [policyId, tenantId]);
128
+ if (existing.rows.length === 0) {
129
+ throw new core_1.PermissionNotFoundError(policyId);
130
+ }
131
+ const current = existing.rows[0];
132
+ // Check if an ancestor has LOCKED this key
133
+ const tenantRes = await client.query(`SELECT ancestry_path FROM tenants WHERE id = $1`, [tenantId]);
134
+ if (tenantRes.rows.length === 0) {
135
+ throw new core_1.TenantNotFoundError(tenantId);
136
+ }
137
+ const ancestorIds = (0, core_1.parseAncestryPath)(tenantRes.rows[0].ancestry_path);
138
+ if (ancestorIds.length > 0) {
139
+ const lockedRes = await client.query(`SELECT * FROM permission_policies
140
+ WHERE tenant_id = ANY($1)
141
+ AND key = $2
142
+ AND mode = $3`, [ancestorIds, current.key, core_1.PermissionMode.LOCKED]);
143
+ if (lockedRes.rows.length > 0) {
144
+ const locker = lockedRes.rows[0];
145
+ throw new core_1.PermissionLockedError(current.key, locker.source_tenant_id);
146
+ }
147
+ }
148
+ const sets = [];
149
+ const values = [];
150
+ let idx = 1;
151
+ if (input.value !== undefined) {
152
+ sets.push(`value = $${idx++}`);
153
+ values.push(JSON.stringify(input.value));
154
+ }
155
+ if (input.mode !== undefined) {
156
+ sets.push(`mode = $${idx++}`);
157
+ values.push(input.mode);
158
+ }
159
+ if (input.revocation_mode !== undefined) {
160
+ sets.push(`revocation_mode = $${idx++}`);
161
+ values.push(input.revocation_mode);
162
+ }
163
+ if (sets.length === 0) {
164
+ return current;
165
+ }
166
+ sets.push(`updated_at = now()`);
167
+ values.push(policyId);
168
+ const res = await client.query(`UPDATE permission_policies SET ${sets.join(", ")} WHERE id = $${idx} RETURNING *`, values);
169
+ return res.rows[0];
170
+ });
171
+ }
172
+ exports.updatePermission = updatePermission;
173
+ /**
174
+ * Delete a permission policy, handling CASCADE / SOFT / PERMANENT revocation modes.
175
+ */
176
+ async function deletePermission(pool, tenantId, policyId) {
177
+ return (0, pool_helpers_js_1.withTransaction)(pool, async (client) => {
178
+ const existing = await client.query(`SELECT * FROM permission_policies WHERE id = $1 AND tenant_id = $2`, [policyId, tenantId]);
179
+ if (existing.rows.length === 0) {
180
+ throw new core_1.PermissionNotFoundError(policyId);
181
+ }
182
+ const policy = existing.rows[0];
183
+ switch (policy.revocation_mode) {
184
+ case core_1.RevocationMode.PERMANENT:
185
+ throw new core_1.PermissionRevocationDeniedError(policy.key);
186
+ case core_1.RevocationMode.CASCADE: {
187
+ // Recursively delete from all descendants
188
+ // Find all descendants via ancestry_ltree
189
+ const descendantsRes = await client.query(`SELECT id FROM tenants
190
+ WHERE ancestry_ltree <@ (SELECT ancestry_ltree FROM tenants WHERE id = $1)
191
+ AND id != $1`, [tenantId]);
192
+ const descendantIds = descendantsRes.rows.map((r) => r.id);
193
+ if (descendantIds.length > 0) {
194
+ await client.query(`DELETE FROM permission_policies
195
+ WHERE tenant_id = ANY($1) AND key = $2`, [descendantIds, policy.key]);
196
+ }
197
+ await client.query(`DELETE FROM permission_policies WHERE id = $1`, [policyId]);
198
+ break;
199
+ }
200
+ case core_1.RevocationMode.SOFT:
201
+ default: {
202
+ // Freeze current values: mark all descendant policies as no longer
203
+ // inheriting from this policy by converting them to LOCKED snapshots
204
+ // but keep their current values intact. Simply delete this policy
205
+ // so descendants inherit the next ancestor up (or nothing).
206
+ await client.query(`DELETE FROM permission_policies WHERE id = $1`, [policyId]);
207
+ break;
208
+ }
209
+ }
210
+ });
211
+ }
212
+ exports.deletePermission = deletePermission;
213
+ //# sourceMappingURL=permission-service.js.map