@getstrata/bootstrap 0.1.1 → 0.2.1
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/context.d.ts +6 -0
- package/dist/bootstrap/createWebRoutes.d.ts +4 -0
- package/dist/bootstrap/discoverListeners.d.ts +5 -0
- package/dist/bootstrap/discoverModules.d.ts +4 -0
- package/dist/bootstrap/env.d.ts +9 -0
- package/dist/bootstrap/httpKernel.d.ts +2 -0
- package/dist/bootstrap/listeners/invalidateCacheOnModelWrite.d.ts +3 -0
- package/dist/bootstrap/modules.d.ts +1 -0
- package/dist/bootstrap/providers/auth.d.ts +3 -0
- package/dist/bootstrap/providers/cache.d.ts +3 -0
- package/dist/bootstrap/providers/config.d.ts +5 -0
- package/dist/bootstrap/providers/events.d.ts +5 -0
- package/dist/bootstrap/providers/index.d.ts +3 -0
- package/dist/bootstrap/providers/listeners.d.ts +3 -0
- package/dist/bootstrap/providers/policy.d.ts +3 -0
- package/dist/bootstrap/providers/queue.d.ts +3 -0
- package/dist/bootstrap/providers/view.d.ts +5 -0
- package/dist/bootstrap/public-api.d.ts +7 -1
- package/dist/bootstrap/routeRegistry.d.ts +14 -0
- package/dist/bootstrap/schedule.d.ts +1 -0
- package/dist/bootstrap/secretsGuard.d.ts +2 -0
- package/dist/cli/commands/scheduleRun.d.ts +3 -0
- package/dist/config/auth.d.ts +7 -0
- package/dist/config/queue.d.ts +8 -0
- package/dist/config/rateLimit.d.ts +2 -1
- package/dist/core/audit/exportAuditLogs.d.ts +8 -0
- package/dist/core/audit/siemFormatter.d.ts +30 -0
- package/dist/core/auth/sessionCookie.d.ts +6 -0
- package/dist/core/auth/sessionGuard.d.ts +9 -0
- package/dist/core/cache/createCacheStore.d.ts +11 -0
- package/dist/core/cache/modelCacheTags.d.ts +3 -0
- package/dist/core/cache/redisCacheStore.d.ts +23 -0
- package/dist/core/cache/repository.d.ts +17 -0
- package/dist/core/cache/simpleCache.d.ts +23 -0
- package/dist/core/cache/simpleCacheStore.d.ts +16 -0
- package/dist/core/cache/store.d.ts +12 -0
- package/dist/core/cache/taggedCache.d.ts +9 -0
- package/dist/core/config/envSchema.d.ts +12 -0
- package/dist/core/database/baseRepository.d.ts +16 -4
- package/dist/core/database/index.d.ts +10 -4
- package/dist/core/database/migrations/types.d.ts +15 -0
- package/dist/core/database/model.d.ts +57 -0
- package/dist/core/database/query.d.ts +25 -14
- package/dist/core/database/relationships.d.ts +32 -2
- package/dist/core/database/repositoryQuery.d.ts +16 -1
- package/dist/core/database/schema/blueprint.d.ts +47 -0
- package/dist/core/database/schema/columnDefinition.d.ts +36 -0
- package/dist/core/database/schema/driver.d.ts +8 -0
- package/dist/core/database/schema/errors.d.ts +4 -0
- package/dist/core/database/schema/grammars/compileStatements.d.ts +8 -0
- package/dist/core/database/schema/grammars/createGrammar.d.ts +4 -0
- package/dist/core/database/schema/grammars/grammar.d.ts +10 -0
- package/dist/core/database/schema/grammars/index.d.ts +7 -0
- package/dist/core/database/schema/grammars/mysqlGrammar.d.ts +2 -0
- package/dist/core/database/schema/grammars/postgresGrammar.d.ts +2 -0
- package/dist/core/database/schema/grammars/sqliteGrammar.d.ts +2 -0
- package/dist/core/database/schema/index.d.ts +12 -0
- package/dist/core/database/schema/schema.d.ts +21 -0
- package/dist/core/database/types.d.ts +39 -2
- package/dist/core/database/whereBuilder.d.ts +17 -0
- package/dist/core/jobs/dispatchWebhookJob.d.ts +14 -0
- package/dist/core/jobs/invalidateCacheTagsJob.d.ts +12 -0
- package/dist/core/pagination/index.d.ts +11 -1
- package/dist/core/queue/createAppQueue.d.ts +6 -0
- package/dist/core/queue/failedJobRepository.d.ts +6 -0
- package/dist/core/queue/failedJobService.d.ts +15 -0
- package/dist/core/queue/failedJobTable.d.ts +3 -0
- package/dist/core/queue/jobRegistry.d.ts +14 -0
- package/dist/core/queue/jobRunner.d.ts +9 -0
- package/dist/core/queue/publicQueue.d.ts +15 -0
- package/dist/core/queue/redisQueue.d.ts +29 -0
- package/dist/core/queue/resilientQueue.d.ts +9 -0
- package/dist/core/queue/types.d.ts +8 -0
- package/dist/core/scheduler/schedule.d.ts +15 -0
- package/dist/core/security/safeFetch.d.ts +8 -0
- package/dist/core/security/safeUrl.d.ts +5 -0
- package/dist/core/view/etaViewEngine.d.ts +15 -0
- package/dist/core/view/htmlResponse.d.ts +6 -0
- package/dist/core/view/index.d.ts +5 -0
- package/dist/core/view/viewEngine.d.ts +6 -0
- package/dist/core/view/webLayoutData.d.ts +17 -0
- package/dist/domain/scim.d.ts +9 -0
- package/dist/index.js +4180 -458
- package/dist/modules/user/apiTokenRepository.d.ts +1 -1
- package/dist/modules/user/apiTokenTable.d.ts +1 -1
- package/dist/modules/user/authService.d.ts +1 -1
- package/dist/modules/user/notificationRepository.d.ts +1 -1
- package/dist/modules/user/notificationService.d.ts +1 -1
- package/dist/modules/user/notificationTable.d.ts +1 -1
- package/dist/modules/user/oauthIdentityRepository.d.ts +2 -2
- package/dist/modules/user/provider.d.ts +1 -1
- package/dist/modules/user/repository.d.ts +1 -1
- package/dist/modules/user/table.d.ts +1 -1
- package/dist/modules/user/tokenService.d.ts +1 -1
- package/package.json +44 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,407 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// ../../src/core/scheduler/schedule.ts
|
|
3
|
+
class Schedule {
|
|
4
|
+
tasks = [];
|
|
5
|
+
command(expression, name, run) {
|
|
6
|
+
this.tasks.push({ expression, name, run });
|
|
7
|
+
return this;
|
|
8
|
+
}
|
|
9
|
+
dueTasks(now = new Date) {
|
|
10
|
+
const minute = now.getMinutes();
|
|
11
|
+
return this.tasks.filter((task) => {
|
|
12
|
+
if (task.expression === "* * * * *") {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (task.expression.startsWith("*/")) {
|
|
16
|
+
const interval = Number.parseInt(task.expression.slice(2), 10);
|
|
17
|
+
return Number.isInteger(interval) && interval > 0 && minute % interval === 0;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
tasksList() {
|
|
23
|
+
return [...this.tasks];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
var appSchedule = new Schedule;
|
|
27
|
+
async function runDueScheduledTasks(schedule = appSchedule, now = new Date) {
|
|
28
|
+
const due = schedule.dueTasks(now);
|
|
29
|
+
for (const task of due) {
|
|
30
|
+
await task.run();
|
|
31
|
+
}
|
|
32
|
+
return due.length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ../../src/config/features.ts
|
|
36
|
+
function readFeatureFlags() {
|
|
37
|
+
return {
|
|
38
|
+
webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
|
|
39
|
+
fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
|
|
40
|
+
auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
|
|
41
|
+
oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
|
|
42
|
+
samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
|
|
43
|
+
scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
|
|
44
|
+
billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
|
|
45
|
+
siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
|
|
46
|
+
publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
|
|
47
|
+
emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
|
|
48
|
+
mfa: (process.env.FEATURE_MFA ?? "false") === "true"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
var featureFlags = readFeatureFlags();
|
|
52
|
+
function isFeatureEnabled(feature) {
|
|
53
|
+
return readFeatureFlags()[feature];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ../../src/config/app.ts
|
|
57
|
+
var appConfig = {
|
|
58
|
+
name: "WorkHub",
|
|
59
|
+
env: process.env.APP_ENV ?? "local",
|
|
60
|
+
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
61
|
+
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
62
|
+
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ../../src/config/database.ts
|
|
66
|
+
function readInteger(name, fallback) {
|
|
67
|
+
const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
|
|
68
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
69
|
+
}
|
|
70
|
+
var databaseConfig = {
|
|
71
|
+
url: process.env.DATABASE_URL ?? "",
|
|
72
|
+
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
73
|
+
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
74
|
+
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
75
|
+
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ../../src/core/database/connectionContext.ts
|
|
79
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
80
|
+
var activeConnection = new AsyncLocalStorage;
|
|
81
|
+
function runWithDatabaseConnection(connection, callback) {
|
|
82
|
+
return activeConnection.run(connection, callback);
|
|
83
|
+
}
|
|
84
|
+
function getActiveDatabaseConnection(fallback) {
|
|
85
|
+
return activeConnection.getStore() ?? fallback;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ../../src/db/connection/createConnection.ts
|
|
89
|
+
var {SQL } = globalThis.Bun;
|
|
90
|
+
function createDatabaseConnection(config) {
|
|
91
|
+
if (!config.url) {
|
|
92
|
+
throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
|
|
93
|
+
}
|
|
94
|
+
return new SQL({
|
|
95
|
+
url: config.url,
|
|
96
|
+
max: config.poolMax,
|
|
97
|
+
idleTimeout: config.idleTimeoutSeconds,
|
|
98
|
+
maxLifetime: config.maxLifetimeSeconds,
|
|
99
|
+
connectionTimeout: config.connectionTimeoutSeconds
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ../../src/db/connection/index.ts
|
|
104
|
+
var connectionHolder = {
|
|
105
|
+
connection: null
|
|
106
|
+
};
|
|
107
|
+
function getDatabase() {
|
|
108
|
+
if (!connectionHolder.connection) {
|
|
109
|
+
connectionHolder.connection = createDatabaseConnection(databaseConfig);
|
|
110
|
+
}
|
|
111
|
+
return connectionHolder.connection;
|
|
112
|
+
}
|
|
113
|
+
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
114
|
+
function resolveDatabase() {
|
|
115
|
+
return getActiveDatabaseConnection(getDatabase());
|
|
116
|
+
}
|
|
117
|
+
function resolveDatabaseForProperty(property) {
|
|
118
|
+
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
119
|
+
return getDatabase();
|
|
120
|
+
}
|
|
121
|
+
return resolveDatabase();
|
|
122
|
+
}
|
|
123
|
+
var db = new Proxy(function database() {}, {
|
|
124
|
+
apply(_target, _thisArg, args) {
|
|
125
|
+
return resolveDatabase()(...args);
|
|
126
|
+
},
|
|
127
|
+
get(_target, property) {
|
|
128
|
+
const connection = resolveDatabaseForProperty(property);
|
|
129
|
+
const value = connection[property];
|
|
130
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
var connection_default = db;
|
|
134
|
+
|
|
135
|
+
// ../../src/core/security/safeFetch.ts
|
|
136
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
137
|
+
async function safeFetch(input, init = {}, options = {}) {
|
|
138
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
139
|
+
const maxRedirects = options.maxRedirects ?? 0;
|
|
140
|
+
const controller = new AbortController;
|
|
141
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
142
|
+
try {
|
|
143
|
+
let currentUrl = input;
|
|
144
|
+
let redirectCount = 0;
|
|
145
|
+
while (true) {
|
|
146
|
+
const response = await fetch(currentUrl, {
|
|
147
|
+
...init,
|
|
148
|
+
signal: controller.signal,
|
|
149
|
+
redirect: "manual"
|
|
150
|
+
});
|
|
151
|
+
if (response.status >= 300 && response.status < 400) {
|
|
152
|
+
const location = response.headers.get("location");
|
|
153
|
+
if (!location || redirectCount >= maxRedirects) {
|
|
154
|
+
return response;
|
|
155
|
+
}
|
|
156
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
157
|
+
redirectCount += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
return response;
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
clearTimeout(timeout);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ../../src/core/errors/http.ts
|
|
168
|
+
class HttpError extends Error {
|
|
169
|
+
status;
|
|
170
|
+
details;
|
|
171
|
+
constructor(status, message, details) {
|
|
172
|
+
super(message);
|
|
173
|
+
this.name = new.target.name;
|
|
174
|
+
this.status = status;
|
|
175
|
+
this.details = details;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
class BadRequestError extends HttpError {
|
|
180
|
+
constructor(message = "Bad Request", details) {
|
|
181
|
+
super(400, message, details);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
class ConflictError extends HttpError {
|
|
185
|
+
constructor(message = "Conflict", details) {
|
|
186
|
+
super(409, message, details);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
class UnprocessableEntityError extends HttpError {
|
|
191
|
+
constructor(message = "Unprocessable Entity", details) {
|
|
192
|
+
super(422, message, details);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
class ForbiddenError extends HttpError {
|
|
196
|
+
constructor(message = "Forbidden", details) {
|
|
197
|
+
super(403, message, details);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
class UnauthorizedError extends HttpError {
|
|
202
|
+
constructor(message = "Unauthorized", details) {
|
|
203
|
+
super(401, message, details);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
class PayloadTooLargeError extends HttpError {
|
|
208
|
+
constructor(message = "Payload Too Large", details) {
|
|
209
|
+
super(413, message, details);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ../../src/core/security/safeUrl.ts
|
|
214
|
+
var BLOCKED_HOSTNAMES = new Set([
|
|
215
|
+
"localhost",
|
|
216
|
+
"127.0.0.1",
|
|
217
|
+
"0.0.0.0",
|
|
218
|
+
"::1",
|
|
219
|
+
"metadata.google.internal"
|
|
220
|
+
]);
|
|
221
|
+
function isPrivateIpv4(hostname) {
|
|
222
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
223
|
+
if (!match) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
227
|
+
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
const [a = 0, b = 0] = octets;
|
|
231
|
+
if (a === 10) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
if (a === 127) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
if (a === 0) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
if (a === 169 && b === 254) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
if (a === 192 && b === 168) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
function isBlockedHostname(hostname) {
|
|
252
|
+
const normalized = hostname.trim().toLowerCase();
|
|
253
|
+
if (normalized.length === 0) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
if (normalized.includes(":")) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
return isPrivateIpv4(normalized);
|
|
266
|
+
}
|
|
267
|
+
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
268
|
+
let parsed;
|
|
269
|
+
try {
|
|
270
|
+
parsed = new URL(rawUrl);
|
|
271
|
+
} catch {
|
|
272
|
+
throw new BadRequestError("Webhook URL is invalid.");
|
|
273
|
+
}
|
|
274
|
+
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
275
|
+
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
276
|
+
}
|
|
277
|
+
if (parsed.username || parsed.password) {
|
|
278
|
+
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
279
|
+
}
|
|
280
|
+
if (isBlockedHostname(parsed.hostname)) {
|
|
281
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
282
|
+
}
|
|
283
|
+
return parsed;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ../../src/core/tenant/databaseTenantContext.ts
|
|
287
|
+
async function runWithMigrationBypass(callback) {
|
|
288
|
+
await connection_default`SELECT set_config('app.bypass_rls', 'true', false)`;
|
|
289
|
+
try {
|
|
290
|
+
return await callback();
|
|
291
|
+
} finally {
|
|
292
|
+
await connection_default`SELECT set_config('app.bypass_rls', 'false', false)`;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ../../src/core/audit/siemFormatter.ts
|
|
297
|
+
function formatSiemAuditEvent(input) {
|
|
298
|
+
return {
|
|
299
|
+
timestamp: input.created_at.toISOString(),
|
|
300
|
+
event_type: "workhub.audit",
|
|
301
|
+
actor_user_id: input.user_id,
|
|
302
|
+
tenant_id: input.tenant_id ?? null,
|
|
303
|
+
trace_id: input.trace_id ?? null,
|
|
304
|
+
action: input.action,
|
|
305
|
+
subject_type: input.subject_type,
|
|
306
|
+
subject_id: input.subject_id,
|
|
307
|
+
ip_address: input.ip_address ?? null,
|
|
308
|
+
user_agent: input.user_agent ?? null,
|
|
309
|
+
checksum: input.checksum ?? null,
|
|
310
|
+
payload: input.payload ?? {}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function formatCefLine(event) {
|
|
314
|
+
const extension = [
|
|
315
|
+
`rt=${event.timestamp}`,
|
|
316
|
+
`suid=${event.actor_user_id ?? "unknown"}`,
|
|
317
|
+
`cs1=${event.action}`,
|
|
318
|
+
`cs1Label=Action`,
|
|
319
|
+
`cs2=${event.subject_type}`,
|
|
320
|
+
`cs2Label=SubjectType`,
|
|
321
|
+
`cs3=${event.subject_id ?? ""}`,
|
|
322
|
+
`cs3Label=SubjectId`,
|
|
323
|
+
`src=${event.ip_address ?? ""}`,
|
|
324
|
+
`request=${event.trace_id ?? ""}`
|
|
325
|
+
].join(" ");
|
|
326
|
+
return `CEF:0|WorkHub|API|1.0|${event.action}|${event.subject_type}|5|${extension}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ../../src/core/audit/exportAuditLogs.ts
|
|
330
|
+
function resolveAuditExportConfig() {
|
|
331
|
+
const endpoint = process.env.SIEM_EXPORT_URL?.trim();
|
|
332
|
+
if (!endpoint) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
assertSafeOutboundUrl(endpoint, { allowHttp: appConfig.env !== "production" });
|
|
336
|
+
const batchSize = Number(process.env.SIEM_EXPORT_BATCH_SIZE ?? "100");
|
|
337
|
+
return {
|
|
338
|
+
endpoint,
|
|
339
|
+
format: process.env.SIEM_EXPORT_FORMAT === "cef" ? "cef" : "json",
|
|
340
|
+
batchSize: Number.isFinite(batchSize) ? batchSize : 100
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async function exportPendingAuditLogs() {
|
|
344
|
+
const config = resolveAuditExportConfig();
|
|
345
|
+
if (!config) {
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
348
|
+
return await runWithMigrationBypass(async () => {
|
|
349
|
+
const rows = await connection_default`
|
|
350
|
+
SELECT
|
|
351
|
+
id,
|
|
352
|
+
user_id,
|
|
353
|
+
action,
|
|
354
|
+
subject_type,
|
|
355
|
+
subject_id,
|
|
356
|
+
payload,
|
|
357
|
+
ip_address,
|
|
358
|
+
user_agent,
|
|
359
|
+
checksum,
|
|
360
|
+
tenant_id,
|
|
361
|
+
trace_id,
|
|
362
|
+
created_at
|
|
363
|
+
FROM audit_log
|
|
364
|
+
WHERE exported_at IS NULL
|
|
365
|
+
ORDER BY id
|
|
366
|
+
LIMIT ${config.batchSize}
|
|
367
|
+
`;
|
|
368
|
+
if (rows.length === 0) {
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
const events = rows.map((row) => formatSiemAuditEvent({
|
|
372
|
+
action: row.action,
|
|
373
|
+
subject_type: row.subject_type,
|
|
374
|
+
subject_id: row.subject_id,
|
|
375
|
+
user_id: row.user_id,
|
|
376
|
+
tenant_id: row.tenant_id,
|
|
377
|
+
trace_id: row.trace_id,
|
|
378
|
+
ip_address: row.ip_address,
|
|
379
|
+
user_agent: row.user_agent,
|
|
380
|
+
checksum: row.checksum,
|
|
381
|
+
payload: row.payload,
|
|
382
|
+
created_at: row.created_at
|
|
383
|
+
}));
|
|
384
|
+
const body = config.format === "cef" ? events.map((event) => formatCefLine(event)).join(`
|
|
385
|
+
`) : JSON.stringify({ events });
|
|
386
|
+
const response = await safeFetch(config.endpoint, {
|
|
387
|
+
method: "POST",
|
|
388
|
+
headers: {
|
|
389
|
+
"content-type": config.format === "cef" ? "text/plain" : "application/json",
|
|
390
|
+
...process.env.SIEM_EXPORT_TOKEN ? { authorization: `Bearer ${process.env.SIEM_EXPORT_TOKEN}` } : {}
|
|
391
|
+
},
|
|
392
|
+
body
|
|
393
|
+
});
|
|
394
|
+
if (!response.ok) {
|
|
395
|
+
throw new Error(`SIEM export failed with status ${response.status}.`);
|
|
396
|
+
}
|
|
397
|
+
const ids = rows.map((row) => row.id);
|
|
398
|
+
for (const id of ids) {
|
|
399
|
+
await connection_default`UPDATE audit_log SET exported_at = NOW() WHERE id = ${id}`;
|
|
400
|
+
}
|
|
401
|
+
return rows.length;
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
2
405
|
// ../../src/core/logging/logger.ts
|
|
3
406
|
class Logger {
|
|
4
407
|
channel;
|
|
@@ -35,8 +438,41 @@ class Logger {
|
|
|
35
438
|
}
|
|
36
439
|
var appLogger = new Logger("app");
|
|
37
440
|
|
|
441
|
+
// ../../src/bootstrap/schedule.ts
|
|
442
|
+
appSchedule.command("* * * * *", "heartbeat", () => {
|
|
443
|
+
appLogger.debug("Scheduler heartbeat");
|
|
444
|
+
});
|
|
445
|
+
appSchedule.command("* * * * *", "audit-export", async () => {
|
|
446
|
+
if (!isFeatureEnabled("siemExport")) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
const exported = await exportPendingAuditLogs();
|
|
451
|
+
if (exported > 0) {
|
|
452
|
+
appLogger.info(`Exported ${exported} audit log entries to SIEM.`);
|
|
453
|
+
}
|
|
454
|
+
} catch (error) {
|
|
455
|
+
appLogger.error("Audit export failed.", { error: String(error) });
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// ../../src/cli/commands/scheduleRun.ts
|
|
460
|
+
async function scheduleRunCommand() {
|
|
461
|
+
const due = appSchedule.dueTasks();
|
|
462
|
+
if (due.length === 0) {
|
|
463
|
+
console.log("No scheduled tasks due.");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
for (const task of due) {
|
|
467
|
+
console.log(`Running scheduled task: ${task.name}`);
|
|
468
|
+
}
|
|
469
|
+
await runDueScheduledTasks(appSchedule);
|
|
470
|
+
}
|
|
38
471
|
// ../../src/bootstrap/config.ts
|
|
39
472
|
var APP_PORT_CONFIG_KEY = "app.port";
|
|
473
|
+
var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
|
|
474
|
+
var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
|
|
475
|
+
var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
|
|
40
476
|
var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
|
|
41
477
|
var DATABASE_URL_CONFIG_KEY = "database.url";
|
|
42
478
|
var CORE_CONFIG_TOKEN = "core.config";
|
|
@@ -46,6 +482,11 @@ var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
|
46
482
|
var CORE_AUTH_TOKEN = "core.auth";
|
|
47
483
|
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
48
484
|
var DEFAULT_APP_PORT = 3000;
|
|
485
|
+
var DEFAULT_CACHE_TTL_MS = 3600000;
|
|
486
|
+
var DEFAULT_CACHE_MAX_ENTRIES = 100;
|
|
487
|
+
var DEFAULT_CACHE_DRIVER = "array";
|
|
488
|
+
var DEFAULT_API_TOKEN = "";
|
|
489
|
+
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
49
490
|
|
|
50
491
|
// ../../src/bootstrap/contracts.ts
|
|
51
492
|
class ServiceContainer {
|
|
@@ -142,22 +583,3594 @@ function requireActiveApplicationContext() {
|
|
|
142
583
|
}
|
|
143
584
|
return activeContext;
|
|
144
585
|
}
|
|
586
|
+
function resolveApplicationCache() {
|
|
587
|
+
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
588
|
+
}
|
|
145
589
|
function resolveApplicationQueue() {
|
|
146
590
|
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
147
591
|
}
|
|
148
|
-
// ../../src/
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
592
|
+
// ../../src/bootstrap/discoverModules.ts
|
|
593
|
+
import { readdirSync } from "fs";
|
|
594
|
+
import { join } from "path";
|
|
595
|
+
import { pathToFileURL } from "url";
|
|
596
|
+
async function loadDiscoveredModules() {
|
|
597
|
+
const modulesDirectory = join(import.meta.dir, "../modules");
|
|
598
|
+
let moduleNames;
|
|
599
|
+
try {
|
|
600
|
+
moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
if (error.code === "ENOENT") {
|
|
603
|
+
return [];
|
|
604
|
+
}
|
|
605
|
+
throw error;
|
|
606
|
+
}
|
|
607
|
+
const modules = await Promise.all(moduleNames.map(async (moduleName) => {
|
|
608
|
+
const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
|
|
609
|
+
const loaded = await import(moduleUrl);
|
|
610
|
+
return loaded.default;
|
|
611
|
+
}));
|
|
612
|
+
return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
613
|
+
}
|
|
614
|
+
var appModules = await loadDiscoveredModules();
|
|
615
|
+
// ../../src/config/auth.ts
|
|
616
|
+
var authConfig = {
|
|
617
|
+
allowDevHeaders: (process.env.AUTH_DEV_HEADERS ?? "true") !== "false",
|
|
618
|
+
tokenDefaultAbilities: ["*"]
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// ../../src/domain/abilities.ts
|
|
622
|
+
var MEMBER_ABILITIES = [
|
|
623
|
+
"organizations:read",
|
|
624
|
+
"projects:read",
|
|
625
|
+
"projects:create",
|
|
626
|
+
"tasks:read",
|
|
627
|
+
"tasks:create",
|
|
628
|
+
"comments:read",
|
|
629
|
+
"comments:create",
|
|
630
|
+
"attachments:read",
|
|
631
|
+
"attachments:create",
|
|
632
|
+
"auth:tokens:read",
|
|
633
|
+
"auth:tokens:write"
|
|
634
|
+
];
|
|
635
|
+
var ADMIN_ABILITIES = [
|
|
636
|
+
...MEMBER_ABILITIES,
|
|
637
|
+
"organizations:create",
|
|
638
|
+
"organizations:update",
|
|
639
|
+
"organizations:delete",
|
|
640
|
+
"projects:update",
|
|
641
|
+
"projects:delete",
|
|
642
|
+
"tasks:update",
|
|
643
|
+
"tasks:delete",
|
|
644
|
+
"comments:update",
|
|
645
|
+
"comments:delete",
|
|
646
|
+
"attachments:delete",
|
|
647
|
+
"webhooks:read",
|
|
648
|
+
"webhooks:write",
|
|
649
|
+
"audit:read"
|
|
650
|
+
];
|
|
651
|
+
var PLATFORM_ADMIN_ABILITIES = ["*"];
|
|
652
|
+
function resolveAbilitiesForRole(role) {
|
|
653
|
+
if (role === "admin") {
|
|
654
|
+
return [...PLATFORM_ADMIN_ABILITIES];
|
|
655
|
+
}
|
|
656
|
+
return [...MEMBER_ABILITIES];
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ../../src/modules/user/provider.ts
|
|
660
|
+
import { OidcProvider } from "@getstrata/core/auth/oauth/oidcProvider";
|
|
661
|
+
import { GitHubOAuthProvider, MockOAuthProvider } from "@getstrata/core/auth/oauth/providers";
|
|
662
|
+
import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
|
|
663
|
+
|
|
664
|
+
// ../../src/modules/user/apiTokenRepository.ts
|
|
665
|
+
import { BaseRepository } from "@getstrata/core/database";
|
|
666
|
+
|
|
667
|
+
// ../../src/modules/user/apiTokenTable.ts
|
|
668
|
+
import { defineTable } from "@getstrata/core/database";
|
|
669
|
+
var apiTokenTable = defineTable({
|
|
670
|
+
name: "api_token",
|
|
671
|
+
primaryKey: "id",
|
|
672
|
+
columns: [
|
|
673
|
+
"id",
|
|
674
|
+
"user_id",
|
|
675
|
+
"name",
|
|
676
|
+
"token_hash",
|
|
677
|
+
"abilities",
|
|
678
|
+
"last_used_at",
|
|
679
|
+
"expires_at",
|
|
680
|
+
"created_at"
|
|
681
|
+
],
|
|
682
|
+
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// ../../src/modules/user/authService.ts
|
|
686
|
+
import { verifyPassword } from "@getstrata/core/auth/password";
|
|
687
|
+
import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
|
|
688
|
+
import { UnauthorizedError as UnauthorizedError2 } from "@getstrata/core/errors/http";
|
|
689
|
+
import { logSecurityEvent } from "@getstrata/core/security/securityEvents";
|
|
690
|
+
import { resolveDefaultTokenExpiryDays } from "@getstrata/core/security/tokenExpiry";
|
|
691
|
+
import { verifyTotp } from "@getstrata/core/security/totp";
|
|
692
|
+
import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
|
|
693
|
+
class AuthService {
|
|
694
|
+
users;
|
|
695
|
+
tokens;
|
|
696
|
+
oauthIdentities;
|
|
697
|
+
oauthProviders = new Map;
|
|
698
|
+
constructor(users, tokens, oauthIdentities) {
|
|
699
|
+
this.users = users;
|
|
700
|
+
this.tokens = tokens;
|
|
701
|
+
this.oauthIdentities = oauthIdentities;
|
|
702
|
+
}
|
|
703
|
+
registerOAuthProvider(provider) {
|
|
704
|
+
this.oauthProviders.set(provider.name, provider);
|
|
705
|
+
}
|
|
706
|
+
getOAuthProvider(name) {
|
|
707
|
+
return this.oauthProviders.get(name);
|
|
708
|
+
}
|
|
709
|
+
async loginWithPassword(email, password, options = {}) {
|
|
710
|
+
const user = await this.users.findByEmail(email);
|
|
711
|
+
if (!user?.password_hash) {
|
|
712
|
+
logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
|
|
713
|
+
throw new UnauthorizedError2("Invalid credentials.");
|
|
714
|
+
}
|
|
715
|
+
const valid = await verifyPassword(password, user.password_hash);
|
|
716
|
+
if (!valid) {
|
|
717
|
+
logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
|
|
718
|
+
throw new UnauthorizedError2("Invalid credentials.");
|
|
719
|
+
}
|
|
720
|
+
if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
|
|
721
|
+
logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
|
|
722
|
+
throw new UnauthorizedError2("Email address is not verified.");
|
|
723
|
+
}
|
|
724
|
+
if (isFeatureEnabled("mfa") && user.mfa_enabled) {
|
|
725
|
+
const mfaSecret = revealMfaSecret(user.mfa_secret);
|
|
726
|
+
if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
|
|
727
|
+
logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
|
|
728
|
+
throw new UnauthorizedError2("Invalid MFA code.");
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
|
|
732
|
+
return await this.tokens.createToken(user.id, {
|
|
733
|
+
name: "password-login",
|
|
734
|
+
abilities: resolveAbilitiesForRole(user.role),
|
|
735
|
+
expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
async loginWithOAuth(providerName, code) {
|
|
739
|
+
const provider = this.oauthProviders.get(providerName);
|
|
740
|
+
if (!provider) {
|
|
741
|
+
throw new UnauthorizedError2("Unsupported OAuth provider.");
|
|
742
|
+
}
|
|
743
|
+
const profile = await provider.exchangeCode(code);
|
|
744
|
+
const user = await this.findOrCreateOAuthUser(providerName, profile);
|
|
745
|
+
logSecurityEvent("auth_login_success", { user_id: user.id, method: `oauth:${providerName}` });
|
|
746
|
+
return await this.tokens.createToken(user.id, {
|
|
747
|
+
name: `${providerName}-oauth`,
|
|
748
|
+
abilities: resolveAbilitiesForRole(user.role),
|
|
749
|
+
expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
buildOAuthAuthorizationUrl(providerName, state) {
|
|
753
|
+
const provider = this.oauthProviders.get(providerName);
|
|
754
|
+
if (!provider) {
|
|
755
|
+
throw new UnauthorizedError2("Unsupported OAuth provider.");
|
|
756
|
+
}
|
|
757
|
+
return provider.getAuthorizationUrl(state);
|
|
758
|
+
}
|
|
759
|
+
async findOrCreateOAuthUser(providerName, profile) {
|
|
760
|
+
const existingIdentity = await this.oauthIdentities.findByProviderUser(providerName, profile.providerUserId);
|
|
761
|
+
if (existingIdentity) {
|
|
762
|
+
return await this.users.findByIdOrThrow(existingIdentity.user_id);
|
|
763
|
+
}
|
|
764
|
+
const existingUser = await this.users.findByEmail(profile.email);
|
|
765
|
+
const user = existingUser ?? await this.users.create({
|
|
766
|
+
name: profile.name,
|
|
767
|
+
email: profile.email,
|
|
768
|
+
role: "member",
|
|
769
|
+
tenant_id: currentTenantId(),
|
|
770
|
+
email_verified_at: new Date,
|
|
771
|
+
created_at: new Date,
|
|
772
|
+
updated_at: new Date
|
|
773
|
+
});
|
|
774
|
+
await this.oauthIdentities.create({
|
|
775
|
+
user_id: user.id,
|
|
776
|
+
provider: providerName,
|
|
777
|
+
provider_user_id: profile.providerUserId,
|
|
778
|
+
email: profile.email,
|
|
779
|
+
created_at: new Date
|
|
780
|
+
});
|
|
781
|
+
return user;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ../../src/modules/user/notificationRepository.ts
|
|
786
|
+
import { BaseRepository as BaseRepository2 } from "@getstrata/core/database";
|
|
787
|
+
|
|
788
|
+
// ../../src/modules/user/notificationTable.ts
|
|
789
|
+
import { defineTable as defineTable2 } from "@getstrata/core/database";
|
|
790
|
+
var notificationTable = defineTable2({
|
|
791
|
+
name: "notification",
|
|
792
|
+
primaryKey: "id",
|
|
793
|
+
columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
|
|
794
|
+
defaultOrderBy: { column: "created_at", direction: "DESC" }
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// ../../src/modules/user/notificationService.ts
|
|
798
|
+
import { NotFoundError } from "@getstrata/core/errors/http";
|
|
799
|
+
|
|
800
|
+
// ../../src/modules/user/oauthIdentityRepository.ts
|
|
801
|
+
import { BaseRepository as BaseRepository3, defineTable as defineTable3 } from "@getstrata/core/database";
|
|
802
|
+
var oauthIdentityTable = defineTable3({
|
|
803
|
+
name: "oauth_identity",
|
|
804
|
+
primaryKey: "id",
|
|
805
|
+
columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// ../../src/modules/user/repository.ts
|
|
809
|
+
import {
|
|
810
|
+
emailLookupForQuery,
|
|
811
|
+
protectEmail,
|
|
812
|
+
revealEmail
|
|
813
|
+
} from "@getstrata/core/crypto/fieldEncryption";
|
|
814
|
+
import { revealMfaSecret as revealMfaSecret2 } from "@getstrata/core/crypto/mfaSecret";
|
|
815
|
+
import { BaseRepository as BaseRepository4 } from "@getstrata/core/database";
|
|
816
|
+
import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
|
|
817
|
+
|
|
818
|
+
// ../../src/modules/user/table.ts
|
|
819
|
+
import { defineTable as defineTable4 } from "@getstrata/core/database";
|
|
820
|
+
var userTable = defineTable4({
|
|
821
|
+
name: "users",
|
|
822
|
+
primaryKey: "id",
|
|
823
|
+
columns: [
|
|
824
|
+
"id",
|
|
825
|
+
"name",
|
|
826
|
+
"email",
|
|
827
|
+
"email_lookup",
|
|
828
|
+
"role",
|
|
829
|
+
"tenant_id",
|
|
830
|
+
"password_hash",
|
|
831
|
+
"email_verified_at",
|
|
832
|
+
"mfa_secret",
|
|
833
|
+
"mfa_enabled",
|
|
834
|
+
"created_at",
|
|
835
|
+
"updated_at"
|
|
836
|
+
],
|
|
837
|
+
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
// ../../src/modules/user/tokenService.ts
|
|
841
|
+
import { hashApiToken } from "@getstrata/core/auth/tokenHash";
|
|
842
|
+
import { ForbiddenError as ForbiddenError2, NotFoundError as NotFoundError2 } from "@getstrata/core/errors/http";
|
|
843
|
+
import { resolveDefaultTokenExpiryDays as resolveDefaultTokenExpiryDays2 } from "@getstrata/core/security/tokenExpiry";
|
|
844
|
+
|
|
845
|
+
// ../../src/modules/user/provider.ts
|
|
846
|
+
var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
|
|
847
|
+
|
|
848
|
+
// ../../src/core/auth/authContext.ts
|
|
849
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
850
|
+
var authContext = new AsyncLocalStorage2;
|
|
851
|
+
function runWithAuthUser(user, callback) {
|
|
852
|
+
return authContext.run(user, callback);
|
|
853
|
+
}
|
|
854
|
+
function currentAuthUser() {
|
|
855
|
+
return authContext.getStore() ?? null;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// ../../src/core/auth/guard.ts
|
|
859
|
+
function devHeaderAbilities(role) {
|
|
860
|
+
if (role === "admin") {
|
|
861
|
+
return [...ADMIN_ABILITIES];
|
|
862
|
+
}
|
|
863
|
+
return [...MEMBER_ABILITIES];
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
class GuestGuard {
|
|
867
|
+
resolve(request) {
|
|
868
|
+
const userId = request.headers.get("x-authenticated-user-id");
|
|
869
|
+
if (!userId) {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
const role = request.headers.get("x-authenticated-user-role");
|
|
873
|
+
return {
|
|
874
|
+
id: userId,
|
|
875
|
+
abilities: devHeaderAbilities(role),
|
|
876
|
+
...role ? { role } : {}
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
class DatabaseTokenGuard {
|
|
881
|
+
container;
|
|
882
|
+
constructor(container) {
|
|
883
|
+
this.container = container;
|
|
884
|
+
}
|
|
885
|
+
async resolve(request) {
|
|
886
|
+
const authorization = request.headers.get("authorization");
|
|
887
|
+
if (!authorization?.startsWith("Bearer ")) {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
const token = authorization.slice("Bearer ".length).trim();
|
|
891
|
+
if (!token) {
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
if (!this.container.has(tokenServiceToken)) {
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
const tokenService = this.container.resolve(tokenServiceToken);
|
|
898
|
+
return await tokenService.resolveUserFromToken(token);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
class CompositeGuard {
|
|
903
|
+
guards;
|
|
904
|
+
constructor(guards) {
|
|
905
|
+
this.guards = guards;
|
|
906
|
+
}
|
|
907
|
+
async resolve(request) {
|
|
908
|
+
for (const guard of this.guards) {
|
|
909
|
+
const user = await Promise.resolve(guard.resolve(request));
|
|
910
|
+
if (user) {
|
|
911
|
+
return user;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
class AuthManager {
|
|
919
|
+
guard;
|
|
920
|
+
constructor(guard) {
|
|
921
|
+
this.guard = guard;
|
|
922
|
+
}
|
|
923
|
+
async resolve(request) {
|
|
924
|
+
if (request) {
|
|
925
|
+
return await Promise.resolve(this.guard.resolve(request));
|
|
926
|
+
}
|
|
927
|
+
return currentAuthUser();
|
|
928
|
+
}
|
|
929
|
+
user(request) {
|
|
930
|
+
return this.resolve(request);
|
|
931
|
+
}
|
|
932
|
+
async check(request) {
|
|
933
|
+
return await this.user(request) !== null;
|
|
934
|
+
}
|
|
935
|
+
async requireUser(request) {
|
|
936
|
+
const user = await this.user(request);
|
|
937
|
+
if (!user) {
|
|
938
|
+
throw new UnauthorizedError;
|
|
939
|
+
}
|
|
940
|
+
return user;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// ../../src/core/auth/sessionCookie.ts
|
|
945
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
946
|
+
var SESSION_COOKIE = "workhub_session";
|
|
947
|
+
var SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
|
|
948
|
+
function resolveSessionSecret() {
|
|
949
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-session-secret";
|
|
950
|
+
}
|
|
951
|
+
function signSession(userId, issuedAt) {
|
|
952
|
+
const payload = `${userId}.${issuedAt}`;
|
|
953
|
+
const signature = createHmac("sha256", resolveSessionSecret()).update(payload).digest("hex");
|
|
954
|
+
return `${payload}.${signature}`;
|
|
955
|
+
}
|
|
956
|
+
function readCookieValue(request, cookieName) {
|
|
957
|
+
const cookieHeader = request.headers.get("cookie");
|
|
958
|
+
if (!cookieHeader) {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
for (const part of cookieHeader.split(";")) {
|
|
962
|
+
const [name, ...rest] = part.trim().split("=");
|
|
963
|
+
if (name === cookieName) {
|
|
964
|
+
return decodeURIComponent(rest.join("="));
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
function readSessionUserId(request) {
|
|
970
|
+
const cookieValue = readCookieValue(request, SESSION_COOKIE);
|
|
971
|
+
if (!cookieValue) {
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
const parts = cookieValue.split(".");
|
|
975
|
+
if (parts.length !== 3) {
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
const [userIdRaw, issuedAtRaw, cookieSignature] = parts;
|
|
979
|
+
const userId = Number.parseInt(String(userIdRaw), 10);
|
|
980
|
+
const issuedAt = Number.parseInt(String(issuedAtRaw), 10);
|
|
981
|
+
if (!Number.isInteger(userId) || userId <= 0 || !Number.isFinite(issuedAt)) {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
if (Date.now() - issuedAt > SESSION_TTL_SECONDS * 1000) {
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
const expectedSignature = signSession(userId, issuedAt).split(".").pop();
|
|
988
|
+
if (!expectedSignature || !cookieSignature) {
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
992
|
+
const actualBuffer = Buffer.from(cookieSignature);
|
|
993
|
+
if (expectedBuffer.length !== actualBuffer.length) {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
return userId;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// ../../src/core/auth/sessionGuard.ts
|
|
1003
|
+
class SessionGuard {
|
|
1004
|
+
container;
|
|
1005
|
+
constructor(container) {
|
|
1006
|
+
this.container = container;
|
|
1007
|
+
}
|
|
1008
|
+
async resolve(request) {
|
|
1009
|
+
const userId = readSessionUserId(request);
|
|
1010
|
+
if (!userId) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
if (!this.container.has(tokenServiceToken)) {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
const tokenService = this.container.resolve(tokenServiceToken);
|
|
1017
|
+
try {
|
|
1018
|
+
const user = await tokenService.findByIdOrThrow(userId);
|
|
1019
|
+
return {
|
|
1020
|
+
id: user.id,
|
|
1021
|
+
role: user.role,
|
|
1022
|
+
abilities: resolveAbilitiesForRole(user.role)
|
|
1023
|
+
};
|
|
1024
|
+
} catch {
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// ../../src/bootstrap/providers/auth.ts
|
|
1031
|
+
var authProvider = {
|
|
1032
|
+
name: "core.auth",
|
|
1033
|
+
register({ container, config }) {
|
|
1034
|
+
config.set("auth.allowDevHeaders", authConfig.allowDevHeaders);
|
|
1035
|
+
const guards = [new DatabaseTokenGuard(container), new SessionGuard(container)];
|
|
1036
|
+
if (authConfig.allowDevHeaders) {
|
|
1037
|
+
guards.push(new GuestGuard);
|
|
1038
|
+
}
|
|
1039
|
+
container.set(CORE_AUTH_TOKEN, new AuthManager(new CompositeGuard(guards)));
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
var auth_default = authProvider;
|
|
1043
|
+
|
|
1044
|
+
// ../../src/core/cache/redisCacheStore.ts
|
|
1045
|
+
var {RedisClient } = globalThis.Bun;
|
|
1046
|
+
var KEY_PREFIX = "workhub:cache:";
|
|
1047
|
+
var TAG_PREFIX = "workhub:cache:tag:";
|
|
1048
|
+
|
|
1049
|
+
class RedisCacheStore {
|
|
1050
|
+
ttlMs;
|
|
1051
|
+
maxEntries;
|
|
1052
|
+
client;
|
|
1053
|
+
inflight = new Map;
|
|
1054
|
+
keyTags = new Map;
|
|
1055
|
+
constructor(redisUrl, ttlMs, maxEntries) {
|
|
1056
|
+
this.ttlMs = ttlMs;
|
|
1057
|
+
this.maxEntries = maxEntries;
|
|
1058
|
+
this.client = new RedisClient(redisUrl);
|
|
1059
|
+
}
|
|
1060
|
+
async get(key) {
|
|
1061
|
+
const raw = await this.client.get(this.storageKey(key));
|
|
1062
|
+
if (raw === null) {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
return JSON.parse(raw);
|
|
1066
|
+
}
|
|
1067
|
+
async set(key, value, ttlMs) {
|
|
1068
|
+
const resolvedTtlMs = ttlMs ?? this.ttlMs;
|
|
1069
|
+
const payload = JSON.stringify(value);
|
|
1070
|
+
if (resolvedTtlMs > 0) {
|
|
1071
|
+
await this.client.psetex(this.storageKey(key), resolvedTtlMs, payload);
|
|
1072
|
+
} else {
|
|
1073
|
+
await this.client.set(this.storageKey(key), payload);
|
|
1074
|
+
}
|
|
1075
|
+
await this.enforceMaxEntries();
|
|
1076
|
+
}
|
|
1077
|
+
async getOrSet(key, loader, ttlMs) {
|
|
1078
|
+
const cached = await this.get(key);
|
|
1079
|
+
if (cached !== undefined) {
|
|
1080
|
+
return cached;
|
|
1081
|
+
}
|
|
1082
|
+
const inflightRequest = this.inflight.get(key);
|
|
1083
|
+
if (inflightRequest) {
|
|
1084
|
+
return inflightRequest;
|
|
1085
|
+
}
|
|
1086
|
+
const pendingRequest = loader().then(async (value) => {
|
|
1087
|
+
await this.set(key, value, ttlMs);
|
|
1088
|
+
return value;
|
|
1089
|
+
}).finally(() => {
|
|
1090
|
+
this.inflight.delete(key);
|
|
1091
|
+
});
|
|
1092
|
+
this.inflight.set(key, pendingRequest);
|
|
1093
|
+
return pendingRequest;
|
|
1094
|
+
}
|
|
1095
|
+
async attachTags(key, tags) {
|
|
1096
|
+
if (tags.length === 0) {
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
let tagsForKey = this.keyTags.get(key);
|
|
1100
|
+
if (!tagsForKey) {
|
|
1101
|
+
tagsForKey = new Set;
|
|
1102
|
+
this.keyTags.set(key, tagsForKey);
|
|
1103
|
+
}
|
|
1104
|
+
for (const tag of tags) {
|
|
1105
|
+
tagsForKey.add(tag);
|
|
1106
|
+
await this.client.sadd(this.tagKey(tag), key);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
async flushTags(tags) {
|
|
1110
|
+
const keysToRemove = new Set;
|
|
1111
|
+
for (const tag of tags) {
|
|
1112
|
+
const members = await this.client.smembers(this.tagKey(tag));
|
|
1113
|
+
for (const member of members) {
|
|
1114
|
+
keysToRemove.add(member);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
let removed = 0;
|
|
1118
|
+
for (const key of keysToRemove) {
|
|
1119
|
+
if (await this.invalidate(key)) {
|
|
1120
|
+
removed += 1;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
for (const tag of tags) {
|
|
1124
|
+
await this.client.del(this.tagKey(tag));
|
|
1125
|
+
}
|
|
1126
|
+
return removed;
|
|
1127
|
+
}
|
|
1128
|
+
async invalidate(key) {
|
|
1129
|
+
const deleted = await this.client.del(this.storageKey(key));
|
|
1130
|
+
await this.detachKeyFromTags(key);
|
|
1131
|
+
return deleted > 0;
|
|
1132
|
+
}
|
|
1133
|
+
async invalidateByPrefix(prefix) {
|
|
1134
|
+
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
1135
|
+
let removed = 0;
|
|
1136
|
+
for (const storageKey of keys) {
|
|
1137
|
+
const key = storageKey.slice(KEY_PREFIX.length);
|
|
1138
|
+
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
1139
|
+
if (await this.invalidate(key)) {
|
|
1140
|
+
removed += 1;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return removed;
|
|
1145
|
+
}
|
|
1146
|
+
async clear() {
|
|
1147
|
+
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
1148
|
+
if (keys.length > 0) {
|
|
1149
|
+
await this.client.del(...keys);
|
|
1150
|
+
}
|
|
1151
|
+
const tagKeys = await this.client.keys(`${TAG_PREFIX}*`);
|
|
1152
|
+
if (tagKeys.length > 0) {
|
|
1153
|
+
await this.client.del(...tagKeys);
|
|
1154
|
+
}
|
|
1155
|
+
this.inflight.clear();
|
|
1156
|
+
this.keyTags.clear();
|
|
1157
|
+
}
|
|
1158
|
+
async size() {
|
|
1159
|
+
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
1160
|
+
return keys.length;
|
|
1161
|
+
}
|
|
1162
|
+
storageKey(key) {
|
|
1163
|
+
return `${KEY_PREFIX}${key}`;
|
|
1164
|
+
}
|
|
1165
|
+
tagKey(tag) {
|
|
1166
|
+
return `${TAG_PREFIX}${tag}`;
|
|
1167
|
+
}
|
|
1168
|
+
async detachKeyFromTags(key) {
|
|
1169
|
+
const tags = this.keyTags.get(key);
|
|
1170
|
+
if (!tags) {
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
for (const tag of tags) {
|
|
1174
|
+
await this.client.srem(this.tagKey(tag), key);
|
|
1175
|
+
}
|
|
1176
|
+
this.keyTags.delete(key);
|
|
1177
|
+
}
|
|
1178
|
+
async enforceMaxEntries() {
|
|
1179
|
+
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
1180
|
+
if (keys.length <= this.maxEntries) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
const overflow = keys.length - this.maxEntries;
|
|
1184
|
+
const keysToRemove = keys.slice(0, overflow);
|
|
1185
|
+
if (keysToRemove.length > 0) {
|
|
1186
|
+
await this.client.del(...keysToRemove);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
var redisCacheStore_default = RedisCacheStore;
|
|
1191
|
+
|
|
1192
|
+
// ../../src/core/cache/simpleCache.ts
|
|
1193
|
+
class SimpleCache {
|
|
1194
|
+
ttlMs;
|
|
1195
|
+
maxEntries;
|
|
1196
|
+
cache = new Map;
|
|
1197
|
+
inflight = new Map;
|
|
1198
|
+
tagIndex = new Map;
|
|
1199
|
+
keyTags = new Map;
|
|
1200
|
+
constructor(ttlMs = 3600000, maxEntries = 100) {
|
|
1201
|
+
this.ttlMs = ttlMs;
|
|
1202
|
+
this.maxEntries = maxEntries;
|
|
1203
|
+
if (!Number.isFinite(ttlMs) || ttlMs < 0) {
|
|
1204
|
+
throw new RangeError("ttlMs must be a non-negative number.");
|
|
1205
|
+
}
|
|
1206
|
+
if (!Number.isInteger(maxEntries) || maxEntries < 1) {
|
|
1207
|
+
throw new RangeError("maxEntries must be a positive integer.");
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
get(key) {
|
|
1211
|
+
return this.getFreshEntry(key)?.value;
|
|
1212
|
+
}
|
|
1213
|
+
set(key, value, ttlMs) {
|
|
1214
|
+
const now = Date.now();
|
|
1215
|
+
const resolvedTtlMs = ttlMs ?? this.ttlMs;
|
|
1216
|
+
this.cache.set(key, {
|
|
1217
|
+
value,
|
|
1218
|
+
expiresAt: now + resolvedTtlMs,
|
|
1219
|
+
lastAccessedAt: now
|
|
1220
|
+
});
|
|
1221
|
+
this.evictOverflow();
|
|
1222
|
+
}
|
|
1223
|
+
async getOrSet(key, loader, ttlMs) {
|
|
1224
|
+
this.pruneExpired();
|
|
1225
|
+
const cachedEntry = this.getFreshEntry(key);
|
|
1226
|
+
if (cachedEntry) {
|
|
1227
|
+
return cachedEntry.value;
|
|
1228
|
+
}
|
|
1229
|
+
const inflightRequest = this.inflight.get(key);
|
|
1230
|
+
if (inflightRequest) {
|
|
1231
|
+
return inflightRequest;
|
|
1232
|
+
}
|
|
1233
|
+
const pendingRequest = loader().then((value) => {
|
|
1234
|
+
this.set(key, value, ttlMs);
|
|
1235
|
+
return value;
|
|
1236
|
+
}).finally(() => {
|
|
1237
|
+
this.inflight.delete(key);
|
|
1238
|
+
});
|
|
1239
|
+
this.inflight.set(key, pendingRequest);
|
|
1240
|
+
return pendingRequest;
|
|
1241
|
+
}
|
|
1242
|
+
attachTags(key, tags) {
|
|
1243
|
+
if (tags.length === 0) {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
let tagsForKey = this.keyTags.get(key);
|
|
1247
|
+
if (!tagsForKey) {
|
|
1248
|
+
tagsForKey = new Set;
|
|
1249
|
+
this.keyTags.set(key, tagsForKey);
|
|
1250
|
+
}
|
|
1251
|
+
for (const tag of tags) {
|
|
1252
|
+
tagsForKey.add(tag);
|
|
1253
|
+
let keysForTag = this.tagIndex.get(tag);
|
|
1254
|
+
if (!keysForTag) {
|
|
1255
|
+
keysForTag = new Set;
|
|
1256
|
+
this.tagIndex.set(tag, keysForTag);
|
|
1257
|
+
}
|
|
1258
|
+
keysForTag.add(key);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
flushTags(tags) {
|
|
1262
|
+
const keysToRemove = new Set;
|
|
1263
|
+
for (const tag of tags) {
|
|
1264
|
+
const keys = this.tagIndex.get(tag);
|
|
1265
|
+
if (!keys) {
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
for (const key of keys) {
|
|
1269
|
+
keysToRemove.add(key);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
let removed = 0;
|
|
1273
|
+
for (const key of keysToRemove) {
|
|
1274
|
+
if (this.invalidate(key)) {
|
|
1275
|
+
removed += 1;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
for (const tag of tags) {
|
|
1279
|
+
this.tagIndex.delete(tag);
|
|
1280
|
+
}
|
|
1281
|
+
return removed;
|
|
1282
|
+
}
|
|
1283
|
+
invalidate(key) {
|
|
1284
|
+
const removed = this.cache.delete(key);
|
|
1285
|
+
if (removed) {
|
|
1286
|
+
this.detachKeyFromTags(key);
|
|
1287
|
+
}
|
|
1288
|
+
return removed;
|
|
1289
|
+
}
|
|
1290
|
+
invalidateByPrefix(prefix) {
|
|
1291
|
+
let removed = 0;
|
|
1292
|
+
for (const key of [...this.cache.keys()]) {
|
|
1293
|
+
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
1294
|
+
if (this.invalidate(key)) {
|
|
1295
|
+
removed += 1;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return removed;
|
|
1300
|
+
}
|
|
1301
|
+
clear() {
|
|
1302
|
+
this.cache.clear();
|
|
1303
|
+
this.inflight.clear();
|
|
1304
|
+
this.tagIndex.clear();
|
|
1305
|
+
this.keyTags.clear();
|
|
1306
|
+
}
|
|
1307
|
+
size() {
|
|
1308
|
+
this.pruneExpired();
|
|
1309
|
+
return this.cache.size;
|
|
1310
|
+
}
|
|
1311
|
+
detachKeyFromTags(key) {
|
|
1312
|
+
const tags = this.keyTags.get(key);
|
|
1313
|
+
if (!tags) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
for (const tag of tags) {
|
|
1317
|
+
const keys = this.tagIndex.get(tag);
|
|
1318
|
+
if (!keys) {
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
keys.delete(key);
|
|
1322
|
+
if (keys.size === 0) {
|
|
1323
|
+
this.tagIndex.delete(tag);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
this.keyTags.delete(key);
|
|
1327
|
+
}
|
|
1328
|
+
getFreshEntry(key) {
|
|
1329
|
+
const entry = this.cache.get(key);
|
|
1330
|
+
if (!entry) {
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
if (entry.expiresAt <= Date.now()) {
|
|
1334
|
+
this.invalidate(key);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
entry.lastAccessedAt = Date.now();
|
|
1338
|
+
return entry;
|
|
1339
|
+
}
|
|
1340
|
+
pruneExpired() {
|
|
1341
|
+
const now = Date.now();
|
|
1342
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
1343
|
+
if (entry.expiresAt <= now) {
|
|
1344
|
+
this.invalidate(key);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
evictOverflow() {
|
|
1349
|
+
while (this.cache.size > this.maxEntries) {
|
|
1350
|
+
let oldestKey;
|
|
1351
|
+
let oldestAccessTime = Number.POSITIVE_INFINITY;
|
|
1352
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
1353
|
+
if (entry.lastAccessedAt < oldestAccessTime) {
|
|
1354
|
+
oldestAccessTime = entry.lastAccessedAt;
|
|
1355
|
+
oldestKey = key;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
if (!oldestKey) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
this.invalidate(oldestKey);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
var simpleCache_default = SimpleCache;
|
|
1366
|
+
|
|
1367
|
+
// ../../src/core/cache/simpleCacheStore.ts
|
|
1368
|
+
class SimpleCacheStore {
|
|
1369
|
+
cache;
|
|
1370
|
+
constructor(cache) {
|
|
1371
|
+
this.cache = cache;
|
|
1372
|
+
}
|
|
1373
|
+
get(key) {
|
|
1374
|
+
return Promise.resolve(this.cache.get(key));
|
|
1375
|
+
}
|
|
1376
|
+
set(key, value, ttlMs) {
|
|
1377
|
+
this.cache.set(key, value, ttlMs);
|
|
1378
|
+
return Promise.resolve();
|
|
1379
|
+
}
|
|
1380
|
+
getOrSet(key, loader, ttlMs) {
|
|
1381
|
+
return this.cache.getOrSet(key, loader, ttlMs);
|
|
1382
|
+
}
|
|
1383
|
+
attachTags(key, tags) {
|
|
1384
|
+
this.cache.attachTags(key, tags);
|
|
1385
|
+
return Promise.resolve();
|
|
1386
|
+
}
|
|
1387
|
+
flushTags(tags) {
|
|
1388
|
+
return Promise.resolve(this.cache.flushTags(tags));
|
|
1389
|
+
}
|
|
1390
|
+
invalidate(key) {
|
|
1391
|
+
return Promise.resolve(this.cache.invalidate(key));
|
|
1392
|
+
}
|
|
1393
|
+
invalidateByPrefix(prefix) {
|
|
1394
|
+
return Promise.resolve(this.cache.invalidateByPrefix(prefix));
|
|
1395
|
+
}
|
|
1396
|
+
clear() {
|
|
1397
|
+
this.cache.clear();
|
|
1398
|
+
return Promise.resolve();
|
|
1399
|
+
}
|
|
1400
|
+
size() {
|
|
1401
|
+
return Promise.resolve(this.cache.size());
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
var simpleCacheStore_default = SimpleCacheStore;
|
|
1405
|
+
|
|
1406
|
+
// ../../src/core/cache/createCacheStore.ts
|
|
1407
|
+
function createCacheStore(options) {
|
|
1408
|
+
if (options.driver === "redis") {
|
|
1409
|
+
if (!options.redisUrl) {
|
|
1410
|
+
throw new Error('CACHE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
1411
|
+
}
|
|
1412
|
+
return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries);
|
|
1413
|
+
}
|
|
1414
|
+
return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// ../../src/core/cache/taggedCache.ts
|
|
1418
|
+
class TaggedCache {
|
|
1419
|
+
store;
|
|
1420
|
+
tags;
|
|
1421
|
+
constructor(store, tags) {
|
|
1422
|
+
this.store = store;
|
|
1423
|
+
this.tags = tags;
|
|
1424
|
+
}
|
|
1425
|
+
async remember(key, callback, ttlMs) {
|
|
1426
|
+
const value = await this.store.getOrSet(key, callback, ttlMs);
|
|
1427
|
+
await this.store.attachTags(key, this.tags);
|
|
1428
|
+
return value;
|
|
1429
|
+
}
|
|
1430
|
+
async flush() {
|
|
1431
|
+
return this.store.flushTags(this.tags);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
var taggedCache_default = TaggedCache;
|
|
1435
|
+
|
|
1436
|
+
// ../../src/core/cache/repository.ts
|
|
1437
|
+
class CacheRepository {
|
|
1438
|
+
store;
|
|
1439
|
+
constructor(store) {
|
|
1440
|
+
this.store = store;
|
|
1441
|
+
}
|
|
1442
|
+
async get(key) {
|
|
1443
|
+
return this.store.get(key);
|
|
1444
|
+
}
|
|
1445
|
+
async remember(key, callback, ttlMs) {
|
|
1446
|
+
return this.store.getOrSet(key, callback, ttlMs);
|
|
1447
|
+
}
|
|
1448
|
+
async forget(key) {
|
|
1449
|
+
return this.store.invalidate(key);
|
|
1450
|
+
}
|
|
1451
|
+
async flush() {
|
|
1452
|
+
await this.store.clear();
|
|
1453
|
+
}
|
|
1454
|
+
tags(...names) {
|
|
1455
|
+
return new taggedCache_default(this.store, names);
|
|
1456
|
+
}
|
|
1457
|
+
async getOrSet(key, loader, ttlMs) {
|
|
1458
|
+
return this.remember(key, loader, ttlMs);
|
|
1459
|
+
}
|
|
1460
|
+
async invalidate(key) {
|
|
1461
|
+
return this.forget(key);
|
|
1462
|
+
}
|
|
1463
|
+
async invalidateByPrefix(prefix) {
|
|
1464
|
+
return this.store.invalidateByPrefix(prefix);
|
|
1465
|
+
}
|
|
1466
|
+
async clear() {
|
|
1467
|
+
await this.flush();
|
|
1468
|
+
}
|
|
1469
|
+
async size() {
|
|
1470
|
+
return this.store.size();
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
var repository_default2 = CacheRepository;
|
|
1474
|
+
|
|
1475
|
+
// ../../src/bootstrap/providers/cache.ts
|
|
1476
|
+
var cacheProvider = {
|
|
1477
|
+
name: "core.cache",
|
|
1478
|
+
register({ container, config, dependencies }) {
|
|
1479
|
+
container.singleton(CORE_CACHE_TOKEN, () => {
|
|
1480
|
+
const store = createCacheStore({
|
|
1481
|
+
driver: config.require(CACHE_DRIVER_CONFIG_KEY),
|
|
1482
|
+
ttlMs: config.require(CACHE_TTL_MS_CONFIG_KEY),
|
|
1483
|
+
maxEntries: config.require(CACHE_MAX_ENTRIES_CONFIG_KEY),
|
|
1484
|
+
redisUrl: config.get(REDIS_URL_CONFIG_KEY) || undefined
|
|
1485
|
+
});
|
|
1486
|
+
return new repository_default2(store);
|
|
1487
|
+
});
|
|
1488
|
+
dependencies.cache = container.resolve(CORE_CACHE_TOKEN);
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
var cache_default = cacheProvider;
|
|
1492
|
+
|
|
1493
|
+
// ../../src/config/queue.ts
|
|
1494
|
+
var queueConfig = {
|
|
1495
|
+
driver: process.env.QUEUE_DRIVER === "redis" || process.env.QUEUE_DRIVER === "async" || process.env.QUEUE_DRIVER === "sync" ? process.env.QUEUE_DRIVER : DEFAULT_QUEUE_DRIVER,
|
|
1496
|
+
maxAttempts: Number(process.env.QUEUE_MAX_ATTEMPTS ?? "3"),
|
|
1497
|
+
backoffMs: Number(process.env.QUEUE_BACKOFF_MS ?? "1000")
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
// ../../src/core/config/envSchema.ts
|
|
1501
|
+
function defineEnvSchema(schema) {
|
|
1502
|
+
return schema;
|
|
1503
|
+
}
|
|
1504
|
+
function validateEnv(schema, env = process.env) {
|
|
1505
|
+
const resolved = {};
|
|
1506
|
+
for (const [name, rule] of Object.entries(schema)) {
|
|
1507
|
+
const rawValue = env[name];
|
|
1508
|
+
const value = rawValue === undefined || rawValue.trim() === "" ? rule.default : rawValue;
|
|
1509
|
+
if (value === undefined || value.trim() === "") {
|
|
1510
|
+
if (rule.required) {
|
|
1511
|
+
throw new Error(`Missing required environment variable "${name}".`);
|
|
1512
|
+
}
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
if (rule.integer) {
|
|
1516
|
+
const parsed = Number.parseInt(value, 10);
|
|
1517
|
+
const minimum = rule.minimum ?? Number.NEGATIVE_INFINITY;
|
|
1518
|
+
if (!Number.isInteger(parsed) || parsed < minimum) {
|
|
1519
|
+
const comparison = minimum === Number.NEGATIVE_INFINITY ? "an integer" : `an integer >= ${minimum}`;
|
|
1520
|
+
throw new Error(`Environment variable "${name}" must be ${comparison}.`);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
if (rule.pattern && !rule.pattern.test(value)) {
|
|
1524
|
+
throw new Error(`Environment variable "${name}" has an invalid format.`);
|
|
1525
|
+
}
|
|
1526
|
+
resolved[name] = value;
|
|
1527
|
+
}
|
|
1528
|
+
return resolved;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// ../../src/bootstrap/env.ts
|
|
1532
|
+
var appEnvSchema = defineEnvSchema({
|
|
1533
|
+
DATABASE_URL: { required: true, pattern: /^postgres(ql)?:\/\// },
|
|
1534
|
+
PORT: {
|
|
1535
|
+
integer: true,
|
|
1536
|
+
minimum: 1,
|
|
1537
|
+
default: String(DEFAULT_APP_PORT)
|
|
1538
|
+
},
|
|
1539
|
+
CACHE_TTL_MS: {
|
|
1540
|
+
integer: true,
|
|
1541
|
+
minimum: 0,
|
|
1542
|
+
default: String(DEFAULT_CACHE_TTL_MS)
|
|
1543
|
+
},
|
|
1544
|
+
CACHE_MAX_ENTRIES: {
|
|
1545
|
+
integer: true,
|
|
1546
|
+
minimum: 1,
|
|
1547
|
+
default: String(DEFAULT_CACHE_MAX_ENTRIES)
|
|
1548
|
+
},
|
|
1549
|
+
CACHE_DRIVER: {
|
|
1550
|
+
default: DEFAULT_CACHE_DRIVER,
|
|
1551
|
+
pattern: /^(array|redis)$/
|
|
1552
|
+
},
|
|
1553
|
+
REDIS_URL: {
|
|
1554
|
+
default: ""
|
|
1555
|
+
},
|
|
1556
|
+
QUEUE_DRIVER: {
|
|
1557
|
+
default: DEFAULT_QUEUE_DRIVER,
|
|
1558
|
+
pattern: /^(sync|async|redis)$/
|
|
1559
|
+
},
|
|
1560
|
+
AUTH_DEV_HEADERS: {
|
|
1561
|
+
default: "true",
|
|
1562
|
+
pattern: /^(true|false|0|1)$/
|
|
1563
|
+
},
|
|
1564
|
+
APP_ENV: {
|
|
1565
|
+
default: "local"
|
|
1566
|
+
},
|
|
1567
|
+
APP_DEBUG: {
|
|
1568
|
+
default: "true"
|
|
1569
|
+
},
|
|
1570
|
+
APP_URL: {
|
|
1571
|
+
default: "http://localhost:3000"
|
|
1572
|
+
},
|
|
1573
|
+
API_PREFIX: {
|
|
1574
|
+
default: "/api/v1"
|
|
1575
|
+
},
|
|
1576
|
+
CORS_ALLOWED_ORIGINS: {
|
|
1577
|
+
default: "*"
|
|
1578
|
+
},
|
|
1579
|
+
QUEUE_MAX_ATTEMPTS: {
|
|
1580
|
+
integer: true,
|
|
1581
|
+
minimum: 1,
|
|
1582
|
+
default: "3"
|
|
1583
|
+
},
|
|
1584
|
+
QUEUE_BACKOFF_MS: {
|
|
1585
|
+
integer: true,
|
|
1586
|
+
minimum: 0,
|
|
1587
|
+
default: "1000"
|
|
1588
|
+
},
|
|
1589
|
+
ADMIN_API_TOKEN: {
|
|
1590
|
+
default: DEFAULT_API_TOKEN
|
|
1591
|
+
},
|
|
1592
|
+
MEMBER_API_TOKEN: {
|
|
1593
|
+
default: ""
|
|
1594
|
+
},
|
|
1595
|
+
DB_POOL_MAX: {
|
|
1596
|
+
integer: true,
|
|
1597
|
+
minimum: 1,
|
|
1598
|
+
default: "10"
|
|
1599
|
+
},
|
|
1600
|
+
DB_POOL_IDLE_TIMEOUT: {
|
|
1601
|
+
integer: true,
|
|
1602
|
+
minimum: 0,
|
|
1603
|
+
default: "30"
|
|
1604
|
+
},
|
|
1605
|
+
DB_POOL_MAX_LIFETIME: {
|
|
1606
|
+
integer: true,
|
|
1607
|
+
minimum: 0,
|
|
1608
|
+
default: "3600"
|
|
1609
|
+
},
|
|
1610
|
+
DB_CONNECTION_TIMEOUT: {
|
|
1611
|
+
integer: true,
|
|
1612
|
+
minimum: 1,
|
|
1613
|
+
default: "10"
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
// ../../src/bootstrap/providers/config.ts
|
|
1618
|
+
function parseInteger(value, envName, minimum) {
|
|
1619
|
+
const parsed = Number.parseInt(value, 10);
|
|
1620
|
+
if (!Number.isInteger(parsed) || parsed < minimum) {
|
|
1621
|
+
const comparison = minimum === 0 ? "a non-negative" : `an integer >= ${minimum}`;
|
|
1622
|
+
throw new Error(`${envName} must be ${comparison} value.`);
|
|
1623
|
+
}
|
|
1624
|
+
return parsed;
|
|
1625
|
+
}
|
|
1626
|
+
var configProvider = {
|
|
1627
|
+
name: "core.config",
|
|
1628
|
+
register({ container, config }) {
|
|
1629
|
+
const env = validateEnv(appEnvSchema);
|
|
1630
|
+
container.set(CORE_CONFIG_TOKEN, config);
|
|
1631
|
+
config.set(DATABASE_URL_CONFIG_KEY, env.DATABASE_URL);
|
|
1632
|
+
config.set(APP_PORT_CONFIG_KEY, parseInteger(env.PORT ?? String(DEFAULT_APP_PORT), "PORT", 1));
|
|
1633
|
+
config.set(CACHE_TTL_MS_CONFIG_KEY, parseInteger(env.CACHE_TTL_MS ?? String(DEFAULT_CACHE_TTL_MS), "CACHE_TTL_MS", 0));
|
|
1634
|
+
config.set(CACHE_MAX_ENTRIES_CONFIG_KEY, parseInteger(env.CACHE_MAX_ENTRIES ?? String(DEFAULT_CACHE_MAX_ENTRIES), "CACHE_MAX_ENTRIES", 1));
|
|
1635
|
+
config.set(CACHE_DRIVER_CONFIG_KEY, env.CACHE_DRIVER ?? DEFAULT_CACHE_DRIVER);
|
|
1636
|
+
config.set(REDIS_URL_CONFIG_KEY, env.REDIS_URL ?? "");
|
|
1637
|
+
config.set("app.env", appConfig.env);
|
|
1638
|
+
config.set("app.debug", appConfig.debug);
|
|
1639
|
+
config.set("app.url", appConfig.url);
|
|
1640
|
+
config.set("app.apiPrefix", appConfig.apiPrefix);
|
|
1641
|
+
config.set("auth.allowDevHeaders", authConfig.allowDevHeaders);
|
|
1642
|
+
config.set("queue.driver", queueConfig.driver);
|
|
1643
|
+
config.set("queue.maxAttempts", queueConfig.maxAttempts);
|
|
1644
|
+
config.set("queue.backoffMs", queueConfig.backoffMs);
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
var config_default = configProvider;
|
|
1648
|
+
|
|
1649
|
+
// ../../src/core/events/eventBus.ts
|
|
1650
|
+
class EventBus {
|
|
1651
|
+
constructor() {}
|
|
1652
|
+
listeners = new Map;
|
|
1653
|
+
listen(event, listener) {
|
|
1654
|
+
const handlers = this.listeners.get(event) ?? new Set;
|
|
1655
|
+
handlers.add(listener);
|
|
1656
|
+
this.listeners.set(event, handlers);
|
|
1657
|
+
return () => {
|
|
1658
|
+
handlers.delete(listener);
|
|
1659
|
+
if (handlers.size === 0) {
|
|
1660
|
+
this.listeners.delete(event);
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
async dispatch(event, payload) {
|
|
1665
|
+
const handlers = this.listeners.get(event);
|
|
1666
|
+
if (!handlers || handlers.size === 0) {
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
for (const handler of handlers) {
|
|
1670
|
+
await handler(payload);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
var eventBus = new EventBus;
|
|
1675
|
+
|
|
1676
|
+
// ../../src/core/events/index.ts
|
|
1677
|
+
function modelEventName(tableName, action) {
|
|
1678
|
+
return `${tableName}.${action}`;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// ../../src/bootstrap/providers/events.ts
|
|
1682
|
+
var CORE_EVENT_BUS_TOKEN = "core.eventBus";
|
|
1683
|
+
var eventsProvider = {
|
|
1684
|
+
name: "core.events",
|
|
1685
|
+
register({ container }) {
|
|
1686
|
+
container.set(CORE_EVENT_BUS_TOKEN, eventBus);
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
var events_default = eventsProvider;
|
|
1690
|
+
|
|
1691
|
+
// ../../src/bootstrap/discoverListeners.ts
|
|
1692
|
+
import { readdirSync as readdirSync2 } from "fs";
|
|
1693
|
+
import { join as join2 } from "path";
|
|
1694
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1695
|
+
async function loadDiscoveredListeners() {
|
|
1696
|
+
const listenersDirectory = join2(import.meta.dir, "../listeners");
|
|
1697
|
+
let entries;
|
|
1698
|
+
try {
|
|
1699
|
+
entries = readdirSync2(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".ts")).map((entry) => entry.name);
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1702
|
+
return [];
|
|
1703
|
+
}
|
|
1704
|
+
throw error;
|
|
1705
|
+
}
|
|
1706
|
+
const listeners = await Promise.all(entries.map(async (fileName) => {
|
|
1707
|
+
const moduleUrl = pathToFileURL2(join2(listenersDirectory, fileName)).href;
|
|
1708
|
+
const loaded = await import(moduleUrl);
|
|
1709
|
+
return loaded.default;
|
|
1710
|
+
}));
|
|
1711
|
+
return listeners.filter((listener) => typeof listener === "function");
|
|
1712
|
+
}
|
|
1713
|
+
var appListeners = await loadDiscoveredListeners();
|
|
1714
|
+
function discoverListeners() {
|
|
1715
|
+
return appListeners;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// ../../src/core/cache/modelCacheTags.ts
|
|
1719
|
+
function cacheTagsForModelWrite(tableName, action) {
|
|
1720
|
+
const module = appModules.find((entry) => entry.tableName === tableName);
|
|
1721
|
+
const baseTags = module?.cacheTags ?? [`${tableName}s`];
|
|
1722
|
+
const isDelete = action === "deleted" || action === "force-deleted";
|
|
1723
|
+
const extraTags = isDelete ? module?.cacheDeleteExtraTags ?? [] : [];
|
|
1724
|
+
return [...new Set([...baseTags, ...extraTags])];
|
|
1725
|
+
}
|
|
1726
|
+
function discoverModelTableNames() {
|
|
1727
|
+
return appModules.map((module) => module.tableName).filter((tableName) => tableName !== undefined);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// ../../src/core/queue/index.ts
|
|
1731
|
+
class Job {
|
|
1732
|
+
maxAttempts;
|
|
1733
|
+
backoffMs;
|
|
1734
|
+
priority;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// ../../src/core/jobs/invalidateCacheTagsJob.ts
|
|
1738
|
+
class InvalidateCacheTagsJob extends Job {
|
|
1739
|
+
cache;
|
|
1740
|
+
constructor(cache) {
|
|
1741
|
+
super();
|
|
1742
|
+
this.cache = cache;
|
|
1743
|
+
}
|
|
1744
|
+
async handle(payload) {
|
|
1745
|
+
await this.cache.tags(...payload.tags).flush();
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
|
|
1749
|
+
|
|
1750
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
1751
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
1752
|
+
class DispatchWebhookJob extends Job {
|
|
1753
|
+
constructor() {
|
|
1754
|
+
super();
|
|
1755
|
+
}
|
|
1756
|
+
maxAttempts = 3;
|
|
1757
|
+
backoffMs = 2000;
|
|
1758
|
+
async handle(payload) {
|
|
1759
|
+
const rows = await connection_default`
|
|
1760
|
+
SELECT id, url, secret
|
|
1761
|
+
FROM webhook
|
|
1762
|
+
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
1763
|
+
LIMIT 1
|
|
1764
|
+
`;
|
|
1765
|
+
const webhook = rows[0];
|
|
1766
|
+
if (!webhook) {
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
1770
|
+
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
1771
|
+
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
1772
|
+
let responseStatus = null;
|
|
1773
|
+
let errorMessage = null;
|
|
1774
|
+
try {
|
|
1775
|
+
const response = await safeFetch(webhook.url, {
|
|
1776
|
+
method: "POST",
|
|
1777
|
+
headers: {
|
|
1778
|
+
"content-type": "application/json",
|
|
1779
|
+
"x-workhub-signature": signature
|
|
1780
|
+
},
|
|
1781
|
+
body
|
|
1782
|
+
});
|
|
1783
|
+
responseStatus = response.status;
|
|
1784
|
+
if (!response.ok) {
|
|
1785
|
+
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
1786
|
+
}
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
1789
|
+
await connection_default`
|
|
1790
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
1791
|
+
VALUES (
|
|
1792
|
+
${webhook.id},
|
|
1793
|
+
${payload.event},
|
|
1794
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
1795
|
+
${responseStatus},
|
|
1796
|
+
${errorMessage}
|
|
1797
|
+
)
|
|
1798
|
+
`;
|
|
1799
|
+
throw error instanceof Error ? error : new Error(errorMessage);
|
|
1800
|
+
}
|
|
1801
|
+
await connection_default`
|
|
1802
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
1803
|
+
VALUES (
|
|
1804
|
+
${webhook.id},
|
|
1805
|
+
${payload.event},
|
|
1806
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
1807
|
+
${responseStatus}
|
|
1808
|
+
)
|
|
1809
|
+
`;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
1813
|
+
|
|
1814
|
+
// ../../src/core/queue/jobRegistry.ts
|
|
1815
|
+
class JobRegistry {
|
|
1816
|
+
constructor() {}
|
|
1817
|
+
factories = new Map;
|
|
1818
|
+
instances = new WeakMap;
|
|
1819
|
+
register(name, factory) {
|
|
1820
|
+
this.factories.set(name, factory);
|
|
1821
|
+
}
|
|
1822
|
+
resolveName(job) {
|
|
1823
|
+
return this.instances.get(job);
|
|
1824
|
+
}
|
|
1825
|
+
track(name, job) {
|
|
1826
|
+
this.instances.set(job, name);
|
|
1827
|
+
return job;
|
|
1828
|
+
}
|
|
1829
|
+
create(name) {
|
|
1830
|
+
const factory = this.factories.get(name);
|
|
1831
|
+
if (!factory) {
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
return factory();
|
|
1835
|
+
}
|
|
1836
|
+
names() {
|
|
1837
|
+
return [...this.factories.keys()];
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
var jobRegistry = new JobRegistry;
|
|
1841
|
+
|
|
1842
|
+
// ../../src/core/pagination/index.ts
|
|
1843
|
+
function buildPaginationMeta(input) {
|
|
1844
|
+
const lastPage = Math.max(1, Math.ceil(input.total / input.perPage));
|
|
1845
|
+
return {
|
|
1846
|
+
page: input.page,
|
|
1847
|
+
per_page: input.perPage,
|
|
1848
|
+
total: input.total,
|
|
1849
|
+
last_page: lastPage
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// ../../src/core/database/errors.ts
|
|
1854
|
+
function isPostgresError(error) {
|
|
1855
|
+
return typeof error === "object" && error !== null && (("errno" in error) || ("code" in error));
|
|
1856
|
+
}
|
|
1857
|
+
function getPostgresSqlState(error) {
|
|
1858
|
+
if (typeof error.errno === "string" && /^\d{5}$/.test(error.errno)) {
|
|
1859
|
+
return error.errno;
|
|
1860
|
+
}
|
|
1861
|
+
if (typeof error.errno === "number") {
|
|
1862
|
+
return String(error.errno).padStart(5, "0");
|
|
1863
|
+
}
|
|
1864
|
+
if (typeof error.code === "string" && /^\d{5}$/.test(error.code)) {
|
|
1865
|
+
return error.code;
|
|
1866
|
+
}
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
function mapDatabaseError(error) {
|
|
1870
|
+
if (error instanceof HttpError) {
|
|
1871
|
+
return error;
|
|
1872
|
+
}
|
|
1873
|
+
if (!isPostgresError(error)) {
|
|
1874
|
+
const message = error instanceof Error ? error.message : "Database operation failed.";
|
|
1875
|
+
return new BadRequestError(message);
|
|
1876
|
+
}
|
|
1877
|
+
const sqlState = getPostgresSqlState(error);
|
|
1878
|
+
switch (sqlState) {
|
|
1879
|
+
case "23505":
|
|
1880
|
+
return new ConflictError(error.detail ?? "A record with these values already exists.", {
|
|
1881
|
+
constraint: error.constraint
|
|
1882
|
+
});
|
|
1883
|
+
case "23503":
|
|
1884
|
+
return new UnprocessableEntityError(error.detail ?? "Referenced record does not exist.", {
|
|
1885
|
+
constraint: error.constraint
|
|
1886
|
+
});
|
|
1887
|
+
case "23502":
|
|
1888
|
+
return new BadRequestError(error.detail ?? "Required field is missing.", {
|
|
1889
|
+
constraint: error.constraint
|
|
1890
|
+
});
|
|
1891
|
+
case "23514":
|
|
1892
|
+
return new BadRequestError(error.detail ?? "Value violates a database constraint.", {
|
|
1893
|
+
constraint: error.constraint
|
|
1894
|
+
});
|
|
1895
|
+
default:
|
|
1896
|
+
return new BadRequestError(error.message ?? "Database operation failed.", {
|
|
1897
|
+
code: error.code,
|
|
1898
|
+
sqlState
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
async function withDatabaseErrorHandling(operation) {
|
|
1903
|
+
try {
|
|
1904
|
+
return await operation();
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
throw mapDatabaseError(error);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// ../../src/core/database/query.ts
|
|
1911
|
+
function quoteIdentifier(identifier) {
|
|
1912
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
1913
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
1914
|
+
}
|
|
1915
|
+
return `"${identifier}"`;
|
|
1916
|
+
}
|
|
1917
|
+
function qualifyColumn(tableName, column) {
|
|
1918
|
+
return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
|
|
1919
|
+
}
|
|
1920
|
+
function resolveQualifiedColumn(defaultTable, columnName) {
|
|
1921
|
+
if (columnName.includes(".")) {
|
|
1922
|
+
const [table, column] = columnName.split(".", 2);
|
|
1923
|
+
if (!table || !column) {
|
|
1924
|
+
throw new Error(`Invalid qualified column: ${columnName}`);
|
|
1925
|
+
}
|
|
1926
|
+
return qualifyColumn(table, column);
|
|
1927
|
+
}
|
|
1928
|
+
return qualifyColumn(defaultTable, columnName);
|
|
1929
|
+
}
|
|
1930
|
+
function parseQualifiedColumn(reference) {
|
|
1931
|
+
const [table, column] = reference.split(".", 2);
|
|
1932
|
+
if (!table || !column) {
|
|
1933
|
+
throw new Error(`Join columns must be qualified as table.column: ${reference}`);
|
|
1934
|
+
}
|
|
1935
|
+
return { table, column };
|
|
1936
|
+
}
|
|
1937
|
+
function normalizeDirection(direction = "ASC") {
|
|
1938
|
+
return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
|
1939
|
+
}
|
|
1940
|
+
function isQueryOperator(value) {
|
|
1941
|
+
return value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value === "object";
|
|
1942
|
+
}
|
|
1943
|
+
function pushParam(values, value) {
|
|
1944
|
+
values.push(value);
|
|
1945
|
+
return `$${values.length}`;
|
|
1946
|
+
}
|
|
1947
|
+
function buildInClause(column, values, params) {
|
|
1948
|
+
if (values.length === 0) {
|
|
1949
|
+
return "1 = 0";
|
|
1950
|
+
}
|
|
1951
|
+
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
1952
|
+
return `${column} IN (${placeholders})`;
|
|
1953
|
+
}
|
|
1954
|
+
function buildOperatorClauses(column, operator, params) {
|
|
1955
|
+
const clauses = [];
|
|
1956
|
+
if (operator.isNull === true) {
|
|
1957
|
+
clauses.push(`${column} IS NULL`);
|
|
1958
|
+
}
|
|
1959
|
+
if (operator.isNull === false) {
|
|
1960
|
+
clauses.push(`${column} IS NOT NULL`);
|
|
1961
|
+
}
|
|
1962
|
+
if (operator.eq !== undefined) {
|
|
1963
|
+
if (operator.eq === null) {
|
|
1964
|
+
clauses.push(`${column} IS NULL`);
|
|
1965
|
+
} else {
|
|
1966
|
+
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
if (operator.in !== undefined) {
|
|
1970
|
+
clauses.push(buildInClause(column, operator.in, params));
|
|
1971
|
+
}
|
|
1972
|
+
if (operator.gt !== undefined) {
|
|
1973
|
+
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
1974
|
+
}
|
|
1975
|
+
if (operator.gte !== undefined) {
|
|
1976
|
+
clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
|
|
1977
|
+
}
|
|
1978
|
+
if (operator.lt !== undefined) {
|
|
1979
|
+
clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
|
|
1980
|
+
}
|
|
1981
|
+
if (operator.lte !== undefined) {
|
|
1982
|
+
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
1983
|
+
}
|
|
1984
|
+
if (operator.ilike !== undefined) {
|
|
1985
|
+
clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
|
|
1986
|
+
}
|
|
1987
|
+
if (operator.tsMatch !== undefined) {
|
|
1988
|
+
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
1989
|
+
}
|
|
1990
|
+
return clauses;
|
|
1991
|
+
}
|
|
1992
|
+
function appendWhereParts(tableName, where, params) {
|
|
1993
|
+
const clauses = [];
|
|
1994
|
+
for (const [columnName, filterValue] of Object.entries(where)) {
|
|
1995
|
+
if (filterValue === undefined) {
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
const column = resolveQualifiedColumn(tableName, columnName);
|
|
1999
|
+
if (Array.isArray(filterValue)) {
|
|
2000
|
+
clauses.push(buildInClause(column, filterValue, params));
|
|
2001
|
+
continue;
|
|
2002
|
+
}
|
|
2003
|
+
if (isQueryOperator(filterValue)) {
|
|
2004
|
+
clauses.push(...buildOperatorClauses(column, filterValue, params));
|
|
2005
|
+
continue;
|
|
2006
|
+
}
|
|
2007
|
+
if (filterValue === null) {
|
|
2008
|
+
clauses.push(`${column} IS NULL`);
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
clauses.push(`${column} = ${pushParam(params, filterValue)}`);
|
|
2012
|
+
}
|
|
2013
|
+
return clauses.join(" AND ");
|
|
2014
|
+
}
|
|
2015
|
+
function buildWhereNodeClause(tableName, node, params) {
|
|
2016
|
+
if ("where" in node) {
|
|
2017
|
+
return appendWhereParts(tableName, node.where, params);
|
|
2018
|
+
}
|
|
2019
|
+
const grouped = buildWhereGroupClause(tableName, node.group, params);
|
|
2020
|
+
if (!grouped) {
|
|
2021
|
+
return "";
|
|
2022
|
+
}
|
|
2023
|
+
return grouped.includes(" OR ") ? `(${grouped})` : grouped;
|
|
2024
|
+
}
|
|
2025
|
+
function buildWhereGroupClause(tableName, nodes, params) {
|
|
2026
|
+
let result = "";
|
|
2027
|
+
for (const node of nodes) {
|
|
2028
|
+
const part = buildWhereNodeClause(tableName, node, params);
|
|
2029
|
+
if (!part) {
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2032
|
+
if (!result) {
|
|
2033
|
+
result = part;
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
2036
|
+
result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
|
|
2037
|
+
}
|
|
2038
|
+
if (!result) {
|
|
2039
|
+
return "";
|
|
2040
|
+
}
|
|
2041
|
+
return result;
|
|
2042
|
+
}
|
|
2043
|
+
function buildAdvancedWhereClause(tableName, where = {}, whereNodes = [], params = []) {
|
|
2044
|
+
const nodes = [];
|
|
2045
|
+
if (Object.keys(where).length > 0) {
|
|
2046
|
+
nodes.push({ kind: "and", where });
|
|
2047
|
+
}
|
|
2048
|
+
nodes.push(...whereNodes);
|
|
2049
|
+
const combined = buildWhereGroupClause(tableName, nodes, params);
|
|
2050
|
+
return {
|
|
2051
|
+
clause: combined ? ` WHERE ${combined}` : "",
|
|
2052
|
+
params
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
function resolveSoftDeleteColumn(table) {
|
|
2056
|
+
if (!table.softDeletes) {
|
|
2057
|
+
return null;
|
|
2058
|
+
}
|
|
2059
|
+
if (table.softDeletes === true) {
|
|
2060
|
+
return "deleted_at";
|
|
2061
|
+
}
|
|
2062
|
+
return table.softDeletes.column ?? "deleted_at";
|
|
2063
|
+
}
|
|
2064
|
+
function appendSoftDeleteScope(table, options, clauses) {
|
|
2065
|
+
const column = resolveSoftDeleteColumn(table);
|
|
2066
|
+
if (!column) {
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
2070
|
+
if (options.onlyTrashed) {
|
|
2071
|
+
clauses.push(`${qualifiedColumn} IS NOT NULL`);
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
if (!options.withTrashed) {
|
|
2075
|
+
clauses.push(`${qualifiedColumn} IS NULL`);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
function buildQueryWhereClause(table, options = {}, whereNodes = [], params = []) {
|
|
2079
|
+
const { clause, params: whereParams } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes, params);
|
|
2080
|
+
const softDeleteClauses = [];
|
|
2081
|
+
appendSoftDeleteScope(table, options, softDeleteClauses);
|
|
2082
|
+
if (softDeleteClauses.length === 0) {
|
|
2083
|
+
return { clause, params: whereParams };
|
|
2084
|
+
}
|
|
2085
|
+
const base = clause.replace(/^ WHERE /, "");
|
|
2086
|
+
const scope = softDeleteClauses.join(" AND ");
|
|
2087
|
+
return {
|
|
2088
|
+
clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
|
|
2089
|
+
params: whereParams
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
function isQueryOrder(value) {
|
|
2093
|
+
return "column" in value;
|
|
2094
|
+
}
|
|
2095
|
+
function normalizeOrderBy(orderBy) {
|
|
2096
|
+
if (!orderBy) {
|
|
2097
|
+
return [];
|
|
2098
|
+
}
|
|
2099
|
+
if (Array.isArray(orderBy)) {
|
|
2100
|
+
return orderBy;
|
|
2101
|
+
}
|
|
2102
|
+
if (isQueryOrder(orderBy)) {
|
|
2103
|
+
return [orderBy];
|
|
2104
|
+
}
|
|
2105
|
+
return Object.entries(orderBy).map(([column, direction]) => ({
|
|
2106
|
+
column,
|
|
2107
|
+
direction
|
|
2108
|
+
}));
|
|
2109
|
+
}
|
|
2110
|
+
function buildOrderByClause(tableName, orderBy) {
|
|
2111
|
+
const parts = normalizeOrderBy(orderBy).map(({ column, direction }) => {
|
|
2112
|
+
return `${resolveQualifiedColumn(tableName, column)} ${normalizeDirection(direction)}`;
|
|
2113
|
+
});
|
|
2114
|
+
return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
|
|
2115
|
+
}
|
|
2116
|
+
function buildGroupByClause(tableName, groupBy) {
|
|
2117
|
+
if (!groupBy) {
|
|
2118
|
+
return "";
|
|
2119
|
+
}
|
|
2120
|
+
const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
|
|
2121
|
+
const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
|
|
2122
|
+
return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
|
|
2123
|
+
}
|
|
2124
|
+
function buildHavingClause(tableName, having, params) {
|
|
2125
|
+
if (!having) {
|
|
2126
|
+
return "";
|
|
2127
|
+
}
|
|
2128
|
+
const body = appendWhereParts(tableName, having, params);
|
|
2129
|
+
return body.length > 0 ? ` HAVING ${body}` : "";
|
|
2130
|
+
}
|
|
2131
|
+
function buildJoinClause(joins = []) {
|
|
2132
|
+
return joins.map((join3) => {
|
|
2133
|
+
const joinType = join3.type === "left" ? "LEFT JOIN" : "INNER JOIN";
|
|
2134
|
+
const onClause = join3.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
|
|
2135
|
+
return ` ${joinType} ${quoteIdentifier(join3.table)} ON ${onClause}`;
|
|
2136
|
+
}).join("");
|
|
2137
|
+
}
|
|
2138
|
+
function buildLimitClause(limit) {
|
|
2139
|
+
if (limit === undefined) {
|
|
2140
|
+
return "";
|
|
2141
|
+
}
|
|
2142
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
2143
|
+
throw new Error("Query limit must be a positive integer.");
|
|
2144
|
+
}
|
|
2145
|
+
return ` LIMIT ${limit}`;
|
|
2146
|
+
}
|
|
2147
|
+
function buildOffsetClause(offset) {
|
|
2148
|
+
if (offset === undefined) {
|
|
2149
|
+
return "";
|
|
2150
|
+
}
|
|
2151
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
2152
|
+
throw new Error("Query offset must be a non-negative integer.");
|
|
2153
|
+
}
|
|
2154
|
+
return ` OFFSET ${offset}`;
|
|
2155
|
+
}
|
|
2156
|
+
function buildReturningColumns(table) {
|
|
2157
|
+
return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
|
|
2158
|
+
}
|
|
2159
|
+
function buildSelectList(table, select, params = []) {
|
|
2160
|
+
if (!select || select.length === 0) {
|
|
2161
|
+
return buildReturningColumns(table);
|
|
2162
|
+
}
|
|
2163
|
+
return select.map((item) => {
|
|
2164
|
+
if (item.kind === "column") {
|
|
2165
|
+
const column2 = qualifyColumn(item.table, item.column);
|
|
2166
|
+
return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
|
|
2167
|
+
}
|
|
2168
|
+
if (item.kind === "literalText") {
|
|
2169
|
+
return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
|
|
2170
|
+
}
|
|
2171
|
+
const column = qualifyColumn(item.table, item.column);
|
|
2172
|
+
const placeholder = pushParam(params, item.query);
|
|
2173
|
+
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
2174
|
+
}).join(", ");
|
|
2175
|
+
}
|
|
2176
|
+
function getDefinedColumnEntries(table, values, options = {}) {
|
|
2177
|
+
const record = values;
|
|
2178
|
+
const excluded = new Set(options.exclude ?? []);
|
|
2179
|
+
return table.columns.flatMap((column) => {
|
|
2180
|
+
if (excluded.has(column) || !Object.hasOwn(record, column)) {
|
|
2181
|
+
return [];
|
|
2182
|
+
}
|
|
2183
|
+
const value = record[column];
|
|
2184
|
+
if (value === undefined) {
|
|
2185
|
+
return [];
|
|
2186
|
+
}
|
|
2187
|
+
return [[column, value]];
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
function buildSelectQuery(table, options = {}, whereNodes = []) {
|
|
2191
|
+
const params = [];
|
|
2192
|
+
const columns = buildSelectList(table, options.select, params);
|
|
2193
|
+
const { clause } = buildQueryWhereClause(table, options, whereNodes, params);
|
|
2194
|
+
const joins = buildJoinClause(options.joins);
|
|
2195
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
2196
|
+
const havingClause = buildHavingClause(table.name, options.having, params);
|
|
2197
|
+
const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
|
|
2198
|
+
const limit = buildLimitClause(options.limit);
|
|
2199
|
+
const offset = buildOffsetClause(options.offset);
|
|
2200
|
+
return {
|
|
2201
|
+
text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
|
|
2202
|
+
params
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
|
|
2206
|
+
const params = [];
|
|
2207
|
+
const { clause, params: whereParams } = buildQueryWhereClause(table, {
|
|
2208
|
+
where,
|
|
2209
|
+
withTrashed: options.withTrashed,
|
|
2210
|
+
onlyTrashed: options.onlyTrashed
|
|
2211
|
+
}, whereNodes);
|
|
2212
|
+
params.push(...whereParams);
|
|
2213
|
+
const joins = buildJoinClause(options.joins);
|
|
2214
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
2215
|
+
return {
|
|
2216
|
+
text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
|
|
2217
|
+
params
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
|
|
2221
|
+
assertSafeProjectionExpression(expression);
|
|
2222
|
+
const params = [];
|
|
2223
|
+
const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
|
|
2224
|
+
params.push(...whereParams);
|
|
2225
|
+
const joins = buildJoinClause(options.joins);
|
|
2226
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
2227
|
+
const orderBy = buildOrderByClause(table.name, options.orderBy);
|
|
2228
|
+
const limit = buildLimitClause(options.limit);
|
|
2229
|
+
return {
|
|
2230
|
+
text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
|
|
2231
|
+
params
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
var SAFE_PROJECTION_EXPRESSION = /^(COUNT\(\*\)|(?:"[\w_]+"(?:\."[\w_]+")?)|(?:AVG|SUM|MIN|MAX)\((?:"[\w_]+"(?:\."[\w_]+")?)\))$/;
|
|
2235
|
+
function assertSafeProjectionExpression(expression) {
|
|
2236
|
+
if (!SAFE_PROJECTION_EXPRESSION.test(expression.trim())) {
|
|
2237
|
+
throw new Error(`Unsafe projection expression: ${expression}`);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
function buildGroupedCountQuery(table, column, where = {}, options = {}) {
|
|
2241
|
+
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
2242
|
+
const { clause, params } = buildQueryWhereClause(table, {
|
|
2243
|
+
where,
|
|
2244
|
+
...options
|
|
2245
|
+
});
|
|
2246
|
+
return {
|
|
2247
|
+
text: `SELECT ${qualifiedColumn} AS ${quoteIdentifier("value")}, COUNT(*) AS ${quoteIdentifier("count")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${qualifiedColumn} ORDER BY ${qualifiedColumn} ASC`,
|
|
2248
|
+
params
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
function buildInsertQuery(table, values) {
|
|
2252
|
+
const entries = getDefinedColumnEntries(table, values);
|
|
2253
|
+
if (entries.length === 0) {
|
|
2254
|
+
throw new Error(`Cannot insert into ${table.name} without any column values.`);
|
|
2255
|
+
}
|
|
2256
|
+
const params = [];
|
|
2257
|
+
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
2258
|
+
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
2259
|
+
const returningColumns = buildReturningColumns(table);
|
|
2260
|
+
return {
|
|
2261
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders}) RETURNING ${returningColumns}`,
|
|
2262
|
+
params
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
function buildUpdateQuery(table, id, changes) {
|
|
2266
|
+
const entries = getDefinedColumnEntries(table, changes, {
|
|
2267
|
+
exclude: [table.primaryKey]
|
|
2268
|
+
});
|
|
2269
|
+
if (entries.length === 0) {
|
|
2270
|
+
throw new Error(`Cannot update ${table.name} without any changed column values.`);
|
|
2271
|
+
}
|
|
2272
|
+
const params = [];
|
|
2273
|
+
const setClause = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`).join(", ");
|
|
2274
|
+
const primaryKeyPlaceholder = pushParam(params, id);
|
|
2275
|
+
const returningColumns = buildReturningColumns(table);
|
|
2276
|
+
const scopeClauses = [];
|
|
2277
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
2278
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
2279
|
+
return {
|
|
2280
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix} RETURNING ${returningColumns}`,
|
|
2281
|
+
params
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
function buildSoftDeleteByIdQuery(table, id, deletedAt) {
|
|
2285
|
+
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
2286
|
+
if (!deletedAtColumn) {
|
|
2287
|
+
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
2288
|
+
}
|
|
2289
|
+
const returningColumns = buildReturningColumns(table);
|
|
2290
|
+
const scopeClauses = [];
|
|
2291
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
2292
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
2293
|
+
return {
|
|
2294
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2${scopeSuffix} RETURNING ${returningColumns}`,
|
|
2295
|
+
params: [deletedAt, id]
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
function buildRestoreByIdQuery(table, id) {
|
|
2299
|
+
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
2300
|
+
if (!deletedAtColumn) {
|
|
2301
|
+
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
2302
|
+
}
|
|
2303
|
+
const returningColumns = buildReturningColumns(table);
|
|
2304
|
+
return {
|
|
2305
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2 AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL RETURNING ${returningColumns}`,
|
|
2306
|
+
params: [null, id]
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
function buildDeleteByIdQuery(table, id) {
|
|
2310
|
+
return {
|
|
2311
|
+
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $1 RETURNING ${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`,
|
|
2312
|
+
params: [id]
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// ../../src/core/database/relationships.ts
|
|
2317
|
+
function indexHasManyRelation(parents, children, relation) {
|
|
2318
|
+
const groups = new Map;
|
|
2319
|
+
for (const parent of parents) {
|
|
2320
|
+
groups.set(parent[relation.localKey], []);
|
|
2321
|
+
}
|
|
2322
|
+
for (const child of children) {
|
|
2323
|
+
const key = child[relation.foreignKey];
|
|
2324
|
+
const group = groups.get(key);
|
|
2325
|
+
if (!group) {
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
group.push(child);
|
|
2329
|
+
}
|
|
2330
|
+
return groups;
|
|
2331
|
+
}
|
|
2332
|
+
function indexBelongsToRelation(children, parents, relation) {
|
|
2333
|
+
const parentsById = new Map;
|
|
2334
|
+
for (const parent of parents) {
|
|
2335
|
+
parentsById.set(parent[relation.ownerKey], parent);
|
|
2336
|
+
}
|
|
2337
|
+
const result = new Map;
|
|
2338
|
+
for (const child of children) {
|
|
2339
|
+
const foreignKey = child[relation.foreignKey];
|
|
2340
|
+
const parent = parentsById.get(foreignKey);
|
|
2341
|
+
if (parent) {
|
|
2342
|
+
result.set(foreignKey, parent);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
return result;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// ../../src/core/database/boundConnection.ts
|
|
2349
|
+
var boundConnectionHolder = {
|
|
2350
|
+
connection: null
|
|
2351
|
+
};
|
|
2352
|
+
function getBoundDatabaseConnection() {
|
|
2353
|
+
return boundConnectionHolder.connection;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// ../../src/core/database/repositoryConnection.ts
|
|
2357
|
+
function resolveRepositoryConnection() {
|
|
2358
|
+
return getBoundDatabaseConnection() ?? connection_default;
|
|
2359
|
+
}
|
|
2360
|
+
var repositoryConnection = new Proxy({}, {
|
|
2361
|
+
get(_target, property) {
|
|
2362
|
+
const connection = resolveRepositoryConnection();
|
|
2363
|
+
const value = connection[property];
|
|
2364
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2367
|
+
|
|
2368
|
+
// ../../src/core/database/whereBuilder.ts
|
|
2369
|
+
class WhereBuilder {
|
|
2370
|
+
nodes = [];
|
|
2371
|
+
where(where) {
|
|
2372
|
+
this.nodes.push({ kind: "and", where });
|
|
2373
|
+
return this;
|
|
2374
|
+
}
|
|
2375
|
+
orWhere(where) {
|
|
2376
|
+
this.nodes.push({ kind: "or", where });
|
|
2377
|
+
return this;
|
|
2378
|
+
}
|
|
2379
|
+
whereGroup(fn) {
|
|
2380
|
+
const nested = new WhereBuilder;
|
|
2381
|
+
fn(nested);
|
|
2382
|
+
if (nested.nodes.length > 0) {
|
|
2383
|
+
this.nodes.push({ kind: "and", group: nested.nodes });
|
|
2384
|
+
}
|
|
2385
|
+
return this;
|
|
2386
|
+
}
|
|
2387
|
+
orWhereGroup(fn) {
|
|
2388
|
+
const nested = new WhereBuilder;
|
|
2389
|
+
fn(nested);
|
|
2390
|
+
if (nested.nodes.length > 0) {
|
|
2391
|
+
this.nodes.push({ kind: "or", group: nested.nodes });
|
|
2392
|
+
}
|
|
2393
|
+
return this;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// ../../src/core/database/repositoryQuery.ts
|
|
2398
|
+
class RepositoryQuery {
|
|
2399
|
+
repository;
|
|
2400
|
+
whereClause;
|
|
2401
|
+
queryOptions;
|
|
2402
|
+
eagerLoads = [];
|
|
2403
|
+
whereNodes = [];
|
|
2404
|
+
constructor(repository, whereClause = {}, queryOptions = {}) {
|
|
2405
|
+
this.repository = repository;
|
|
2406
|
+
this.whereClause = whereClause;
|
|
2407
|
+
this.queryOptions = queryOptions;
|
|
2408
|
+
}
|
|
2409
|
+
where(input) {
|
|
2410
|
+
if (typeof input === "function") {
|
|
2411
|
+
const builder = new WhereBuilder;
|
|
2412
|
+
input(builder);
|
|
2413
|
+
this.whereNodes.push(...builder.nodes);
|
|
2414
|
+
return this;
|
|
2415
|
+
}
|
|
2416
|
+
this.whereClause = { ...this.whereClause, ...input };
|
|
2417
|
+
return this;
|
|
2418
|
+
}
|
|
2419
|
+
orWhere(input) {
|
|
2420
|
+
if (typeof input === "function") {
|
|
2421
|
+
const builder = new WhereBuilder;
|
|
2422
|
+
input(builder);
|
|
2423
|
+
if (builder.nodes.length > 0) {
|
|
2424
|
+
this.whereNodes.push({ kind: "or", group: builder.nodes });
|
|
2425
|
+
}
|
|
2426
|
+
return this;
|
|
2427
|
+
}
|
|
2428
|
+
this.whereNodes.push({ kind: "or", where: input });
|
|
2429
|
+
return this;
|
|
2430
|
+
}
|
|
2431
|
+
orderBy(orderBy) {
|
|
2432
|
+
this.queryOptions = { ...this.queryOptions, orderBy };
|
|
2433
|
+
return this;
|
|
2434
|
+
}
|
|
2435
|
+
limit(limit) {
|
|
2436
|
+
this.queryOptions = { ...this.queryOptions, limit };
|
|
2437
|
+
return this;
|
|
2438
|
+
}
|
|
2439
|
+
offset(offset) {
|
|
2440
|
+
this.queryOptions = { ...this.queryOptions, offset };
|
|
2441
|
+
return this;
|
|
2442
|
+
}
|
|
2443
|
+
join(left, right) {
|
|
2444
|
+
return this.addJoin("inner", left, right);
|
|
2445
|
+
}
|
|
2446
|
+
leftJoin(left, right) {
|
|
2447
|
+
return this.addJoin("left", left, right);
|
|
2448
|
+
}
|
|
2449
|
+
groupBy(groupBy) {
|
|
2450
|
+
this.queryOptions = { ...this.queryOptions, groupBy };
|
|
2451
|
+
return this;
|
|
2452
|
+
}
|
|
2453
|
+
having(having) {
|
|
2454
|
+
this.queryOptions = { ...this.queryOptions, having };
|
|
2455
|
+
return this;
|
|
2456
|
+
}
|
|
2457
|
+
withHasMany(as, relation, childRepository, options = {}) {
|
|
2458
|
+
this.eagerLoads.push({
|
|
2459
|
+
kind: "hasMany",
|
|
2460
|
+
as,
|
|
2461
|
+
relation,
|
|
2462
|
+
repository: childRepository,
|
|
2463
|
+
options
|
|
2464
|
+
});
|
|
2465
|
+
return this;
|
|
2466
|
+
}
|
|
2467
|
+
withBelongsTo(as, relation, parentRepository, options = {}) {
|
|
2468
|
+
this.eagerLoads.push({
|
|
2469
|
+
kind: "belongsTo",
|
|
2470
|
+
as,
|
|
2471
|
+
relation,
|
|
2472
|
+
repository: parentRepository,
|
|
2473
|
+
options
|
|
2474
|
+
});
|
|
2475
|
+
return this;
|
|
2476
|
+
}
|
|
2477
|
+
async get() {
|
|
2478
|
+
const rows = await this.repository.findAll(this.buildOptions());
|
|
2479
|
+
return await this.attach(rows);
|
|
2480
|
+
}
|
|
2481
|
+
async first() {
|
|
2482
|
+
const rows = await this.get();
|
|
2483
|
+
return rows[0] ?? null;
|
|
2484
|
+
}
|
|
2485
|
+
async paginate(options) {
|
|
2486
|
+
return await this.repository.paginate({
|
|
2487
|
+
...this.buildOptions(),
|
|
2488
|
+
page: options.page,
|
|
2489
|
+
perPage: options.perPage
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
buildOptions() {
|
|
2493
|
+
return {
|
|
2494
|
+
...this.queryOptions,
|
|
2495
|
+
where: this.whereClause,
|
|
2496
|
+
whereNodes: this.whereNodes
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
addJoin(type, left, right) {
|
|
2500
|
+
const leftRef = parseQualifiedColumn(left);
|
|
2501
|
+
const rightRef = parseQualifiedColumn(right);
|
|
2502
|
+
const table = type === "inner" ? rightRef.table : rightRef.table;
|
|
2503
|
+
const joins = this.queryOptions.joins ?? [];
|
|
2504
|
+
const existing = joins.find((join3) => join3.table === table && join3.type === type);
|
|
2505
|
+
if (existing) {
|
|
2506
|
+
existing.on.push({ left: leftRef, right: rightRef });
|
|
2507
|
+
return this;
|
|
2508
|
+
}
|
|
2509
|
+
this.queryOptions = {
|
|
2510
|
+
...this.queryOptions,
|
|
2511
|
+
joins: [
|
|
2512
|
+
...joins,
|
|
2513
|
+
{
|
|
2514
|
+
type,
|
|
2515
|
+
table,
|
|
2516
|
+
on: [{ left: leftRef, right: rightRef }]
|
|
2517
|
+
}
|
|
2518
|
+
]
|
|
2519
|
+
};
|
|
2520
|
+
return this;
|
|
2521
|
+
}
|
|
2522
|
+
async attach(rows) {
|
|
2523
|
+
if (rows.length === 0 || this.eagerLoads.length === 0) {
|
|
2524
|
+
return rows.map((row) => ({ ...row }));
|
|
2525
|
+
}
|
|
2526
|
+
let result = rows.map((row) => ({ ...row }));
|
|
2527
|
+
for (const load of this.eagerLoads) {
|
|
2528
|
+
if (load.kind === "hasMany") {
|
|
2529
|
+
const relation2 = load.relation;
|
|
2530
|
+
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
|
|
2531
|
+
result = result.map((row) => ({
|
|
2532
|
+
...row,
|
|
2533
|
+
[load.as]: grouped2.get(row[relation2.localKey]) ?? []
|
|
2534
|
+
}));
|
|
2535
|
+
continue;
|
|
2536
|
+
}
|
|
2537
|
+
const relation = load.relation;
|
|
2538
|
+
const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
|
|
2539
|
+
result = result.map((row) => ({
|
|
2540
|
+
...row,
|
|
2541
|
+
[load.as]: grouped.get(row[relation.foreignKey])
|
|
2542
|
+
}));
|
|
2543
|
+
}
|
|
2544
|
+
return result;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
// ../../src/core/database/baseRepository.ts
|
|
2549
|
+
class BaseRepository5 {
|
|
2550
|
+
table;
|
|
2551
|
+
connection;
|
|
2552
|
+
constructor(table, connection = repositoryConnection) {
|
|
2553
|
+
this.table = table;
|
|
2554
|
+
this.connection = connection;
|
|
2555
|
+
}
|
|
2556
|
+
async findAll(options = {}) {
|
|
2557
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2558
|
+
const { whereNodes, ...queryOptions } = options;
|
|
2559
|
+
const { text, params } = buildSelectQuery(this.table, queryOptions, whereNodes ?? []);
|
|
2560
|
+
return await this.connection.unsafe(text, params);
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
async paginate(options) {
|
|
2564
|
+
const { whereNodes, where = {}, page, perPage, ...queryOptions } = options;
|
|
2565
|
+
const total = await this.countWhere(where, {
|
|
2566
|
+
withTrashed: options.withTrashed,
|
|
2567
|
+
onlyTrashed: options.onlyTrashed,
|
|
2568
|
+
joins: options.joins,
|
|
2569
|
+
groupBy: options.groupBy
|
|
2570
|
+
}, whereNodes);
|
|
2571
|
+
const offset = (page - 1) * perPage;
|
|
2572
|
+
const data = await this.findAll({
|
|
2573
|
+
...queryOptions,
|
|
2574
|
+
where,
|
|
2575
|
+
whereNodes,
|
|
2576
|
+
limit: perPage,
|
|
2577
|
+
offset
|
|
2578
|
+
});
|
|
2579
|
+
return {
|
|
2580
|
+
data,
|
|
2581
|
+
meta: buildPaginationMeta({ page, perPage, total })
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
async chunk(count, callback, options = {}) {
|
|
2585
|
+
if (!Number.isInteger(count) || count <= 0) {
|
|
2586
|
+
throw new Error("Chunk size must be a positive integer.");
|
|
2587
|
+
}
|
|
2588
|
+
let offset = 0;
|
|
2589
|
+
while (true) {
|
|
2590
|
+
const rows = await this.findAll({
|
|
2591
|
+
...options,
|
|
2592
|
+
limit: count,
|
|
2593
|
+
offset
|
|
2594
|
+
});
|
|
2595
|
+
if (rows.length === 0) {
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
const shouldContinue = await callback(rows);
|
|
2599
|
+
if (shouldContinue === false || rows.length < count) {
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
offset += count;
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
async cursorPaginate(options) {
|
|
2606
|
+
const {
|
|
2607
|
+
perPage,
|
|
2608
|
+
cursor,
|
|
2609
|
+
cursorColumn = this.table.primaryKey,
|
|
2610
|
+
direction = "asc",
|
|
2611
|
+
where = {},
|
|
2612
|
+
whereNodes,
|
|
2613
|
+
...queryOptions
|
|
2614
|
+
} = options;
|
|
2615
|
+
if (!Number.isInteger(perPage) || perPage <= 0) {
|
|
2616
|
+
throw new Error("Cursor page size must be a positive integer.");
|
|
2617
|
+
}
|
|
2618
|
+
const cursorWhere = { ...where };
|
|
2619
|
+
if (cursor !== undefined) {
|
|
2620
|
+
cursorWhere[cursorColumn] = direction === "asc" ? { gt: cursor } : { lt: cursor };
|
|
2621
|
+
}
|
|
2622
|
+
const rows = await this.findAll({
|
|
2623
|
+
...queryOptions,
|
|
2624
|
+
where: cursorWhere,
|
|
2625
|
+
whereNodes,
|
|
2626
|
+
orderBy: { [cursorColumn]: direction },
|
|
2627
|
+
limit: perPage + 1
|
|
2628
|
+
});
|
|
2629
|
+
const hasMore = rows.length > perPage;
|
|
2630
|
+
const data = hasMore ? rows.slice(0, perPage) : rows;
|
|
2631
|
+
const nextCursor = hasMore ? data[data.length - 1]?.[cursorColumn] ?? null : null;
|
|
2632
|
+
const prevCursor = cursor ?? null;
|
|
2633
|
+
return {
|
|
2634
|
+
data,
|
|
2635
|
+
meta: {
|
|
2636
|
+
per_page: perPage,
|
|
2637
|
+
next_cursor: nextCursor,
|
|
2638
|
+
prev_cursor: prevCursor,
|
|
2639
|
+
has_more: hasMore
|
|
2640
|
+
}
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
async findById(id) {
|
|
2644
|
+
return await this.firstOrNull({
|
|
2645
|
+
[this.table.primaryKey]: id
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
async findByIdOrThrow(id, errorFactory) {
|
|
2649
|
+
const record = await this.findById(id);
|
|
2650
|
+
if (record) {
|
|
2651
|
+
return record;
|
|
2652
|
+
}
|
|
2653
|
+
throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
|
|
2654
|
+
}
|
|
2655
|
+
async findByIds(ids) {
|
|
2656
|
+
const uniqueIds = [...new Set(ids)];
|
|
2657
|
+
if (uniqueIds.length === 0) {
|
|
2658
|
+
return [];
|
|
2659
|
+
}
|
|
2660
|
+
return await this.findWhere({
|
|
2661
|
+
[this.table.primaryKey]: uniqueIds
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
async firstOrNull(where, options = {}) {
|
|
2665
|
+
const [record] = await this.findAll({ ...options, where, limit: 1 });
|
|
2666
|
+
return record ?? null;
|
|
2667
|
+
}
|
|
2668
|
+
async create(values) {
|
|
2669
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2670
|
+
const { text, params } = buildInsertQuery(this.table, values);
|
|
2671
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
2672
|
+
if (!record) {
|
|
2673
|
+
throw new Error(`Insert into ${this.table.name} did not return a record.`);
|
|
2674
|
+
}
|
|
2675
|
+
const entity = record;
|
|
2676
|
+
await eventBus.dispatch(modelEventName(this.table.name, "created"), entity);
|
|
2677
|
+
return entity;
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
async updateById(id, changes) {
|
|
2681
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2682
|
+
const { text, params } = buildUpdateQuery(this.table, id, changes);
|
|
2683
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
2684
|
+
const entity = record ?? null;
|
|
2685
|
+
if (entity) {
|
|
2686
|
+
await eventBus.dispatch(modelEventName(this.table.name, "updated"), entity);
|
|
2687
|
+
}
|
|
2688
|
+
return entity;
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
async updateByIdOrThrow(id, changes, errorFactory) {
|
|
2692
|
+
const record = await this.updateById(id, changes);
|
|
2693
|
+
if (record) {
|
|
2694
|
+
return record;
|
|
2695
|
+
}
|
|
2696
|
+
throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
|
|
2697
|
+
}
|
|
2698
|
+
async deleteById(id) {
|
|
2699
|
+
if (resolveSoftDeleteColumn(this.table)) {
|
|
2700
|
+
return await this.softDeleteById(id);
|
|
2701
|
+
}
|
|
2702
|
+
return await this.forceDeleteById(id);
|
|
2703
|
+
}
|
|
2704
|
+
async softDeleteById(id) {
|
|
2705
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2706
|
+
const { text, params } = buildSoftDeleteByIdQuery(this.table, id, new Date);
|
|
2707
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
2708
|
+
if (!record) {
|
|
2709
|
+
return false;
|
|
2710
|
+
}
|
|
2711
|
+
await eventBus.dispatch(modelEventName(this.table.name, "deleted"), record);
|
|
2712
|
+
return true;
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
async forceDeleteById(id) {
|
|
2716
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2717
|
+
const { text, params } = buildDeleteByIdQuery(this.table, id);
|
|
2718
|
+
const [row] = await this.connection.unsafe(text, params);
|
|
2719
|
+
if (!row) {
|
|
2720
|
+
return false;
|
|
2721
|
+
}
|
|
2722
|
+
await eventBus.dispatch(modelEventName(this.table.name, "force-deleted"), {
|
|
2723
|
+
id
|
|
2724
|
+
});
|
|
2725
|
+
return true;
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
async restoreById(id) {
|
|
2729
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2730
|
+
const { text, params } = buildRestoreByIdQuery(this.table, id);
|
|
2731
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
2732
|
+
if (!record) {
|
|
2733
|
+
return null;
|
|
2734
|
+
}
|
|
2735
|
+
const entity = record;
|
|
2736
|
+
await eventBus.dispatch(modelEventName(this.table.name, "restored"), entity);
|
|
2737
|
+
return entity;
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
withConnection(connection) {
|
|
2741
|
+
const clone = Object.create(Object.getPrototypeOf(this));
|
|
2742
|
+
Object.assign(clone, this);
|
|
2743
|
+
clone.connection = connection;
|
|
2744
|
+
return clone;
|
|
2745
|
+
}
|
|
2746
|
+
getConnection() {
|
|
2747
|
+
return this.connection;
|
|
2748
|
+
}
|
|
2749
|
+
getTable() {
|
|
2750
|
+
return this.table;
|
|
2751
|
+
}
|
|
2752
|
+
query(where = {}) {
|
|
2753
|
+
return new RepositoryQuery(this, where);
|
|
2754
|
+
}
|
|
2755
|
+
async findWhere(where, options = {}) {
|
|
2756
|
+
return await this.findAll({ ...options, where });
|
|
2757
|
+
}
|
|
2758
|
+
async countWhere(where = {}, options = {}, whereNodes = []) {
|
|
2759
|
+
const { text, params } = buildCountQuery(this.table, where, options, whereNodes);
|
|
2760
|
+
const [row] = await this.connection.unsafe(text, params);
|
|
2761
|
+
return Number(row?.count ?? 0);
|
|
2762
|
+
}
|
|
2763
|
+
async averageColumn(column, where = {}) {
|
|
2764
|
+
const qualifiedColumn = qualifyColumn(this.table.name, column);
|
|
2765
|
+
return await this.averageExpression(`AVG(${qualifiedColumn})`, "value", where);
|
|
2766
|
+
}
|
|
2767
|
+
async averageExpression(expression, alias, where = {}) {
|
|
2768
|
+
const { text, params } = buildProjectionQuery(this.table, expression, alias, { where });
|
|
2769
|
+
const [row] = await this.connection.unsafe(text, params);
|
|
2770
|
+
return Math.round(Number(row?.[alias] ?? 0));
|
|
2771
|
+
}
|
|
2772
|
+
async pluckNumberValues(expression, alias, options = {}) {
|
|
2773
|
+
const { text, params } = buildProjectionQuery(this.table, expression, alias, options);
|
|
2774
|
+
const rows = await this.connection.unsafe(text, params);
|
|
2775
|
+
return rows.flatMap((row) => {
|
|
2776
|
+
const value = row[alias];
|
|
2777
|
+
return value === null || value === undefined ? [] : [Number(value)];
|
|
2778
|
+
});
|
|
2779
|
+
}
|
|
2780
|
+
async countGroupedBy(column, where = {}) {
|
|
2781
|
+
const { text, params } = buildGroupedCountQuery(this.table, column, where);
|
|
2782
|
+
const rows = await this.connection.unsafe(text, params);
|
|
2783
|
+
return rows.map(({ value, count }) => ({
|
|
2784
|
+
value,
|
|
2785
|
+
count: Number(count)
|
|
2786
|
+
}));
|
|
2787
|
+
}
|
|
2788
|
+
async findByHasManyRelation(relation, parentId, options = {}) {
|
|
2789
|
+
return await this.findWhere({
|
|
2790
|
+
[relation.foreignKey]: parentId
|
|
2791
|
+
}, options);
|
|
2792
|
+
}
|
|
2793
|
+
async loadHasManyForParents(parents, relation, options = {}) {
|
|
2794
|
+
if (parents.length === 0) {
|
|
2795
|
+
return indexHasManyRelation(parents, [], relation);
|
|
2796
|
+
}
|
|
2797
|
+
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
2798
|
+
const children = await this.findWhere({
|
|
2799
|
+
[relation.foreignKey]: parentIds
|
|
2800
|
+
}, options);
|
|
2801
|
+
return indexHasManyRelation(parents, children, relation);
|
|
2802
|
+
}
|
|
2803
|
+
async loadBelongsToForParents(children, relation, parentRepository, options = {}) {
|
|
2804
|
+
if (children.length === 0) {
|
|
2805
|
+
return new Map;
|
|
2806
|
+
}
|
|
2807
|
+
const ownerIds = [...new Set(children.map((child) => child[relation.foreignKey]))];
|
|
2808
|
+
const parents = await parentRepository.withConnection(this.connection).findWhere({
|
|
2809
|
+
[relation.ownerKey]: ownerIds
|
|
2810
|
+
}, options);
|
|
2811
|
+
return indexBelongsToRelation(children, parents, relation);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
var baseRepository_default = BaseRepository5;
|
|
2815
|
+
// ../../src/core/database/model.ts
|
|
2816
|
+
var modelRepositories = new WeakMap;
|
|
2817
|
+
var modelGlobalScopes = new WeakMap;
|
|
2818
|
+
var modelBooted = new WeakSet;
|
|
2819
|
+
// ../../src/core/database/schema/columnDefinition.ts
|
|
2820
|
+
class ColumnDefinition {
|
|
2821
|
+
name;
|
|
2822
|
+
kind;
|
|
2823
|
+
length;
|
|
2824
|
+
isNullable = false;
|
|
2825
|
+
isPrimary = false;
|
|
2826
|
+
isUnique = false;
|
|
2827
|
+
autoIncrement = false;
|
|
2828
|
+
defaultValue;
|
|
2829
|
+
checkExpression;
|
|
2830
|
+
foreignKey;
|
|
2831
|
+
constructor(name, kind) {
|
|
2832
|
+
this.name = name;
|
|
2833
|
+
this.kind = kind;
|
|
2834
|
+
}
|
|
2835
|
+
nullable() {
|
|
2836
|
+
this.isNullable = true;
|
|
2837
|
+
return this;
|
|
2838
|
+
}
|
|
2839
|
+
notNullable() {
|
|
2840
|
+
this.isNullable = false;
|
|
2841
|
+
return this;
|
|
2842
|
+
}
|
|
2843
|
+
default(value) {
|
|
2844
|
+
if (typeof value === "boolean") {
|
|
2845
|
+
this.defaultValue = value ? "TRUE" : "FALSE";
|
|
2846
|
+
return this;
|
|
2847
|
+
}
|
|
2848
|
+
if (typeof value === "number") {
|
|
2849
|
+
this.defaultValue = String(value);
|
|
2850
|
+
return this;
|
|
2851
|
+
}
|
|
2852
|
+
this.defaultValue = `'${value.replace(/'/g, "''")}'`;
|
|
2853
|
+
return this;
|
|
2854
|
+
}
|
|
2855
|
+
defaultRaw(expression) {
|
|
2856
|
+
this.defaultValue = expression;
|
|
2857
|
+
return this;
|
|
2858
|
+
}
|
|
2859
|
+
unique() {
|
|
2860
|
+
this.isUnique = true;
|
|
2861
|
+
return this;
|
|
2862
|
+
}
|
|
2863
|
+
primary() {
|
|
2864
|
+
this.isPrimary = true;
|
|
2865
|
+
return this;
|
|
2866
|
+
}
|
|
2867
|
+
check(expression) {
|
|
2868
|
+
this.checkExpression = expression;
|
|
2869
|
+
return this;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
class ForeignIdColumnDefinition extends ColumnDefinition {
|
|
2874
|
+
constructor(name) {
|
|
2875
|
+
super(name, "foreignId");
|
|
2876
|
+
this.notNullable();
|
|
2877
|
+
}
|
|
2878
|
+
references(table, column = "id") {
|
|
2879
|
+
this.foreignKey = {
|
|
2880
|
+
referencesTable: table,
|
|
2881
|
+
referencesColumn: column
|
|
2882
|
+
};
|
|
2883
|
+
return this;
|
|
2884
|
+
}
|
|
2885
|
+
constrained(table) {
|
|
2886
|
+
const referencesTable = table ?? inferReferencedTable(this.name);
|
|
2887
|
+
return this.references(referencesTable);
|
|
2888
|
+
}
|
|
2889
|
+
cascadeOnDelete() {
|
|
2890
|
+
if (!this.foreignKey) {
|
|
2891
|
+
throw new Error(`Foreign key is not defined for column ${this.name}`);
|
|
2892
|
+
}
|
|
2893
|
+
this.foreignKey.onDelete = "cascade";
|
|
2894
|
+
return this;
|
|
2895
|
+
}
|
|
2896
|
+
nullOnDelete() {
|
|
2897
|
+
if (!this.foreignKey) {
|
|
2898
|
+
throw new Error(`Foreign key is not defined for column ${this.name}`);
|
|
2899
|
+
}
|
|
2900
|
+
this.foreignKey.onDelete = "set null";
|
|
2901
|
+
return this;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
function inferReferencedTable(columnName) {
|
|
2905
|
+
if (!columnName.endsWith("_id")) {
|
|
2906
|
+
throw new Error(`Cannot infer referenced table from column ${columnName}`);
|
|
2907
|
+
}
|
|
2908
|
+
return columnName.slice(0, -3);
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
// ../../src/core/database/schema/blueprint.ts
|
|
2912
|
+
class Blueprint {
|
|
2913
|
+
table;
|
|
2914
|
+
action;
|
|
2915
|
+
columns = [];
|
|
2916
|
+
indexes = [];
|
|
2917
|
+
droppedColumns = [];
|
|
2918
|
+
droppedIndexes = [];
|
|
2919
|
+
constructor(table, action) {
|
|
2920
|
+
this.table = table;
|
|
2921
|
+
this.action = action;
|
|
2922
|
+
}
|
|
2923
|
+
id(name = "id") {
|
|
2924
|
+
const column = new ColumnDefinition(name, "id");
|
|
2925
|
+
column.primary();
|
|
2926
|
+
column.autoIncrement = true;
|
|
2927
|
+
this.columns.push(column);
|
|
2928
|
+
return column;
|
|
2929
|
+
}
|
|
2930
|
+
string(name, length) {
|
|
2931
|
+
const column = new ColumnDefinition(name, "string");
|
|
2932
|
+
column.length = length;
|
|
2933
|
+
column.notNullable();
|
|
2934
|
+
this.columns.push(column);
|
|
2935
|
+
return column;
|
|
2936
|
+
}
|
|
2937
|
+
text(name) {
|
|
2938
|
+
const column = new ColumnDefinition(name, "text");
|
|
2939
|
+
column.notNullable();
|
|
2940
|
+
this.columns.push(column);
|
|
2941
|
+
return column;
|
|
2942
|
+
}
|
|
2943
|
+
boolean(name) {
|
|
2944
|
+
const column = new ColumnDefinition(name, "boolean");
|
|
2945
|
+
column.notNullable();
|
|
2946
|
+
this.columns.push(column);
|
|
2947
|
+
return column;
|
|
2948
|
+
}
|
|
2949
|
+
integer(name) {
|
|
2950
|
+
const column = new ColumnDefinition(name, "integer");
|
|
2951
|
+
column.notNullable();
|
|
2952
|
+
this.columns.push(column);
|
|
2953
|
+
return column;
|
|
2954
|
+
}
|
|
2955
|
+
bigInteger(name) {
|
|
2956
|
+
const column = new ColumnDefinition(name, "bigInteger");
|
|
2957
|
+
column.notNullable();
|
|
2958
|
+
this.columns.push(column);
|
|
2959
|
+
return column;
|
|
2960
|
+
}
|
|
2961
|
+
timestamp(name) {
|
|
2962
|
+
const column = new ColumnDefinition(name, "timestamp");
|
|
2963
|
+
column.notNullable();
|
|
2964
|
+
this.columns.push(column);
|
|
2965
|
+
return column;
|
|
2966
|
+
}
|
|
2967
|
+
json(name) {
|
|
2968
|
+
const column = new ColumnDefinition(name, "json");
|
|
2969
|
+
column.notNullable();
|
|
2970
|
+
this.columns.push(column);
|
|
2971
|
+
return column;
|
|
2972
|
+
}
|
|
2973
|
+
jsonb(name) {
|
|
2974
|
+
const column = new ColumnDefinition(name, "jsonb");
|
|
2975
|
+
column.notNullable();
|
|
2976
|
+
this.columns.push(column);
|
|
2977
|
+
return column;
|
|
2978
|
+
}
|
|
2979
|
+
foreignId(name) {
|
|
2980
|
+
const column = new ForeignIdColumnDefinition(name);
|
|
2981
|
+
this.columns.push(column);
|
|
2982
|
+
return column;
|
|
2983
|
+
}
|
|
2984
|
+
timestamps() {
|
|
2985
|
+
this.timestamp("created_at").defaultRaw("NOW()");
|
|
2986
|
+
this.timestamp("updated_at").defaultRaw("NOW()");
|
|
2987
|
+
}
|
|
2988
|
+
softDeletes() {
|
|
2989
|
+
this.timestamp("deleted_at").nullable();
|
|
2990
|
+
}
|
|
2991
|
+
dropColumn(name) {
|
|
2992
|
+
this.droppedColumns.push(name);
|
|
2993
|
+
}
|
|
2994
|
+
dropSoftDeletes() {
|
|
2995
|
+
this.dropColumn("deleted_at");
|
|
2996
|
+
this.dropIndex(`idx_${this.table}_deleted_at`);
|
|
2997
|
+
}
|
|
2998
|
+
dropIndex(name) {
|
|
2999
|
+
this.droppedIndexes.push(name);
|
|
3000
|
+
}
|
|
3001
|
+
unique(columns, name) {
|
|
3002
|
+
this.indexes.push({
|
|
3003
|
+
name,
|
|
3004
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
3005
|
+
kind: "unique"
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
index(columns, options = {}) {
|
|
3009
|
+
this.indexes.push({
|
|
3010
|
+
name: options.name,
|
|
3011
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
3012
|
+
kind: "index",
|
|
3013
|
+
order: options.order
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
partialIndex(columns, where, nameOrOptions) {
|
|
3017
|
+
const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions ?? {};
|
|
3018
|
+
this.indexes.push({
|
|
3019
|
+
name: options.name,
|
|
3020
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
3021
|
+
kind: options.unique ? "uniquePartial" : "partial",
|
|
3022
|
+
where
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
fullText(columns, name) {
|
|
3026
|
+
this.indexes.push({
|
|
3027
|
+
name,
|
|
3028
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
3029
|
+
kind: "fullText"
|
|
3030
|
+
});
|
|
3031
|
+
}
|
|
3032
|
+
ginIndex(column, name) {
|
|
3033
|
+
this.indexes.push({
|
|
3034
|
+
name,
|
|
3035
|
+
columns: [column],
|
|
3036
|
+
kind: "gin"
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
// ../../src/core/database/schema/errors.ts
|
|
3041
|
+
class UnsupportedSchemaFeatureError extends Error {
|
|
3042
|
+
constructor(feature, driver) {
|
|
3043
|
+
super(`${feature} is not supported for the ${driver} driver`);
|
|
3044
|
+
this.name = "UnsupportedSchemaFeatureError";
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
// ../../src/core/database/schema/grammars/grammar.ts
|
|
3048
|
+
function compileColumnType(driver, column) {
|
|
3049
|
+
switch (column.kind) {
|
|
3050
|
+
case "id":
|
|
3051
|
+
return compileIdType(driver);
|
|
3052
|
+
case "string":
|
|
3053
|
+
return compileStringType(driver, column.length);
|
|
3054
|
+
case "text":
|
|
3055
|
+
return compileTextType(driver);
|
|
3056
|
+
case "boolean":
|
|
3057
|
+
return compileBooleanType(driver);
|
|
3058
|
+
case "integer":
|
|
3059
|
+
case "foreignId":
|
|
3060
|
+
return compileIntegerType(driver);
|
|
3061
|
+
case "bigInteger":
|
|
3062
|
+
return compileBigIntegerType(driver);
|
|
3063
|
+
case "timestamp":
|
|
3064
|
+
return compileTimestampType(driver);
|
|
3065
|
+
case "json":
|
|
3066
|
+
return compileJsonType(driver);
|
|
3067
|
+
case "jsonb":
|
|
3068
|
+
return compileJsonbType(driver);
|
|
3069
|
+
default:
|
|
3070
|
+
throw new Error(`Unsupported column kind: ${column.kind}`);
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
function compileIdType(driver) {
|
|
3074
|
+
switch (driver) {
|
|
3075
|
+
case "pgsql":
|
|
3076
|
+
return "SERIAL";
|
|
3077
|
+
case "mysql":
|
|
3078
|
+
return "BIGINT UNSIGNED";
|
|
3079
|
+
case "sqlite":
|
|
3080
|
+
return "INTEGER";
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
function compileStringType(driver, length) {
|
|
3084
|
+
switch (driver) {
|
|
3085
|
+
case "pgsql":
|
|
3086
|
+
return "TEXT";
|
|
3087
|
+
case "mysql":
|
|
3088
|
+
return length ? `VARCHAR(${length})` : "VARCHAR(255)";
|
|
3089
|
+
case "sqlite":
|
|
3090
|
+
return "TEXT";
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
function compileTextType(driver) {
|
|
3094
|
+
switch (driver) {
|
|
3095
|
+
case "pgsql":
|
|
3096
|
+
case "sqlite":
|
|
3097
|
+
return "TEXT";
|
|
3098
|
+
case "mysql":
|
|
3099
|
+
return "TEXT";
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
function compileBooleanType(driver) {
|
|
3103
|
+
switch (driver) {
|
|
3104
|
+
case "pgsql":
|
|
3105
|
+
return "BOOLEAN";
|
|
3106
|
+
case "mysql":
|
|
3107
|
+
return "BOOLEAN";
|
|
3108
|
+
case "sqlite":
|
|
3109
|
+
return "INTEGER";
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
function compileIntegerType(driver) {
|
|
3113
|
+
switch (driver) {
|
|
3114
|
+
case "pgsql":
|
|
3115
|
+
return "INTEGER";
|
|
3116
|
+
case "mysql":
|
|
3117
|
+
return "INT";
|
|
3118
|
+
case "sqlite":
|
|
3119
|
+
return "INTEGER";
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
function compileBigIntegerType(driver) {
|
|
3123
|
+
switch (driver) {
|
|
3124
|
+
case "pgsql":
|
|
3125
|
+
return "BIGINT";
|
|
3126
|
+
case "mysql":
|
|
3127
|
+
return "BIGINT";
|
|
3128
|
+
case "sqlite":
|
|
3129
|
+
return "INTEGER";
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
function compileTimestampType(driver) {
|
|
3133
|
+
switch (driver) {
|
|
3134
|
+
case "pgsql":
|
|
3135
|
+
return "TIMESTAMPTZ";
|
|
3136
|
+
case "mysql":
|
|
3137
|
+
return "TIMESTAMP";
|
|
3138
|
+
case "sqlite":
|
|
3139
|
+
return "TEXT";
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
function compileJsonType(driver) {
|
|
3143
|
+
switch (driver) {
|
|
3144
|
+
case "pgsql":
|
|
3145
|
+
return "JSONB";
|
|
3146
|
+
case "mysql":
|
|
3147
|
+
return "JSON";
|
|
3148
|
+
case "sqlite":
|
|
3149
|
+
return "TEXT";
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
function compileJsonbType(driver) {
|
|
3153
|
+
switch (driver) {
|
|
3154
|
+
case "pgsql":
|
|
3155
|
+
return "JSONB";
|
|
3156
|
+
case "mysql":
|
|
3157
|
+
return "JSON";
|
|
3158
|
+
case "sqlite":
|
|
3159
|
+
return "TEXT";
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
// ../../src/core/database/schema/grammars/compileStatements.ts
|
|
3164
|
+
function compileCreateTable(driver, blueprint) {
|
|
3165
|
+
const table = quoteIdentifier(blueprint.table);
|
|
3166
|
+
const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
|
|
3167
|
+
for (const index of blueprint.indexes) {
|
|
3168
|
+
if (index.kind === "unique" && index.columns.length > 1) {
|
|
3169
|
+
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
3170
|
+
parts.push(`UNIQUE (${columns})`);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
|
|
3174
|
+
${parts.join(`,
|
|
3175
|
+
`)}
|
|
3176
|
+
)`];
|
|
3177
|
+
for (const index of blueprint.indexes) {
|
|
3178
|
+
if (index.kind === "unique" && index.columns.length === 1) {
|
|
3179
|
+
continue;
|
|
3180
|
+
}
|
|
3181
|
+
if (index.kind === "index") {
|
|
3182
|
+
statements.push(compileIndex(driver, blueprint.table, index));
|
|
3183
|
+
} else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
|
|
3184
|
+
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
return statements;
|
|
3188
|
+
}
|
|
3189
|
+
function compileAlterTable(driver, blueprint) {
|
|
3190
|
+
const statements = [];
|
|
3191
|
+
const table = quoteIdentifier(blueprint.table);
|
|
3192
|
+
for (const column of blueprint.columns) {
|
|
3193
|
+
const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
|
|
3194
|
+
statements.push(`ALTER TABLE ${table}
|
|
3195
|
+
${addPrefix} ${compileColumn(driver, column, "alter")}`);
|
|
3196
|
+
}
|
|
3197
|
+
for (const columnName of blueprint.droppedColumns) {
|
|
3198
|
+
const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
|
|
3199
|
+
statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
|
|
3200
|
+
}
|
|
3201
|
+
for (const indexName of blueprint.droppedIndexes) {
|
|
3202
|
+
statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
|
|
3203
|
+
}
|
|
3204
|
+
for (const index of blueprint.indexes) {
|
|
3205
|
+
if (index.kind === "index" || index.kind === "unique") {
|
|
3206
|
+
statements.push(compileIndex(driver, blueprint.table, index));
|
|
3207
|
+
} else {
|
|
3208
|
+
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
return statements;
|
|
3212
|
+
}
|
|
3213
|
+
function compileDropTable(driver, tableName) {
|
|
3214
|
+
const cascade = driver === "pgsql" ? " CASCADE" : "";
|
|
3215
|
+
return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
|
|
3216
|
+
}
|
|
3217
|
+
function compileColumn(driver, column, mode) {
|
|
3218
|
+
const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
|
|
3219
|
+
if (column.autoIncrement && driver === "mysql") {
|
|
3220
|
+
parts[1] = `${parts[1]} AUTO_INCREMENT`;
|
|
3221
|
+
}
|
|
3222
|
+
if (column.isPrimary && mode === "create") {
|
|
3223
|
+
if (driver === "sqlite") {
|
|
3224
|
+
parts.push("PRIMARY KEY AUTOINCREMENT");
|
|
3225
|
+
} else {
|
|
3226
|
+
parts.push("PRIMARY KEY");
|
|
3227
|
+
}
|
|
3228
|
+
} else if (!column.isNullable) {
|
|
3229
|
+
parts.push("NOT NULL");
|
|
3230
|
+
} else if (column.isNullable) {
|
|
3231
|
+
parts.push("NULL");
|
|
3232
|
+
}
|
|
3233
|
+
if (column.defaultValue !== undefined) {
|
|
3234
|
+
parts.push(`DEFAULT ${column.defaultValue}`);
|
|
3235
|
+
}
|
|
3236
|
+
if (column.isUnique) {
|
|
3237
|
+
parts.push("UNIQUE");
|
|
3238
|
+
}
|
|
3239
|
+
if (column.checkExpression) {
|
|
3240
|
+
parts.push(`CHECK (${column.checkExpression})`);
|
|
3241
|
+
}
|
|
3242
|
+
if (column.foreignKey) {
|
|
3243
|
+
const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
|
|
3244
|
+
const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
|
|
3245
|
+
let clause = `REFERENCES ${reference}`;
|
|
3246
|
+
if (onDelete === "cascade") {
|
|
3247
|
+
clause += " ON DELETE CASCADE";
|
|
3248
|
+
} else if (onDelete === "set null") {
|
|
3249
|
+
clause += " ON DELETE SET NULL";
|
|
3250
|
+
}
|
|
3251
|
+
parts.push(clause);
|
|
3252
|
+
}
|
|
3253
|
+
return parts.join(" ");
|
|
3254
|
+
}
|
|
3255
|
+
function compileIndex(_driver, tableName, index) {
|
|
3256
|
+
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
|
|
3257
|
+
const columns = index.columns.map((column) => {
|
|
3258
|
+
const quoted = quoteIdentifier(column);
|
|
3259
|
+
if (index.order === "desc") {
|
|
3260
|
+
return `${quoted} DESC`;
|
|
3261
|
+
}
|
|
3262
|
+
return quoted;
|
|
3263
|
+
}).join(", ");
|
|
3264
|
+
const unique = index.kind === "unique" ? "UNIQUE " : "";
|
|
3265
|
+
return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
|
|
3266
|
+
}
|
|
3267
|
+
function compileSpecialIndex(driver, tableName, index) {
|
|
3268
|
+
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
|
|
3269
|
+
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
3270
|
+
switch (index.kind) {
|
|
3271
|
+
case "partial":
|
|
3272
|
+
case "uniquePartial": {
|
|
3273
|
+
if (driver !== "pgsql") {
|
|
3274
|
+
throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
|
|
3275
|
+
}
|
|
3276
|
+
const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
|
|
3277
|
+
return [
|
|
3278
|
+
`CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
|
|
3279
|
+
];
|
|
3280
|
+
}
|
|
3281
|
+
case "gin": {
|
|
3282
|
+
if (driver !== "pgsql") {
|
|
3283
|
+
throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
|
|
3284
|
+
}
|
|
3285
|
+
return [
|
|
3286
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
|
|
3287
|
+
];
|
|
3288
|
+
}
|
|
3289
|
+
case "fullText": {
|
|
3290
|
+
if (driver === "mysql") {
|
|
3291
|
+
return [
|
|
3292
|
+
`CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
|
|
3293
|
+
];
|
|
3294
|
+
}
|
|
3295
|
+
if (driver === "pgsql") {
|
|
3296
|
+
throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
|
|
3297
|
+
}
|
|
3298
|
+
throw new UnsupportedSchemaFeatureError("fullText()", driver);
|
|
3299
|
+
}
|
|
3300
|
+
default:
|
|
3301
|
+
return [];
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
function defaultIndexName(tableName, columns, kind) {
|
|
3305
|
+
return `idx_${tableName}_${columns.join("_")}_${kind}`;
|
|
3306
|
+
}
|
|
3307
|
+
function compileBlueprint(driver, blueprint) {
|
|
3308
|
+
switch (blueprint.action) {
|
|
3309
|
+
case "create":
|
|
3310
|
+
return compileCreateTable(driver, blueprint);
|
|
3311
|
+
case "alter":
|
|
3312
|
+
return compileAlterTable(driver, blueprint);
|
|
3313
|
+
case "drop":
|
|
3314
|
+
return compileDropTable(driver, blueprint.table);
|
|
3315
|
+
default:
|
|
3316
|
+
throw new Error(`Unsupported blueprint action: ${blueprint.action}`);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
// ../../src/core/database/schema/grammars/createGrammar.ts
|
|
3320
|
+
function createGrammar(driver) {
|
|
3321
|
+
return {
|
|
3322
|
+
driver,
|
|
3323
|
+
compile(blueprint) {
|
|
3324
|
+
return compileBlueprint(driver, blueprint);
|
|
3325
|
+
}
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
// ../../src/core/database/schema/grammars/mysqlGrammar.ts
|
|
3330
|
+
var MySqlGrammar = createGrammar("mysql");
|
|
3331
|
+
|
|
3332
|
+
// ../../src/core/database/schema/grammars/postgresGrammar.ts
|
|
3333
|
+
var PostgresGrammar = createGrammar("pgsql");
|
|
3334
|
+
|
|
3335
|
+
// ../../src/core/database/schema/grammars/sqliteGrammar.ts
|
|
3336
|
+
var SqliteGrammar = createGrammar("sqlite");
|
|
3337
|
+
|
|
3338
|
+
// ../../src/core/database/schema/grammars/index.ts
|
|
3339
|
+
function grammarForDriver(driver) {
|
|
3340
|
+
switch (driver) {
|
|
3341
|
+
case "pgsql":
|
|
3342
|
+
return PostgresGrammar;
|
|
3343
|
+
case "mysql":
|
|
3344
|
+
return MySqlGrammar;
|
|
3345
|
+
case "sqlite":
|
|
3346
|
+
return SqliteGrammar;
|
|
3347
|
+
default:
|
|
3348
|
+
throw new Error(`Unsupported database driver: ${driver}`);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
// ../../src/core/database/schema/schema.ts
|
|
3352
|
+
class SchemaBuilder {
|
|
3353
|
+
#driver;
|
|
3354
|
+
#statements = [];
|
|
3355
|
+
constructor(driver) {
|
|
3356
|
+
this.#driver = driver;
|
|
3357
|
+
}
|
|
3358
|
+
create(table, callback) {
|
|
3359
|
+
const blueprint = new Blueprint(table, "create");
|
|
3360
|
+
callback(blueprint);
|
|
3361
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3362
|
+
return this;
|
|
3363
|
+
}
|
|
3364
|
+
table(table, callback) {
|
|
3365
|
+
const blueprint = new Blueprint(table, "alter");
|
|
3366
|
+
callback(blueprint);
|
|
3367
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3368
|
+
return this;
|
|
3369
|
+
}
|
|
3370
|
+
drop(table) {
|
|
3371
|
+
const blueprint = new Blueprint(table, "drop");
|
|
3372
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3373
|
+
return this;
|
|
3374
|
+
}
|
|
3375
|
+
toSql() {
|
|
3376
|
+
return [...this.#statements];
|
|
3377
|
+
}
|
|
3378
|
+
async execute(db2) {
|
|
3379
|
+
for (const statement of this.#statements) {
|
|
3380
|
+
await db2.unsafe(statement);
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
// ../../src/core/database/table.ts
|
|
3385
|
+
function defineTable5(definition) {
|
|
3386
|
+
return definition;
|
|
3387
|
+
}
|
|
3388
|
+
// ../../src/core/queue/failedJobTable.ts
|
|
3389
|
+
var failedJobTable = defineTable5({
|
|
3390
|
+
name: "failed_job",
|
|
3391
|
+
primaryKey: "id",
|
|
3392
|
+
columns: ["id", "job_name", "payload", "exception", "failed_at"],
|
|
3393
|
+
defaultOrderBy: { column: "failed_at", direction: "DESC" }
|
|
3394
|
+
});
|
|
3395
|
+
|
|
3396
|
+
// ../../src/core/queue/failedJobRepository.ts
|
|
3397
|
+
class FailedJobRepository extends baseRepository_default {
|
|
3398
|
+
constructor() {
|
|
3399
|
+
super(failedJobTable);
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
var failedJobRepository_default = FailedJobRepository;
|
|
3403
|
+
|
|
3404
|
+
// ../../src/core/queue/failedJobService.ts
|
|
3405
|
+
class FailedJobService {
|
|
3406
|
+
repository;
|
|
3407
|
+
constructor(repository) {
|
|
3408
|
+
this.repository = repository;
|
|
3409
|
+
}
|
|
3410
|
+
async recordFailure(input) {
|
|
3411
|
+
return await this.repository.create({
|
|
3412
|
+
job_name: input.jobName,
|
|
3413
|
+
payload: input.payload,
|
|
3414
|
+
exception: input.exception,
|
|
3415
|
+
failed_at: new Date
|
|
3416
|
+
});
|
|
3417
|
+
}
|
|
3418
|
+
listRecent(limit = 50) {
|
|
3419
|
+
return this.repository.findAll({
|
|
3420
|
+
limit,
|
|
3421
|
+
orderBy: { column: "failed_at", direction: "DESC" }
|
|
3422
|
+
});
|
|
3423
|
+
}
|
|
3424
|
+
async retry(id) {
|
|
3425
|
+
const failedJob = await this.repository.findByIdOrThrow(id, (jobId) => new Error(`Failed job ${jobId} not found.`));
|
|
3426
|
+
await this.repository.deleteById(id);
|
|
3427
|
+
return failedJob;
|
|
3428
|
+
}
|
|
3429
|
+
async flush() {
|
|
3430
|
+
const jobs = await this.repository.findAll();
|
|
3431
|
+
let deleted = 0;
|
|
3432
|
+
for (const job of jobs) {
|
|
3433
|
+
if (await this.repository.deleteById(job.id)) {
|
|
3434
|
+
deleted += 1;
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
return deleted;
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
var failedJobService_default = FailedJobService;
|
|
3441
|
+
|
|
3442
|
+
// ../../src/core/queue/redisQueue.ts
|
|
3443
|
+
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
3444
|
+
|
|
3445
|
+
// ../../src/core/queue/jobRunner.ts
|
|
3446
|
+
async function runQueueJob(envelope, failedJobs) {
|
|
3447
|
+
const job = jobRegistry.create(envelope.name);
|
|
3448
|
+
if (!job) {
|
|
3449
|
+
throw new Error(`Unknown job "${envelope.name}".`);
|
|
3450
|
+
}
|
|
3451
|
+
const attempts = envelope.attempts ?? 0;
|
|
3452
|
+
try {
|
|
3453
|
+
await job.handle(envelope.payload);
|
|
3454
|
+
} catch (error) {
|
|
3455
|
+
const nextAttempt = attempts + 1;
|
|
3456
|
+
const maxAttempts = job.maxAttempts ?? queueConfig.maxAttempts;
|
|
3457
|
+
if (nextAttempt < maxAttempts) {
|
|
3458
|
+
const backoffMs = job.backoffMs ?? queueConfig.backoffMs;
|
|
3459
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs * nextAttempt));
|
|
3460
|
+
await runQueueJob({
|
|
3461
|
+
...envelope,
|
|
3462
|
+
attempts: nextAttempt
|
|
3463
|
+
}, failedJobs);
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
3466
|
+
await failedJobs.recordFailure({
|
|
3467
|
+
jobName: envelope.name,
|
|
3468
|
+
payload: envelope.payload,
|
|
3469
|
+
exception: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
3470
|
+
});
|
|
3471
|
+
throw error;
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
// ../../src/core/queue/redisQueue.ts
|
|
3476
|
+
var QUEUE_LIST_KEY = "workhub:queue:default";
|
|
3477
|
+
var QUEUE_HIGH_KEY = "workhub:queue:high";
|
|
3478
|
+
var QUEUE_LOW_KEY = "workhub:queue:low";
|
|
3479
|
+
function queueKeyForPriority(priority = "default") {
|
|
3480
|
+
switch (priority) {
|
|
3481
|
+
case "high":
|
|
3482
|
+
return QUEUE_HIGH_KEY;
|
|
3483
|
+
case "low":
|
|
3484
|
+
return QUEUE_LOW_KEY;
|
|
3485
|
+
default:
|
|
3486
|
+
return QUEUE_LIST_KEY;
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
class RedisQueue {
|
|
3490
|
+
client;
|
|
3491
|
+
constructor(redisUrl) {
|
|
3492
|
+
this.client = new RedisClient2(redisUrl);
|
|
3493
|
+
}
|
|
3494
|
+
async dispatch(job, payload) {
|
|
3495
|
+
const name = jobRegistry.resolveName(job);
|
|
3496
|
+
if (!name) {
|
|
3497
|
+
throw new Error("Job is not registered with the queue worker registry.");
|
|
3498
|
+
}
|
|
3499
|
+
const envelope = {
|
|
3500
|
+
name,
|
|
3501
|
+
payload,
|
|
3502
|
+
attempts: 0
|
|
3503
|
+
};
|
|
3504
|
+
const queueKey = queueKeyForPriority(job.priority);
|
|
3505
|
+
await this.client.lpush(queueKey, JSON.stringify(envelope));
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
// ../../src/core/queue/resilientQueue.ts
|
|
3510
|
+
class ResilientQueue {
|
|
3511
|
+
failedJobs;
|
|
3512
|
+
asyncDispatch;
|
|
3513
|
+
constructor(failedJobs, asyncDispatch = false) {
|
|
3514
|
+
this.failedJobs = failedJobs;
|
|
3515
|
+
this.asyncDispatch = asyncDispatch;
|
|
3516
|
+
}
|
|
3517
|
+
async dispatch(job, payload) {
|
|
3518
|
+
const name = jobRegistry.resolveName(job);
|
|
3519
|
+
if (!name) {
|
|
3520
|
+
throw new Error("Job is not registered with the queue worker registry.");
|
|
3521
|
+
}
|
|
3522
|
+
const envelope = {
|
|
3523
|
+
name,
|
|
3524
|
+
payload,
|
|
3525
|
+
attempts: 0
|
|
3526
|
+
};
|
|
3527
|
+
if (this.asyncDispatch) {
|
|
3528
|
+
setTimeout(() => {
|
|
3529
|
+
runQueueJob(envelope, this.failedJobs).catch((error) => {
|
|
3530
|
+
console.error("[ResilientQueue] Job failed:", error);
|
|
3531
|
+
});
|
|
3532
|
+
}, 0);
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
await runQueueJob(envelope, this.failedJobs);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
// ../../src/core/queue/publicQueue.ts
|
|
3540
|
+
function createFailedJobService() {
|
|
3541
|
+
return new failedJobService_default(new failedJobRepository_default);
|
|
3542
|
+
}
|
|
3543
|
+
function createTrackedJob(name, job) {
|
|
3544
|
+
return jobRegistry.track(name, job);
|
|
3545
|
+
}
|
|
3546
|
+
function createProductionQueue(driver, options = {}) {
|
|
3547
|
+
options.registerJobs?.();
|
|
3548
|
+
const failedJobs = options.failedJobs ?? createFailedJobService();
|
|
3549
|
+
if (driver === "redis") {
|
|
3550
|
+
if (!options.redisUrl) {
|
|
3551
|
+
throw new Error('QUEUE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
3552
|
+
}
|
|
3553
|
+
return new RedisQueue(options.redisUrl);
|
|
3554
|
+
}
|
|
3555
|
+
return new ResilientQueue(failedJobs, driver === "async");
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
// ../../src/core/queue/createAppQueue.ts
|
|
3559
|
+
var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
3560
|
+
function registerDefaultJobs() {
|
|
3561
|
+
jobRegistry.register("cache.invalidate-tags", () => {
|
|
3562
|
+
return new invalidateCacheTagsJob_default(resolveApplicationCache());
|
|
3563
|
+
});
|
|
3564
|
+
jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
|
|
3565
|
+
}
|
|
3566
|
+
function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService()) {
|
|
3567
|
+
return createProductionQueue(driver, {
|
|
3568
|
+
redisUrl,
|
|
3569
|
+
failedJobs,
|
|
3570
|
+
registerJobs: registerDefaultJobs
|
|
3571
|
+
});
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
3575
|
+
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
3576
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
|
|
3577
|
+
for (const tableName of discoverModelTableNames()) {
|
|
3578
|
+
for (const action of MODEL_WRITE_ACTIONS) {
|
|
3579
|
+
bus.listen(modelEventName(tableName, action), async () => {
|
|
3580
|
+
const tags = cacheTagsForModelWrite(tableName, action);
|
|
3581
|
+
if (tags.length === 0) {
|
|
3582
|
+
return;
|
|
3583
|
+
}
|
|
3584
|
+
const cache = resolveApplicationCache();
|
|
3585
|
+
const queue = resolveApplicationQueue();
|
|
3586
|
+
const job = createTrackedJob("cache.invalidate-tags", new invalidateCacheTagsJob_default(cache));
|
|
3587
|
+
await queue.dispatch(job, { tags });
|
|
3588
|
+
});
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
// ../../src/bootstrap/providers/listeners.ts
|
|
3594
|
+
var registeredListenerGroups = new Set;
|
|
3595
|
+
function registerListenerGroup(name, register) {
|
|
3596
|
+
if (registeredListenerGroups.has(name)) {
|
|
3597
|
+
return;
|
|
3598
|
+
}
|
|
3599
|
+
registeredListenerGroups.add(name);
|
|
3600
|
+
register();
|
|
3601
|
+
}
|
|
3602
|
+
var listenersProvider = {
|
|
3603
|
+
name: "core.listeners",
|
|
3604
|
+
boot() {
|
|
3605
|
+
registerListenerGroup("cache.invalidate-on-model-write", () => {
|
|
3606
|
+
registerInvalidateCacheOnModelWriteListeners();
|
|
3607
|
+
});
|
|
3608
|
+
for (const [index, registerListener] of discoverListeners().entries()) {
|
|
3609
|
+
registerListenerGroup(`app.listener.${index}`, registerListener);
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
};
|
|
3613
|
+
var listeners_default = listenersProvider;
|
|
3614
|
+
|
|
3615
|
+
// ../../src/core/auth/policy.ts
|
|
3616
|
+
var BLOCKED_POLICY_ACTIONS = new Set([
|
|
3617
|
+
"constructor",
|
|
3618
|
+
"toString",
|
|
3619
|
+
"valueOf",
|
|
3620
|
+
"hasOwnProperty",
|
|
3621
|
+
"isPrototypeOf",
|
|
3622
|
+
"propertyIsEnumerable",
|
|
3623
|
+
"__proto__"
|
|
3624
|
+
]);
|
|
3625
|
+
|
|
3626
|
+
class PolicyGate {
|
|
3627
|
+
constructor() {}
|
|
3628
|
+
policies = new Map;
|
|
3629
|
+
register(resource, policy) {
|
|
3630
|
+
this.policies.set(resource, policy);
|
|
3631
|
+
}
|
|
3632
|
+
allows(resource, action, user, model) {
|
|
3633
|
+
const policy = this.policies.get(resource);
|
|
3634
|
+
if (!policy) {
|
|
3635
|
+
return false;
|
|
3636
|
+
}
|
|
3637
|
+
if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
|
|
3638
|
+
return false;
|
|
3639
|
+
}
|
|
3640
|
+
const handler = policy[action];
|
|
3641
|
+
if (typeof handler !== "function") {
|
|
3642
|
+
return false;
|
|
3643
|
+
}
|
|
3644
|
+
const resolvedUser = user === undefined ? currentAuthUser() : user;
|
|
3645
|
+
return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
|
|
3646
|
+
}
|
|
3647
|
+
authorize(resource, action, user, model) {
|
|
3648
|
+
if (!this.allows(resource, action, user, model)) {
|
|
3649
|
+
throw new ForbiddenError;
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
// ../../src/bootstrap/providers/policy.ts
|
|
3655
|
+
var policyProvider = {
|
|
3656
|
+
name: "core.policy",
|
|
3657
|
+
register({ container }) {
|
|
3658
|
+
container.set(CORE_POLICY_GATE_TOKEN, new PolicyGate);
|
|
3659
|
+
}
|
|
3660
|
+
};
|
|
3661
|
+
var policy_default = policyProvider;
|
|
3662
|
+
|
|
3663
|
+
// ../../src/bootstrap/providers/queue.ts
|
|
3664
|
+
var queueProvider = {
|
|
3665
|
+
name: "core.queue",
|
|
3666
|
+
register({ container, config }) {
|
|
3667
|
+
const configuredDriver = process.env.QUEUE_DRIVER ?? DEFAULT_QUEUE_DRIVER;
|
|
3668
|
+
const driver = configuredDriver === "async" || configuredDriver === "redis" || configuredDriver === "sync" ? configuredDriver : DEFAULT_QUEUE_DRIVER;
|
|
3669
|
+
config.set("queue.driver", driver);
|
|
3670
|
+
const failedJobs = createFailedJobService();
|
|
3671
|
+
container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
|
|
3672
|
+
container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs));
|
|
3673
|
+
}
|
|
3674
|
+
};
|
|
3675
|
+
var queue_default = queueProvider;
|
|
3676
|
+
|
|
3677
|
+
// ../../src/config/frontend.ts
|
|
3678
|
+
function readFrontendMode() {
|
|
3679
|
+
const mode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
3680
|
+
if (mode === "server-htmx") {
|
|
3681
|
+
return "server-htmx";
|
|
3682
|
+
}
|
|
3683
|
+
if (mode === "spa-react") {
|
|
3684
|
+
return "spa-react";
|
|
3685
|
+
}
|
|
3686
|
+
return "api";
|
|
3687
|
+
}
|
|
3688
|
+
function isViewsEnabled() {
|
|
3689
|
+
return readFrontendMode() === "server-htmx";
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
// ../../src/core/http/requestMetaContext.ts
|
|
3693
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
3694
|
+
var requestMetaContext = new AsyncLocalStorage3;
|
|
3695
|
+
function runWithRequestMeta(meta, callback) {
|
|
3696
|
+
return requestMetaContext.run(meta, callback);
|
|
3697
|
+
}
|
|
3698
|
+
function currentRequestMeta() {
|
|
3699
|
+
return requestMetaContext.getStore() ?? {
|
|
3700
|
+
ipAddress: null,
|
|
3701
|
+
userAgent: null
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
// ../../src/core/view/etaViewEngine.ts
|
|
3706
|
+
import { join as join3 } from "path";
|
|
3707
|
+
import { Eta } from "eta";
|
|
3708
|
+
var DEFAULT_VIEWS_DIRECTORY = join3(process.cwd(), "resources/views");
|
|
3709
|
+
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
3710
|
+
|
|
3711
|
+
class EtaViewEngine {
|
|
3712
|
+
eta;
|
|
3713
|
+
resolveLayoutData;
|
|
3714
|
+
constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
|
|
3715
|
+
this.eta = new Eta({
|
|
3716
|
+
views: viewsDirectory,
|
|
3717
|
+
autoTrim: false
|
|
3718
|
+
});
|
|
3719
|
+
this.resolveLayoutData = resolveLayoutData;
|
|
3720
|
+
}
|
|
3721
|
+
async render(name, data = {}, options = {}) {
|
|
3722
|
+
const template = name.endsWith(".eta") ? name : `${name}.eta`;
|
|
3723
|
+
const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
|
|
3724
|
+
const mergedData = { ...layoutData, ...data };
|
|
3725
|
+
const body = await this.eta.renderAsync(template, mergedData);
|
|
3726
|
+
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
3727
|
+
if (layout === false) {
|
|
3728
|
+
return body;
|
|
3729
|
+
}
|
|
3730
|
+
const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
|
|
3731
|
+
return await this.eta.renderAsync(layoutTemplate, {
|
|
3732
|
+
...mergedData,
|
|
3733
|
+
body
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
// ../../src/core/view/htmlResponse.ts
|
|
3738
|
+
function htmlResponse(html, init = {}) {
|
|
3739
|
+
return new Response(html, {
|
|
3740
|
+
status: init.status ?? 200,
|
|
3741
|
+
statusText: init.statusText,
|
|
3742
|
+
headers: {
|
|
3743
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
3744
|
+
}
|
|
3745
|
+
});
|
|
3746
|
+
}
|
|
3747
|
+
// ../../src/core/http/csrfToken.ts
|
|
3748
|
+
import { createHmac as createHmac3, randomBytes, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
3749
|
+
var CSRF_COOKIE = "workhub_csrf";
|
|
3750
|
+
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
3751
|
+
function resolveCsrfSecret() {
|
|
3752
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
|
|
3753
|
+
}
|
|
3754
|
+
function signCsrfToken(token, issuedAt) {
|
|
3755
|
+
const payload = `${token}.${issuedAt}`;
|
|
3756
|
+
const signature = createHmac3("sha256", resolveCsrfSecret()).update(payload).digest("hex");
|
|
3757
|
+
return `${payload}.${signature}`;
|
|
3758
|
+
}
|
|
3759
|
+
function readCsrfCookie(request) {
|
|
3760
|
+
const cookieHeader = request.headers.get("cookie");
|
|
3761
|
+
if (!cookieHeader) {
|
|
3762
|
+
return null;
|
|
3763
|
+
}
|
|
3764
|
+
for (const part of cookieHeader.split(";")) {
|
|
3765
|
+
const [name, ...rest] = part.trim().split("=");
|
|
3766
|
+
if (name === CSRF_COOKIE) {
|
|
3767
|
+
return decodeURIComponent(rest.join("="));
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
return null;
|
|
3771
|
+
}
|
|
3772
|
+
function parseSignedCsrfValue(cookieValue) {
|
|
3773
|
+
const parts = cookieValue.split(".");
|
|
3774
|
+
if (parts.length !== 3) {
|
|
3775
|
+
return null;
|
|
3776
|
+
}
|
|
3777
|
+
const [token, issuedAtRaw, cookieSignature] = parts;
|
|
3778
|
+
if (!token || !issuedAtRaw || !cookieSignature) {
|
|
3779
|
+
return null;
|
|
3780
|
+
}
|
|
3781
|
+
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
3782
|
+
if (!Number.isFinite(issuedAt)) {
|
|
3783
|
+
return null;
|
|
3784
|
+
}
|
|
3785
|
+
if (Date.now() - issuedAt > CSRF_TTL_MS) {
|
|
3786
|
+
return null;
|
|
3787
|
+
}
|
|
3788
|
+
const expectedSignature = signCsrfToken(token, issuedAt).split(".").pop();
|
|
3789
|
+
if (!expectedSignature) {
|
|
3790
|
+
return null;
|
|
3791
|
+
}
|
|
3792
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
3793
|
+
const actualBuffer = Buffer.from(cookieSignature);
|
|
3794
|
+
if (expectedBuffer.length !== actualBuffer.length) {
|
|
3795
|
+
return null;
|
|
3796
|
+
}
|
|
3797
|
+
if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
|
|
3798
|
+
return null;
|
|
3799
|
+
}
|
|
3800
|
+
return { token, issuedAt };
|
|
3801
|
+
}
|
|
3802
|
+
function createCsrfTokenCookie() {
|
|
3803
|
+
const token = randomBytes(24).toString("hex");
|
|
3804
|
+
const issuedAt = Date.now();
|
|
3805
|
+
const value = signCsrfToken(token, issuedAt);
|
|
3806
|
+
return {
|
|
3807
|
+
token,
|
|
3808
|
+
cookie: `${CSRF_COOKIE}=${encodeURIComponent(value)}; Path=/; SameSite=Lax; Max-Age=3600`
|
|
3809
|
+
};
|
|
3810
|
+
}
|
|
3811
|
+
function resolveCsrfToken(request) {
|
|
3812
|
+
const cookieValue = readCsrfCookie(request);
|
|
3813
|
+
if (cookieValue) {
|
|
3814
|
+
const parsed = parseSignedCsrfValue(cookieValue);
|
|
3815
|
+
if (parsed) {
|
|
3816
|
+
return { token: parsed.token };
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
return createCsrfTokenCookie();
|
|
3820
|
+
}
|
|
3821
|
+
function readSubmittedCsrfToken(request) {
|
|
3822
|
+
const headerToken = request.headers.get("x-csrf-token")?.trim();
|
|
3823
|
+
if (headerToken) {
|
|
3824
|
+
return headerToken;
|
|
3825
|
+
}
|
|
3826
|
+
return null;
|
|
3827
|
+
}
|
|
3828
|
+
async function readSubmittedCsrfTokenFromBody(request) {
|
|
3829
|
+
const headerToken = readSubmittedCsrfToken(request);
|
|
3830
|
+
if (headerToken) {
|
|
3831
|
+
return headerToken;
|
|
3832
|
+
}
|
|
3833
|
+
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
3834
|
+
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
3835
|
+
const formData = await request.clone().formData();
|
|
3836
|
+
const field = formData.get("_token");
|
|
3837
|
+
if (typeof field === "string" && field.trim().length > 0) {
|
|
3838
|
+
return field.trim();
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
return null;
|
|
3842
|
+
}
|
|
3843
|
+
function verifyCsrfToken(request, submittedToken) {
|
|
3844
|
+
if (!submittedToken) {
|
|
3845
|
+
return false;
|
|
3846
|
+
}
|
|
3847
|
+
const cookieValue = readCsrfCookie(request);
|
|
3848
|
+
if (!cookieValue) {
|
|
3849
|
+
return false;
|
|
3850
|
+
}
|
|
3851
|
+
const parsed = parseSignedCsrfValue(cookieValue);
|
|
3852
|
+
if (!parsed) {
|
|
3853
|
+
return false;
|
|
3854
|
+
}
|
|
3855
|
+
const submittedBuffer = Buffer.from(submittedToken);
|
|
3856
|
+
const expectedBuffer = Buffer.from(parsed.token);
|
|
3857
|
+
if (submittedBuffer.length !== expectedBuffer.length) {
|
|
3858
|
+
return false;
|
|
3859
|
+
}
|
|
3860
|
+
return timingSafeEqual2(submittedBuffer, expectedBuffer);
|
|
3861
|
+
}
|
|
3862
|
+
function resolveCsrfTokenForRequest(request) {
|
|
3863
|
+
const metaToken = currentRequestMeta().csrfToken;
|
|
3864
|
+
if (metaToken) {
|
|
3865
|
+
return metaToken;
|
|
3866
|
+
}
|
|
3867
|
+
return resolveCsrfToken(request).token;
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
// ../../src/core/http/flashSession.ts
|
|
3871
|
+
import { createHmac as createHmac4, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
3872
|
+
var FLASH_COOKIE = "workhub_flash";
|
|
3873
|
+
var FLASH_TTL_MS = 60 * 1000;
|
|
3874
|
+
function resolveFlashSecret() {
|
|
3875
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
3876
|
+
}
|
|
3877
|
+
function signFlashPayload(payload, issuedAt) {
|
|
3878
|
+
const signature = createHmac4("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
3879
|
+
return `${payload}.${issuedAt}.${signature}`;
|
|
3880
|
+
}
|
|
3881
|
+
function readFlashCookie(request) {
|
|
3882
|
+
const cookieHeader = request.headers.get("cookie");
|
|
3883
|
+
if (!cookieHeader) {
|
|
3884
|
+
return null;
|
|
3885
|
+
}
|
|
3886
|
+
for (const part of cookieHeader.split(";")) {
|
|
3887
|
+
const [name, ...rest] = part.trim().split("=");
|
|
3888
|
+
if (name === FLASH_COOKIE) {
|
|
3889
|
+
return decodeURIComponent(rest.join("="));
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
return null;
|
|
3893
|
+
}
|
|
3894
|
+
function parseFlashCookie(cookieValue) {
|
|
3895
|
+
const parts = cookieValue.split(".");
|
|
3896
|
+
if (parts.length < 3) {
|
|
3897
|
+
return null;
|
|
3898
|
+
}
|
|
3899
|
+
const signature = parts.pop();
|
|
3900
|
+
const issuedAtRaw = parts.pop();
|
|
3901
|
+
const payload = parts.join(".");
|
|
3902
|
+
if (!signature || !issuedAtRaw || !payload) {
|
|
3903
|
+
return null;
|
|
3904
|
+
}
|
|
3905
|
+
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
3906
|
+
if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
|
|
3907
|
+
return null;
|
|
3908
|
+
}
|
|
3909
|
+
const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
|
|
3910
|
+
if (!expectedSignature) {
|
|
3911
|
+
return null;
|
|
3912
|
+
}
|
|
3913
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
3914
|
+
const actualBuffer = Buffer.from(signature);
|
|
3915
|
+
if (expectedBuffer.length !== actualBuffer.length) {
|
|
3916
|
+
return null;
|
|
3917
|
+
}
|
|
3918
|
+
if (!timingSafeEqual3(expectedBuffer, actualBuffer)) {
|
|
3919
|
+
return null;
|
|
3920
|
+
}
|
|
3921
|
+
try {
|
|
3922
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
3923
|
+
if (!parsed?.message || typeof parsed.message !== "string") {
|
|
3924
|
+
return null;
|
|
3925
|
+
}
|
|
3926
|
+
if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
|
|
3927
|
+
return null;
|
|
3928
|
+
}
|
|
3929
|
+
return parsed;
|
|
3930
|
+
} catch {
|
|
3931
|
+
return null;
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
function clearFlashCookie() {
|
|
3935
|
+
return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
3936
|
+
}
|
|
3937
|
+
function pullFlash(request) {
|
|
3938
|
+
const cookieValue = readFlashCookie(request);
|
|
3939
|
+
if (!cookieValue) {
|
|
3940
|
+
return null;
|
|
3941
|
+
}
|
|
3942
|
+
return parseFlashCookie(cookieValue);
|
|
3943
|
+
}
|
|
3944
|
+
function withFlashClear(response) {
|
|
3945
|
+
const headers = new Headers(response.headers);
|
|
3946
|
+
headers.append("set-cookie", clearFlashCookie());
|
|
3947
|
+
return new Response(response.body, {
|
|
3948
|
+
status: response.status,
|
|
3949
|
+
statusText: response.statusText,
|
|
3950
|
+
headers
|
|
3951
|
+
});
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
// ../../src/core/view/webLayoutData.ts
|
|
3955
|
+
async function resolveWebLayoutData(container, request) {
|
|
3956
|
+
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
3957
|
+
const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
|
|
3958
|
+
const authUser = currentAuthUser();
|
|
3959
|
+
if (!authUser) {
|
|
3960
|
+
return { authUser: null, csrfToken, flash };
|
|
3961
|
+
}
|
|
3962
|
+
const userId = Number(authUser.id);
|
|
3963
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
3964
|
+
return { authUser: null, csrfToken, flash };
|
|
3965
|
+
}
|
|
3966
|
+
if (!container.has(tokenServiceToken)) {
|
|
3967
|
+
return {
|
|
3968
|
+
authUser: {
|
|
3969
|
+
id: userId,
|
|
3970
|
+
email: "",
|
|
3971
|
+
role: authUser.role ?? "member"
|
|
3972
|
+
},
|
|
3973
|
+
csrfToken,
|
|
3974
|
+
flash
|
|
3975
|
+
};
|
|
3976
|
+
}
|
|
3977
|
+
const tokenService = container.resolve(tokenServiceToken);
|
|
3978
|
+
try {
|
|
3979
|
+
const user = await tokenService.findByIdOrThrow(userId);
|
|
3980
|
+
return {
|
|
3981
|
+
authUser: {
|
|
3982
|
+
id: userId,
|
|
3983
|
+
email: user.email ?? "",
|
|
3984
|
+
role: authUser.role ?? user.role ?? "member"
|
|
3985
|
+
},
|
|
3986
|
+
csrfToken,
|
|
3987
|
+
flash
|
|
3988
|
+
};
|
|
3989
|
+
} catch {
|
|
3990
|
+
return { authUser: null, csrfToken, flash };
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
// ../../src/bootstrap/providers/view.ts
|
|
3994
|
+
var CORE_VIEW_TOKEN = "core.view";
|
|
3995
|
+
var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
|
|
3996
|
+
var viewProvider = {
|
|
3997
|
+
name: "view",
|
|
3998
|
+
register({ container, config }) {
|
|
3999
|
+
if (!isViewsEnabled()) {
|
|
4000
|
+
return;
|
|
4001
|
+
}
|
|
4002
|
+
const viewsDirectory = process.env.VIEW_DIRECTORY?.trim() || DEFAULT_VIEWS_DIRECTORY;
|
|
4003
|
+
config.set(VIEW_DIRECTORY_CONFIG_KEY, viewsDirectory);
|
|
4004
|
+
container.set(CORE_VIEW_TOKEN, new EtaViewEngine(viewsDirectory, () => resolveWebLayoutData(container, currentRequestMeta().request)));
|
|
4005
|
+
}
|
|
4006
|
+
};
|
|
4007
|
+
|
|
4008
|
+
// ../../src/bootstrap/providers/index.ts
|
|
4009
|
+
var coreProviders = [
|
|
4010
|
+
config_default,
|
|
4011
|
+
cache_default,
|
|
4012
|
+
auth_default,
|
|
4013
|
+
events_default,
|
|
4014
|
+
policy_default,
|
|
4015
|
+
queue_default,
|
|
4016
|
+
listeners_default,
|
|
4017
|
+
viewProvider
|
|
4018
|
+
];
|
|
4019
|
+
|
|
4020
|
+
// ../../src/domain/auth.ts
|
|
4021
|
+
var TEST_ADMIN_API_TOKEN = "workhub-admin-test-token";
|
|
4022
|
+
var TEST_MEMBER_API_TOKEN = "workhub-member-test-token";
|
|
4023
|
+
|
|
4024
|
+
// ../../src/domain/scim.ts
|
|
4025
|
+
var TEST_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
|
|
4026
|
+
var DEFAULT_SCIM_BEARER_TOKEN = TEST_SCIM_BEARER_TOKEN;
|
|
4027
|
+
|
|
4028
|
+
// ../../src/bootstrap/secretsGuard.ts
|
|
4029
|
+
var DEFAULT_TOKENS = new Set([TEST_ADMIN_API_TOKEN, TEST_MEMBER_API_TOKEN]);
|
|
4030
|
+
var DEFAULT_SCIM_TOKENS = new Set([TEST_SCIM_BEARER_TOKEN, DEFAULT_SCIM_BEARER_TOKEN]);
|
|
4031
|
+
function assertProductionSecrets(env = process.env) {
|
|
4032
|
+
const appEnv = env.APP_ENV ?? appConfig.env;
|
|
4033
|
+
if (appEnv !== "production") {
|
|
4034
|
+
return;
|
|
4035
|
+
}
|
|
4036
|
+
const adminToken = env.ADMIN_API_TOKEN ?? TEST_ADMIN_API_TOKEN;
|
|
4037
|
+
const memberToken = env.MEMBER_API_TOKEN ?? TEST_MEMBER_API_TOKEN;
|
|
4038
|
+
const scimToken = env.SCIM_BEARER_TOKEN ?? DEFAULT_SCIM_BEARER_TOKEN;
|
|
4039
|
+
const encryptionEnabled = env.FEATURE_FIELD_ENCRYPTION !== "false";
|
|
4040
|
+
const devHeadersEnabled = (env.AUTH_DEV_HEADERS ?? "true") !== "false";
|
|
4041
|
+
if (devHeadersEnabled) {
|
|
4042
|
+
throw new Error("Production startup blocked: set AUTH_DEV_HEADERS=false to disable development auth headers.");
|
|
4043
|
+
}
|
|
4044
|
+
if (DEFAULT_TOKENS.has(adminToken) || DEFAULT_TOKENS.has(memberToken)) {
|
|
4045
|
+
throw new Error("Production startup blocked: rotate ADMIN_API_TOKEN and MEMBER_API_TOKEN away from default WorkHub test values.");
|
|
4046
|
+
}
|
|
4047
|
+
if (DEFAULT_SCIM_TOKENS.has(scimToken)) {
|
|
4048
|
+
throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from default WorkHub test values.");
|
|
4049
|
+
}
|
|
4050
|
+
if (encryptionEnabled && !env.KMS_ENCRYPTION_KEY?.trim()) {
|
|
4051
|
+
throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
|
|
4052
|
+
}
|
|
4053
|
+
if (!env.SIEM_EXPORT_URL?.trim() && isFeatureEnabled("siemExport")) {
|
|
4054
|
+
console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
|
|
4055
|
+
}
|
|
4056
|
+
const billingEnabled = (env.FEATURE_BILLING ?? "true") !== "false";
|
|
4057
|
+
if (billingEnabled && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
|
|
4058
|
+
throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
|
|
4059
|
+
}
|
|
4060
|
+
const corsOrigins = (env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim());
|
|
4061
|
+
if (corsOrigins.includes("*")) {
|
|
4062
|
+
throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
|
|
4063
|
+
}
|
|
4064
|
+
if ((env.FEATURE_PUBLIC_READS ?? "true") !== "false") {
|
|
4065
|
+
throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
|
|
4066
|
+
}
|
|
4067
|
+
if (!env.OAUTH_STATE_SECRET?.trim()) {
|
|
4068
|
+
throw new Error("Production startup blocked: set OAUTH_STATE_SECRET for OAuth CSRF protection.");
|
|
4069
|
+
}
|
|
4070
|
+
if (!env.TOKEN_HASH_PEPPER?.trim()) {
|
|
4071
|
+
throw new Error("Production startup blocked: set TOKEN_HASH_PEPPER for API token hashing.");
|
|
4072
|
+
}
|
|
4073
|
+
if (!env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim()) {
|
|
4074
|
+
throw new Error("Production startup blocked: set API_TOKEN_DEFAULT_EXPIRY_DAYS to enforce token rotation.");
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
// ../../src/bootstrap/context.ts
|
|
4079
|
+
function collectProviders(modules = appModules) {
|
|
4080
|
+
return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
|
|
4081
|
+
}
|
|
4082
|
+
function runProviderPhase(providers, phase, context) {
|
|
4083
|
+
for (const provider of providers) {
|
|
4084
|
+
provider[phase]?.(context);
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
function createAppContext() {
|
|
4088
|
+
assertProductionSecrets();
|
|
4089
|
+
const container = new ServiceContainer;
|
|
4090
|
+
const config = new ConfigStore;
|
|
4091
|
+
const dependencies = {
|
|
4092
|
+
container
|
|
4093
|
+
};
|
|
4094
|
+
const context = {
|
|
4095
|
+
container,
|
|
4096
|
+
config,
|
|
4097
|
+
dependencies
|
|
4098
|
+
};
|
|
4099
|
+
const providers = collectProviders();
|
|
4100
|
+
runProviderPhase(providers, "register", context);
|
|
4101
|
+
runProviderPhase(providers, "boot", context);
|
|
4102
|
+
assertAppDependenciesComplete(dependencies);
|
|
4103
|
+
const appContext = {
|
|
4104
|
+
container,
|
|
4105
|
+
config,
|
|
4106
|
+
dependencies
|
|
4107
|
+
};
|
|
4108
|
+
setActiveApplicationContext(appContext);
|
|
4109
|
+
return appContext;
|
|
4110
|
+
}
|
|
4111
|
+
// ../../src/bootstrap/createWebRoutes.ts
|
|
4112
|
+
import { join as join4 } from "path";
|
|
4113
|
+
|
|
4114
|
+
// ../../src/core/http/middleware.ts
|
|
4115
|
+
function isRouteHandler(value) {
|
|
4116
|
+
return typeof value === "function";
|
|
4117
|
+
}
|
|
4118
|
+
function isMethodRouteMap(value) {
|
|
4119
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
4120
|
+
return false;
|
|
4121
|
+
}
|
|
4122
|
+
const entries = Object.entries(value);
|
|
4123
|
+
return entries.length > 0 && entries.every(([, handler]) => isRouteHandler(handler));
|
|
4124
|
+
}
|
|
4125
|
+
function composeMiddleware(...middleware) {
|
|
4126
|
+
return (handler) => {
|
|
4127
|
+
return async (request) => {
|
|
4128
|
+
let index = 0;
|
|
4129
|
+
const dispatch = async () => {
|
|
4130
|
+
if (index >= middleware.length) {
|
|
4131
|
+
return await handler(request);
|
|
4132
|
+
}
|
|
4133
|
+
const current = middleware[index];
|
|
4134
|
+
index += 1;
|
|
4135
|
+
if (!current) {
|
|
4136
|
+
return await handler(request);
|
|
4137
|
+
}
|
|
4138
|
+
return await current(request, dispatch);
|
|
4139
|
+
};
|
|
4140
|
+
return await dispatch();
|
|
4141
|
+
};
|
|
4142
|
+
};
|
|
4143
|
+
}
|
|
4144
|
+
async function requestIdMiddleware(request, next) {
|
|
4145
|
+
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
4146
|
+
const response = await next();
|
|
4147
|
+
const headers = new Headers(response.headers);
|
|
4148
|
+
headers.set("x-request-id", requestId);
|
|
4149
|
+
return new Response(response.body, {
|
|
4150
|
+
status: response.status,
|
|
4151
|
+
statusText: response.statusText,
|
|
4152
|
+
headers
|
|
4153
|
+
});
|
|
4154
|
+
}
|
|
4155
|
+
function wrapRouteHandler(handler, middleware) {
|
|
4156
|
+
if (isMethodRouteMap(handler)) {
|
|
4157
|
+
const wrapped = {};
|
|
4158
|
+
for (const [method, routeHandler] of Object.entries(handler)) {
|
|
4159
|
+
wrapped[method] = composeMiddleware(...middleware)(routeHandler);
|
|
4160
|
+
}
|
|
4161
|
+
return wrapped;
|
|
153
4162
|
}
|
|
154
|
-
if (
|
|
155
|
-
return
|
|
4163
|
+
if (isRouteHandler(handler)) {
|
|
4164
|
+
return composeMiddleware(...middleware)(handler);
|
|
156
4165
|
}
|
|
157
|
-
return
|
|
4166
|
+
return handler;
|
|
158
4167
|
}
|
|
159
|
-
function
|
|
160
|
-
|
|
4168
|
+
function applyMiddlewareToRoutes(routes, middleware) {
|
|
4169
|
+
const wrapped = {};
|
|
4170
|
+
for (const [path, routeHandler] of Object.entries(routes)) {
|
|
4171
|
+
wrapped[path] = wrapRouteHandler(routeHandler, middleware);
|
|
4172
|
+
}
|
|
4173
|
+
return wrapped;
|
|
161
4174
|
}
|
|
162
4175
|
|
|
163
4176
|
// ../../src/config/rateLimit.ts
|
|
@@ -186,79 +4199,16 @@ function resolveLoginRateLimit() {
|
|
|
186
4199
|
decaySeconds: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, defaults.decaySeconds)
|
|
187
4200
|
};
|
|
188
4201
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
|
|
196
|
-
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
197
|
-
}
|
|
198
|
-
var databaseConfig = {
|
|
199
|
-
url: process.env.DATABASE_URL ?? "",
|
|
200
|
-
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
201
|
-
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
202
|
-
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
203
|
-
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
// ../../src/core/database/connectionContext.ts
|
|
207
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
208
|
-
var activeConnection = new AsyncLocalStorage;
|
|
209
|
-
function runWithDatabaseConnection(connection, callback) {
|
|
210
|
-
return activeConnection.run(connection, callback);
|
|
211
|
-
}
|
|
212
|
-
function getActiveDatabaseConnection(fallback) {
|
|
213
|
-
return activeConnection.getStore() ?? fallback;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ../../src/db/connection/createConnection.ts
|
|
217
|
-
var {SQL } = globalThis.Bun;
|
|
218
|
-
function createDatabaseConnection(config) {
|
|
219
|
-
if (!config.url) {
|
|
220
|
-
throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
|
|
221
|
-
}
|
|
222
|
-
return new SQL({
|
|
223
|
-
url: config.url,
|
|
224
|
-
max: config.poolMax,
|
|
225
|
-
idleTimeout: config.idleTimeoutSeconds,
|
|
226
|
-
maxLifetime: config.maxLifetimeSeconds,
|
|
227
|
-
connectionTimeout: config.connectionTimeoutSeconds
|
|
228
|
-
});
|
|
4202
|
+
function resolveRegisterRateLimit() {
|
|
4203
|
+
const defaults = isLocalAppEnv() ? { maxAttempts: 100, decaySeconds: 60 } : { maxAttempts: 10, decaySeconds: 60 };
|
|
4204
|
+
return {
|
|
4205
|
+
maxAttempts: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
|
|
4206
|
+
decaySeconds: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_WINDOW_SECONDS ?? process.env.REGISTER_RATE_LIMIT_WINDOW_MS ? String(Number(process.env.REGISTER_RATE_LIMIT_WINDOW_MS) / 1000) : undefined, defaults.decaySeconds)
|
|
4207
|
+
};
|
|
229
4208
|
}
|
|
230
4209
|
|
|
231
|
-
// ../../src/
|
|
232
|
-
|
|
233
|
-
connection: null
|
|
234
|
-
};
|
|
235
|
-
function getDatabase() {
|
|
236
|
-
if (!connectionHolder.connection) {
|
|
237
|
-
connectionHolder.connection = createDatabaseConnection(databaseConfig);
|
|
238
|
-
}
|
|
239
|
-
return connectionHolder.connection;
|
|
240
|
-
}
|
|
241
|
-
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
242
|
-
function resolveDatabase() {
|
|
243
|
-
return getActiveDatabaseConnection(getDatabase());
|
|
244
|
-
}
|
|
245
|
-
function resolveDatabaseForProperty(property) {
|
|
246
|
-
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
247
|
-
return getDatabase();
|
|
248
|
-
}
|
|
249
|
-
return resolveDatabase();
|
|
250
|
-
}
|
|
251
|
-
var db = new Proxy(function database() {}, {
|
|
252
|
-
apply(_target, _thisArg, args) {
|
|
253
|
-
return resolveDatabase()(...args);
|
|
254
|
-
},
|
|
255
|
-
get(_target, property) {
|
|
256
|
-
const connection = resolveDatabaseForProperty(property);
|
|
257
|
-
const value = connection[property];
|
|
258
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
var connection_default = db;
|
|
4210
|
+
// ../../src/core/auth/membershipContext.ts
|
|
4211
|
+
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
262
4212
|
|
|
263
4213
|
// ../../src/modules/organization/memberRepository.ts
|
|
264
4214
|
class OrganizationMemberRepository {
|
|
@@ -311,45 +4261,6 @@ class OrganizationMemberRepository {
|
|
|
311
4261
|
}
|
|
312
4262
|
var memberRepository_default = OrganizationMemberRepository;
|
|
313
4263
|
|
|
314
|
-
// ../../src/core/errors/http.ts
|
|
315
|
-
class HttpError extends Error {
|
|
316
|
-
status;
|
|
317
|
-
details;
|
|
318
|
-
constructor(status, message, details) {
|
|
319
|
-
super(message);
|
|
320
|
-
this.name = new.target.name;
|
|
321
|
-
this.status = status;
|
|
322
|
-
this.details = details;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
class ForbiddenError extends HttpError {
|
|
326
|
-
constructor(message = "Forbidden", details) {
|
|
327
|
-
super(403, message, details);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
class UnauthorizedError extends HttpError {
|
|
332
|
-
constructor(message = "Unauthorized", details) {
|
|
333
|
-
super(401, message, details);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
class PayloadTooLargeError extends HttpError {
|
|
338
|
-
constructor(message = "Payload Too Large", details) {
|
|
339
|
-
super(413, message, details);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// ../../src/core/auth/authContext.ts
|
|
344
|
-
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
345
|
-
var authContext = new AsyncLocalStorage2;
|
|
346
|
-
function runWithAuthUser(user, callback) {
|
|
347
|
-
return authContext.run(user, callback);
|
|
348
|
-
}
|
|
349
|
-
function currentAuthUser() {
|
|
350
|
-
return authContext.getStore() ?? null;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
4264
|
// ../../src/core/auth/accessControl.ts
|
|
354
4265
|
function isGlobalAdmin(user) {
|
|
355
4266
|
return user?.role === "admin";
|
|
@@ -363,7 +4274,7 @@ function resolveUserId(user) {
|
|
|
363
4274
|
}
|
|
364
4275
|
|
|
365
4276
|
// ../../src/core/auth/membershipContext.ts
|
|
366
|
-
var membershipContext = new
|
|
4277
|
+
var membershipContext = new AsyncLocalStorage4;
|
|
367
4278
|
var membershipRepository = new memberRepository_default;
|
|
368
4279
|
async function runWithMembershipContext(callback) {
|
|
369
4280
|
const user = currentAuthUser();
|
|
@@ -462,173 +4373,42 @@ var corsConfig = {
|
|
|
462
4373
|
"X-Authenticated-User-Role",
|
|
463
4374
|
"If-Match",
|
|
464
4375
|
"If-None-Match"
|
|
465
|
-
],
|
|
466
|
-
maxAgeSeconds: 86400
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
// ../../src/core/http/corsMiddleware.ts
|
|
470
|
-
function createCorsMiddleware() {
|
|
471
|
-
return async (request, next) => {
|
|
472
|
-
if (request.method === "OPTIONS") {
|
|
473
|
-
return new Response(null, {
|
|
474
|
-
status: 204,
|
|
475
|
-
headers: buildCorsHeaders(request)
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
const response = await next();
|
|
479
|
-
const headers = new Headers(response.headers);
|
|
480
|
-
for (const [key, value] of buildCorsHeaders(request)) {
|
|
481
|
-
headers.set(key, value);
|
|
482
|
-
}
|
|
483
|
-
return new Response(response.body, {
|
|
484
|
-
status: response.status,
|
|
485
|
-
statusText: response.statusText,
|
|
486
|
-
headers
|
|
487
|
-
});
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
function buildCorsHeaders(request) {
|
|
491
|
-
const headers = new Headers;
|
|
492
|
-
const origin = request.headers.get("origin");
|
|
493
|
-
const allowedOrigins = corsConfig.allowedOrigins;
|
|
494
|
-
const allowOrigin = allowedOrigins.includes("*") || origin && allowedOrigins.includes(origin) ? origin ?? "*" : allowedOrigins[0] ?? "*";
|
|
495
|
-
headers.set("Access-Control-Allow-Origin", allowOrigin);
|
|
496
|
-
headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
|
|
497
|
-
headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
|
|
498
|
-
headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
|
|
499
|
-
headers.set("Vary", "Origin");
|
|
500
|
-
return headers;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// ../../src/core/http/csrfToken.ts
|
|
504
|
-
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
505
|
-
|
|
506
|
-
// ../../src/core/http/requestMetaContext.ts
|
|
507
|
-
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
508
|
-
var requestMetaContext = new AsyncLocalStorage4;
|
|
509
|
-
function runWithRequestMeta(meta, callback) {
|
|
510
|
-
return requestMetaContext.run(meta, callback);
|
|
511
|
-
}
|
|
512
|
-
function currentRequestMeta() {
|
|
513
|
-
return requestMetaContext.getStore() ?? {
|
|
514
|
-
ipAddress: null,
|
|
515
|
-
userAgent: null
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// ../../src/core/http/csrfToken.ts
|
|
520
|
-
var CSRF_COOKIE = "workhub_csrf";
|
|
521
|
-
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
522
|
-
function resolveCsrfSecret() {
|
|
523
|
-
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
|
|
524
|
-
}
|
|
525
|
-
function signCsrfToken(token, issuedAt) {
|
|
526
|
-
const payload = `${token}.${issuedAt}`;
|
|
527
|
-
const signature = createHmac("sha256", resolveCsrfSecret()).update(payload).digest("hex");
|
|
528
|
-
return `${payload}.${signature}`;
|
|
529
|
-
}
|
|
530
|
-
function readCsrfCookie(request) {
|
|
531
|
-
const cookieHeader = request.headers.get("cookie");
|
|
532
|
-
if (!cookieHeader) {
|
|
533
|
-
return null;
|
|
534
|
-
}
|
|
535
|
-
for (const part of cookieHeader.split(";")) {
|
|
536
|
-
const [name, ...rest] = part.trim().split("=");
|
|
537
|
-
if (name === CSRF_COOKIE) {
|
|
538
|
-
return decodeURIComponent(rest.join("="));
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
return null;
|
|
542
|
-
}
|
|
543
|
-
function parseSignedCsrfValue(cookieValue) {
|
|
544
|
-
const parts = cookieValue.split(".");
|
|
545
|
-
if (parts.length !== 3) {
|
|
546
|
-
return null;
|
|
547
|
-
}
|
|
548
|
-
const [token, issuedAtRaw, cookieSignature] = parts;
|
|
549
|
-
if (!token || !issuedAtRaw || !cookieSignature) {
|
|
550
|
-
return null;
|
|
551
|
-
}
|
|
552
|
-
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
553
|
-
if (!Number.isFinite(issuedAt)) {
|
|
554
|
-
return null;
|
|
555
|
-
}
|
|
556
|
-
if (Date.now() - issuedAt > CSRF_TTL_MS) {
|
|
557
|
-
return null;
|
|
558
|
-
}
|
|
559
|
-
const expectedSignature = signCsrfToken(token, issuedAt).split(".").pop();
|
|
560
|
-
if (!expectedSignature) {
|
|
561
|
-
return null;
|
|
562
|
-
}
|
|
563
|
-
const expectedBuffer = Buffer.from(expectedSignature);
|
|
564
|
-
const actualBuffer = Buffer.from(cookieSignature);
|
|
565
|
-
if (expectedBuffer.length !== actualBuffer.length) {
|
|
566
|
-
return null;
|
|
567
|
-
}
|
|
568
|
-
if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
|
|
569
|
-
return null;
|
|
570
|
-
}
|
|
571
|
-
return { token, issuedAt };
|
|
572
|
-
}
|
|
573
|
-
function createCsrfTokenCookie() {
|
|
574
|
-
const token = randomBytes(24).toString("hex");
|
|
575
|
-
const issuedAt = Date.now();
|
|
576
|
-
const value = signCsrfToken(token, issuedAt);
|
|
577
|
-
return {
|
|
578
|
-
token,
|
|
579
|
-
cookie: `${CSRF_COOKIE}=${encodeURIComponent(value)}; Path=/; SameSite=Lax; Max-Age=3600`
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
function resolveCsrfToken(request) {
|
|
583
|
-
const cookieValue = readCsrfCookie(request);
|
|
584
|
-
if (cookieValue) {
|
|
585
|
-
const parsed = parseSignedCsrfValue(cookieValue);
|
|
586
|
-
if (parsed) {
|
|
587
|
-
return { token: parsed.token };
|
|
4376
|
+
],
|
|
4377
|
+
maxAgeSeconds: 86400
|
|
4378
|
+
};
|
|
4379
|
+
|
|
4380
|
+
// ../../src/core/http/corsMiddleware.ts
|
|
4381
|
+
function createCorsMiddleware() {
|
|
4382
|
+
return async (request, next) => {
|
|
4383
|
+
if (request.method === "OPTIONS") {
|
|
4384
|
+
return new Response(null, {
|
|
4385
|
+
status: 204,
|
|
4386
|
+
headers: buildCorsHeaders(request)
|
|
4387
|
+
});
|
|
588
4388
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const headerToken = request.headers.get("x-csrf-token")?.trim();
|
|
594
|
-
if (headerToken) {
|
|
595
|
-
return headerToken;
|
|
596
|
-
}
|
|
597
|
-
return null;
|
|
598
|
-
}
|
|
599
|
-
async function readSubmittedCsrfTokenFromBody(request) {
|
|
600
|
-
const headerToken = readSubmittedCsrfToken(request);
|
|
601
|
-
if (headerToken) {
|
|
602
|
-
return headerToken;
|
|
603
|
-
}
|
|
604
|
-
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
605
|
-
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
606
|
-
const formData = await request.clone().formData();
|
|
607
|
-
const field = formData.get("_token");
|
|
608
|
-
if (typeof field === "string" && field.trim().length > 0) {
|
|
609
|
-
return field.trim();
|
|
4389
|
+
const response = await next();
|
|
4390
|
+
const headers = new Headers(response.headers);
|
|
4391
|
+
for (const [key, value] of buildCorsHeaders(request)) {
|
|
4392
|
+
headers.set(key, value);
|
|
610
4393
|
}
|
|
611
|
-
|
|
612
|
-
|
|
4394
|
+
return new Response(response.body, {
|
|
4395
|
+
status: response.status,
|
|
4396
|
+
statusText: response.statusText,
|
|
4397
|
+
headers
|
|
4398
|
+
});
|
|
4399
|
+
};
|
|
613
4400
|
}
|
|
614
|
-
function
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
626
|
-
const submittedBuffer = Buffer.from(submittedToken);
|
|
627
|
-
const expectedBuffer = Buffer.from(parsed.token);
|
|
628
|
-
if (submittedBuffer.length !== expectedBuffer.length) {
|
|
629
|
-
return false;
|
|
630
|
-
}
|
|
631
|
-
return timingSafeEqual(submittedBuffer, expectedBuffer);
|
|
4401
|
+
function buildCorsHeaders(request) {
|
|
4402
|
+
const headers = new Headers;
|
|
4403
|
+
const origin = request.headers.get("origin");
|
|
4404
|
+
const allowedOrigins = corsConfig.allowedOrigins;
|
|
4405
|
+
const allowOrigin = allowedOrigins.includes("*") || origin && allowedOrigins.includes(origin) ? origin ?? "*" : allowedOrigins[0] ?? "*";
|
|
4406
|
+
headers.set("Access-Control-Allow-Origin", allowOrigin);
|
|
4407
|
+
headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
|
|
4408
|
+
headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
|
|
4409
|
+
headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
|
|
4410
|
+
headers.set("Vary", "Origin");
|
|
4411
|
+
return headers;
|
|
632
4412
|
}
|
|
633
4413
|
|
|
634
4414
|
// ../../src/core/http/csrfMiddleware.ts
|
|
@@ -663,90 +4443,6 @@ function createCsrfMiddleware() {
|
|
|
663
4443
|
};
|
|
664
4444
|
}
|
|
665
4445
|
|
|
666
|
-
// ../../src/core/http/flashSession.ts
|
|
667
|
-
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
668
|
-
var FLASH_COOKIE = "workhub_flash";
|
|
669
|
-
var FLASH_TTL_MS = 60 * 1000;
|
|
670
|
-
function resolveFlashSecret() {
|
|
671
|
-
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
672
|
-
}
|
|
673
|
-
function signFlashPayload(payload, issuedAt) {
|
|
674
|
-
const signature = createHmac2("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
675
|
-
return `${payload}.${issuedAt}.${signature}`;
|
|
676
|
-
}
|
|
677
|
-
function readFlashCookie(request) {
|
|
678
|
-
const cookieHeader = request.headers.get("cookie");
|
|
679
|
-
if (!cookieHeader) {
|
|
680
|
-
return null;
|
|
681
|
-
}
|
|
682
|
-
for (const part of cookieHeader.split(";")) {
|
|
683
|
-
const [name, ...rest] = part.trim().split("=");
|
|
684
|
-
if (name === FLASH_COOKIE) {
|
|
685
|
-
return decodeURIComponent(rest.join("="));
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
return null;
|
|
689
|
-
}
|
|
690
|
-
function parseFlashCookie(cookieValue) {
|
|
691
|
-
const parts = cookieValue.split(".");
|
|
692
|
-
if (parts.length < 3) {
|
|
693
|
-
return null;
|
|
694
|
-
}
|
|
695
|
-
const signature = parts.pop();
|
|
696
|
-
const issuedAtRaw = parts.pop();
|
|
697
|
-
const payload = parts.join(".");
|
|
698
|
-
if (!signature || !issuedAtRaw || !payload) {
|
|
699
|
-
return null;
|
|
700
|
-
}
|
|
701
|
-
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
702
|
-
if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
|
|
703
|
-
return null;
|
|
704
|
-
}
|
|
705
|
-
const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
|
|
706
|
-
if (!expectedSignature) {
|
|
707
|
-
return null;
|
|
708
|
-
}
|
|
709
|
-
const expectedBuffer = Buffer.from(expectedSignature);
|
|
710
|
-
const actualBuffer = Buffer.from(signature);
|
|
711
|
-
if (expectedBuffer.length !== actualBuffer.length) {
|
|
712
|
-
return null;
|
|
713
|
-
}
|
|
714
|
-
if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
|
|
715
|
-
return null;
|
|
716
|
-
}
|
|
717
|
-
try {
|
|
718
|
-
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
719
|
-
if (!parsed?.message || typeof parsed.message !== "string") {
|
|
720
|
-
return null;
|
|
721
|
-
}
|
|
722
|
-
if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
|
|
723
|
-
return null;
|
|
724
|
-
}
|
|
725
|
-
return parsed;
|
|
726
|
-
} catch {
|
|
727
|
-
return null;
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
function clearFlashCookie() {
|
|
731
|
-
return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
732
|
-
}
|
|
733
|
-
function pullFlash(request) {
|
|
734
|
-
const cookieValue = readFlashCookie(request);
|
|
735
|
-
if (!cookieValue) {
|
|
736
|
-
return null;
|
|
737
|
-
}
|
|
738
|
-
return parseFlashCookie(cookieValue);
|
|
739
|
-
}
|
|
740
|
-
function withFlashClear(response) {
|
|
741
|
-
const headers = new Headers(response.headers);
|
|
742
|
-
headers.append("set-cookie", clearFlashCookie());
|
|
743
|
-
return new Response(response.body, {
|
|
744
|
-
status: response.status,
|
|
745
|
-
statusText: response.statusText,
|
|
746
|
-
headers
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
|
|
750
4446
|
// ../../src/core/http/flashMiddleware.ts
|
|
751
4447
|
function createFlashMiddleware() {
|
|
752
4448
|
return async (request, next) => {
|
|
@@ -763,7 +4459,7 @@ function createFlashMiddleware() {
|
|
|
763
4459
|
}
|
|
764
4460
|
|
|
765
4461
|
// ../../src/core/http/loginThrottleMiddleware.ts
|
|
766
|
-
var {RedisClient } = globalThis.Bun;
|
|
4462
|
+
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
767
4463
|
function resolveLoginIdentity(request) {
|
|
768
4464
|
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
769
4465
|
}
|
|
@@ -782,7 +4478,7 @@ async function resolveLoginEmail(request) {
|
|
|
782
4478
|
}
|
|
783
4479
|
}
|
|
784
4480
|
function createLoginThrottleMiddleware(options) {
|
|
785
|
-
const client = new
|
|
4481
|
+
const client = new RedisClient3(options.redisUrl);
|
|
786
4482
|
const prefix = options.keyPrefix ?? "workhub:login-throttle:";
|
|
787
4483
|
return async (request, next) => {
|
|
788
4484
|
const identity = resolveLoginIdentity(request);
|
|
@@ -918,38 +4614,6 @@ function createMetricsMiddleware() {
|
|
|
918
4614
|
};
|
|
919
4615
|
}
|
|
920
4616
|
|
|
921
|
-
// ../../src/core/http/middleware.ts
|
|
922
|
-
function composeMiddleware(...middleware) {
|
|
923
|
-
return (handler) => {
|
|
924
|
-
return async (request) => {
|
|
925
|
-
let index = 0;
|
|
926
|
-
const dispatch = async () => {
|
|
927
|
-
if (index >= middleware.length) {
|
|
928
|
-
return await handler(request);
|
|
929
|
-
}
|
|
930
|
-
const current = middleware[index];
|
|
931
|
-
index += 1;
|
|
932
|
-
if (!current) {
|
|
933
|
-
return await handler(request);
|
|
934
|
-
}
|
|
935
|
-
return await current(request, dispatch);
|
|
936
|
-
};
|
|
937
|
-
return await dispatch();
|
|
938
|
-
};
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
async function requestIdMiddleware(request, next) {
|
|
942
|
-
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
943
|
-
const response = await next();
|
|
944
|
-
const headers = new Headers(response.headers);
|
|
945
|
-
headers.set("x-request-id", requestId);
|
|
946
|
-
return new Response(response.body, {
|
|
947
|
-
status: response.status,
|
|
948
|
-
statusText: response.statusText,
|
|
949
|
-
headers
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
|
|
953
4617
|
// ../../src/core/http/requireAbilityMiddleware.ts
|
|
954
4618
|
function createRequireAbilityMiddleware(abilityChecker) {
|
|
955
4619
|
return (ability) => {
|
|
@@ -980,7 +4644,7 @@ function createRequireAuthMiddleware(auth) {
|
|
|
980
4644
|
}
|
|
981
4645
|
|
|
982
4646
|
// ../../src/core/security/securityEvents.ts
|
|
983
|
-
function
|
|
4647
|
+
function logSecurityEvent2(event, details = {}) {
|
|
984
4648
|
const meta = currentRequestMeta();
|
|
985
4649
|
const user = currentAuthUser();
|
|
986
4650
|
console.log(JSON.stringify({
|
|
@@ -999,7 +4663,7 @@ function createRequireGlobalAdminMiddleware() {
|
|
|
999
4663
|
return async (_request, next) => {
|
|
1000
4664
|
const user = currentAuthUser();
|
|
1001
4665
|
if (!isGlobalAdmin(user)) {
|
|
1002
|
-
|
|
4666
|
+
logSecurityEvent2("privilege_escalation_blocked", {
|
|
1003
4667
|
required_role: "platform_admin",
|
|
1004
4668
|
path: new URL(_request.url).pathname
|
|
1005
4669
|
});
|
|
@@ -1056,15 +4720,6 @@ function withMiddleware(...middleware) {
|
|
|
1056
4720
|
};
|
|
1057
4721
|
}
|
|
1058
4722
|
|
|
1059
|
-
// ../../src/config/app.ts
|
|
1060
|
-
var appConfig = {
|
|
1061
|
-
name: "WorkHub",
|
|
1062
|
-
env: process.env.APP_ENV ?? "local",
|
|
1063
|
-
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
1064
|
-
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
1065
|
-
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
1066
|
-
};
|
|
1067
|
-
|
|
1068
4723
|
// ../../src/config/contentSecurityPolicy.ts
|
|
1069
4724
|
function strictApiContentSecurityPolicy() {
|
|
1070
4725
|
return "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
|
|
@@ -1131,7 +4786,7 @@ function createSecurityHeadersMiddleware() {
|
|
|
1131
4786
|
}
|
|
1132
4787
|
|
|
1133
4788
|
// ../../src/core/http/throttleMiddleware.ts
|
|
1134
|
-
var {RedisClient:
|
|
4789
|
+
var {RedisClient: RedisClient4 } = globalThis.Bun;
|
|
1135
4790
|
|
|
1136
4791
|
// ../../src/core/tenant/tenantContext.ts
|
|
1137
4792
|
import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
|
|
@@ -1165,7 +4820,7 @@ function resolveThrottleIdentity(request) {
|
|
|
1165
4820
|
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
1166
4821
|
}
|
|
1167
4822
|
function createThrottleMiddleware(options) {
|
|
1168
|
-
const client = new
|
|
4823
|
+
const client = new RedisClient4(options.redisUrl);
|
|
1169
4824
|
const prefix = options.keyPrefix ?? "workhub:throttle:";
|
|
1170
4825
|
return async (request, next) => {
|
|
1171
4826
|
const identity = resolveThrottleIdentity(request);
|
|
@@ -1212,42 +4867,11 @@ function createRequestLoggingMiddleware() {
|
|
|
1212
4867
|
};
|
|
1213
4868
|
}
|
|
1214
4869
|
|
|
1215
|
-
// ../../src/config/features.ts
|
|
1216
|
-
function readFeatureFlags() {
|
|
1217
|
-
return {
|
|
1218
|
-
webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
|
|
1219
|
-
fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
|
|
1220
|
-
auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
|
|
1221
|
-
oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
|
|
1222
|
-
samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
|
|
1223
|
-
scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
|
|
1224
|
-
billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
|
|
1225
|
-
siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
|
|
1226
|
-
publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
|
|
1227
|
-
emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
|
|
1228
|
-
mfa: (process.env.FEATURE_MFA ?? "false") === "true"
|
|
1229
|
-
};
|
|
1230
|
-
}
|
|
1231
|
-
var featureFlags = readFeatureFlags();
|
|
1232
|
-
function isFeatureEnabled(feature) {
|
|
1233
|
-
return readFeatureFlags()[feature];
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
4870
|
// ../../src/core/security/publicReads.ts
|
|
1237
4871
|
function isPublicReadsEnabled() {
|
|
1238
4872
|
return isFeatureEnabled("publicReads");
|
|
1239
4873
|
}
|
|
1240
4874
|
|
|
1241
|
-
// ../../src/core/tenant/databaseTenantContext.ts
|
|
1242
|
-
async function runWithMigrationBypass(callback) {
|
|
1243
|
-
await connection_default`SELECT set_config('app.bypass_rls', 'true', false)`;
|
|
1244
|
-
try {
|
|
1245
|
-
return await callback();
|
|
1246
|
-
} finally {
|
|
1247
|
-
await connection_default`SELECT set_config('app.bypass_rls', 'false', false)`;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
4875
|
// ../../src/core/tenant/resolveTenant.ts
|
|
1252
4876
|
async function resolveTenant(tenantId) {
|
|
1253
4877
|
const rows = await connection_default`
|
|
@@ -1543,29 +5167,41 @@ class HttpKernel {
|
|
|
1543
5167
|
return withMiddleware(createAuthorizeMiddleware(gate, auth, resource, action))(handler);
|
|
1544
5168
|
}
|
|
1545
5169
|
wrapLogin(handler) {
|
|
5170
|
+
return this.wrapThrottle("login", resolveLoginRateLimit(), handler);
|
|
5171
|
+
}
|
|
5172
|
+
wrapRegister(handler) {
|
|
5173
|
+
return this.wrapThrottle("register", resolveRegisterRateLimit(), handler);
|
|
5174
|
+
}
|
|
5175
|
+
wrapThrottle(scope, rateLimit, handler) {
|
|
1546
5176
|
const middleware = [];
|
|
1547
|
-
const
|
|
5177
|
+
const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
|
|
1548
5178
|
if (this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
|
|
1549
5179
|
const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
|
|
1550
5180
|
const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
|
|
1551
5181
|
if (redisUrl) {
|
|
1552
|
-
|
|
5182
|
+
const throttle = scope === "login" ? createLoginThrottleMiddleware({
|
|
1553
5183
|
redisUrl,
|
|
1554
|
-
maxAttempts:
|
|
1555
|
-
decaySeconds:
|
|
1556
|
-
})
|
|
5184
|
+
maxAttempts: rateLimit.maxAttempts,
|
|
5185
|
+
decaySeconds: rateLimit.decaySeconds
|
|
5186
|
+
}) : createThrottleMiddleware({
|
|
5187
|
+
redisUrl,
|
|
5188
|
+
maxAttempts: rateLimit.maxAttempts,
|
|
5189
|
+
decaySeconds: rateLimit.decaySeconds,
|
|
5190
|
+
keyPrefix: memoryKeyPrefix
|
|
5191
|
+
});
|
|
5192
|
+
middleware.push(throttle);
|
|
1557
5193
|
} else {
|
|
1558
5194
|
middleware.push(createMemoryThrottleMiddleware({
|
|
1559
|
-
maxAttempts:
|
|
1560
|
-
decaySeconds:
|
|
1561
|
-
keyPrefix:
|
|
5195
|
+
maxAttempts: rateLimit.maxAttempts,
|
|
5196
|
+
decaySeconds: rateLimit.decaySeconds,
|
|
5197
|
+
keyPrefix: memoryKeyPrefix
|
|
1562
5198
|
}));
|
|
1563
5199
|
}
|
|
1564
5200
|
} else {
|
|
1565
5201
|
middleware.push(createMemoryThrottleMiddleware({
|
|
1566
|
-
maxAttempts:
|
|
1567
|
-
decaySeconds:
|
|
1568
|
-
keyPrefix:
|
|
5202
|
+
maxAttempts: rateLimit.maxAttempts,
|
|
5203
|
+
decaySeconds: rateLimit.decaySeconds,
|
|
5204
|
+
keyPrefix: memoryKeyPrefix
|
|
1569
5205
|
}));
|
|
1570
5206
|
}
|
|
1571
5207
|
if (middleware.length === 0) {
|
|
@@ -1577,6 +5213,82 @@ class HttpKernel {
|
|
|
1577
5213
|
function createHttpKernel(dependencies) {
|
|
1578
5214
|
return new HttpKernel(dependencies);
|
|
1579
5215
|
}
|
|
5216
|
+
|
|
5217
|
+
// ../../src/bootstrap/routeRegistry.ts
|
|
5218
|
+
class RouteRegistry {
|
|
5219
|
+
routes = [];
|
|
5220
|
+
register(route) {
|
|
5221
|
+
this.routes.push(route);
|
|
5222
|
+
}
|
|
5223
|
+
clear() {
|
|
5224
|
+
this.routes.length = 0;
|
|
5225
|
+
}
|
|
5226
|
+
list() {
|
|
5227
|
+
return [...this.routes].sort((left, right) => left.path.localeCompare(right.path));
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
var routeRegistry = new RouteRegistry;
|
|
5231
|
+
|
|
5232
|
+
// ../../src/bootstrap/createWebRoutes.ts
|
|
5233
|
+
function registerRoute(method, path, middleware) {
|
|
5234
|
+
routeRegistry.register({ method, path, middleware });
|
|
5235
|
+
}
|
|
5236
|
+
function registerRouteMap(routes, middleware) {
|
|
5237
|
+
const registered = {};
|
|
5238
|
+
for (const [path, handler] of Object.entries(routes)) {
|
|
5239
|
+
if (handler && typeof handler === "object" && !Array.isArray(handler)) {
|
|
5240
|
+
const methodMap = handler;
|
|
5241
|
+
registered[path] = methodMap;
|
|
5242
|
+
for (const method of Object.keys(methodMap)) {
|
|
5243
|
+
registerRoute(method.toUpperCase(), path, middleware);
|
|
5244
|
+
}
|
|
5245
|
+
continue;
|
|
5246
|
+
}
|
|
5247
|
+
registered[path] = handler;
|
|
5248
|
+
registerRoute("GET", path, middleware);
|
|
5249
|
+
}
|
|
5250
|
+
return registered;
|
|
5251
|
+
}
|
|
5252
|
+
function createWebRoutes(dependencies) {
|
|
5253
|
+
const kernel = createHttpKernel(dependencies);
|
|
5254
|
+
const middleware = [...kernel.globalMiddleware(), ...kernel.group("web")];
|
|
5255
|
+
const moduleRoutes = {
|
|
5256
|
+
"/": () => Response.redirect("/organizations", 302)
|
|
5257
|
+
};
|
|
5258
|
+
registerRoute("GET", "/", ["global", "web"]);
|
|
5259
|
+
for (const module of appModules) {
|
|
5260
|
+
if (!module.webRoutes) {
|
|
5261
|
+
continue;
|
|
5262
|
+
}
|
|
5263
|
+
Object.assign(moduleRoutes, module.webRoutes({
|
|
5264
|
+
dependencies,
|
|
5265
|
+
cachedJson: async () => htmlResponse(""),
|
|
5266
|
+
kernel
|
|
5267
|
+
}));
|
|
5268
|
+
}
|
|
5269
|
+
const wrappedRoutes = applyMiddlewareToRoutes(registerRouteMap(moduleRoutes, ["global", "web"]), middleware);
|
|
5270
|
+
wrappedRoutes["/assets/*"] = async (request) => {
|
|
5271
|
+
registerRoute("GET", "/assets/*", ["global", "web"]);
|
|
5272
|
+
const pathname = new URL(request.url).pathname;
|
|
5273
|
+
const relativePath = pathname.replace(/^\//, "");
|
|
5274
|
+
const file = Bun.file(join4(process.cwd(), "public", relativePath));
|
|
5275
|
+
if (!await file.exists()) {
|
|
5276
|
+
return htmlResponse("Not Found", { status: 404 });
|
|
5277
|
+
}
|
|
5278
|
+
return new Response(file);
|
|
5279
|
+
};
|
|
5280
|
+
registerRoute("GET", "/assets/*", ["global", "web"]);
|
|
5281
|
+
return wrappedRoutes;
|
|
5282
|
+
}
|
|
5283
|
+
function mergeWebRoutes(dependencies, routes) {
|
|
5284
|
+
if (!isViewsEnabled()) {
|
|
5285
|
+
return routes;
|
|
5286
|
+
}
|
|
5287
|
+
return {
|
|
5288
|
+
...createWebRoutes(dependencies),
|
|
5289
|
+
...routes
|
|
5290
|
+
};
|
|
5291
|
+
}
|
|
1580
5292
|
// ../../src/bootstrap/prefixRouteMap.ts
|
|
1581
5293
|
function prefixRouteMap(prefix, routes) {
|
|
1582
5294
|
const normalizedPrefix = prefix.replace(/\/$/, "");
|
|
@@ -1738,16 +5450,26 @@ function slugify(value) {
|
|
|
1738
5450
|
export {
|
|
1739
5451
|
slugify,
|
|
1740
5452
|
setActiveApplicationContext,
|
|
5453
|
+
scheduleRunCommand,
|
|
5454
|
+
runProviderPhase,
|
|
5455
|
+
runDueScheduledTasks,
|
|
1741
5456
|
resolveService,
|
|
1742
5457
|
resolveApplicationQueue,
|
|
1743
5458
|
prefixRouteMap,
|
|
1744
5459
|
parseFormBody,
|
|
5460
|
+
mergeWebRoutes,
|
|
1745
5461
|
getRequiredDependency,
|
|
1746
5462
|
createWebServer,
|
|
5463
|
+
createWebRoutes,
|
|
1747
5464
|
createHttpKernel,
|
|
1748
5465
|
createCsrfProtection,
|
|
5466
|
+
createAppContext,
|
|
5467
|
+
coreProviders,
|
|
5468
|
+
collectProviders,
|
|
1749
5469
|
assertAppDependenciesComplete,
|
|
5470
|
+
appSchedule,
|
|
1750
5471
|
ServiceContainer,
|
|
5472
|
+
Schedule,
|
|
1751
5473
|
REDIS_URL_CONFIG_KEY,
|
|
1752
5474
|
DEFAULT_APP_PORT,
|
|
1753
5475
|
DATABASE_URL_CONFIG_KEY,
|