@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.
- package/dist/__tests__/crypto.test.d.ts +2 -0
- package/dist/__tests__/crypto.test.d.ts.map +1 -0
- package/dist/__tests__/crypto.test.js +41 -0
- package/dist/__tests__/crypto.test.js.map +1 -0
- package/dist/crypto.d.ts +7 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +74 -0
- package/dist/crypto.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/pool-helpers.d.ts +4 -0
- package/dist/pool-helpers.d.ts.map +1 -0
- package/dist/pool-helpers.js +31 -0
- package/dist/pool-helpers.js.map +1 -0
- package/dist/services/__tests__/audit-service.test.d.ts +2 -0
- package/dist/services/__tests__/audit-service.test.d.ts.map +1 -0
- package/dist/services/__tests__/audit-service.test.js +220 -0
- package/dist/services/__tests__/audit-service.test.js.map +1 -0
- package/dist/services/__tests__/config-service.test.d.ts +2 -0
- package/dist/services/__tests__/config-service.test.d.ts.map +1 -0
- package/dist/services/__tests__/config-service.test.js +555 -0
- package/dist/services/__tests__/config-service.test.js.map +1 -0
- package/dist/services/__tests__/consent-service.test.d.ts +2 -0
- package/dist/services/__tests__/consent-service.test.d.ts.map +1 -0
- package/dist/services/__tests__/consent-service.test.js +159 -0
- package/dist/services/__tests__/consent-service.test.js.map +1 -0
- package/dist/services/__tests__/event-service.test.d.ts +2 -0
- package/dist/services/__tests__/event-service.test.d.ts.map +1 -0
- package/dist/services/__tests__/event-service.test.js +151 -0
- package/dist/services/__tests__/event-service.test.js.map +1 -0
- package/dist/services/__tests__/permission-service.test.d.ts +2 -0
- package/dist/services/__tests__/permission-service.test.d.ts.map +1 -0
- package/dist/services/__tests__/permission-service.test.js +470 -0
- package/dist/services/__tests__/permission-service.test.js.map +1 -0
- package/dist/services/__tests__/retention-service.test.d.ts +2 -0
- package/dist/services/__tests__/retention-service.test.d.ts.map +1 -0
- package/dist/services/__tests__/retention-service.test.js +126 -0
- package/dist/services/__tests__/retention-service.test.js.map +1 -0
- package/dist/services/__tests__/tenant-service.test.d.ts +2 -0
- package/dist/services/__tests__/tenant-service.test.d.ts.map +1 -0
- package/dist/services/__tests__/tenant-service.test.js +451 -0
- package/dist/services/__tests__/tenant-service.test.js.map +1 -0
- package/dist/services/__tests__/test-helpers.d.ts +9 -0
- package/dist/services/__tests__/test-helpers.d.ts.map +1 -0
- package/dist/services/__tests__/test-helpers.js +20 -0
- package/dist/services/__tests__/test-helpers.js.map +1 -0
- package/dist/services/__tests__/webhook-service.test.d.ts +2 -0
- package/dist/services/__tests__/webhook-service.test.d.ts.map +1 -0
- package/dist/services/__tests__/webhook-service.test.js +174 -0
- package/dist/services/__tests__/webhook-service.test.js.map +1 -0
- package/dist/services/api-key-service.d.ts +61 -0
- package/dist/services/api-key-service.d.ts.map +1 -0
- package/dist/services/api-key-service.js +164 -0
- package/dist/services/api-key-service.js.map +1 -0
- package/dist/services/audit-service.d.ts +6 -0
- package/dist/services/audit-service.d.ts.map +1 -0
- package/dist/services/audit-service.js +85 -0
- package/dist/services/audit-service.js.map +1 -0
- package/dist/services/config-service.d.ts +33 -0
- package/dist/services/config-service.d.ts.map +1 -0
- package/dist/services/config-service.js +271 -0
- package/dist/services/config-service.js.map +1 -0
- package/dist/services/consent-service.d.ts +7 -0
- package/dist/services/consent-service.d.ts.map +1 -0
- package/dist/services/consent-service.js +71 -0
- package/dist/services/consent-service.js.map +1 -0
- package/dist/services/event-service.d.ts +39 -0
- package/dist/services/event-service.d.ts.map +1 -0
- package/dist/services/event-service.js +331 -0
- package/dist/services/event-service.js.map +1 -0
- package/dist/services/key-rotation-service.d.ts +15 -0
- package/dist/services/key-rotation-service.d.ts.map +1 -0
- package/dist/services/key-rotation-service.js +41 -0
- package/dist/services/key-rotation-service.js.map +1 -0
- package/dist/services/permission-service.d.ts +21 -0
- package/dist/services/permission-service.d.ts.map +1 -0
- package/dist/services/permission-service.js +213 -0
- package/dist/services/permission-service.js.map +1 -0
- package/dist/services/region-service.d.ts +9 -0
- package/dist/services/region-service.d.ts.map +1 -0
- package/dist/services/region-service.js +105 -0
- package/dist/services/region-service.js.map +1 -0
- package/dist/services/retention-service.d.ts +19 -0
- package/dist/services/retention-service.d.ts.map +1 -0
- package/dist/services/retention-service.js +88 -0
- package/dist/services/retention-service.js.map +1 -0
- package/dist/services/role-service.d.ts +35 -0
- package/dist/services/role-service.d.ts.map +1 -0
- package/dist/services/role-service.js +101 -0
- package/dist/services/role-service.js.map +1 -0
- package/dist/services/tenant-service.d.ts +25 -0
- package/dist/services/tenant-service.d.ts.map +1 -0
- package/dist/services/tenant-service.js +335 -0
- package/dist/services/tenant-service.js.map +1 -0
- package/dist/services/webhook-service.d.ts +12 -0
- package/dist/services/webhook-service.d.ts.map +1 -0
- package/dist/services/webhook-service.js +129 -0
- package/dist/services/webhook-service.js.map +1 -0
- package/dist/stratum.d.ts +107 -0
- package/dist/stratum.d.ts.map +1 -0
- package/dist/stratum.js +531 -0
- package/dist/stratum.js.map +1 -0
- package/dist/telemetry.d.ts +27 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +94 -0
- package/dist/telemetry.js.map +1 -0
- 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
|