@getstrata/bootstrap 0.2.13 → 0.2.17
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/http/securedRouteModelBinding.d.ts +2 -11
- package/dist/bootstrap/membershipService.d.ts +1 -3
- package/dist/core/auth/membershipService.d.ts +1 -1
- package/dist/core/auth/resolveMembershipService.d.ts +3 -0
- package/dist/core/http/securedRouteModelBinding.d.ts +11 -2
- package/dist/core/queue/createAppQueue.d.ts +0 -1
- package/dist/core/queue/jobRegistry.d.ts +0 -1
- package/dist/entries/cache/modelCacheTags.js +22 -0
- package/dist/entries/context.js +128 -119
- package/dist/entries/http/securedRouteModelBinding.js +100 -99
- package/dist/entries/membershipService.js +100 -99
- package/dist/entries/providers.js +214 -205
- package/dist/entries/queue/defaultJobs.js +12 -2
- package/dist/entries/web/forms.js +53 -0
- package/dist/entries/web/routing.js +624 -0
- package/dist/entries/web/server.js +57 -0
- package/dist/entries/web/session.js +99 -0
- package/dist/entries/web/slug.js +8 -0
- package/dist/framework/public-api.d.ts +0 -1
- package/dist/index.js +82 -73
- package/package.json +33 -3
- package/dist/core/cache/modelCacheTags.d.ts +0 -1
|
@@ -1,4 +1,103 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// ../../src/core/contracts/applicationContext.ts
|
|
3
|
+
function getRequiredDependency(dependencies, key) {
|
|
4
|
+
const dependency = dependencies[key];
|
|
5
|
+
if (dependency === undefined) {
|
|
6
|
+
throw new Error(`Required dependency "${String(key)}" is not registered.`);
|
|
7
|
+
}
|
|
8
|
+
return dependency;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ../../src/core/contracts/serviceTokens.ts
|
|
12
|
+
var CORE_CONFIG_TOKEN = "core.config";
|
|
13
|
+
var CORE_CACHE_TOKEN = "core.cache";
|
|
14
|
+
var CORE_QUEUE_TOKEN = "core.queue";
|
|
15
|
+
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
16
|
+
var CORE_AUTH_TOKEN = "core.auth";
|
|
17
|
+
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
18
|
+
|
|
19
|
+
// ../../src/core/logging/logger.ts
|
|
20
|
+
class Logger {
|
|
21
|
+
channel;
|
|
22
|
+
constructor(channel = "app") {
|
|
23
|
+
this.channel = channel;
|
|
24
|
+
}
|
|
25
|
+
write(level, message, context = {}) {
|
|
26
|
+
const entry = {
|
|
27
|
+
level,
|
|
28
|
+
channel: this.channel,
|
|
29
|
+
message,
|
|
30
|
+
timestamp: new Date().toISOString(),
|
|
31
|
+
...context
|
|
32
|
+
};
|
|
33
|
+
const line = JSON.stringify(entry);
|
|
34
|
+
if (level === "error") {
|
|
35
|
+
console.error(line);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.log(line);
|
|
39
|
+
}
|
|
40
|
+
debug(message, context) {
|
|
41
|
+
this.write("debug", message, context);
|
|
42
|
+
}
|
|
43
|
+
info(message, context) {
|
|
44
|
+
this.write("info", message, context);
|
|
45
|
+
}
|
|
46
|
+
warn(message, context) {
|
|
47
|
+
this.write("warn", message, context);
|
|
48
|
+
}
|
|
49
|
+
error(message, context) {
|
|
50
|
+
this.write("error", message, context);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
var appLogger = new Logger("app");
|
|
54
|
+
|
|
55
|
+
// ../../src/core/runtime/applicationRegistry.ts
|
|
56
|
+
var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
|
|
57
|
+
var activeContext;
|
|
58
|
+
function readStoredApplicationContext() {
|
|
59
|
+
if (activeContext) {
|
|
60
|
+
return activeContext;
|
|
61
|
+
}
|
|
62
|
+
const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
|
|
63
|
+
if (globalContext) {
|
|
64
|
+
activeContext = globalContext;
|
|
65
|
+
}
|
|
66
|
+
return activeContext;
|
|
67
|
+
}
|
|
68
|
+
function setActiveApplicationContext(context) {
|
|
69
|
+
activeContext = context;
|
|
70
|
+
globalThis[APPLICATION_CONTEXT_KEY] = context;
|
|
71
|
+
}
|
|
72
|
+
function requireActiveApplicationContext() {
|
|
73
|
+
const context = readStoredApplicationContext();
|
|
74
|
+
if (!context) {
|
|
75
|
+
throw new Error("The application context has not been bootstrapped.");
|
|
76
|
+
}
|
|
77
|
+
return context;
|
|
78
|
+
}
|
|
79
|
+
function resolveApplicationCache() {
|
|
80
|
+
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
81
|
+
}
|
|
82
|
+
function resolveApplicationQueue() {
|
|
83
|
+
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
84
|
+
}
|
|
85
|
+
function resolveApplicationAuth() {
|
|
86
|
+
return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
|
|
87
|
+
}
|
|
88
|
+
function resolveApplicationPolicyGate() {
|
|
89
|
+
return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
|
|
90
|
+
}
|
|
91
|
+
function resolveApplicationConfig() {
|
|
92
|
+
return requireActiveApplicationContext().config;
|
|
93
|
+
}
|
|
94
|
+
function resolveApplicationLogger() {
|
|
95
|
+
return appLogger;
|
|
96
|
+
}
|
|
97
|
+
function resolveApplicationDependencies() {
|
|
98
|
+
return requireActiveApplicationContext().dependencies;
|
|
99
|
+
}
|
|
100
|
+
|
|
2
101
|
// ../../src/core/errors/http.ts
|
|
3
102
|
class HttpError extends Error {
|
|
4
103
|
status;
|
|
@@ -301,105 +400,7 @@ class MembershipService {
|
|
|
301
400
|
}
|
|
302
401
|
var membershipService_default = MembershipService;
|
|
303
402
|
|
|
304
|
-
// ../../src/core/
|
|
305
|
-
function getRequiredDependency(dependencies, key) {
|
|
306
|
-
const dependency = dependencies[key];
|
|
307
|
-
if (dependency === undefined) {
|
|
308
|
-
throw new Error(`Required dependency "${String(key)}" is not registered.`);
|
|
309
|
-
}
|
|
310
|
-
return dependency;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// ../../src/core/contracts/serviceTokens.ts
|
|
314
|
-
var CORE_CONFIG_TOKEN = "core.config";
|
|
315
|
-
var CORE_CACHE_TOKEN = "core.cache";
|
|
316
|
-
var CORE_QUEUE_TOKEN = "core.queue";
|
|
317
|
-
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
318
|
-
var CORE_AUTH_TOKEN = "core.auth";
|
|
319
|
-
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
320
|
-
|
|
321
|
-
// ../../src/core/logging/logger.ts
|
|
322
|
-
class Logger {
|
|
323
|
-
channel;
|
|
324
|
-
constructor(channel = "app") {
|
|
325
|
-
this.channel = channel;
|
|
326
|
-
}
|
|
327
|
-
write(level, message, context = {}) {
|
|
328
|
-
const entry = {
|
|
329
|
-
level,
|
|
330
|
-
channel: this.channel,
|
|
331
|
-
message,
|
|
332
|
-
timestamp: new Date().toISOString(),
|
|
333
|
-
...context
|
|
334
|
-
};
|
|
335
|
-
const line = JSON.stringify(entry);
|
|
336
|
-
if (level === "error") {
|
|
337
|
-
console.error(line);
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
console.log(line);
|
|
341
|
-
}
|
|
342
|
-
debug(message, context) {
|
|
343
|
-
this.write("debug", message, context);
|
|
344
|
-
}
|
|
345
|
-
info(message, context) {
|
|
346
|
-
this.write("info", message, context);
|
|
347
|
-
}
|
|
348
|
-
warn(message, context) {
|
|
349
|
-
this.write("warn", message, context);
|
|
350
|
-
}
|
|
351
|
-
error(message, context) {
|
|
352
|
-
this.write("error", message, context);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
var appLogger = new Logger("app");
|
|
356
|
-
|
|
357
|
-
// ../../src/core/runtime/applicationRegistry.ts
|
|
358
|
-
var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
|
|
359
|
-
var activeContext;
|
|
360
|
-
function readStoredApplicationContext() {
|
|
361
|
-
if (activeContext) {
|
|
362
|
-
return activeContext;
|
|
363
|
-
}
|
|
364
|
-
const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
|
|
365
|
-
if (globalContext) {
|
|
366
|
-
activeContext = globalContext;
|
|
367
|
-
}
|
|
368
|
-
return activeContext;
|
|
369
|
-
}
|
|
370
|
-
function setActiveApplicationContext(context) {
|
|
371
|
-
activeContext = context;
|
|
372
|
-
globalThis[APPLICATION_CONTEXT_KEY] = context;
|
|
373
|
-
}
|
|
374
|
-
function requireActiveApplicationContext() {
|
|
375
|
-
const context = readStoredApplicationContext();
|
|
376
|
-
if (!context) {
|
|
377
|
-
throw new Error("The application context has not been bootstrapped.");
|
|
378
|
-
}
|
|
379
|
-
return context;
|
|
380
|
-
}
|
|
381
|
-
function resolveApplicationCache() {
|
|
382
|
-
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
383
|
-
}
|
|
384
|
-
function resolveApplicationQueue() {
|
|
385
|
-
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
386
|
-
}
|
|
387
|
-
function resolveApplicationAuth() {
|
|
388
|
-
return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
|
|
389
|
-
}
|
|
390
|
-
function resolveApplicationPolicyGate() {
|
|
391
|
-
return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
|
|
392
|
-
}
|
|
393
|
-
function resolveApplicationConfig() {
|
|
394
|
-
return requireActiveApplicationContext().config;
|
|
395
|
-
}
|
|
396
|
-
function resolveApplicationLogger() {
|
|
397
|
-
return appLogger;
|
|
398
|
-
}
|
|
399
|
-
function resolveApplicationDependencies() {
|
|
400
|
-
return requireActiveApplicationContext().dependencies;
|
|
401
|
-
}
|
|
402
|
-
// ../../src/bootstrap/membershipService.ts
|
|
403
|
+
// ../../src/core/auth/resolveMembershipService.ts
|
|
403
404
|
function resolveMembershipService() {
|
|
404
405
|
const dependencies = resolveApplicationDependencies();
|
|
405
406
|
if (dependencies.container.has("core.membership")) {
|
|
@@ -3085,7 +3085,6 @@ var failedJobService_default = FailedJobService;
|
|
|
3085
3085
|
|
|
3086
3086
|
// ../../src/core/queue/jobRegistry.ts
|
|
3087
3087
|
class JobRegistry {
|
|
3088
|
-
constructor() {}
|
|
3089
3088
|
factories = new Map;
|
|
3090
3089
|
instances = new WeakMap;
|
|
3091
3090
|
register(name, factory) {
|
|
@@ -3109,7 +3108,17 @@ class JobRegistry {
|
|
|
3109
3108
|
return [...this.factories.keys()];
|
|
3110
3109
|
}
|
|
3111
3110
|
}
|
|
3112
|
-
var
|
|
3111
|
+
var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
|
|
3112
|
+
function readSharedJobRegistry() {
|
|
3113
|
+
const globalRegistry = globalThis[JOB_REGISTRY_KEY];
|
|
3114
|
+
if (globalRegistry) {
|
|
3115
|
+
return globalRegistry;
|
|
3116
|
+
}
|
|
3117
|
+
const registry = new JobRegistry;
|
|
3118
|
+
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
3119
|
+
return registry;
|
|
3120
|
+
}
|
|
3121
|
+
var jobRegistry = readSharedJobRegistry();
|
|
3113
3122
|
|
|
3114
3123
|
// ../../src/core/queue/jobRunner.ts
|
|
3115
3124
|
async function runQueueJob(envelope, failedJobs) {
|
|
@@ -3225,192 +3234,15 @@ function createProductionQueue(driver, options = {}) {
|
|
|
3225
3234
|
return new ResilientQueue(failedJobs, driver === "async");
|
|
3226
3235
|
}
|
|
3227
3236
|
|
|
3228
|
-
// ../../src/core/
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
"127.0.0.1",
|
|
3237
|
-
"0.0.0.0",
|
|
3238
|
-
"::1",
|
|
3239
|
-
"metadata.google.internal"
|
|
3240
|
-
]);
|
|
3241
|
-
function isPrivateIpv4(hostname) {
|
|
3242
|
-
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
3243
|
-
if (!match) {
|
|
3244
|
-
return false;
|
|
3245
|
-
}
|
|
3246
|
-
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
3247
|
-
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
3248
|
-
return true;
|
|
3249
|
-
}
|
|
3250
|
-
const [a = 0, b = 0] = octets;
|
|
3251
|
-
if (a === 10) {
|
|
3252
|
-
return true;
|
|
3253
|
-
}
|
|
3254
|
-
if (a === 127) {
|
|
3255
|
-
return true;
|
|
3256
|
-
}
|
|
3257
|
-
if (a === 0) {
|
|
3258
|
-
return true;
|
|
3259
|
-
}
|
|
3260
|
-
if (a === 169 && b === 254) {
|
|
3261
|
-
return true;
|
|
3262
|
-
}
|
|
3263
|
-
if (a === 172 && b >= 16 && b <= 31) {
|
|
3264
|
-
return true;
|
|
3265
|
-
}
|
|
3266
|
-
if (a === 192 && b === 168) {
|
|
3267
|
-
return true;
|
|
3268
|
-
}
|
|
3269
|
-
return false;
|
|
3270
|
-
}
|
|
3271
|
-
function isBlockedHostname(hostname) {
|
|
3272
|
-
const normalized = hostname.trim().toLowerCase();
|
|
3273
|
-
if (normalized.length === 0) {
|
|
3274
|
-
return true;
|
|
3275
|
-
}
|
|
3276
|
-
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
3277
|
-
return true;
|
|
3278
|
-
}
|
|
3279
|
-
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
3280
|
-
return true;
|
|
3281
|
-
}
|
|
3282
|
-
if (normalized.includes(":")) {
|
|
3283
|
-
return true;
|
|
3284
|
-
}
|
|
3285
|
-
return isPrivateIpv4(normalized);
|
|
3286
|
-
}
|
|
3287
|
-
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
3288
|
-
let parsed;
|
|
3289
|
-
try {
|
|
3290
|
-
parsed = new URL(rawUrl);
|
|
3291
|
-
} catch {
|
|
3292
|
-
throw new BadRequestError("Webhook URL is invalid.");
|
|
3293
|
-
}
|
|
3294
|
-
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
3295
|
-
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
3296
|
-
}
|
|
3297
|
-
if (parsed.username || parsed.password) {
|
|
3298
|
-
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
3299
|
-
}
|
|
3300
|
-
if (isBlockedHostname(parsed.hostname)) {
|
|
3301
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
3302
|
-
}
|
|
3303
|
-
return parsed;
|
|
3304
|
-
}
|
|
3305
|
-
function isBlockedIpAddress(address) {
|
|
3306
|
-
return isBlockedHostname(address.trim().toLowerCase());
|
|
3307
|
-
}
|
|
3308
|
-
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
3309
|
-
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
3310
|
-
if (options.resolveDns === false) {
|
|
3311
|
-
return parsed;
|
|
3312
|
-
}
|
|
3313
|
-
const hostname = parsed.hostname.trim().toLowerCase();
|
|
3314
|
-
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
3315
|
-
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
3316
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
3317
|
-
}
|
|
3318
|
-
return parsed;
|
|
3319
|
-
}
|
|
3320
|
-
|
|
3321
|
-
// ../../src/core/security/safeFetch.ts
|
|
3322
|
-
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
3323
|
-
async function safeFetch(input, init = {}, options = {}) {
|
|
3324
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
3325
|
-
const maxRedirects = options.maxRedirects ?? 0;
|
|
3326
|
-
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
3327
|
-
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
3328
|
-
const controller = new AbortController;
|
|
3329
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
3330
|
-
try {
|
|
3331
|
-
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
3332
|
-
let redirectCount = 0;
|
|
3333
|
-
while (true) {
|
|
3334
|
-
const response = await fetch(currentUrl, {
|
|
3335
|
-
...init,
|
|
3336
|
-
signal: controller.signal,
|
|
3337
|
-
redirect: "manual"
|
|
3338
|
-
});
|
|
3339
|
-
if (response.status >= 300 && response.status < 400) {
|
|
3340
|
-
const location = response.headers.get("location");
|
|
3341
|
-
if (!location || redirectCount >= maxRedirects) {
|
|
3342
|
-
return response;
|
|
3343
|
-
}
|
|
3344
|
-
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
3345
|
-
redirectCount += 1;
|
|
3346
|
-
continue;
|
|
3347
|
-
}
|
|
3348
|
-
return response;
|
|
3349
|
-
}
|
|
3350
|
-
} finally {
|
|
3351
|
-
clearTimeout(timeout);
|
|
3352
|
-
}
|
|
3353
|
-
}
|
|
3354
|
-
|
|
3355
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3356
|
-
class DispatchWebhookJob extends Job {
|
|
3357
|
-
maxAttempts = 3;
|
|
3358
|
-
backoffMs = 2000;
|
|
3359
|
-
async handle(payload) {
|
|
3360
|
-
const rows = await repositoryConnection`
|
|
3361
|
-
SELECT id, url, secret
|
|
3362
|
-
FROM webhook
|
|
3363
|
-
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
3364
|
-
LIMIT 1
|
|
3365
|
-
`;
|
|
3366
|
-
const webhook = rows[0];
|
|
3367
|
-
if (!webhook) {
|
|
3368
|
-
return;
|
|
3369
|
-
}
|
|
3370
|
-
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
3371
|
-
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
3372
|
-
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
3373
|
-
let responseStatus = null;
|
|
3374
|
-
let errorMessage = null;
|
|
3375
|
-
try {
|
|
3376
|
-
const response = await safeFetch(webhook.url, {
|
|
3377
|
-
method: "POST",
|
|
3378
|
-
headers: {
|
|
3379
|
-
"content-type": "application/json",
|
|
3380
|
-
"x-workhub-signature": signature
|
|
3381
|
-
},
|
|
3382
|
-
body
|
|
3383
|
-
}, { allowHttp: appConfig.env !== "production" });
|
|
3384
|
-
responseStatus = response.status;
|
|
3385
|
-
if (!response.ok) {
|
|
3386
|
-
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
3387
|
-
}
|
|
3388
|
-
} catch (error) {
|
|
3389
|
-
errorMessage = error instanceof Error ? error.message : String(error);
|
|
3390
|
-
await repositoryConnection`
|
|
3391
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
3392
|
-
VALUES (
|
|
3393
|
-
${webhook.id},
|
|
3394
|
-
${payload.event},
|
|
3395
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
3396
|
-
${responseStatus},
|
|
3397
|
-
${errorMessage}
|
|
3398
|
-
)
|
|
3399
|
-
`;
|
|
3400
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
3401
|
-
}
|
|
3402
|
-
await repositoryConnection`
|
|
3403
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
3404
|
-
VALUES (
|
|
3405
|
-
${webhook.id},
|
|
3406
|
-
${payload.event},
|
|
3407
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
3408
|
-
${responseStatus}
|
|
3409
|
-
)
|
|
3410
|
-
`;
|
|
3411
|
-
}
|
|
3237
|
+
// ../../src/core/queue/createAppQueue.ts
|
|
3238
|
+
var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
3239
|
+
function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
|
|
3240
|
+
return createProductionQueue(driver, {
|
|
3241
|
+
redisUrl,
|
|
3242
|
+
failedJobs,
|
|
3243
|
+
registerJobs
|
|
3244
|
+
});
|
|
3412
3245
|
}
|
|
3413
|
-
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
3414
3246
|
|
|
3415
3247
|
// ../../src/core/contracts/applicationContext.ts
|
|
3416
3248
|
function getRequiredDependency(dependencies, key) {
|
|
@@ -3502,24 +3334,6 @@ function resolveApplicationLogger() {
|
|
|
3502
3334
|
function resolveApplicationDependencies() {
|
|
3503
3335
|
return requireActiveApplicationContext().dependencies;
|
|
3504
3336
|
}
|
|
3505
|
-
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
3506
|
-
function registerDefaultJobs() {
|
|
3507
|
-
jobRegistry.register("cache.invalidate-tags", () => {
|
|
3508
|
-
return new invalidateCacheTagsJob_default(resolveApplicationCache());
|
|
3509
|
-
});
|
|
3510
|
-
jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
|
|
3511
|
-
}
|
|
3512
|
-
|
|
3513
|
-
// ../../src/core/queue/createAppQueue.ts
|
|
3514
|
-
var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
3515
|
-
function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
|
|
3516
|
-
return createProductionQueue(driver, {
|
|
3517
|
-
redisUrl,
|
|
3518
|
-
failedJobs,
|
|
3519
|
-
registerJobs
|
|
3520
|
-
});
|
|
3521
|
-
}
|
|
3522
|
-
|
|
3523
3337
|
// ../../src/bootstrap/discoverModules.ts
|
|
3524
3338
|
var appModules = [];
|
|
3525
3339
|
function discoverModules() {
|
|
@@ -3633,6 +3447,201 @@ var policyProvider = {
|
|
|
3633
3447
|
};
|
|
3634
3448
|
var policy_default = policyProvider;
|
|
3635
3449
|
|
|
3450
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3451
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
3452
|
+
|
|
3453
|
+
// ../../src/core/security/safeUrl.ts
|
|
3454
|
+
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
3455
|
+
var dnsLookup = dnsLookupImpl;
|
|
3456
|
+
var BLOCKED_HOSTNAMES = new Set([
|
|
3457
|
+
"localhost",
|
|
3458
|
+
"127.0.0.1",
|
|
3459
|
+
"0.0.0.0",
|
|
3460
|
+
"::1",
|
|
3461
|
+
"metadata.google.internal"
|
|
3462
|
+
]);
|
|
3463
|
+
function isPrivateIpv4(hostname) {
|
|
3464
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
3465
|
+
if (!match) {
|
|
3466
|
+
return false;
|
|
3467
|
+
}
|
|
3468
|
+
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
3469
|
+
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
3470
|
+
return true;
|
|
3471
|
+
}
|
|
3472
|
+
const [a = 0, b = 0] = octets;
|
|
3473
|
+
if (a === 10) {
|
|
3474
|
+
return true;
|
|
3475
|
+
}
|
|
3476
|
+
if (a === 127) {
|
|
3477
|
+
return true;
|
|
3478
|
+
}
|
|
3479
|
+
if (a === 0) {
|
|
3480
|
+
return true;
|
|
3481
|
+
}
|
|
3482
|
+
if (a === 169 && b === 254) {
|
|
3483
|
+
return true;
|
|
3484
|
+
}
|
|
3485
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
3486
|
+
return true;
|
|
3487
|
+
}
|
|
3488
|
+
if (a === 192 && b === 168) {
|
|
3489
|
+
return true;
|
|
3490
|
+
}
|
|
3491
|
+
return false;
|
|
3492
|
+
}
|
|
3493
|
+
function isBlockedHostname(hostname) {
|
|
3494
|
+
const normalized = hostname.trim().toLowerCase();
|
|
3495
|
+
if (normalized.length === 0) {
|
|
3496
|
+
return true;
|
|
3497
|
+
}
|
|
3498
|
+
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
3499
|
+
return true;
|
|
3500
|
+
}
|
|
3501
|
+
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
3502
|
+
return true;
|
|
3503
|
+
}
|
|
3504
|
+
if (normalized.includes(":")) {
|
|
3505
|
+
return true;
|
|
3506
|
+
}
|
|
3507
|
+
return isPrivateIpv4(normalized);
|
|
3508
|
+
}
|
|
3509
|
+
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
3510
|
+
let parsed;
|
|
3511
|
+
try {
|
|
3512
|
+
parsed = new URL(rawUrl);
|
|
3513
|
+
} catch {
|
|
3514
|
+
throw new BadRequestError("Webhook URL is invalid.");
|
|
3515
|
+
}
|
|
3516
|
+
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
3517
|
+
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
3518
|
+
}
|
|
3519
|
+
if (parsed.username || parsed.password) {
|
|
3520
|
+
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
3521
|
+
}
|
|
3522
|
+
if (isBlockedHostname(parsed.hostname)) {
|
|
3523
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
3524
|
+
}
|
|
3525
|
+
return parsed;
|
|
3526
|
+
}
|
|
3527
|
+
function isBlockedIpAddress(address) {
|
|
3528
|
+
return isBlockedHostname(address.trim().toLowerCase());
|
|
3529
|
+
}
|
|
3530
|
+
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
3531
|
+
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
3532
|
+
if (options.resolveDns === false) {
|
|
3533
|
+
return parsed;
|
|
3534
|
+
}
|
|
3535
|
+
const hostname = parsed.hostname.trim().toLowerCase();
|
|
3536
|
+
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
3537
|
+
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
3538
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
3539
|
+
}
|
|
3540
|
+
return parsed;
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
// ../../src/core/security/safeFetch.ts
|
|
3544
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
3545
|
+
async function safeFetch(input, init = {}, options = {}) {
|
|
3546
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
3547
|
+
const maxRedirects = options.maxRedirects ?? 0;
|
|
3548
|
+
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
3549
|
+
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
3550
|
+
const controller = new AbortController;
|
|
3551
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
3552
|
+
try {
|
|
3553
|
+
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
3554
|
+
let redirectCount = 0;
|
|
3555
|
+
while (true) {
|
|
3556
|
+
const response = await fetch(currentUrl, {
|
|
3557
|
+
...init,
|
|
3558
|
+
signal: controller.signal,
|
|
3559
|
+
redirect: "manual"
|
|
3560
|
+
});
|
|
3561
|
+
if (response.status >= 300 && response.status < 400) {
|
|
3562
|
+
const location = response.headers.get("location");
|
|
3563
|
+
if (!location || redirectCount >= maxRedirects) {
|
|
3564
|
+
return response;
|
|
3565
|
+
}
|
|
3566
|
+
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
3567
|
+
redirectCount += 1;
|
|
3568
|
+
continue;
|
|
3569
|
+
}
|
|
3570
|
+
return response;
|
|
3571
|
+
}
|
|
3572
|
+
} finally {
|
|
3573
|
+
clearTimeout(timeout);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3578
|
+
class DispatchWebhookJob extends Job {
|
|
3579
|
+
maxAttempts = 3;
|
|
3580
|
+
backoffMs = 2000;
|
|
3581
|
+
async handle(payload) {
|
|
3582
|
+
const rows = await repositoryConnection`
|
|
3583
|
+
SELECT id, url, secret
|
|
3584
|
+
FROM webhook
|
|
3585
|
+
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
3586
|
+
LIMIT 1
|
|
3587
|
+
`;
|
|
3588
|
+
const webhook = rows[0];
|
|
3589
|
+
if (!webhook) {
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
3593
|
+
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
3594
|
+
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
3595
|
+
let responseStatus = null;
|
|
3596
|
+
let errorMessage = null;
|
|
3597
|
+
try {
|
|
3598
|
+
const response = await safeFetch(webhook.url, {
|
|
3599
|
+
method: "POST",
|
|
3600
|
+
headers: {
|
|
3601
|
+
"content-type": "application/json",
|
|
3602
|
+
"x-workhub-signature": signature
|
|
3603
|
+
},
|
|
3604
|
+
body
|
|
3605
|
+
}, { allowHttp: appConfig.env !== "production" });
|
|
3606
|
+
responseStatus = response.status;
|
|
3607
|
+
if (!response.ok) {
|
|
3608
|
+
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
3609
|
+
}
|
|
3610
|
+
} catch (error) {
|
|
3611
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
3612
|
+
await repositoryConnection`
|
|
3613
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
3614
|
+
VALUES (
|
|
3615
|
+
${webhook.id},
|
|
3616
|
+
${payload.event},
|
|
3617
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
3618
|
+
${responseStatus},
|
|
3619
|
+
${errorMessage}
|
|
3620
|
+
)
|
|
3621
|
+
`;
|
|
3622
|
+
throw error instanceof Error ? error : new Error(errorMessage);
|
|
3623
|
+
}
|
|
3624
|
+
await repositoryConnection`
|
|
3625
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
3626
|
+
VALUES (
|
|
3627
|
+
${webhook.id},
|
|
3628
|
+
${payload.event},
|
|
3629
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
3630
|
+
${responseStatus}
|
|
3631
|
+
)
|
|
3632
|
+
`;
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
3636
|
+
|
|
3637
|
+
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
3638
|
+
function registerDefaultJobs() {
|
|
3639
|
+
jobRegistry.register("cache.invalidate-tags", () => {
|
|
3640
|
+
return new invalidateCacheTagsJob_default(resolveApplicationCache());
|
|
3641
|
+
});
|
|
3642
|
+
jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3636
3645
|
// ../../src/bootstrap/providers/queue.ts
|
|
3637
3646
|
var queueProvider = {
|
|
3638
3647
|
name: "core.queue",
|
|
@@ -349,7 +349,6 @@ var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
|
|
|
349
349
|
|
|
350
350
|
// ../../src/core/queue/jobRegistry.ts
|
|
351
351
|
class JobRegistry {
|
|
352
|
-
constructor() {}
|
|
353
352
|
factories = new Map;
|
|
354
353
|
instances = new WeakMap;
|
|
355
354
|
register(name, factory) {
|
|
@@ -373,7 +372,17 @@ class JobRegistry {
|
|
|
373
372
|
return [...this.factories.keys()];
|
|
374
373
|
}
|
|
375
374
|
}
|
|
376
|
-
var
|
|
375
|
+
var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
|
|
376
|
+
function readSharedJobRegistry() {
|
|
377
|
+
const globalRegistry = globalThis[JOB_REGISTRY_KEY];
|
|
378
|
+
if (globalRegistry) {
|
|
379
|
+
return globalRegistry;
|
|
380
|
+
}
|
|
381
|
+
const registry = new JobRegistry;
|
|
382
|
+
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
383
|
+
return registry;
|
|
384
|
+
}
|
|
385
|
+
var jobRegistry = readSharedJobRegistry();
|
|
377
386
|
|
|
378
387
|
// ../../src/core/contracts/applicationContext.ts
|
|
379
388
|
function getRequiredDependency(dependencies, key) {
|
|
@@ -473,6 +482,7 @@ function resolveApplicationLogger() {
|
|
|
473
482
|
function resolveApplicationDependencies() {
|
|
474
483
|
return requireActiveApplicationContext().dependencies;
|
|
475
484
|
}
|
|
485
|
+
|
|
476
486
|
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
477
487
|
function registerDefaultJobs() {
|
|
478
488
|
jobRegistry.register("cache.invalidate-tags", () => {
|