@getstrata/bootstrap 0.2.25 → 0.2.28
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/bootstrap/applicationRegistry.d.ts +1 -1
- package/dist/bootstrap/config.d.ts +1 -1
- package/dist/bootstrap/listeners/invalidateCacheOnModelWrite.d.ts +1 -1
- package/dist/entries/applicationRegistry.js +1 -1
- package/dist/entries/buildModuleRoutes.js +4 -1
- package/dist/entries/buildWebModuleRoutes.js +4 -1
- package/dist/entries/config.js +1 -1
- package/dist/entries/context.js +150 -126
- package/dist/entries/createSpaRoutes.js +62 -0
- package/dist/entries/createWebRoutes.js +4 -1
- package/dist/entries/dependencies.js +150 -126
- package/dist/entries/health.js +212 -0
- package/dist/entries/httpKernel.js +4 -1
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +118 -0
- package/dist/entries/metricsRoutes.js +16 -0
- package/dist/entries/providers/view.js +23 -1
- package/dist/entries/providers.js +227 -203
- package/dist/entries/web/routing.js +4 -1
- package/dist/index.js +443 -557
- package/package.json +22 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/bootstrap/public-api.ts
|
|
3
|
-
import { appSchedule as
|
|
3
|
+
import { appSchedule as appSchedule3, runDueScheduledTasks as runDueScheduledTasks2, Schedule as Schedule2 } from "@getstrata/core/scheduler/schedule";
|
|
4
4
|
|
|
5
5
|
// ../../src/core/scheduler/schedule.ts
|
|
6
6
|
class Schedule {
|
|
@@ -35,452 +35,37 @@ async function runDueScheduledTasks(schedule = appSchedule, now = new Date) {
|
|
|
35
35
|
return due.length;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// ../../src/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
env: process.env.APP_ENV ?? "local",
|
|
63
|
-
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
64
|
-
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
65
|
-
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// ../../src/core/database/boundConnection.ts
|
|
69
|
-
var boundConnectionHolder = {
|
|
70
|
-
connection: null
|
|
71
|
-
};
|
|
72
|
-
function getBoundDatabaseConnection() {
|
|
73
|
-
return boundConnectionHolder.connection;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ../../src/core/runtime/asyncContextStore.ts
|
|
77
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
78
|
-
function createAsyncContextStore(key) {
|
|
79
|
-
const symbol = Symbol.for(key);
|
|
80
|
-
const globalRecord = globalThis;
|
|
81
|
-
const existing = globalRecord[symbol];
|
|
82
|
-
if (existing) {
|
|
83
|
-
return existing;
|
|
84
|
-
}
|
|
85
|
-
const store = new AsyncLocalStorage;
|
|
86
|
-
globalRecord[symbol] = store;
|
|
87
|
-
return store;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ../../src/core/database/connectionContext.ts
|
|
91
|
-
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
92
|
-
function getActiveDatabaseConnection(fallback) {
|
|
93
|
-
return activeConnection.getStore() ?? fallback;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ../../src/core/database/queryProxy.ts
|
|
97
|
-
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
98
|
-
function createDatabaseQueryProxy(pool) {
|
|
99
|
-
function resolveDatabase() {
|
|
100
|
-
return getActiveDatabaseConnection(pool);
|
|
101
|
-
}
|
|
102
|
-
function resolveDatabaseForProperty(property) {
|
|
103
|
-
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
104
|
-
return pool;
|
|
105
|
-
}
|
|
106
|
-
return resolveDatabase();
|
|
107
|
-
}
|
|
108
|
-
return new Proxy(function database() {}, {
|
|
109
|
-
apply(_target, _thisArg, args) {
|
|
110
|
-
return resolveDatabase()(...args);
|
|
111
|
-
},
|
|
112
|
-
get(_target, property) {
|
|
113
|
-
const connection = resolveDatabaseForProperty(property);
|
|
114
|
-
const value = connection[property];
|
|
115
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ../../src/core/database/defaultConnection.ts
|
|
121
|
-
var defaultPool = {
|
|
122
|
-
connection: null
|
|
123
|
-
};
|
|
124
|
-
var defaultQuery = {
|
|
125
|
-
connection: null
|
|
126
|
-
};
|
|
127
|
-
function registerDefaultDatabasePool(connection) {
|
|
128
|
-
defaultPool.connection = connection;
|
|
129
|
-
defaultQuery.connection = createDatabaseQueryProxy(connection);
|
|
130
|
-
}
|
|
131
|
-
function getDefaultDatabaseQuery() {
|
|
132
|
-
if (!defaultQuery.connection) {
|
|
133
|
-
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
134
|
-
}
|
|
135
|
-
return defaultQuery.connection;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ../../src/core/database/repositoryConnection.ts
|
|
139
|
-
function resolveRepositoryConnection() {
|
|
140
|
-
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
141
|
-
}
|
|
142
|
-
var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
143
|
-
apply(_target, _thisArg, args) {
|
|
144
|
-
return resolveRepositoryConnection()(...args);
|
|
145
|
-
},
|
|
146
|
-
get(_target, property) {
|
|
147
|
-
const connection = resolveRepositoryConnection();
|
|
148
|
-
const value = connection[property];
|
|
149
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// ../../src/core/security/safeUrl.ts
|
|
154
|
-
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
155
|
-
|
|
156
|
-
// ../../src/core/errors/http.ts
|
|
157
|
-
class HttpError extends Error {
|
|
158
|
-
status;
|
|
159
|
-
details;
|
|
160
|
-
constructor(status, message, details) {
|
|
161
|
-
super(message);
|
|
162
|
-
this.name = new.target.name;
|
|
163
|
-
this.status = status;
|
|
164
|
-
this.details = details;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
class BadRequestError extends HttpError {
|
|
169
|
-
constructor(message = "Bad Request", details) {
|
|
170
|
-
super(400, message, details);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
class ConflictError extends HttpError {
|
|
174
|
-
constructor(message = "Conflict", details) {
|
|
175
|
-
super(409, message, details);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
class UnprocessableEntityError extends HttpError {
|
|
180
|
-
constructor(message = "Unprocessable Entity", details) {
|
|
181
|
-
super(422, message, details);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
class ForbiddenError extends HttpError {
|
|
185
|
-
constructor(message = "Forbidden", details) {
|
|
186
|
-
super(403, message, details);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
class UnauthorizedError extends HttpError {
|
|
191
|
-
constructor(message = "Unauthorized", details) {
|
|
192
|
-
super(401, message, details);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
class PreconditionFailedError extends HttpError {
|
|
196
|
-
constructor(message = "Precondition Failed", details) {
|
|
197
|
-
super(412, message, details);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ../../src/core/security/safeUrl.ts
|
|
202
|
-
var dnsLookup = dnsLookupImpl;
|
|
203
|
-
var BLOCKED_HOSTNAMES = new Set([
|
|
204
|
-
"localhost",
|
|
205
|
-
"127.0.0.1",
|
|
206
|
-
"0.0.0.0",
|
|
207
|
-
"::1",
|
|
208
|
-
"metadata.google.internal"
|
|
209
|
-
]);
|
|
210
|
-
function isPrivateIpv4(hostname) {
|
|
211
|
-
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
212
|
-
if (!match) {
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
216
|
-
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
const [a = 0, b = 0] = octets;
|
|
220
|
-
if (a === 10) {
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
if (a === 127) {
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
if (a === 0) {
|
|
227
|
-
return true;
|
|
228
|
-
}
|
|
229
|
-
if (a === 169 && b === 254) {
|
|
230
|
-
return true;
|
|
231
|
-
}
|
|
232
|
-
if (a === 172 && b >= 16 && b <= 31) {
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
if (a === 192 && b === 168) {
|
|
236
|
-
return true;
|
|
237
|
-
}
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
function isBlockedHostname(hostname) {
|
|
241
|
-
const normalized = hostname.trim().toLowerCase();
|
|
242
|
-
if (normalized.length === 0) {
|
|
243
|
-
return true;
|
|
244
|
-
}
|
|
245
|
-
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
246
|
-
return true;
|
|
247
|
-
}
|
|
248
|
-
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
249
|
-
return true;
|
|
250
|
-
}
|
|
251
|
-
if (normalized.includes(":")) {
|
|
252
|
-
return true;
|
|
253
|
-
}
|
|
254
|
-
return isPrivateIpv4(normalized);
|
|
255
|
-
}
|
|
256
|
-
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
257
|
-
let parsed;
|
|
258
|
-
try {
|
|
259
|
-
parsed = new URL(rawUrl);
|
|
260
|
-
} catch {
|
|
261
|
-
throw new BadRequestError("Webhook URL is invalid.");
|
|
262
|
-
}
|
|
263
|
-
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
264
|
-
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
265
|
-
}
|
|
266
|
-
if (parsed.username || parsed.password) {
|
|
267
|
-
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
268
|
-
}
|
|
269
|
-
if (isBlockedHostname(parsed.hostname)) {
|
|
270
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
271
|
-
}
|
|
272
|
-
return parsed;
|
|
273
|
-
}
|
|
274
|
-
function isBlockedIpAddress(address) {
|
|
275
|
-
return isBlockedHostname(address.trim().toLowerCase());
|
|
276
|
-
}
|
|
277
|
-
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
278
|
-
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
279
|
-
if (options.resolveDns === false) {
|
|
280
|
-
return parsed;
|
|
281
|
-
}
|
|
282
|
-
const hostname = parsed.hostname.trim().toLowerCase();
|
|
283
|
-
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
284
|
-
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
285
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
286
|
-
}
|
|
287
|
-
return parsed;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// ../../src/core/security/safeFetch.ts
|
|
291
|
-
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
292
|
-
async function safeFetch(input, init = {}, options = {}) {
|
|
293
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
294
|
-
const maxRedirects = options.maxRedirects ?? 0;
|
|
295
|
-
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
296
|
-
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
297
|
-
const controller = new AbortController;
|
|
298
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
299
|
-
try {
|
|
300
|
-
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
301
|
-
let redirectCount = 0;
|
|
302
|
-
while (true) {
|
|
303
|
-
const response = await fetch(currentUrl, {
|
|
304
|
-
...init,
|
|
305
|
-
signal: controller.signal,
|
|
306
|
-
redirect: "manual"
|
|
307
|
-
});
|
|
308
|
-
if (response.status >= 300 && response.status < 400) {
|
|
309
|
-
const location = response.headers.get("location");
|
|
310
|
-
if (!location || redirectCount >= maxRedirects) {
|
|
311
|
-
return response;
|
|
312
|
-
}
|
|
313
|
-
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
314
|
-
redirectCount += 1;
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
return response;
|
|
318
|
-
}
|
|
319
|
-
} finally {
|
|
320
|
-
clearTimeout(timeout);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// ../../src/core/tenant/databaseTenantContext.ts
|
|
325
|
-
async function runWithMigrationBypass(callback) {
|
|
326
|
-
await repositoryConnection`SELECT set_config('app.bypass_rls', 'true', false)`;
|
|
327
|
-
try {
|
|
328
|
-
return await callback();
|
|
329
|
-
} finally {
|
|
330
|
-
await repositoryConnection`SELECT set_config('app.bypass_rls', 'false', false)`;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// ../../src/core/audit/siemFormatter.ts
|
|
335
|
-
function formatSiemAuditEvent(input) {
|
|
336
|
-
return {
|
|
337
|
-
timestamp: input.created_at.toISOString(),
|
|
338
|
-
event_type: "workhub.audit",
|
|
339
|
-
actor_user_id: input.user_id,
|
|
340
|
-
tenant_id: input.tenant_id ?? null,
|
|
341
|
-
trace_id: input.trace_id ?? null,
|
|
342
|
-
action: input.action,
|
|
343
|
-
subject_type: input.subject_type,
|
|
344
|
-
subject_id: input.subject_id,
|
|
345
|
-
ip_address: input.ip_address ?? null,
|
|
346
|
-
user_agent: input.user_agent ?? null,
|
|
347
|
-
checksum: input.checksum ?? null,
|
|
348
|
-
payload: input.payload ?? {}
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
function formatCefLine(event) {
|
|
352
|
-
const extension = [
|
|
353
|
-
`rt=${event.timestamp}`,
|
|
354
|
-
`suid=${event.actor_user_id ?? "unknown"}`,
|
|
355
|
-
`cs1=${event.action}`,
|
|
356
|
-
`cs1Label=Action`,
|
|
357
|
-
`cs2=${event.subject_type}`,
|
|
358
|
-
`cs2Label=SubjectType`,
|
|
359
|
-
`cs3=${event.subject_id ?? ""}`,
|
|
360
|
-
`cs3Label=SubjectId`,
|
|
361
|
-
`src=${event.ip_address ?? ""}`,
|
|
362
|
-
`request=${event.trace_id ?? ""}`
|
|
363
|
-
].join(" ");
|
|
364
|
-
return `CEF:0|WorkHub|API|1.0|${event.action}|${event.subject_type}|5|${extension}`;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// ../../src/core/audit/exportAuditLogs.ts
|
|
368
|
-
function resolveAuditExportConfig() {
|
|
369
|
-
const endpoint = process.env.SIEM_EXPORT_URL?.trim();
|
|
370
|
-
if (!endpoint) {
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
assertSafeOutboundUrl(endpoint, { allowHttp: appConfig.env !== "production" });
|
|
374
|
-
const batchSize = Number(process.env.SIEM_EXPORT_BATCH_SIZE ?? "100");
|
|
375
|
-
return {
|
|
376
|
-
endpoint,
|
|
377
|
-
format: process.env.SIEM_EXPORT_FORMAT === "cef" ? "cef" : "json",
|
|
378
|
-
batchSize: Number.isFinite(batchSize) ? batchSize : 100
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
async function exportPendingAuditLogs() {
|
|
382
|
-
const config = resolveAuditExportConfig();
|
|
383
|
-
if (!config) {
|
|
384
|
-
return 0;
|
|
385
|
-
}
|
|
386
|
-
return await runWithMigrationBypass(async () => {
|
|
387
|
-
const rows = await repositoryConnection`
|
|
388
|
-
SELECT
|
|
389
|
-
id,
|
|
390
|
-
user_id,
|
|
391
|
-
action,
|
|
392
|
-
subject_type,
|
|
393
|
-
subject_id,
|
|
394
|
-
payload,
|
|
395
|
-
ip_address,
|
|
396
|
-
user_agent,
|
|
397
|
-
checksum,
|
|
398
|
-
tenant_id,
|
|
399
|
-
trace_id,
|
|
400
|
-
created_at
|
|
401
|
-
FROM audit_log
|
|
402
|
-
WHERE exported_at IS NULL
|
|
403
|
-
ORDER BY id
|
|
404
|
-
LIMIT ${config.batchSize}
|
|
405
|
-
`;
|
|
406
|
-
if (rows.length === 0) {
|
|
407
|
-
return 0;
|
|
408
|
-
}
|
|
409
|
-
const events = rows.map((row) => formatSiemAuditEvent({
|
|
410
|
-
action: row.action,
|
|
411
|
-
subject_type: row.subject_type,
|
|
412
|
-
subject_id: row.subject_id,
|
|
413
|
-
user_id: row.user_id,
|
|
414
|
-
tenant_id: row.tenant_id,
|
|
415
|
-
trace_id: row.trace_id,
|
|
416
|
-
ip_address: row.ip_address,
|
|
417
|
-
user_agent: row.user_agent,
|
|
418
|
-
checksum: row.checksum,
|
|
419
|
-
payload: row.payload,
|
|
420
|
-
created_at: row.created_at
|
|
421
|
-
}));
|
|
422
|
-
const body = config.format === "cef" ? events.map((event) => formatCefLine(event)).join(`
|
|
423
|
-
`) : JSON.stringify({ events });
|
|
424
|
-
const response = await safeFetch(config.endpoint, {
|
|
425
|
-
method: "POST",
|
|
426
|
-
headers: {
|
|
427
|
-
"content-type": config.format === "cef" ? "text/plain" : "application/json",
|
|
428
|
-
...process.env.SIEM_EXPORT_TOKEN ? { authorization: `Bearer ${process.env.SIEM_EXPORT_TOKEN}` } : {}
|
|
429
|
-
},
|
|
430
|
-
body
|
|
431
|
-
}, { allowHttp: appConfig.env !== "production" });
|
|
432
|
-
if (!response.ok) {
|
|
433
|
-
throw new Error(`SIEM export failed with status ${response.status}.`);
|
|
434
|
-
}
|
|
435
|
-
const ids = rows.map((row) => row.id);
|
|
436
|
-
for (const id of ids) {
|
|
437
|
-
await repositoryConnection`UPDATE audit_log SET exported_at = NOW() WHERE id = ${id}`;
|
|
438
|
-
}
|
|
439
|
-
return rows.length;
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// ../../src/core/logging/logger.ts
|
|
444
|
-
class Logger {
|
|
445
|
-
channel;
|
|
446
|
-
constructor(channel = "app") {
|
|
447
|
-
this.channel = channel;
|
|
448
|
-
}
|
|
449
|
-
write(level, message, context = {}) {
|
|
450
|
-
const entry = {
|
|
451
|
-
level,
|
|
452
|
-
channel: this.channel,
|
|
453
|
-
message,
|
|
454
|
-
timestamp: new Date().toISOString(),
|
|
455
|
-
...context
|
|
456
|
-
};
|
|
457
|
-
const line = JSON.stringify(entry);
|
|
458
|
-
if (level === "error") {
|
|
459
|
-
console.error(line);
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
console.log(line);
|
|
463
|
-
}
|
|
464
|
-
debug(message, context) {
|
|
465
|
-
this.write("debug", message, context);
|
|
466
|
-
}
|
|
467
|
-
info(message, context) {
|
|
468
|
-
this.write("info", message, context);
|
|
469
|
-
}
|
|
470
|
-
warn(message, context) {
|
|
471
|
-
this.write("warn", message, context);
|
|
472
|
-
}
|
|
473
|
-
error(message, context) {
|
|
474
|
-
this.write("error", message, context);
|
|
475
|
-
}
|
|
38
|
+
// ../../src/bootstrap/schedule.ts
|
|
39
|
+
import { exportPendingAuditLogs } from "@getstrata/core/audit/exportAuditLogs";
|
|
40
|
+
import { appLogger } from "@getstrata/core/logging/logger";
|
|
41
|
+
import { appSchedule as appSchedule2 } from "@getstrata/core/scheduler/schedule";
|
|
42
|
+
|
|
43
|
+
// ../../src/config/features.ts
|
|
44
|
+
function readFeatureFlags() {
|
|
45
|
+
return {
|
|
46
|
+
webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
|
|
47
|
+
fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
|
|
48
|
+
auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
|
|
49
|
+
oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
|
|
50
|
+
samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
|
|
51
|
+
scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
|
|
52
|
+
billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
|
|
53
|
+
siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
|
|
54
|
+
publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
|
|
55
|
+
emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
|
|
56
|
+
mfa: (process.env.FEATURE_MFA ?? "false") === "true"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
var featureFlags = readFeatureFlags();
|
|
60
|
+
function isFeatureEnabled(feature) {
|
|
61
|
+
return readFeatureFlags()[feature];
|
|
476
62
|
}
|
|
477
|
-
var appLogger = new Logger("app");
|
|
478
63
|
|
|
479
64
|
// ../../src/bootstrap/schedule.ts
|
|
480
|
-
|
|
65
|
+
appSchedule2.command("* * * * *", "heartbeat", () => {
|
|
481
66
|
appLogger.debug("Scheduler heartbeat");
|
|
482
67
|
});
|
|
483
|
-
|
|
68
|
+
appSchedule2.command("* * * * *", "audit-export", async () => {
|
|
484
69
|
if (!isFeatureEnabled("siemExport")) {
|
|
485
70
|
return;
|
|
486
71
|
}
|
|
@@ -517,7 +102,7 @@ import {
|
|
|
517
102
|
resolveApplicationPolicyGate,
|
|
518
103
|
resolveApplicationQueue,
|
|
519
104
|
setActiveApplicationContext
|
|
520
|
-
} from "@getstrata/core/runtime/applicationRegistry
|
|
105
|
+
} from "@getstrata/core/runtime/applicationRegistry";
|
|
521
106
|
// ../../src/bootstrap/buildModuleRoutes.ts
|
|
522
107
|
import { conditionalJsonResponse } from "@getstrata/core/http";
|
|
523
108
|
import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
|
|
@@ -606,7 +191,7 @@ import {
|
|
|
606
191
|
CORE_POLICY_GATE_TOKEN,
|
|
607
192
|
CORE_QUEUE_TOKEN,
|
|
608
193
|
CORE_TOKEN_SERVICE_TOKEN
|
|
609
|
-
} from "@getstrata/core/contracts/serviceTokens
|
|
194
|
+
} from "@getstrata/core/contracts/serviceTokens";
|
|
610
195
|
var APP_PORT_CONFIG_KEY = "app.port";
|
|
611
196
|
var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
|
|
612
197
|
var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
|
|
@@ -1032,6 +617,68 @@ var databaseConfig = {
|
|
|
1032
617
|
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
1033
618
|
};
|
|
1034
619
|
|
|
620
|
+
// ../../src/core/runtime/asyncContextStore.ts
|
|
621
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
622
|
+
function createAsyncContextStore(key) {
|
|
623
|
+
const symbol = Symbol.for(key);
|
|
624
|
+
const globalRecord = globalThis;
|
|
625
|
+
const existing = globalRecord[symbol];
|
|
626
|
+
if (existing) {
|
|
627
|
+
return existing;
|
|
628
|
+
}
|
|
629
|
+
const store = new AsyncLocalStorage;
|
|
630
|
+
globalRecord[symbol] = store;
|
|
631
|
+
return store;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ../../src/core/database/connectionContext.ts
|
|
635
|
+
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
636
|
+
function getActiveDatabaseConnection(fallback) {
|
|
637
|
+
return activeConnection.getStore() ?? fallback;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ../../src/core/database/queryProxy.ts
|
|
641
|
+
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
642
|
+
function createDatabaseQueryProxy(pool) {
|
|
643
|
+
function resolveDatabase() {
|
|
644
|
+
return getActiveDatabaseConnection(pool);
|
|
645
|
+
}
|
|
646
|
+
function resolveDatabaseForProperty(property) {
|
|
647
|
+
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
648
|
+
return pool;
|
|
649
|
+
}
|
|
650
|
+
return resolveDatabase();
|
|
651
|
+
}
|
|
652
|
+
return new Proxy(function database() {}, {
|
|
653
|
+
apply(_target, _thisArg, args) {
|
|
654
|
+
return resolveDatabase()(...args);
|
|
655
|
+
},
|
|
656
|
+
get(_target, property) {
|
|
657
|
+
const connection = resolveDatabaseForProperty(property);
|
|
658
|
+
const value = connection[property];
|
|
659
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ../../src/core/database/defaultConnection.ts
|
|
665
|
+
var defaultPool = {
|
|
666
|
+
connection: null
|
|
667
|
+
};
|
|
668
|
+
var defaultQuery = {
|
|
669
|
+
connection: null
|
|
670
|
+
};
|
|
671
|
+
function registerDefaultDatabasePool(connection) {
|
|
672
|
+
defaultPool.connection = connection;
|
|
673
|
+
defaultQuery.connection = createDatabaseQueryProxy(connection);
|
|
674
|
+
}
|
|
675
|
+
function getDefaultDatabaseQuery() {
|
|
676
|
+
if (!defaultQuery.connection) {
|
|
677
|
+
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
678
|
+
}
|
|
679
|
+
return defaultQuery.connection;
|
|
680
|
+
}
|
|
681
|
+
|
|
1035
682
|
// ../../src/db/connection/createConnection.ts
|
|
1036
683
|
var {SQL } = globalThis.Bun;
|
|
1037
684
|
function createDatabaseConnection(config) {
|
|
@@ -1094,7 +741,7 @@ var apiTokenTable = defineTable({
|
|
|
1094
741
|
// ../../src/modules/user/authService.ts
|
|
1095
742
|
import { verifyPassword } from "@getstrata/core/auth/password";
|
|
1096
743
|
import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
|
|
1097
|
-
import { UnauthorizedError
|
|
744
|
+
import { UnauthorizedError } from "@getstrata/core/errors/http";
|
|
1098
745
|
import { logSecurityEvent } from "@getstrata/core/security/securityEvents";
|
|
1099
746
|
import { resolveDefaultTokenExpiryDays } from "@getstrata/core/security/tokenExpiry";
|
|
1100
747
|
import { verifyTotp } from "@getstrata/core/security/totp";
|
|
@@ -1119,22 +766,22 @@ class AuthService {
|
|
|
1119
766
|
const user = await this.users.findByEmail(email);
|
|
1120
767
|
if (!user?.password_hash) {
|
|
1121
768
|
logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
|
|
1122
|
-
throw new
|
|
769
|
+
throw new UnauthorizedError("Invalid credentials.");
|
|
1123
770
|
}
|
|
1124
771
|
const valid = await verifyPassword(password, user.password_hash);
|
|
1125
772
|
if (!valid) {
|
|
1126
773
|
logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
|
|
1127
|
-
throw new
|
|
774
|
+
throw new UnauthorizedError("Invalid credentials.");
|
|
1128
775
|
}
|
|
1129
776
|
if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
|
|
1130
777
|
logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
|
|
1131
|
-
throw new
|
|
778
|
+
throw new UnauthorizedError("Email address is not verified.");
|
|
1132
779
|
}
|
|
1133
780
|
if (isFeatureEnabled("mfa") && user.mfa_enabled) {
|
|
1134
781
|
const mfaSecret = revealMfaSecret(user.mfa_secret);
|
|
1135
782
|
if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
|
|
1136
783
|
logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
|
|
1137
|
-
throw new
|
|
784
|
+
throw new UnauthorizedError("Invalid MFA code.");
|
|
1138
785
|
}
|
|
1139
786
|
}
|
|
1140
787
|
logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
|
|
@@ -1147,7 +794,7 @@ class AuthService {
|
|
|
1147
794
|
async loginWithOAuth(providerName, code) {
|
|
1148
795
|
const provider = this.oauthProviders.get(providerName);
|
|
1149
796
|
if (!provider) {
|
|
1150
|
-
throw new
|
|
797
|
+
throw new UnauthorizedError("Unsupported OAuth provider.");
|
|
1151
798
|
}
|
|
1152
799
|
const profile = await provider.exchangeCode(code);
|
|
1153
800
|
const user = await this.findOrCreateOAuthUser(providerName, profile);
|
|
@@ -1161,7 +808,7 @@ class AuthService {
|
|
|
1161
808
|
buildOAuthAuthorizationUrl(providerName, state) {
|
|
1162
809
|
const provider = this.oauthProviders.get(providerName);
|
|
1163
810
|
if (!provider) {
|
|
1164
|
-
throw new
|
|
811
|
+
throw new UnauthorizedError("Unsupported OAuth provider.");
|
|
1165
812
|
}
|
|
1166
813
|
return provider.getAuthorizationUrl(state);
|
|
1167
814
|
}
|
|
@@ -1248,12 +895,57 @@ var userTable = defineTable4({
|
|
|
1248
895
|
|
|
1249
896
|
// ../../src/modules/user/tokenService.ts
|
|
1250
897
|
import { hashApiToken } from "@getstrata/core/auth/tokenHash";
|
|
1251
|
-
import { ForbiddenError
|
|
898
|
+
import { ForbiddenError, NotFoundError as NotFoundError2 } from "@getstrata/core/errors/http";
|
|
1252
899
|
import { resolveDefaultTokenExpiryDays as resolveDefaultTokenExpiryDays2 } from "@getstrata/core/security/tokenExpiry";
|
|
1253
900
|
|
|
1254
901
|
// ../../src/modules/user/provider.ts
|
|
1255
902
|
var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
|
|
1256
903
|
|
|
904
|
+
// ../../src/core/errors/http.ts
|
|
905
|
+
class HttpError extends Error {
|
|
906
|
+
status;
|
|
907
|
+
details;
|
|
908
|
+
constructor(status, message, details) {
|
|
909
|
+
super(message);
|
|
910
|
+
this.name = new.target.name;
|
|
911
|
+
this.status = status;
|
|
912
|
+
this.details = details;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
class BadRequestError extends HttpError {
|
|
917
|
+
constructor(message = "Bad Request", details) {
|
|
918
|
+
super(400, message, details);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
class ConflictError extends HttpError {
|
|
922
|
+
constructor(message = "Conflict", details) {
|
|
923
|
+
super(409, message, details);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
class UnprocessableEntityError extends HttpError {
|
|
928
|
+
constructor(message = "Unprocessable Entity", details) {
|
|
929
|
+
super(422, message, details);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
class ForbiddenError2 extends HttpError {
|
|
933
|
+
constructor(message = "Forbidden", details) {
|
|
934
|
+
super(403, message, details);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
class UnauthorizedError2 extends HttpError {
|
|
939
|
+
constructor(message = "Unauthorized", details) {
|
|
940
|
+
super(401, message, details);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
class PreconditionFailedError extends HttpError {
|
|
944
|
+
constructor(message = "Precondition Failed", details) {
|
|
945
|
+
super(412, message, details);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
1257
949
|
// ../../src/core/auth/authContext.ts
|
|
1258
950
|
var authContext = createAsyncContextStore("@getstrata/authContext");
|
|
1259
951
|
function currentAuthUser() {
|
|
@@ -1340,7 +1032,7 @@ class AuthManager {
|
|
|
1340
1032
|
async requireUser(request) {
|
|
1341
1033
|
const user = await this.user(request);
|
|
1342
1034
|
if (!user) {
|
|
1343
|
-
throw new
|
|
1035
|
+
throw new UnauthorizedError2;
|
|
1344
1036
|
}
|
|
1345
1037
|
return user;
|
|
1346
1038
|
}
|
|
@@ -1895,6 +1587,15 @@ var cacheProvider = {
|
|
|
1895
1587
|
};
|
|
1896
1588
|
var cache_default = cacheProvider;
|
|
1897
1589
|
|
|
1590
|
+
// ../../src/config/app.ts
|
|
1591
|
+
var appConfig = {
|
|
1592
|
+
name: "WorkHub",
|
|
1593
|
+
env: process.env.APP_ENV ?? "local",
|
|
1594
|
+
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
1595
|
+
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
1596
|
+
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
1597
|
+
};
|
|
1598
|
+
|
|
1898
1599
|
// ../../src/config/queue.ts
|
|
1899
1600
|
var queueConfig = {
|
|
1900
1601
|
driver: process.env.QUEUE_DRIVER === "redis" || process.env.QUEUE_DRIVER === "async" || process.env.QUEUE_DRIVER === "sync" ? process.env.QUEUE_DRIVER : DEFAULT_QUEUE_DRIVER,
|
|
@@ -2117,25 +1818,103 @@ function discoverListeners() {
|
|
|
2117
1818
|
return appListeners;
|
|
2118
1819
|
}
|
|
2119
1820
|
|
|
2120
|
-
// ../../src/
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
1821
|
+
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
1822
|
+
import { eventBus as eventBus2, modelEventName as modelEventName2 } from "@getstrata/core/events";
|
|
1823
|
+
import InvalidateCacheTagsJob from "@getstrata/core/jobs/invalidateCacheTagsJob";
|
|
1824
|
+
import { createTrackedJob } from "@getstrata/core/queue/createAppQueue";
|
|
1825
|
+
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
1826
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2) {
|
|
1827
|
+
for (const tableName of discoverModelTableNames()) {
|
|
1828
|
+
for (const action of MODEL_WRITE_ACTIONS) {
|
|
1829
|
+
bus.listen(modelEventName2(tableName, action), async () => {
|
|
1830
|
+
const tags = cacheTagsForModelWrite(tableName, action);
|
|
1831
|
+
if (tags.length === 0) {
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
let cache;
|
|
1835
|
+
let queue;
|
|
1836
|
+
try {
|
|
1837
|
+
cache = resolveApplicationCache();
|
|
1838
|
+
queue = resolveApplicationQueue();
|
|
1839
|
+
} catch {
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
|
|
1843
|
+
await queue.dispatch(job, { tags });
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
2125
1847
|
}
|
|
2126
1848
|
|
|
2127
|
-
// ../../src/
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
this.cache = cache;
|
|
1849
|
+
// ../../src/bootstrap/providers/listeners.ts
|
|
1850
|
+
var registeredListenerGroups = new Set;
|
|
1851
|
+
function registerListenerGroup(name, register) {
|
|
1852
|
+
if (registeredListenerGroups.has(name)) {
|
|
1853
|
+
return;
|
|
2133
1854
|
}
|
|
2134
|
-
|
|
2135
|
-
|
|
1855
|
+
registeredListenerGroups.add(name);
|
|
1856
|
+
register();
|
|
1857
|
+
}
|
|
1858
|
+
var listenersProvider = {
|
|
1859
|
+
name: "core.listeners",
|
|
1860
|
+
boot() {
|
|
1861
|
+
registerListenerGroup("cache.invalidate-on-model-write", () => {
|
|
1862
|
+
registerInvalidateCacheOnModelWriteListeners();
|
|
1863
|
+
});
|
|
1864
|
+
for (const [index, registerListener] of discoverListeners().entries()) {
|
|
1865
|
+
registerListenerGroup(`app.listener.${index}`, registerListener);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
var listeners_default = listenersProvider;
|
|
1870
|
+
|
|
1871
|
+
// ../../src/core/auth/policy.ts
|
|
1872
|
+
var BLOCKED_POLICY_ACTIONS = new Set([
|
|
1873
|
+
"constructor",
|
|
1874
|
+
"toString",
|
|
1875
|
+
"valueOf",
|
|
1876
|
+
"hasOwnProperty",
|
|
1877
|
+
"isPrototypeOf",
|
|
1878
|
+
"propertyIsEnumerable",
|
|
1879
|
+
"__proto__"
|
|
1880
|
+
]);
|
|
1881
|
+
|
|
1882
|
+
class PolicyGate {
|
|
1883
|
+
constructor() {}
|
|
1884
|
+
policies = new Map;
|
|
1885
|
+
register(resource, policy) {
|
|
1886
|
+
this.policies.set(resource, policy);
|
|
1887
|
+
}
|
|
1888
|
+
allows(resource, action, user, model) {
|
|
1889
|
+
const policy = this.policies.get(resource);
|
|
1890
|
+
if (!policy) {
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
|
|
1894
|
+
return false;
|
|
1895
|
+
}
|
|
1896
|
+
const handler = policy[action];
|
|
1897
|
+
if (typeof handler !== "function") {
|
|
1898
|
+
return false;
|
|
1899
|
+
}
|
|
1900
|
+
const resolvedUser = user === undefined ? currentAuthUser() : user;
|
|
1901
|
+
return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
|
|
1902
|
+
}
|
|
1903
|
+
authorize(resource, action, user, model) {
|
|
1904
|
+
if (!this.allows(resource, action, user, model)) {
|
|
1905
|
+
throw new ForbiddenError2;
|
|
1906
|
+
}
|
|
2136
1907
|
}
|
|
2137
1908
|
}
|
|
2138
|
-
|
|
1909
|
+
|
|
1910
|
+
// ../../src/bootstrap/providers/policy.ts
|
|
1911
|
+
var policyProvider = {
|
|
1912
|
+
name: "core.policy",
|
|
1913
|
+
register({ container }) {
|
|
1914
|
+
container.set(CORE_POLICY_GATE_TOKEN, new PolicyGate);
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
var policy_default = policyProvider;
|
|
2139
1918
|
|
|
2140
1919
|
// ../../src/core/pagination/index.ts
|
|
2141
1920
|
function buildPaginationMeta(input) {
|
|
@@ -2673,8 +2452,31 @@ function indexMorphToRelation(children, parentsByType, relation) {
|
|
|
2673
2452
|
result.set(child[relation.morphIdKey], parent);
|
|
2674
2453
|
}
|
|
2675
2454
|
}
|
|
2676
|
-
return result;
|
|
2677
|
-
}
|
|
2455
|
+
return result;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// ../../src/core/database/boundConnection.ts
|
|
2459
|
+
var boundConnectionHolder = {
|
|
2460
|
+
connection: null
|
|
2461
|
+
};
|
|
2462
|
+
function getBoundDatabaseConnection() {
|
|
2463
|
+
return boundConnectionHolder.connection;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
// ../../src/core/database/repositoryConnection.ts
|
|
2467
|
+
function resolveRepositoryConnection() {
|
|
2468
|
+
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
2469
|
+
}
|
|
2470
|
+
var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
2471
|
+
apply(_target, _thisArg, args) {
|
|
2472
|
+
return resolveRepositoryConnection()(...args);
|
|
2473
|
+
},
|
|
2474
|
+
get(_target, property) {
|
|
2475
|
+
const connection = resolveRepositoryConnection();
|
|
2476
|
+
const value = connection[property];
|
|
2477
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
2478
|
+
}
|
|
2479
|
+
});
|
|
2678
2480
|
|
|
2679
2481
|
// ../../src/core/database/whereBuilder.ts
|
|
2680
2482
|
class WhereBuilder {
|
|
@@ -4000,9 +3802,6 @@ class ResilientQueue {
|
|
|
4000
3802
|
function createFailedJobService() {
|
|
4001
3803
|
return new failedJobService_default(new failedJobRepository_default);
|
|
4002
3804
|
}
|
|
4003
|
-
function createTrackedJob(name, job) {
|
|
4004
|
-
return jobRegistry.track(name, job);
|
|
4005
|
-
}
|
|
4006
3805
|
function createProductionQueue(driver, options = {}) {
|
|
4007
3806
|
options.registerJobs?.();
|
|
4008
3807
|
const failedJobs = options.failedJobs ?? createFailedJobService();
|
|
@@ -4025,103 +3824,141 @@ function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(),
|
|
|
4025
3824
|
});
|
|
4026
3825
|
}
|
|
4027
3826
|
|
|
4028
|
-
// ../../src/
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
return;
|
|
4037
|
-
}
|
|
4038
|
-
let cache;
|
|
4039
|
-
let queue;
|
|
4040
|
-
try {
|
|
4041
|
-
cache = resolveApplicationCache();
|
|
4042
|
-
queue = resolveApplicationQueue();
|
|
4043
|
-
} catch {
|
|
4044
|
-
return;
|
|
4045
|
-
}
|
|
4046
|
-
const job = createTrackedJob("cache.invalidate-tags", new invalidateCacheTagsJob_default(cache));
|
|
4047
|
-
await queue.dispatch(job, { tags });
|
|
4048
|
-
});
|
|
4049
|
-
}
|
|
4050
|
-
}
|
|
3827
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3828
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
3829
|
+
|
|
3830
|
+
// ../../src/core/queue/index.ts
|
|
3831
|
+
class Job {
|
|
3832
|
+
maxAttempts;
|
|
3833
|
+
backoffMs;
|
|
3834
|
+
priority;
|
|
4051
3835
|
}
|
|
4052
3836
|
|
|
4053
|
-
// ../../src/
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
3837
|
+
// ../../src/core/security/safeUrl.ts
|
|
3838
|
+
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
3839
|
+
var dnsLookup = dnsLookupImpl;
|
|
3840
|
+
var BLOCKED_HOSTNAMES = new Set([
|
|
3841
|
+
"localhost",
|
|
3842
|
+
"127.0.0.1",
|
|
3843
|
+
"0.0.0.0",
|
|
3844
|
+
"::1",
|
|
3845
|
+
"metadata.google.internal"
|
|
3846
|
+
]);
|
|
3847
|
+
function isPrivateIpv4(hostname) {
|
|
3848
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
3849
|
+
if (!match) {
|
|
3850
|
+
return false;
|
|
4058
3851
|
}
|
|
4059
|
-
|
|
4060
|
-
|
|
3852
|
+
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
3853
|
+
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
3854
|
+
return true;
|
|
3855
|
+
}
|
|
3856
|
+
const [a = 0, b = 0] = octets;
|
|
3857
|
+
if (a === 10) {
|
|
3858
|
+
return true;
|
|
3859
|
+
}
|
|
3860
|
+
if (a === 127) {
|
|
3861
|
+
return true;
|
|
3862
|
+
}
|
|
3863
|
+
if (a === 0) {
|
|
3864
|
+
return true;
|
|
3865
|
+
}
|
|
3866
|
+
if (a === 169 && b === 254) {
|
|
3867
|
+
return true;
|
|
3868
|
+
}
|
|
3869
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
3870
|
+
return true;
|
|
3871
|
+
}
|
|
3872
|
+
if (a === 192 && b === 168) {
|
|
3873
|
+
return true;
|
|
3874
|
+
}
|
|
3875
|
+
return false;
|
|
4061
3876
|
}
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
registerInvalidateCacheOnModelWriteListeners();
|
|
4067
|
-
});
|
|
4068
|
-
for (const [index, registerListener] of discoverListeners().entries()) {
|
|
4069
|
-
registerListenerGroup(`app.listener.${index}`, registerListener);
|
|
4070
|
-
}
|
|
3877
|
+
function isBlockedHostname(hostname) {
|
|
3878
|
+
const normalized = hostname.trim().toLowerCase();
|
|
3879
|
+
if (normalized.length === 0) {
|
|
3880
|
+
return true;
|
|
4071
3881
|
}
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
// ../../src/core/auth/policy.ts
|
|
4076
|
-
var BLOCKED_POLICY_ACTIONS = new Set([
|
|
4077
|
-
"constructor",
|
|
4078
|
-
"toString",
|
|
4079
|
-
"valueOf",
|
|
4080
|
-
"hasOwnProperty",
|
|
4081
|
-
"isPrototypeOf",
|
|
4082
|
-
"propertyIsEnumerable",
|
|
4083
|
-
"__proto__"
|
|
4084
|
-
]);
|
|
4085
|
-
|
|
4086
|
-
class PolicyGate {
|
|
4087
|
-
constructor() {}
|
|
4088
|
-
policies = new Map;
|
|
4089
|
-
register(resource, policy) {
|
|
4090
|
-
this.policies.set(resource, policy);
|
|
3882
|
+
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
3883
|
+
return true;
|
|
4091
3884
|
}
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
if (!policy) {
|
|
4095
|
-
return false;
|
|
4096
|
-
}
|
|
4097
|
-
if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
|
|
4098
|
-
return false;
|
|
4099
|
-
}
|
|
4100
|
-
const handler = policy[action];
|
|
4101
|
-
if (typeof handler !== "function") {
|
|
4102
|
-
return false;
|
|
4103
|
-
}
|
|
4104
|
-
const resolvedUser = user === undefined ? currentAuthUser() : user;
|
|
4105
|
-
return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
|
|
3885
|
+
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
3886
|
+
return true;
|
|
4106
3887
|
}
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
3888
|
+
if (normalized.includes(":")) {
|
|
3889
|
+
return true;
|
|
3890
|
+
}
|
|
3891
|
+
return isPrivateIpv4(normalized);
|
|
3892
|
+
}
|
|
3893
|
+
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
3894
|
+
let parsed;
|
|
3895
|
+
try {
|
|
3896
|
+
parsed = new URL(rawUrl);
|
|
3897
|
+
} catch {
|
|
3898
|
+
throw new BadRequestError("Webhook URL is invalid.");
|
|
3899
|
+
}
|
|
3900
|
+
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
3901
|
+
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
3902
|
+
}
|
|
3903
|
+
if (parsed.username || parsed.password) {
|
|
3904
|
+
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
3905
|
+
}
|
|
3906
|
+
if (isBlockedHostname(parsed.hostname)) {
|
|
3907
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
3908
|
+
}
|
|
3909
|
+
return parsed;
|
|
3910
|
+
}
|
|
3911
|
+
function isBlockedIpAddress(address) {
|
|
3912
|
+
return isBlockedHostname(address.trim().toLowerCase());
|
|
3913
|
+
}
|
|
3914
|
+
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
3915
|
+
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
3916
|
+
if (options.resolveDns === false) {
|
|
3917
|
+
return parsed;
|
|
3918
|
+
}
|
|
3919
|
+
const hostname = parsed.hostname.trim().toLowerCase();
|
|
3920
|
+
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
3921
|
+
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
3922
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
4111
3923
|
}
|
|
3924
|
+
return parsed;
|
|
4112
3925
|
}
|
|
4113
3926
|
|
|
4114
|
-
// ../../src/
|
|
4115
|
-
var
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
3927
|
+
// ../../src/core/security/safeFetch.ts
|
|
3928
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
3929
|
+
async function safeFetch(input, init = {}, options = {}) {
|
|
3930
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
3931
|
+
const maxRedirects = options.maxRedirects ?? 0;
|
|
3932
|
+
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
3933
|
+
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
3934
|
+
const controller = new AbortController;
|
|
3935
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
3936
|
+
try {
|
|
3937
|
+
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
3938
|
+
let redirectCount = 0;
|
|
3939
|
+
while (true) {
|
|
3940
|
+
const response = await fetch(currentUrl, {
|
|
3941
|
+
...init,
|
|
3942
|
+
signal: controller.signal,
|
|
3943
|
+
redirect: "manual"
|
|
3944
|
+
});
|
|
3945
|
+
if (response.status >= 300 && response.status < 400) {
|
|
3946
|
+
const location = response.headers.get("location");
|
|
3947
|
+
if (!location || redirectCount >= maxRedirects) {
|
|
3948
|
+
return response;
|
|
3949
|
+
}
|
|
3950
|
+
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
3951
|
+
redirectCount += 1;
|
|
3952
|
+
continue;
|
|
3953
|
+
}
|
|
3954
|
+
return response;
|
|
3955
|
+
}
|
|
3956
|
+
} finally {
|
|
3957
|
+
clearTimeout(timeout);
|
|
4119
3958
|
}
|
|
4120
|
-
}
|
|
4121
|
-
var policy_default = policyProvider;
|
|
3959
|
+
}
|
|
4122
3960
|
|
|
4123
3961
|
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
4124
|
-
import { createHmac as createHmac2 } from "crypto";
|
|
4125
3962
|
class DispatchWebhookJob extends Job {
|
|
4126
3963
|
maxAttempts = 3;
|
|
4127
3964
|
backoffMs = 2000;
|
|
@@ -4181,6 +4018,19 @@ class DispatchWebhookJob extends Job {
|
|
|
4181
4018
|
}
|
|
4182
4019
|
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
4183
4020
|
|
|
4021
|
+
// ../../src/core/jobs/invalidateCacheTagsJob.ts
|
|
4022
|
+
class InvalidateCacheTagsJob2 extends Job {
|
|
4023
|
+
cache;
|
|
4024
|
+
constructor(cache) {
|
|
4025
|
+
super();
|
|
4026
|
+
this.cache = cache;
|
|
4027
|
+
}
|
|
4028
|
+
async handle(payload) {
|
|
4029
|
+
await this.cache.tags(...payload.tags).flush();
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
var invalidateCacheTagsJob_default = InvalidateCacheTagsJob2;
|
|
4033
|
+
|
|
4184
4034
|
// ../../src/core/contracts/di.ts
|
|
4185
4035
|
function getRequiredDependency2(dependencies, key) {
|
|
4186
4036
|
const dependency = dependencies[key];
|
|
@@ -4194,6 +4044,42 @@ function getRequiredDependency2(dependencies, key) {
|
|
|
4194
4044
|
var CORE_POLICY_GATE_TOKEN2 = "core.policyGate";
|
|
4195
4045
|
var CORE_AUTH_TOKEN2 = "core.auth";
|
|
4196
4046
|
|
|
4047
|
+
// ../../src/core/logging/logger.ts
|
|
4048
|
+
class Logger {
|
|
4049
|
+
channel;
|
|
4050
|
+
constructor(channel = "app") {
|
|
4051
|
+
this.channel = channel;
|
|
4052
|
+
}
|
|
4053
|
+
write(level, message, context = {}) {
|
|
4054
|
+
const entry = {
|
|
4055
|
+
level,
|
|
4056
|
+
channel: this.channel,
|
|
4057
|
+
message,
|
|
4058
|
+
timestamp: new Date().toISOString(),
|
|
4059
|
+
...context
|
|
4060
|
+
};
|
|
4061
|
+
const line = JSON.stringify(entry);
|
|
4062
|
+
if (level === "error") {
|
|
4063
|
+
console.error(line);
|
|
4064
|
+
return;
|
|
4065
|
+
}
|
|
4066
|
+
console.log(line);
|
|
4067
|
+
}
|
|
4068
|
+
debug(message, context) {
|
|
4069
|
+
this.write("debug", message, context);
|
|
4070
|
+
}
|
|
4071
|
+
info(message, context) {
|
|
4072
|
+
this.write("info", message, context);
|
|
4073
|
+
}
|
|
4074
|
+
warn(message, context) {
|
|
4075
|
+
this.write("warn", message, context);
|
|
4076
|
+
}
|
|
4077
|
+
error(message, context) {
|
|
4078
|
+
this.write("error", message, context);
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
var appLogger2 = new Logger("app");
|
|
4082
|
+
|
|
4197
4083
|
// ../../src/core/runtime/applicationRegistry.ts
|
|
4198
4084
|
var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
|
|
4199
4085
|
var activeContext;
|
|
@@ -5151,7 +5037,7 @@ export {
|
|
|
5151
5037
|
buildModuleRoutes,
|
|
5152
5038
|
assertProductionSecrets,
|
|
5153
5039
|
assertAppDependenciesComplete,
|
|
5154
|
-
|
|
5040
|
+
appSchedule3 as appSchedule,
|
|
5155
5041
|
ServiceContainer,
|
|
5156
5042
|
Schedule2 as Schedule,
|
|
5157
5043
|
RouteRegistry,
|