@getstrata/core 0.3.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/bootstrap/httpKernel.d.ts +2 -0
  2. package/dist/config/queue.d.ts +8 -0
  3. package/dist/config/rateLimit.d.ts +2 -1
  4. package/dist/core/database/baseRepository.d.ts +16 -4
  5. package/dist/core/database/boundConnection.d.ts +5 -0
  6. package/dist/core/database/index.d.ts +10 -4
  7. package/dist/core/database/migrations/advisoryLock.d.ts +6 -0
  8. package/dist/core/database/migrations/runner.d.ts +8 -2
  9. package/dist/core/database/migrations/types.d.ts +1 -0
  10. package/dist/core/database/model.d.ts +46 -17
  11. package/dist/core/database/query.d.ts +26 -14
  12. package/dist/core/database/relationships.d.ts +32 -2
  13. package/dist/core/database/repositoryConnection.d.ts +4 -0
  14. package/dist/core/database/repositoryQuery.d.ts +16 -1
  15. package/dist/core/database/schema/blueprint.d.ts +47 -0
  16. package/dist/core/database/schema/columnDefinition.d.ts +36 -0
  17. package/dist/core/database/schema/driver.d.ts +8 -0
  18. package/dist/core/database/schema/errors.d.ts +4 -0
  19. package/dist/core/database/schema/grammars/compileStatements.d.ts +8 -0
  20. package/dist/core/database/schema/grammars/createGrammar.d.ts +4 -0
  21. package/dist/core/database/schema/grammars/grammar.d.ts +10 -0
  22. package/dist/core/database/schema/grammars/index.d.ts +7 -0
  23. package/dist/core/database/schema/grammars/mysqlGrammar.d.ts +2 -0
  24. package/dist/core/database/schema/grammars/postgresGrammar.d.ts +2 -0
  25. package/dist/core/database/schema/grammars/sqliteGrammar.d.ts +2 -0
  26. package/dist/core/database/schema/index.d.ts +12 -0
  27. package/dist/core/database/schema/schema.d.ts +21 -0
  28. package/dist/core/database/seeders/runner.d.ts +6 -0
  29. package/dist/core/database/seeders/types.d.ts +8 -0
  30. package/dist/core/database/types.d.ts +39 -2
  31. package/dist/core/database/whereBuilder.d.ts +17 -0
  32. package/dist/core/http/index.d.ts +1 -1
  33. package/dist/core/http/securedRouteModelBinding.d.ts +2 -1
  34. package/dist/core/lifecycle/gracefulShutdown.d.ts +6 -0
  35. package/dist/core/pagination/index.d.ts +11 -1
  36. package/dist/core/queue/failedJobRepository.d.ts +6 -0
  37. package/dist/core/queue/failedJobService.d.ts +15 -0
  38. package/dist/core/queue/failedJobTable.d.ts +3 -0
  39. package/dist/core/queue/jobRegistry.d.ts +14 -0
  40. package/dist/core/queue/jobRunner.d.ts +9 -0
  41. package/dist/core/queue/publicQueue.d.ts +15 -0
  42. package/dist/core/queue/redisQueue.d.ts +29 -0
  43. package/dist/core/queue/resilientQueue.d.ts +9 -0
  44. package/dist/core/queue/types.d.ts +8 -0
  45. package/dist/core/scheduler/schedule.d.ts +15 -0
  46. package/dist/entries/auth/accessControl.js +113 -0
  47. package/dist/entries/auth/authContext.js +15 -0
  48. package/dist/entries/auth/guard.js +2870 -0
  49. package/dist/entries/auth/membershipContext.js +276 -0
  50. package/dist/entries/auth/membershipScope.js +390 -0
  51. package/dist/entries/auth/membershipService.js +477 -0
  52. package/dist/entries/auth/oauth/oidcProvider.js +49 -0
  53. package/dist/entries/auth/oauth/providers.js +71 -0
  54. package/dist/entries/auth/oauth/samlProvider.js +26 -0
  55. package/dist/entries/auth/oauth/types.js +1 -0
  56. package/dist/entries/auth/password.js +15 -0
  57. package/dist/entries/auth/policy.js +134 -0
  58. package/dist/entries/auth/sessionCookie.js +75 -0
  59. package/dist/entries/auth/tokenHash.js +17 -0
  60. package/dist/entries/cache/tags.js +13 -0
  61. package/dist/entries/crypto/fieldEncryption.js +93 -0
  62. package/dist/entries/crypto/mfaSecret.js +105 -0
  63. package/dist/entries/database/factory.js +17 -0
  64. package/dist/entries/database/seeders.js +30 -0
  65. package/dist/entries/database/types.js +1 -0
  66. package/dist/entries/database.js +2218 -0
  67. package/dist/entries/errors/http.js +78 -0
  68. package/dist/entries/http/contentNegotiation.js +26 -0
  69. package/dist/entries/http/csrfToken.js +147 -0
  70. package/dist/entries/http/etag.js +170 -0
  71. package/dist/entries/http/flashSession.js +106 -0
  72. package/dist/entries/http/middleware.js +68 -0
  73. package/dist/entries/http/parseFormBody.js +88 -0
  74. package/dist/entries/http/requestMetaContext.js +18 -0
  75. package/dist/entries/http/resources.js +19 -0
  76. package/dist/entries/http/webErrorResponse.js +3143 -0
  77. package/dist/entries/http/webFormRequest.js +293 -0
  78. package/dist/entries/http.js +3924 -0
  79. package/dist/entries/jobs/dispatchWebhookJob.js +342 -0
  80. package/dist/entries/lifecycle/gracefulShutdown.js +50 -0
  81. package/dist/entries/metrics/prometheus.js +70 -0
  82. package/dist/entries/pagination.js +14 -0
  83. package/dist/entries/queue/createAppQueue.js +2848 -0
  84. package/dist/entries/queue/failedJobService.js +38 -0
  85. package/dist/entries/queue/jobRegistry.js +32 -0
  86. package/dist/entries/queue/jobRunner.js +75 -0
  87. package/dist/entries/queue/publicQueue.js +2476 -0
  88. package/dist/entries/queue/queueMetrics.js +2898 -0
  89. package/dist/entries/queue/types.js +1 -0
  90. package/dist/entries/security/oauthState.js +75 -0
  91. package/dist/entries/security/publicReads.js +33 -0
  92. package/dist/entries/security/safeUrl.js +143 -0
  93. package/dist/entries/security/securityEvents.js +41 -0
  94. package/dist/entries/security/stripeWebhook.js +115 -0
  95. package/dist/entries/security/tokenExpiry.js +16 -0
  96. package/dist/entries/security/totp.js +51 -0
  97. package/dist/entries/storage/storage.js +123 -0
  98. package/dist/entries/tenant/tenantContext.js +30 -0
  99. package/dist/entries/tenant/tenantMiddleware.js +312 -0
  100. package/dist/entries/tracing/traceContext.js +15 -0
  101. package/dist/entries/validation/rules.js +232 -0
  102. package/dist/entries/view.js +3072 -0
  103. package/dist/framework/public-api.d.ts +65 -38
  104. package/dist/index.js +3213 -281
  105. package/dist/modules/user/apiTokenRepository.d.ts +1 -1
  106. package/dist/modules/user/apiTokenTable.d.ts +1 -1
  107. package/dist/modules/user/authService.d.ts +1 -1
  108. package/dist/modules/user/notificationRepository.d.ts +1 -1
  109. package/dist/modules/user/notificationService.d.ts +1 -1
  110. package/dist/modules/user/notificationTable.d.ts +1 -1
  111. package/dist/modules/user/oauthIdentityRepository.d.ts +2 -2
  112. package/dist/modules/user/provider.d.ts +1 -1
  113. package/dist/modules/user/repository.d.ts +1 -1
  114. package/dist/modules/user/table.d.ts +1 -1
  115. package/dist/modules/user/tokenService.d.ts +1 -1
  116. package/package.json +289 -3
package/dist/index.js CHANGED
@@ -135,6 +135,13 @@ class UnauthorizedError extends HttpError {
135
135
  super(401, message, details);
136
136
  }
137
137
  }
138
+
139
+ class PayloadTooLargeError extends HttpError {
140
+ constructor(message = "Payload Too Large", details) {
141
+ super(413, message, details);
142
+ }
143
+ }
144
+
138
145
  class PreconditionFailedError extends HttpError {
139
146
  constructor(message = "Precondition Failed", details) {
140
147
  super(412, message, details);
@@ -157,6 +164,15 @@ class Policy {
157
164
  return false;
158
165
  }
159
166
  }
167
+ var BLOCKED_POLICY_ACTIONS = new Set([
168
+ "constructor",
169
+ "toString",
170
+ "valueOf",
171
+ "hasOwnProperty",
172
+ "isPrototypeOf",
173
+ "propertyIsEnumerable",
174
+ "__proto__"
175
+ ]);
160
176
 
161
177
  class PolicyGate {
162
178
  constructor() {}
@@ -169,6 +185,9 @@ class PolicyGate {
169
185
  if (!policy) {
170
186
  return false;
171
187
  }
188
+ if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
189
+ return false;
190
+ }
172
191
  const handler = policy[action];
173
192
  if (typeof handler !== "function") {
174
193
  return false;
@@ -248,76 +267,6 @@ var CACHE_TAGS = {
248
267
  attachments: "attachments",
249
268
  reports: "reports"
250
269
  };
251
- // ../../src/config/database.ts
252
- function readInteger(name, fallback) {
253
- const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
254
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
255
- }
256
- var databaseConfig = {
257
- url: process.env.DATABASE_URL ?? "",
258
- poolMax: readInteger("DB_POOL_MAX", 10),
259
- idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
260
- maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
261
- connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
262
- };
263
-
264
- // ../../src/core/database/connectionContext.ts
265
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
266
- var activeConnection = new AsyncLocalStorage2;
267
- function getActiveDatabaseConnection(fallback) {
268
- return activeConnection.getStore() ?? fallback;
269
- }
270
-
271
- // ../../src/db/connection/createConnection.ts
272
- var {SQL } = globalThis.Bun;
273
- function createDatabaseConnection(config) {
274
- if (!config.url) {
275
- throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
276
- }
277
- return new SQL({
278
- url: config.url,
279
- max: config.poolMax,
280
- idleTimeout: config.idleTimeoutSeconds,
281
- maxLifetime: config.maxLifetimeSeconds,
282
- connectionTimeout: config.connectionTimeoutSeconds
283
- });
284
- }
285
-
286
- // ../../src/db/connection/index.ts
287
- var connectionHolder = {
288
- connection: null
289
- };
290
- function getDatabase() {
291
- if (!connectionHolder.connection) {
292
- connectionHolder.connection = createDatabaseConnection(databaseConfig);
293
- }
294
- return connectionHolder.connection;
295
- }
296
- function resetDatabaseConnectionForTests(connection) {
297
- connectionHolder.connection = connection;
298
- }
299
- var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
300
- function resolveDatabase() {
301
- return getActiveDatabaseConnection(getDatabase());
302
- }
303
- function resolveDatabaseForProperty(property) {
304
- if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
305
- return getDatabase();
306
- }
307
- return resolveDatabase();
308
- }
309
- var db = new Proxy(function database() {}, {
310
- apply(_target, _thisArg, args) {
311
- return resolveDatabase()(...args);
312
- },
313
- get(_target, property) {
314
- const connection = resolveDatabaseForProperty(property);
315
- const value = connection[property];
316
- return typeof value === "function" ? value.bind(connection) : value;
317
- }
318
- });
319
- var connection_default = db;
320
-
321
270
  // ../../src/core/events/eventBus.ts
322
271
  class EventBus {
323
272
  constructor() {}
@@ -428,6 +377,23 @@ function quoteIdentifier(identifier) {
428
377
  function qualifyColumn(tableName, column) {
429
378
  return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
430
379
  }
380
+ function resolveQualifiedColumn(defaultTable, columnName) {
381
+ if (columnName.includes(".")) {
382
+ const [table, column] = columnName.split(".", 2);
383
+ if (!table || !column) {
384
+ throw new Error(`Invalid qualified column: ${columnName}`);
385
+ }
386
+ return qualifyColumn(table, column);
387
+ }
388
+ return qualifyColumn(defaultTable, columnName);
389
+ }
390
+ function parseQualifiedColumn(reference) {
391
+ const [table, column] = reference.split(".", 2);
392
+ if (!table || !column) {
393
+ throw new Error(`Join columns must be qualified as table.column: ${reference}`);
394
+ }
395
+ return { table, column };
396
+ }
431
397
  function normalizeDirection(direction = "ASC") {
432
398
  return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
433
399
  }
@@ -475,16 +441,21 @@ function buildOperatorClauses(column, operator, params) {
475
441
  if (operator.lte !== undefined) {
476
442
  clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
477
443
  }
444
+ if (operator.ilike !== undefined) {
445
+ clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
446
+ }
447
+ if (operator.tsMatch !== undefined) {
448
+ clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
449
+ }
478
450
  return clauses;
479
451
  }
480
- function buildWhereClause(tableName, where = {}) {
452
+ function appendWhereParts(tableName, where, params) {
481
453
  const clauses = [];
482
- const params = [];
483
454
  for (const [columnName, filterValue] of Object.entries(where)) {
484
455
  if (filterValue === undefined) {
485
456
  continue;
486
457
  }
487
- const column = qualifyColumn(tableName, columnName);
458
+ const column = resolveQualifiedColumn(tableName, columnName);
488
459
  if (Array.isArray(filterValue)) {
489
460
  clauses.push(buildInClause(column, filterValue, params));
490
461
  continue;
@@ -499,8 +470,54 @@ function buildWhereClause(tableName, where = {}) {
499
470
  }
500
471
  clauses.push(`${column} = ${pushParam(params, filterValue)}`);
501
472
  }
473
+ return clauses.join(" AND ");
474
+ }
475
+ function buildWhereClause(tableName, where = {}) {
476
+ const params = [];
477
+ const body = appendWhereParts(tableName, where, params);
478
+ return {
479
+ clause: body.length > 0 ? ` WHERE ${body}` : "",
480
+ params
481
+ };
482
+ }
483
+ function buildWhereNodeClause(tableName, node, params) {
484
+ if ("where" in node) {
485
+ return appendWhereParts(tableName, node.where, params);
486
+ }
487
+ const grouped = buildWhereGroupClause(tableName, node.group, params);
488
+ if (!grouped) {
489
+ return "";
490
+ }
491
+ return grouped.includes(" OR ") ? `(${grouped})` : grouped;
492
+ }
493
+ function buildWhereGroupClause(tableName, nodes, params) {
494
+ let result = "";
495
+ for (const node of nodes) {
496
+ const part = buildWhereNodeClause(tableName, node, params);
497
+ if (!part) {
498
+ continue;
499
+ }
500
+ if (!result) {
501
+ result = part;
502
+ continue;
503
+ }
504
+ result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
505
+ }
506
+ if (!result) {
507
+ return "";
508
+ }
509
+ return result;
510
+ }
511
+ function buildAdvancedWhereClause(tableName, where = {}, whereNodes = []) {
512
+ const params = [];
513
+ const nodes = [];
514
+ if (Object.keys(where).length > 0) {
515
+ nodes.push({ kind: "and", where });
516
+ }
517
+ nodes.push(...whereNodes);
518
+ const combined = buildWhereGroupClause(tableName, nodes, params);
502
519
  return {
503
- clause: clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "",
520
+ clause: combined ? ` WHERE ${combined}` : "",
504
521
  params
505
522
  };
506
523
  }
@@ -527,12 +544,17 @@ function appendSoftDeleteScope(table, options, clauses) {
527
544
  clauses.push(`${qualifiedColumn} IS NULL`);
528
545
  }
529
546
  }
530
- function buildQueryWhereClause(table, options = {}) {
531
- const { clause, params } = buildWhereClause(table.name, options.where ?? {});
532
- const clauses = clause.length > 0 ? clause.replace(/^ WHERE /, "").split(" AND ") : [];
533
- appendSoftDeleteScope(table, options, clauses);
547
+ function buildQueryWhereClause(table, options = {}, whereNodes = []) {
548
+ const { clause, params } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes);
549
+ const softDeleteClauses = [];
550
+ appendSoftDeleteScope(table, options, softDeleteClauses);
551
+ if (softDeleteClauses.length === 0) {
552
+ return { clause, params };
553
+ }
554
+ const base = clause.replace(/^ WHERE /, "");
555
+ const scope = softDeleteClauses.join(" AND ");
534
556
  return {
535
- clause: clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "",
557
+ clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
536
558
  params
537
559
  };
538
560
  }
@@ -556,10 +578,32 @@ function normalizeOrderBy(orderBy) {
556
578
  }
557
579
  function buildOrderByClause(tableName, orderBy) {
558
580
  const parts = normalizeOrderBy(orderBy).map(({ column, direction }) => {
559
- return `${qualifyColumn(tableName, column)} ${normalizeDirection(direction)}`;
581
+ return `${resolveQualifiedColumn(tableName, column)} ${normalizeDirection(direction)}`;
560
582
  });
561
583
  return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
562
584
  }
585
+ function buildGroupByClause(tableName, groupBy) {
586
+ if (!groupBy) {
587
+ return "";
588
+ }
589
+ const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
590
+ const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
591
+ return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
592
+ }
593
+ function buildHavingClause(tableName, having, params) {
594
+ if (!having) {
595
+ return "";
596
+ }
597
+ const body = appendWhereParts(tableName, having, params);
598
+ return body.length > 0 ? ` HAVING ${body}` : "";
599
+ }
600
+ function buildJoinClause(joins = []) {
601
+ return joins.map((join) => {
602
+ const joinType = join.type === "left" ? "LEFT JOIN" : "INNER JOIN";
603
+ const onClause = join.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
604
+ return ` ${joinType} ${quoteIdentifier(join.table)} ON ${onClause}`;
605
+ }).join("");
606
+ }
563
607
  function buildLimitClause(limit) {
564
608
  if (limit === undefined) {
565
609
  return "";
@@ -581,6 +625,23 @@ function buildOffsetClause(offset) {
581
625
  function buildReturningColumns(table) {
582
626
  return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
583
627
  }
628
+ function buildSelectList(table, select, params = []) {
629
+ if (!select || select.length === 0) {
630
+ return buildReturningColumns(table);
631
+ }
632
+ return select.map((item) => {
633
+ if (item.kind === "column") {
634
+ const column2 = qualifyColumn(item.table, item.column);
635
+ return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
636
+ }
637
+ if (item.kind === "literalText") {
638
+ return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
639
+ }
640
+ const column = qualifyColumn(item.table, item.column);
641
+ const placeholder = pushParam(params, item.query);
642
+ return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
643
+ }).join(", ");
644
+ }
584
645
  function getDefinedColumnEntries(table, values, options = {}) {
585
646
  const record = values;
586
647
  const excluded = new Set(options.exclude ?? []);
@@ -595,36 +656,57 @@ function getDefinedColumnEntries(table, values, options = {}) {
595
656
  return [[column, value]];
596
657
  });
597
658
  }
598
- function buildSelectQuery(table, options = {}) {
599
- const columns = buildReturningColumns(table);
600
- const { clause, params } = buildQueryWhereClause(table, options);
659
+ function buildSelectQuery(table, options = {}, whereNodes = []) {
660
+ const params = [];
661
+ const columns = buildSelectList(table, options.select, params);
662
+ const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
663
+ params.push(...whereParams);
664
+ const joins = buildJoinClause(options.joins);
665
+ const groupBy = buildGroupByClause(table.name, options.groupBy);
666
+ const havingClause = buildHavingClause(table.name, options.having, params);
601
667
  const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
602
668
  const limit = buildLimitClause(options.limit);
603
669
  const offset = buildOffsetClause(options.offset);
604
670
  return {
605
- text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${clause}${orderBy}${limit}${offset}`,
671
+ text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
606
672
  params
607
673
  };
608
674
  }
609
- function buildCountQuery(table, where = {}, options = {}) {
610
- const { clause, params } = buildQueryWhereClause(table, {
675
+ function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
676
+ const params = [];
677
+ const { clause, params: whereParams } = buildQueryWhereClause(table, {
611
678
  where,
612
- ...options
613
- });
679
+ withTrashed: options.withTrashed,
680
+ onlyTrashed: options.onlyTrashed
681
+ }, whereNodes);
682
+ params.push(...whereParams);
683
+ const joins = buildJoinClause(options.joins);
684
+ const groupBy = buildGroupByClause(table.name, options.groupBy);
614
685
  return {
615
- text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${clause}`,
686
+ text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
616
687
  params
617
688
  };
618
689
  }
619
- function buildProjectionQuery(table, expression, alias, options = {}) {
620
- const { clause, params } = buildQueryWhereClause(table, options);
690
+ function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
691
+ assertSafeProjectionExpression(expression);
692
+ const params = [];
693
+ const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
694
+ params.push(...whereParams);
695
+ const joins = buildJoinClause(options.joins);
696
+ const groupBy = buildGroupByClause(table.name, options.groupBy);
621
697
  const orderBy = buildOrderByClause(table.name, options.orderBy);
622
698
  const limit = buildLimitClause(options.limit);
623
699
  return {
624
- text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${clause}${orderBy}${limit}`,
700
+ text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
625
701
  params
626
702
  };
627
703
  }
704
+ var SAFE_PROJECTION_EXPRESSION = /^(COUNT\(\*\)|(?:"[\w_]+"(?:\."[\w_]+")?)|(?:AVG|SUM|MIN|MAX)\((?:"[\w_]+"(?:\."[\w_]+")?)\))$/;
705
+ function assertSafeProjectionExpression(expression) {
706
+ if (!SAFE_PROJECTION_EXPRESSION.test(expression.trim())) {
707
+ throw new Error(`Unsafe projection expression: ${expression}`);
708
+ }
709
+ }
628
710
  function buildGroupedCountQuery(table, column, where = {}, options = {}) {
629
711
  const qualifiedColumn = qualifyColumn(table.name, column);
630
712
  const { clause, params } = buildQueryWhereClause(table, {
@@ -708,12 +790,24 @@ function hasMany(definition) {
708
790
  ...definition
709
791
  };
710
792
  }
793
+ function hasOne(definition) {
794
+ return {
795
+ type: "hasOne",
796
+ ...definition
797
+ };
798
+ }
711
799
  function belongsTo(definition) {
712
800
  return {
713
801
  type: "belongsTo",
714
802
  ...definition
715
803
  };
716
804
  }
805
+ function belongsToMany(definition) {
806
+ return {
807
+ type: "belongsToMany",
808
+ ...definition
809
+ };
810
+ }
717
811
  function indexHasManyRelation(parents, children, relation) {
718
812
  const groups = new Map;
719
813
  for (const parent of parents) {
@@ -729,6 +823,15 @@ function indexHasManyRelation(parents, children, relation) {
729
823
  }
730
824
  return groups;
731
825
  }
826
+ function indexHasOneRelation(parents, children, relation) {
827
+ const grouped = indexHasManyRelation(parents, children, relation);
828
+ const result = new Map;
829
+ for (const parent of parents) {
830
+ const matches = grouped.get(parent[relation.localKey]) ?? [];
831
+ result.set(parent[relation.localKey], matches[0]);
832
+ }
833
+ return result;
834
+ }
732
835
  function indexBelongsToRelation(children, parents, relation) {
733
836
  const parentsById = new Map;
734
837
  for (const parent of parents) {
@@ -744,6 +847,149 @@ function indexBelongsToRelation(children, parents, relation) {
744
847
  }
745
848
  return result;
746
849
  }
850
+ function indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation) {
851
+ const relatedById = new Map;
852
+ for (const related of relatedRows) {
853
+ relatedById.set(related[relation.relatedKey], related);
854
+ }
855
+ const groups = new Map;
856
+ for (const parent of parents) {
857
+ groups.set(parent[relation.parentKey], []);
858
+ }
859
+ for (const pivot of pivotRows) {
860
+ const parentId = pivot[relation.foreignPivotKey];
861
+ const relatedId = pivot[relation.relatedPivotKey];
862
+ const group = groups.get(parentId);
863
+ const related = relatedById.get(relatedId);
864
+ if (!group || !related) {
865
+ continue;
866
+ }
867
+ group.push(related);
868
+ }
869
+ return groups;
870
+ }
871
+
872
+ // ../../src/config/database.ts
873
+ function readInteger(name, fallback) {
874
+ const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
875
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
876
+ }
877
+ var databaseConfig = {
878
+ url: process.env.DATABASE_URL ?? "",
879
+ poolMax: readInteger("DB_POOL_MAX", 10),
880
+ idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
881
+ maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
882
+ connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
883
+ };
884
+
885
+ // ../../src/core/database/connectionContext.ts
886
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
887
+ var activeConnection = new AsyncLocalStorage2;
888
+ function runWithDatabaseConnection(connection, callback) {
889
+ return activeConnection.run(connection, callback);
890
+ }
891
+ function getActiveDatabaseConnection(fallback) {
892
+ return activeConnection.getStore() ?? fallback;
893
+ }
894
+
895
+ // ../../src/db/connection/createConnection.ts
896
+ var {SQL } = globalThis.Bun;
897
+ function createDatabaseConnection(config) {
898
+ if (!config.url) {
899
+ throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
900
+ }
901
+ return new SQL({
902
+ url: config.url,
903
+ max: config.poolMax,
904
+ idleTimeout: config.idleTimeoutSeconds,
905
+ maxLifetime: config.maxLifetimeSeconds,
906
+ connectionTimeout: config.connectionTimeoutSeconds
907
+ });
908
+ }
909
+
910
+ // ../../src/db/connection/index.ts
911
+ var connectionHolder = {
912
+ connection: null
913
+ };
914
+ function getDatabase() {
915
+ if (!connectionHolder.connection) {
916
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
917
+ }
918
+ return connectionHolder.connection;
919
+ }
920
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
921
+ function resolveDatabase() {
922
+ return getActiveDatabaseConnection(getDatabase());
923
+ }
924
+ function resolveDatabaseForProperty(property) {
925
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
926
+ return getDatabase();
927
+ }
928
+ return resolveDatabase();
929
+ }
930
+ var db = new Proxy(function database() {}, {
931
+ apply(_target, _thisArg, args) {
932
+ return resolveDatabase()(...args);
933
+ },
934
+ get(_target, property) {
935
+ const connection = resolveDatabaseForProperty(property);
936
+ const value = connection[property];
937
+ return typeof value === "function" ? value.bind(connection) : value;
938
+ }
939
+ });
940
+ var connection_default = db;
941
+
942
+ // ../../src/core/database/boundConnection.ts
943
+ var boundConnectionHolder = {
944
+ connection: null
945
+ };
946
+ function bindDatabaseConnection(connection) {
947
+ boundConnectionHolder.connection = connection;
948
+ }
949
+ function getBoundDatabaseConnection() {
950
+ return boundConnectionHolder.connection;
951
+ }
952
+
953
+ // ../../src/core/database/repositoryConnection.ts
954
+ function resolveRepositoryConnection() {
955
+ return getBoundDatabaseConnection() ?? connection_default;
956
+ }
957
+ var repositoryConnection = new Proxy({}, {
958
+ get(_target, property) {
959
+ const connection = resolveRepositoryConnection();
960
+ const value = connection[property];
961
+ return typeof value === "function" ? value.bind(connection) : value;
962
+ }
963
+ });
964
+
965
+ // ../../src/core/database/whereBuilder.ts
966
+ class WhereBuilder {
967
+ nodes = [];
968
+ where(where) {
969
+ this.nodes.push({ kind: "and", where });
970
+ return this;
971
+ }
972
+ orWhere(where) {
973
+ this.nodes.push({ kind: "or", where });
974
+ return this;
975
+ }
976
+ whereGroup(fn) {
977
+ const nested = new WhereBuilder;
978
+ fn(nested);
979
+ if (nested.nodes.length > 0) {
980
+ this.nodes.push({ kind: "and", group: nested.nodes });
981
+ }
982
+ return this;
983
+ }
984
+ orWhereGroup(fn) {
985
+ const nested = new WhereBuilder;
986
+ fn(nested);
987
+ if (nested.nodes.length > 0) {
988
+ this.nodes.push({ kind: "or", group: nested.nodes });
989
+ }
990
+ return this;
991
+ }
992
+ }
747
993
 
748
994
  // ../../src/core/database/repositoryQuery.ts
749
995
  class RepositoryQuery {
@@ -751,13 +997,32 @@ class RepositoryQuery {
751
997
  whereClause;
752
998
  queryOptions;
753
999
  eagerLoads = [];
1000
+ whereNodes = [];
754
1001
  constructor(repository, whereClause = {}, queryOptions = {}) {
755
1002
  this.repository = repository;
756
1003
  this.whereClause = whereClause;
757
1004
  this.queryOptions = queryOptions;
758
1005
  }
759
- where(where) {
760
- this.whereClause = { ...this.whereClause, ...where };
1006
+ where(input) {
1007
+ if (typeof input === "function") {
1008
+ const builder = new WhereBuilder;
1009
+ input(builder);
1010
+ this.whereNodes.push(...builder.nodes);
1011
+ return this;
1012
+ }
1013
+ this.whereClause = { ...this.whereClause, ...input };
1014
+ return this;
1015
+ }
1016
+ orWhere(input) {
1017
+ if (typeof input === "function") {
1018
+ const builder = new WhereBuilder;
1019
+ input(builder);
1020
+ if (builder.nodes.length > 0) {
1021
+ this.whereNodes.push({ kind: "or", group: builder.nodes });
1022
+ }
1023
+ return this;
1024
+ }
1025
+ this.whereNodes.push({ kind: "or", where: input });
761
1026
  return this;
762
1027
  }
763
1028
  orderBy(orderBy) {
@@ -768,6 +1033,24 @@ class RepositoryQuery {
768
1033
  this.queryOptions = { ...this.queryOptions, limit };
769
1034
  return this;
770
1035
  }
1036
+ offset(offset) {
1037
+ this.queryOptions = { ...this.queryOptions, offset };
1038
+ return this;
1039
+ }
1040
+ join(left, right) {
1041
+ return this.addJoin("inner", left, right);
1042
+ }
1043
+ leftJoin(left, right) {
1044
+ return this.addJoin("left", left, right);
1045
+ }
1046
+ groupBy(groupBy) {
1047
+ this.queryOptions = { ...this.queryOptions, groupBy };
1048
+ return this;
1049
+ }
1050
+ having(having) {
1051
+ this.queryOptions = { ...this.queryOptions, having };
1052
+ return this;
1053
+ }
771
1054
  withHasMany(as, relation, childRepository, options = {}) {
772
1055
  this.eagerLoads.push({
773
1056
  kind: "hasMany",
@@ -789,16 +1072,50 @@ class RepositoryQuery {
789
1072
  return this;
790
1073
  }
791
1074
  async get() {
792
- const rows = await this.repository.findAll({
793
- ...this.queryOptions,
794
- where: this.whereClause
795
- });
1075
+ const rows = await this.repository.findAll(this.buildOptions());
796
1076
  return await this.attach(rows);
797
1077
  }
798
1078
  async first() {
799
1079
  const rows = await this.get();
800
1080
  return rows[0] ?? null;
801
1081
  }
1082
+ async paginate(options) {
1083
+ return await this.repository.paginate({
1084
+ ...this.buildOptions(),
1085
+ page: options.page,
1086
+ perPage: options.perPage
1087
+ });
1088
+ }
1089
+ buildOptions() {
1090
+ return {
1091
+ ...this.queryOptions,
1092
+ where: this.whereClause,
1093
+ whereNodes: this.whereNodes
1094
+ };
1095
+ }
1096
+ addJoin(type, left, right) {
1097
+ const leftRef = parseQualifiedColumn(left);
1098
+ const rightRef = parseQualifiedColumn(right);
1099
+ const table = type === "inner" ? rightRef.table : rightRef.table;
1100
+ const joins = this.queryOptions.joins ?? [];
1101
+ const existing = joins.find((join) => join.table === table && join.type === type);
1102
+ if (existing) {
1103
+ existing.on.push({ left: leftRef, right: rightRef });
1104
+ return this;
1105
+ }
1106
+ this.queryOptions = {
1107
+ ...this.queryOptions,
1108
+ joins: [
1109
+ ...joins,
1110
+ {
1111
+ type,
1112
+ table,
1113
+ on: [{ left: leftRef, right: rightRef }]
1114
+ }
1115
+ ]
1116
+ };
1117
+ return this;
1118
+ }
802
1119
  async attach(rows) {
803
1120
  if (rows.length === 0 || this.eagerLoads.length === 0) {
804
1121
  return rows.map((row) => ({ ...row }));
@@ -829,27 +1146,30 @@ class RepositoryQuery {
829
1146
  class BaseRepository {
830
1147
  table;
831
1148
  connection;
832
- constructor(table, connection = connection_default) {
1149
+ constructor(table, connection = repositoryConnection) {
833
1150
  this.table = table;
834
1151
  this.connection = connection;
835
1152
  }
836
1153
  async findAll(options = {}) {
837
1154
  return await withDatabaseErrorHandling(async () => {
838
- const { text, params } = buildSelectQuery(this.table, options);
1155
+ const { whereNodes, ...queryOptions } = options;
1156
+ const { text, params } = buildSelectQuery(this.table, queryOptions, whereNodes ?? []);
839
1157
  return await this.connection.unsafe(text, params);
840
1158
  });
841
1159
  }
842
1160
  async paginate(options) {
843
- const where = options.where ?? {};
1161
+ const { whereNodes, where = {}, page, perPage, ...queryOptions } = options;
844
1162
  const total = await this.countWhere(where, {
845
1163
  withTrashed: options.withTrashed,
846
- onlyTrashed: options.onlyTrashed
847
- });
848
- const offset = (options.page - 1) * options.perPage;
849
- const { page, perPage, ...queryOptions } = options;
1164
+ onlyTrashed: options.onlyTrashed,
1165
+ joins: options.joins,
1166
+ groupBy: options.groupBy
1167
+ }, whereNodes);
1168
+ const offset = (page - 1) * perPage;
850
1169
  const data = await this.findAll({
851
1170
  ...queryOptions,
852
1171
  where,
1172
+ whereNodes,
853
1173
  limit: perPage,
854
1174
  offset
855
1175
  });
@@ -858,6 +1178,65 @@ class BaseRepository {
858
1178
  meta: buildPaginationMeta({ page, perPage, total })
859
1179
  };
860
1180
  }
1181
+ async chunk(count, callback, options = {}) {
1182
+ if (!Number.isInteger(count) || count <= 0) {
1183
+ throw new Error("Chunk size must be a positive integer.");
1184
+ }
1185
+ let offset = 0;
1186
+ while (true) {
1187
+ const rows = await this.findAll({
1188
+ ...options,
1189
+ limit: count,
1190
+ offset
1191
+ });
1192
+ if (rows.length === 0) {
1193
+ return;
1194
+ }
1195
+ const shouldContinue = await callback(rows);
1196
+ if (shouldContinue === false || rows.length < count) {
1197
+ return;
1198
+ }
1199
+ offset += count;
1200
+ }
1201
+ }
1202
+ async cursorPaginate(options) {
1203
+ const {
1204
+ perPage,
1205
+ cursor,
1206
+ cursorColumn = this.table.primaryKey,
1207
+ direction = "asc",
1208
+ where = {},
1209
+ whereNodes,
1210
+ ...queryOptions
1211
+ } = options;
1212
+ if (!Number.isInteger(perPage) || perPage <= 0) {
1213
+ throw new Error("Cursor page size must be a positive integer.");
1214
+ }
1215
+ const cursorWhere = { ...where };
1216
+ if (cursor !== undefined) {
1217
+ cursorWhere[cursorColumn] = direction === "asc" ? { gt: cursor } : { lt: cursor };
1218
+ }
1219
+ const rows = await this.findAll({
1220
+ ...queryOptions,
1221
+ where: cursorWhere,
1222
+ whereNodes,
1223
+ orderBy: { [cursorColumn]: direction },
1224
+ limit: perPage + 1
1225
+ });
1226
+ const hasMore = rows.length > perPage;
1227
+ const data = hasMore ? rows.slice(0, perPage) : rows;
1228
+ const nextCursor = hasMore ? data[data.length - 1]?.[cursorColumn] ?? null : null;
1229
+ const prevCursor = cursor ?? null;
1230
+ return {
1231
+ data,
1232
+ meta: {
1233
+ per_page: perPage,
1234
+ next_cursor: nextCursor,
1235
+ prev_cursor: prevCursor,
1236
+ has_more: hasMore
1237
+ }
1238
+ };
1239
+ }
861
1240
  async findById(id) {
862
1241
  return await this.firstOrNull({
863
1242
  [this.table.primaryKey]: id
@@ -964,14 +1343,17 @@ class BaseRepository {
964
1343
  getConnection() {
965
1344
  return this.connection;
966
1345
  }
1346
+ getTable() {
1347
+ return this.table;
1348
+ }
967
1349
  query(where = {}) {
968
1350
  return new RepositoryQuery(this, where);
969
1351
  }
970
1352
  async findWhere(where, options = {}) {
971
1353
  return await this.findAll({ ...options, where });
972
1354
  }
973
- async countWhere(where = {}, options = {}) {
974
- const { text, params } = buildCountQuery(this.table, where, options);
1355
+ async countWhere(where = {}, options = {}, whereNodes = []) {
1356
+ const { text, params } = buildCountQuery(this.table, where, options, whereNodes);
975
1357
  const [row] = await this.connection.unsafe(text, params);
976
1358
  return Number(row?.count ?? 0);
977
1359
  }
@@ -1028,8 +1410,18 @@ class BaseRepository {
1028
1410
  }
1029
1411
  var baseRepository_default = BaseRepository;
1030
1412
  // ../../src/core/database/bindConnection.ts
1031
- function bindDatabaseConnection(connection) {
1032
- resetDatabaseConnectionForTests(connection);
1413
+ function bindDatabaseConnection2(connection) {
1414
+ bindDatabaseConnection(connection);
1415
+ }
1416
+ // ../../src/core/database/migrations/advisoryLock.ts
1417
+ var MIGRATION_LOCK_KEY = 42424242;
1418
+ async function withMigrationLock(db2, callback, lockKey = MIGRATION_LOCK_KEY) {
1419
+ await db2.unsafe("SELECT pg_advisory_lock($1)", [lockKey]);
1420
+ try {
1421
+ return await callback();
1422
+ } finally {
1423
+ await db2.unsafe("SELECT pg_advisory_unlock($1)", [lockKey]);
1424
+ }
1033
1425
  }
1034
1426
  // ../../src/core/database/migrations/runner.ts
1035
1427
  import { readdir } from "fs/promises";
@@ -1055,7 +1447,7 @@ async function getAppliedMigrations(db2) {
1055
1447
  }
1056
1448
  async function loadMigrationsFromDirectory(directory) {
1057
1449
  const entries = await readdir(directory);
1058
- const migrationFiles = entries.filter((entry) => entry.endsWith(".ts") && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
1450
+ const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
1059
1451
  const loadedMigrations = await Promise.all(migrationFiles.map(async (fileName) => {
1060
1452
  const moduleUrl = pathToFileURL(join(directory, fileName)).href;
1061
1453
  const module = await import(moduleUrl);
@@ -1072,7 +1464,7 @@ async function getMigrationStatus(db2, migrations) {
1072
1464
  batch: appliedByName.get(name) ?? null
1073
1465
  }));
1074
1466
  }
1075
- async function migrateDatabase(db2, migrations, options = {}) {
1467
+ async function runPendingMigrations(db2, migrations, options = {}) {
1076
1468
  const applied = await getAppliedMigrations(db2);
1077
1469
  const appliedNames = new Set(applied.map(({ name }) => name));
1078
1470
  const nextBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0) + 1;
@@ -1080,10 +1472,20 @@ async function migrateDatabase(db2, migrations, options = {}) {
1080
1472
  for (const migration of pendingMigrations) {
1081
1473
  options.onMigration?.(migration.name);
1082
1474
  await migration.up(db2);
1083
- await db2.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name, batch) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING`, [migration.name, nextBatch]);
1475
+ const inserted = await db2.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name, batch) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING RETURNING name`, [migration.name, nextBatch]);
1476
+ if (inserted.length === 0) {
1477
+ throw new Error(`Migration ${migration.name} was applied but not recorded.`);
1478
+ }
1084
1479
  }
1085
1480
  return pendingMigrations.length;
1086
1481
  }
1482
+ async function migrateDatabase(db2, migrations, options = {}) {
1483
+ const { advisoryLock = false, onMigration } = options;
1484
+ if (advisoryLock) {
1485
+ return withMigrationLock(db2, () => runPendingMigrations(db2, migrations, { onMigration }));
1486
+ }
1487
+ return runPendingMigrations(db2, migrations, { onMigration });
1488
+ }
1087
1489
  async function rollbackDatabase(db2, migrations, options = {}) {
1088
1490
  const applied = await getAppliedMigrations(db2);
1089
1491
  if (applied.length === 0) {
@@ -1104,20 +1506,29 @@ async function rollbackDatabase(db2, migrations, options = {}) {
1104
1506
  return rolledBack;
1105
1507
  }
1106
1508
  async function freshDatabase(db2, migrations, options = {}) {
1107
- const applied = await getAppliedMigrations(db2);
1108
- const appliedNames = new Set(applied.map(({ name }) => name));
1109
- const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
1110
- for (const migration of [...appliedMigrations].reverse()) {
1111
- options.onMigration?.(migration.name);
1112
- await migration.down(db2);
1113
- }
1114
- if (appliedMigrations.length > 0) {
1115
- await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
1509
+ const runFresh = async () => {
1510
+ const applied = await getAppliedMigrations(db2);
1511
+ const appliedNames = new Set(applied.map(({ name }) => name));
1512
+ const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
1513
+ for (const migration of [...appliedMigrations].reverse()) {
1514
+ options.onMigration?.(migration.name);
1515
+ await migration.down(db2);
1516
+ }
1517
+ if (appliedMigrations.length > 0) {
1518
+ await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
1519
+ }
1520
+ await runPendingMigrations(db2, migrations, options);
1521
+ };
1522
+ if (options.advisoryLock) {
1523
+ await withMigrationLock(db2, runFresh);
1524
+ return;
1116
1525
  }
1117
- await migrateDatabase(db2, migrations, options);
1526
+ await runFresh();
1118
1527
  }
1119
1528
  // ../../src/core/database/model.ts
1120
1529
  var modelRepositories = new WeakMap;
1530
+ var modelGlobalScopes = new WeakMap;
1531
+ var modelBooted = new WeakSet;
1121
1532
  function resolveModelRepository(model) {
1122
1533
  const repository = modelRepositories.get(model);
1123
1534
  if (!repository) {
@@ -1125,13 +1536,121 @@ function resolveModelRepository(model) {
1125
1536
  }
1126
1537
  return repository;
1127
1538
  }
1539
+ function modelStatics(model) {
1540
+ return model;
1541
+ }
1542
+ function ensureBooted(model) {
1543
+ if (modelBooted.has(model)) {
1544
+ return;
1545
+ }
1546
+ modelBooted.add(model);
1547
+ const boot = model.boot;
1548
+ if (typeof boot === "function") {
1549
+ boot.call(model);
1550
+ }
1551
+ }
1552
+ function getGlobalScopes(model) {
1553
+ return modelGlobalScopes.get(model) ?? [];
1554
+ }
1555
+ function hydrateValue(value, cast) {
1556
+ if (value === null || value === undefined) {
1557
+ return value;
1558
+ }
1559
+ switch (cast) {
1560
+ case "date":
1561
+ case "datetime":
1562
+ return value instanceof Date ? value : new Date(String(value));
1563
+ case "json":
1564
+ return typeof value === "string" ? JSON.parse(value) : value;
1565
+ case "bool":
1566
+ case "boolean":
1567
+ return value === true || value === 1 || value === "1" || value === "true";
1568
+ default:
1569
+ return value;
1570
+ }
1571
+ }
1572
+ function dehydrateValue(value, cast) {
1573
+ if (value === null || value === undefined) {
1574
+ return value;
1575
+ }
1576
+ switch (cast) {
1577
+ case "date":
1578
+ case "datetime":
1579
+ return value instanceof Date ? value : new Date(String(value));
1580
+ case "json":
1581
+ return typeof value === "string" ? value : JSON.stringify(value);
1582
+ case "bool":
1583
+ case "boolean":
1584
+ return Boolean(value);
1585
+ default:
1586
+ return value;
1587
+ }
1588
+ }
1589
+ function filterMassAssignable(fillable, guarded, input) {
1590
+ const resolvedGuarded = guarded ?? true;
1591
+ if (fillable && fillable.length > 0) {
1592
+ const allowed = new Set(fillable);
1593
+ return Object.fromEntries(Object.entries(input).filter(([key]) => allowed.has(key)));
1594
+ }
1595
+ if (resolvedGuarded === true || resolvedGuarded.includes("*")) {
1596
+ return {};
1597
+ }
1598
+ const blocked = new Set(resolvedGuarded);
1599
+ return Object.fromEntries(Object.entries(input).filter(([key]) => !blocked.has(key)));
1600
+ }
1601
+ function applyCasts(values, casts, direction) {
1602
+ if (Object.keys(casts).length === 0) {
1603
+ return values;
1604
+ }
1605
+ const result = { ...values };
1606
+ const castFn = direction === "hydrate" ? hydrateValue : dehydrateValue;
1607
+ for (const [key, cast] of Object.entries(casts)) {
1608
+ if (key in result && cast) {
1609
+ result[key] = castFn(result[key], cast);
1610
+ }
1611
+ }
1612
+ return result;
1613
+ }
1614
+ function applyTimestampsOnCreate(columns, values, enabled) {
1615
+ if (!enabled) {
1616
+ return values;
1617
+ }
1618
+ const now = new Date;
1619
+ const result = { ...values };
1620
+ if (columns.includes("created_at")) {
1621
+ result.created_at = now;
1622
+ }
1623
+ if (columns.includes("updated_at")) {
1624
+ result.updated_at = now;
1625
+ }
1626
+ return result;
1627
+ }
1628
+ function applyTimestampsOnUpdate(columns, values, enabled) {
1629
+ if (!enabled) {
1630
+ return values;
1631
+ }
1632
+ const result = { ...values };
1633
+ if (columns.includes("updated_at")) {
1634
+ result.updated_at = new Date;
1635
+ }
1636
+ return result;
1637
+ }
1128
1638
 
1129
1639
  class Model {
1130
1640
  attributes;
1131
1641
  repository;
1132
- constructor(attributes, repository) {
1642
+ static $fillable;
1643
+ static $guarded;
1644
+ static $casts = {};
1645
+ static $timestamps = true;
1646
+ _exists;
1647
+ constructor(attributes, repository, exists = true) {
1133
1648
  this.attributes = attributes;
1134
1649
  this.repository = repository;
1650
+ this._exists = exists;
1651
+ }
1652
+ get $exists() {
1653
+ return this._exists;
1135
1654
  }
1136
1655
  get(key) {
1137
1656
  return this.attributes[key];
@@ -1145,43 +1664,169 @@ class Model {
1145
1664
  primaryKey() {
1146
1665
  throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
1147
1666
  }
1667
+ static primaryKeyField() {
1668
+ return resolveModelRepository(Model).getTable().primaryKey;
1669
+ }
1670
+ static hydrateAttributes(attributes) {
1671
+ const casts = Model.$casts ?? {};
1672
+ return applyCasts(attributes, casts, "hydrate");
1673
+ }
1674
+ static dehydrateAttributes(attributes) {
1675
+ const casts = Model.$casts ?? {};
1676
+ return applyCasts(attributes, casts, "dehydrate");
1677
+ }
1678
+ static fromRecord(record, repository, exists = true) {
1679
+ const hydrated = Model.hydrateAttributes(record);
1680
+ return new Model(hydrated, repository, exists);
1681
+ }
1682
+ static boot() {}
1683
+ static addGlobalScope(_name, scope) {
1684
+ ensureBooted(Model);
1685
+ const existing = modelGlobalScopes.get(Model) ?? [];
1686
+ modelGlobalScopes.set(Model, [
1687
+ ...existing,
1688
+ scope
1689
+ ]);
1690
+ }
1148
1691
  static repository() {
1149
1692
  return resolveModelRepository(this);
1150
1693
  }
1151
1694
  static query() {
1152
- return resolveModelRepository(this).query();
1695
+ ensureBooted(this);
1696
+ const repository = resolveModelRepository(Model);
1697
+ let query = repository.query();
1698
+ for (const scope of getGlobalScopes(Model)) {
1699
+ query = scope(query);
1700
+ }
1701
+ return query;
1702
+ }
1703
+ static async create(attributes) {
1704
+ const statics = modelStatics(this);
1705
+ ensureBooted(Model);
1706
+ const repository = resolveModelRepository(Model);
1707
+ const table = repository.getTable();
1708
+ const timestamps = statics.$timestamps ?? true;
1709
+ const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
1710
+ const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
1711
+ const payload = statics.dehydrateAttributes(withTimestamps);
1712
+ const record = await repository.create(payload);
1713
+ return statics.fromRecord(record, repository, true);
1153
1714
  }
1154
1715
  static async find(id) {
1155
- const repository = resolveModelRepository(this);
1156
- const record = await repository.findById(id);
1157
- return record ? new this(record, repository) : null;
1716
+ const statics = modelStatics(this);
1717
+ const repository = resolveModelRepository(Model);
1718
+ const primaryKey = statics.primaryKeyField();
1719
+ const record = await Model.query.call(Model).where({ [primaryKey]: id }).first();
1720
+ return record ? statics.fromRecord(record, repository, true) : null;
1158
1721
  }
1159
1722
  static async findOrFail(id, errorFactory) {
1160
- const repository = resolveModelRepository(this);
1161
- const record = await repository.findByIdOrThrow(id, errorFactory ?? ((value) => new Error(`Record ${String(value)} not found.`)));
1162
- return new this(record, repository);
1723
+ const model = await Model.find.call(Model, id);
1724
+ if (model) {
1725
+ return model;
1726
+ }
1727
+ throw errorFactory?.(id) ?? new NotFoundError(`${Model.name} ${String(id)} not found.`);
1163
1728
  }
1164
1729
  static async all(options = {}) {
1165
- const repository = resolveModelRepository(this);
1166
- const rows = await repository.findAll(options);
1167
- return rows.map((row) => new this(row, repository));
1730
+ const statics = modelStatics(this);
1731
+ const repository = resolveModelRepository(Model);
1732
+ let query = Model.query.call(Model);
1733
+ if (options.orderBy) {
1734
+ query = query.orderBy(options.orderBy);
1735
+ }
1736
+ if (options.limit !== undefined) {
1737
+ query = query.limit(options.limit);
1738
+ }
1739
+ const rows = await query.get();
1740
+ return rows.map((row) => statics.fromRecord(row, repository, true));
1168
1741
  }
1169
1742
  static async firstWhere(where, options = {}) {
1170
- const repository = resolveModelRepository(this);
1171
- const rows = await repository.findAll({ ...options, where, limit: 1 });
1172
- const record = rows[0];
1173
- return record ? new this(record, repository) : null;
1743
+ const statics = modelStatics(this);
1744
+ const repository = resolveModelRepository(Model);
1745
+ let query = Model.query.call(Model).where(where);
1746
+ if (options.orderBy) {
1747
+ query = query.orderBy(options.orderBy);
1748
+ }
1749
+ const record = await query.first();
1750
+ return record ? statics.fromRecord(record, repository, true) : null;
1751
+ }
1752
+ async save() {
1753
+ const ModelClass = modelStatics(this.constructor);
1754
+ const timestamps = ModelClass.$timestamps ?? true;
1755
+ const casts = ModelClass.$casts ?? {};
1756
+ const table = this.repository.getTable();
1757
+ if (this.$exists) {
1758
+ const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
1759
+ const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
1760
+ this.attributes = ModelClass.hydrateAttributes(record2);
1761
+ return this;
1762
+ }
1763
+ const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
1764
+ const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
1765
+ const payload = ModelClass.dehydrateAttributes(withTimestamps);
1766
+ const record = await this.repository.create(payload);
1767
+ this.attributes = ModelClass.hydrateAttributes(record);
1768
+ this._exists = true;
1769
+ return this;
1770
+ }
1771
+ async update(changes) {
1772
+ const ModelClass = modelStatics(this.constructor);
1773
+ const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, changes);
1774
+ Object.assign(this.attributes, assignable);
1775
+ return await this.save();
1776
+ }
1777
+ async delete() {
1778
+ if (resolveSoftDeleteColumn(this.repository.getTable())) {
1779
+ return await this.repository.deleteById(this.id);
1780
+ }
1781
+ return await this.repository.forceDeleteById(this.id);
1782
+ }
1783
+ async forceDelete() {
1784
+ return await this.repository.forceDeleteById(this.id);
1785
+ }
1786
+ async restore() {
1787
+ const ModelClass = modelStatics(this.constructor);
1788
+ const record = await this.repository.restoreById(this.id);
1789
+ if (!record) {
1790
+ return null;
1791
+ }
1792
+ this.attributes = ModelClass.hydrateAttributes(record);
1793
+ return this;
1174
1794
  }
1175
1795
  async loadHasMany(as, relation, childRepository, options = {}) {
1176
1796
  const grouped = await childRepository.withConnection(this.repository.getConnection()).loadHasManyForParents([this.attributes], relation, options);
1177
1797
  const loaded = grouped.get(this.attributes[relation.localKey]) ?? [];
1178
1798
  return Object.assign(this, { [as]: loaded });
1179
1799
  }
1800
+ async loadHasOne(as, relation, childRepository, options = {}) {
1801
+ const loaded = await this.loadHasMany(as, relation, childRepository, { ...options, limit: 1 });
1802
+ const value = loaded[as]?.[0];
1803
+ return Object.assign(this, { [as]: value });
1804
+ }
1180
1805
  async loadBelongsTo(as, relation, parentRepository, options = {}) {
1181
1806
  const grouped = await this.repository.loadBelongsToForParents([this.attributes], relation, parentRepository, options);
1182
1807
  const loaded = grouped.get(this.attributes[relation.foreignKey]);
1183
1808
  return Object.assign(this, { [as]: loaded });
1184
1809
  }
1810
+ async loadBelongsToMany(as, relation, relatedRepository, options = {}) {
1811
+ const connection = this.repository.getConnection();
1812
+ const parentId = this.attributes[relation.parentKey];
1813
+ const pivotRows = await connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = $1`, [parentId]);
1814
+ if (pivotRows.length === 0) {
1815
+ return Object.assign(this, { [as]: [] });
1816
+ }
1817
+ const relatedIds = [
1818
+ ...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
1819
+ ];
1820
+ const relatedRows = await relatedRepository.withConnection(connection).findAll({
1821
+ ...options,
1822
+ where: {
1823
+ [relation.relatedKey]: relatedIds
1824
+ }
1825
+ });
1826
+ const grouped = indexBelongsToManyRelation([this.attributes], pivotRows, relatedRows, relation);
1827
+ const loaded = grouped.get(parentId) ?? [];
1828
+ return Object.assign(this, { [as]: loaded });
1829
+ }
1185
1830
  mergeAttributes(patch) {
1186
1831
  Object.assign(this.attributes, patch);
1187
1832
  return this;
@@ -1189,12 +1834,676 @@ class Model {
1189
1834
  }
1190
1835
  function registerModelRepository(model, repository) {
1191
1836
  modelRepositories.set(model, repository);
1837
+ ensureBooted(model);
1192
1838
  return model;
1193
1839
  }
1840
+ // ../../src/core/database/schema/columnDefinition.ts
1841
+ class ColumnDefinition {
1842
+ name;
1843
+ kind;
1844
+ length;
1845
+ isNullable = false;
1846
+ isPrimary = false;
1847
+ isUnique = false;
1848
+ autoIncrement = false;
1849
+ defaultValue;
1850
+ checkExpression;
1851
+ foreignKey;
1852
+ constructor(name, kind) {
1853
+ this.name = name;
1854
+ this.kind = kind;
1855
+ }
1856
+ nullable() {
1857
+ this.isNullable = true;
1858
+ return this;
1859
+ }
1860
+ notNullable() {
1861
+ this.isNullable = false;
1862
+ return this;
1863
+ }
1864
+ default(value) {
1865
+ if (typeof value === "boolean") {
1866
+ this.defaultValue = value ? "TRUE" : "FALSE";
1867
+ return this;
1868
+ }
1869
+ if (typeof value === "number") {
1870
+ this.defaultValue = String(value);
1871
+ return this;
1872
+ }
1873
+ this.defaultValue = `'${value.replace(/'/g, "''")}'`;
1874
+ return this;
1875
+ }
1876
+ defaultRaw(expression) {
1877
+ this.defaultValue = expression;
1878
+ return this;
1879
+ }
1880
+ unique() {
1881
+ this.isUnique = true;
1882
+ return this;
1883
+ }
1884
+ primary() {
1885
+ this.isPrimary = true;
1886
+ return this;
1887
+ }
1888
+ check(expression) {
1889
+ this.checkExpression = expression;
1890
+ return this;
1891
+ }
1892
+ }
1893
+
1894
+ class ForeignIdColumnDefinition extends ColumnDefinition {
1895
+ constructor(name) {
1896
+ super(name, "foreignId");
1897
+ this.notNullable();
1898
+ }
1899
+ references(table, column = "id") {
1900
+ this.foreignKey = {
1901
+ referencesTable: table,
1902
+ referencesColumn: column
1903
+ };
1904
+ return this;
1905
+ }
1906
+ constrained(table) {
1907
+ const referencesTable = table ?? inferReferencedTable(this.name);
1908
+ return this.references(referencesTable);
1909
+ }
1910
+ cascadeOnDelete() {
1911
+ if (!this.foreignKey) {
1912
+ throw new Error(`Foreign key is not defined for column ${this.name}`);
1913
+ }
1914
+ this.foreignKey.onDelete = "cascade";
1915
+ return this;
1916
+ }
1917
+ nullOnDelete() {
1918
+ if (!this.foreignKey) {
1919
+ throw new Error(`Foreign key is not defined for column ${this.name}`);
1920
+ }
1921
+ this.foreignKey.onDelete = "set null";
1922
+ return this;
1923
+ }
1924
+ }
1925
+ function inferReferencedTable(columnName) {
1926
+ if (!columnName.endsWith("_id")) {
1927
+ throw new Error(`Cannot infer referenced table from column ${columnName}`);
1928
+ }
1929
+ return columnName.slice(0, -3);
1930
+ }
1931
+
1932
+ // ../../src/core/database/schema/blueprint.ts
1933
+ class Blueprint {
1934
+ table;
1935
+ action;
1936
+ columns = [];
1937
+ indexes = [];
1938
+ droppedColumns = [];
1939
+ droppedIndexes = [];
1940
+ constructor(table, action) {
1941
+ this.table = table;
1942
+ this.action = action;
1943
+ }
1944
+ id(name = "id") {
1945
+ const column = new ColumnDefinition(name, "id");
1946
+ column.primary();
1947
+ column.autoIncrement = true;
1948
+ this.columns.push(column);
1949
+ return column;
1950
+ }
1951
+ string(name, length) {
1952
+ const column = new ColumnDefinition(name, "string");
1953
+ column.length = length;
1954
+ column.notNullable();
1955
+ this.columns.push(column);
1956
+ return column;
1957
+ }
1958
+ text(name) {
1959
+ const column = new ColumnDefinition(name, "text");
1960
+ column.notNullable();
1961
+ this.columns.push(column);
1962
+ return column;
1963
+ }
1964
+ boolean(name) {
1965
+ const column = new ColumnDefinition(name, "boolean");
1966
+ column.notNullable();
1967
+ this.columns.push(column);
1968
+ return column;
1969
+ }
1970
+ integer(name) {
1971
+ const column = new ColumnDefinition(name, "integer");
1972
+ column.notNullable();
1973
+ this.columns.push(column);
1974
+ return column;
1975
+ }
1976
+ bigInteger(name) {
1977
+ const column = new ColumnDefinition(name, "bigInteger");
1978
+ column.notNullable();
1979
+ this.columns.push(column);
1980
+ return column;
1981
+ }
1982
+ timestamp(name) {
1983
+ const column = new ColumnDefinition(name, "timestamp");
1984
+ column.notNullable();
1985
+ this.columns.push(column);
1986
+ return column;
1987
+ }
1988
+ json(name) {
1989
+ const column = new ColumnDefinition(name, "json");
1990
+ column.notNullable();
1991
+ this.columns.push(column);
1992
+ return column;
1993
+ }
1994
+ jsonb(name) {
1995
+ const column = new ColumnDefinition(name, "jsonb");
1996
+ column.notNullable();
1997
+ this.columns.push(column);
1998
+ return column;
1999
+ }
2000
+ foreignId(name) {
2001
+ const column = new ForeignIdColumnDefinition(name);
2002
+ this.columns.push(column);
2003
+ return column;
2004
+ }
2005
+ timestamps() {
2006
+ this.timestamp("created_at").defaultRaw("NOW()");
2007
+ this.timestamp("updated_at").defaultRaw("NOW()");
2008
+ }
2009
+ softDeletes() {
2010
+ this.timestamp("deleted_at").nullable();
2011
+ }
2012
+ dropColumn(name) {
2013
+ this.droppedColumns.push(name);
2014
+ }
2015
+ dropSoftDeletes() {
2016
+ this.dropColumn("deleted_at");
2017
+ this.dropIndex(`idx_${this.table}_deleted_at`);
2018
+ }
2019
+ dropIndex(name) {
2020
+ this.droppedIndexes.push(name);
2021
+ }
2022
+ unique(columns, name) {
2023
+ this.indexes.push({
2024
+ name,
2025
+ columns: Array.isArray(columns) ? columns : [columns],
2026
+ kind: "unique"
2027
+ });
2028
+ }
2029
+ index(columns, options = {}) {
2030
+ this.indexes.push({
2031
+ name: options.name,
2032
+ columns: Array.isArray(columns) ? columns : [columns],
2033
+ kind: "index",
2034
+ order: options.order
2035
+ });
2036
+ }
2037
+ partialIndex(columns, where, nameOrOptions) {
2038
+ const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions ?? {};
2039
+ this.indexes.push({
2040
+ name: options.name,
2041
+ columns: Array.isArray(columns) ? columns : [columns],
2042
+ kind: options.unique ? "uniquePartial" : "partial",
2043
+ where
2044
+ });
2045
+ }
2046
+ fullText(columns, name) {
2047
+ this.indexes.push({
2048
+ name,
2049
+ columns: Array.isArray(columns) ? columns : [columns],
2050
+ kind: "fullText"
2051
+ });
2052
+ }
2053
+ ginIndex(column, name) {
2054
+ this.indexes.push({
2055
+ name,
2056
+ columns: [column],
2057
+ kind: "gin"
2058
+ });
2059
+ }
2060
+ }
2061
+ // ../../src/core/database/schema/driver.ts
2062
+ function normalizeConnectionName(connection) {
2063
+ const normalized = connection.trim().toLowerCase();
2064
+ if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
2065
+ return "pgsql";
2066
+ }
2067
+ if (normalized === "mysql" || normalized === "mariadb") {
2068
+ return "mysql";
2069
+ }
2070
+ if (normalized === "sqlite") {
2071
+ return "sqlite";
2072
+ }
2073
+ throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
2074
+ }
2075
+ function resolveDriverFromUrl(url) {
2076
+ const normalized = url.trim().toLowerCase();
2077
+ if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
2078
+ return "pgsql";
2079
+ }
2080
+ if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
2081
+ return "mysql";
2082
+ }
2083
+ if (normalized.startsWith("sqlite:")) {
2084
+ return "sqlite";
2085
+ }
2086
+ return null;
2087
+ }
2088
+ function resolveDatabaseDriver(options = {}) {
2089
+ const connection = options.connection ?? process.env.DB_CONNECTION;
2090
+ if (connection) {
2091
+ return normalizeConnectionName(connection);
2092
+ }
2093
+ const url = options.url ?? process.env.DATABASE_URL ?? "";
2094
+ const fromUrl = resolveDriverFromUrl(url);
2095
+ if (fromUrl) {
2096
+ return fromUrl;
2097
+ }
2098
+ return "pgsql";
2099
+ }
2100
+ // ../../src/core/database/schema/errors.ts
2101
+ class UnsupportedSchemaFeatureError extends Error {
2102
+ constructor(feature, driver) {
2103
+ super(`${feature} is not supported for the ${driver} driver`);
2104
+ this.name = "UnsupportedSchemaFeatureError";
2105
+ }
2106
+ }
2107
+ // ../../src/core/database/schema/grammars/grammar.ts
2108
+ function compileColumnType(driver, column) {
2109
+ switch (column.kind) {
2110
+ case "id":
2111
+ return compileIdType(driver);
2112
+ case "string":
2113
+ return compileStringType(driver, column.length);
2114
+ case "text":
2115
+ return compileTextType(driver);
2116
+ case "boolean":
2117
+ return compileBooleanType(driver);
2118
+ case "integer":
2119
+ case "foreignId":
2120
+ return compileIntegerType(driver);
2121
+ case "bigInteger":
2122
+ return compileBigIntegerType(driver);
2123
+ case "timestamp":
2124
+ return compileTimestampType(driver);
2125
+ case "json":
2126
+ return compileJsonType(driver);
2127
+ case "jsonb":
2128
+ return compileJsonbType(driver);
2129
+ default:
2130
+ throw new Error(`Unsupported column kind: ${column.kind}`);
2131
+ }
2132
+ }
2133
+ function compileIdType(driver) {
2134
+ switch (driver) {
2135
+ case "pgsql":
2136
+ return "SERIAL";
2137
+ case "mysql":
2138
+ return "BIGINT UNSIGNED";
2139
+ case "sqlite":
2140
+ return "INTEGER";
2141
+ }
2142
+ }
2143
+ function compileStringType(driver, length) {
2144
+ switch (driver) {
2145
+ case "pgsql":
2146
+ return "TEXT";
2147
+ case "mysql":
2148
+ return length ? `VARCHAR(${length})` : "VARCHAR(255)";
2149
+ case "sqlite":
2150
+ return "TEXT";
2151
+ }
2152
+ }
2153
+ function compileTextType(driver) {
2154
+ switch (driver) {
2155
+ case "pgsql":
2156
+ case "sqlite":
2157
+ return "TEXT";
2158
+ case "mysql":
2159
+ return "TEXT";
2160
+ }
2161
+ }
2162
+ function compileBooleanType(driver) {
2163
+ switch (driver) {
2164
+ case "pgsql":
2165
+ return "BOOLEAN";
2166
+ case "mysql":
2167
+ return "BOOLEAN";
2168
+ case "sqlite":
2169
+ return "INTEGER";
2170
+ }
2171
+ }
2172
+ function compileIntegerType(driver) {
2173
+ switch (driver) {
2174
+ case "pgsql":
2175
+ return "INTEGER";
2176
+ case "mysql":
2177
+ return "INT";
2178
+ case "sqlite":
2179
+ return "INTEGER";
2180
+ }
2181
+ }
2182
+ function compileBigIntegerType(driver) {
2183
+ switch (driver) {
2184
+ case "pgsql":
2185
+ return "BIGINT";
2186
+ case "mysql":
2187
+ return "BIGINT";
2188
+ case "sqlite":
2189
+ return "INTEGER";
2190
+ }
2191
+ }
2192
+ function compileTimestampType(driver) {
2193
+ switch (driver) {
2194
+ case "pgsql":
2195
+ return "TIMESTAMPTZ";
2196
+ case "mysql":
2197
+ return "TIMESTAMP";
2198
+ case "sqlite":
2199
+ return "TEXT";
2200
+ }
2201
+ }
2202
+ function compileJsonType(driver) {
2203
+ switch (driver) {
2204
+ case "pgsql":
2205
+ return "JSONB";
2206
+ case "mysql":
2207
+ return "JSON";
2208
+ case "sqlite":
2209
+ return "TEXT";
2210
+ }
2211
+ }
2212
+ function compileJsonbType(driver) {
2213
+ switch (driver) {
2214
+ case "pgsql":
2215
+ return "JSONB";
2216
+ case "mysql":
2217
+ return "JSON";
2218
+ case "sqlite":
2219
+ return "TEXT";
2220
+ }
2221
+ }
2222
+
2223
+ // ../../src/core/database/schema/grammars/compileStatements.ts
2224
+ function compileCreateTable(driver, blueprint) {
2225
+ const table = quoteIdentifier(blueprint.table);
2226
+ const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
2227
+ for (const index of blueprint.indexes) {
2228
+ if (index.kind === "unique" && index.columns.length > 1) {
2229
+ const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
2230
+ parts.push(`UNIQUE (${columns})`);
2231
+ }
2232
+ }
2233
+ const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
2234
+ ${parts.join(`,
2235
+ `)}
2236
+ )`];
2237
+ for (const index of blueprint.indexes) {
2238
+ if (index.kind === "unique" && index.columns.length === 1) {
2239
+ continue;
2240
+ }
2241
+ if (index.kind === "index") {
2242
+ statements.push(compileIndex(driver, blueprint.table, index));
2243
+ } else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
2244
+ statements.push(...compileSpecialIndex(driver, blueprint.table, index));
2245
+ }
2246
+ }
2247
+ return statements;
2248
+ }
2249
+ function compileAlterTable(driver, blueprint) {
2250
+ const statements = [];
2251
+ const table = quoteIdentifier(blueprint.table);
2252
+ for (const column of blueprint.columns) {
2253
+ const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
2254
+ statements.push(`ALTER TABLE ${table}
2255
+ ${addPrefix} ${compileColumn(driver, column, "alter")}`);
2256
+ }
2257
+ for (const columnName of blueprint.droppedColumns) {
2258
+ const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
2259
+ statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
2260
+ }
2261
+ for (const indexName of blueprint.droppedIndexes) {
2262
+ statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
2263
+ }
2264
+ for (const index of blueprint.indexes) {
2265
+ if (index.kind === "index" || index.kind === "unique") {
2266
+ statements.push(compileIndex(driver, blueprint.table, index));
2267
+ } else {
2268
+ statements.push(...compileSpecialIndex(driver, blueprint.table, index));
2269
+ }
2270
+ }
2271
+ return statements;
2272
+ }
2273
+ function compileDropTable(driver, tableName) {
2274
+ const cascade = driver === "pgsql" ? " CASCADE" : "";
2275
+ return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
2276
+ }
2277
+ function compileColumn(driver, column, mode) {
2278
+ const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
2279
+ if (column.autoIncrement && driver === "mysql") {
2280
+ parts[1] = `${parts[1]} AUTO_INCREMENT`;
2281
+ }
2282
+ if (column.isPrimary && mode === "create") {
2283
+ if (driver === "sqlite") {
2284
+ parts.push("PRIMARY KEY AUTOINCREMENT");
2285
+ } else {
2286
+ parts.push("PRIMARY KEY");
2287
+ }
2288
+ } else if (!column.isNullable) {
2289
+ parts.push("NOT NULL");
2290
+ } else if (column.isNullable) {
2291
+ parts.push("NULL");
2292
+ }
2293
+ if (column.defaultValue !== undefined) {
2294
+ parts.push(`DEFAULT ${column.defaultValue}`);
2295
+ }
2296
+ if (column.isUnique) {
2297
+ parts.push("UNIQUE");
2298
+ }
2299
+ if (column.checkExpression) {
2300
+ parts.push(`CHECK (${column.checkExpression})`);
2301
+ }
2302
+ if (column.foreignKey) {
2303
+ const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
2304
+ const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
2305
+ let clause = `REFERENCES ${reference}`;
2306
+ if (onDelete === "cascade") {
2307
+ clause += " ON DELETE CASCADE";
2308
+ } else if (onDelete === "set null") {
2309
+ clause += " ON DELETE SET NULL";
2310
+ }
2311
+ parts.push(clause);
2312
+ }
2313
+ return parts.join(" ");
2314
+ }
2315
+ function compileIndex(_driver, tableName, index) {
2316
+ const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
2317
+ const columns = index.columns.map((column) => {
2318
+ const quoted = quoteIdentifier(column);
2319
+ if (index.order === "desc") {
2320
+ return `${quoted} DESC`;
2321
+ }
2322
+ return quoted;
2323
+ }).join(", ");
2324
+ const unique = index.kind === "unique" ? "UNIQUE " : "";
2325
+ return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
2326
+ }
2327
+ function compileSpecialIndex(driver, tableName, index) {
2328
+ const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
2329
+ const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
2330
+ switch (index.kind) {
2331
+ case "partial":
2332
+ case "uniquePartial": {
2333
+ if (driver !== "pgsql") {
2334
+ throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
2335
+ }
2336
+ const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
2337
+ return [
2338
+ `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
2339
+ ];
2340
+ }
2341
+ case "gin": {
2342
+ if (driver !== "pgsql") {
2343
+ throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
2344
+ }
2345
+ return [
2346
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
2347
+ ];
2348
+ }
2349
+ case "fullText": {
2350
+ if (driver === "mysql") {
2351
+ return [
2352
+ `CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
2353
+ ];
2354
+ }
2355
+ if (driver === "pgsql") {
2356
+ throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
2357
+ }
2358
+ throw new UnsupportedSchemaFeatureError("fullText()", driver);
2359
+ }
2360
+ default:
2361
+ return [];
2362
+ }
2363
+ }
2364
+ function defaultIndexName(tableName, columns, kind) {
2365
+ return `idx_${tableName}_${columns.join("_")}_${kind}`;
2366
+ }
2367
+ function compileBlueprint(driver, blueprint) {
2368
+ switch (blueprint.action) {
2369
+ case "create":
2370
+ return compileCreateTable(driver, blueprint);
2371
+ case "alter":
2372
+ return compileAlterTable(driver, blueprint);
2373
+ case "drop":
2374
+ return compileDropTable(driver, blueprint.table);
2375
+ default:
2376
+ throw new Error(`Unsupported blueprint action: ${blueprint.action}`);
2377
+ }
2378
+ }
2379
+ // ../../src/core/database/schema/grammars/createGrammar.ts
2380
+ function createGrammar(driver) {
2381
+ return {
2382
+ driver,
2383
+ compile(blueprint) {
2384
+ return compileBlueprint(driver, blueprint);
2385
+ }
2386
+ };
2387
+ }
2388
+
2389
+ // ../../src/core/database/schema/grammars/mysqlGrammar.ts
2390
+ var MySqlGrammar = createGrammar("mysql");
2391
+
2392
+ // ../../src/core/database/schema/grammars/postgresGrammar.ts
2393
+ var PostgresGrammar = createGrammar("pgsql");
2394
+
2395
+ // ../../src/core/database/schema/grammars/sqliteGrammar.ts
2396
+ var SqliteGrammar = createGrammar("sqlite");
2397
+
2398
+ // ../../src/core/database/schema/grammars/index.ts
2399
+ function grammarForDriver(driver) {
2400
+ switch (driver) {
2401
+ case "pgsql":
2402
+ return PostgresGrammar;
2403
+ case "mysql":
2404
+ return MySqlGrammar;
2405
+ case "sqlite":
2406
+ return SqliteGrammar;
2407
+ default:
2408
+ throw new Error(`Unsupported database driver: ${driver}`);
2409
+ }
2410
+ }
2411
+ // ../../src/core/database/schema/schema.ts
2412
+ class SchemaBuilder {
2413
+ #driver;
2414
+ #statements = [];
2415
+ constructor(driver) {
2416
+ this.#driver = driver;
2417
+ }
2418
+ create(table, callback) {
2419
+ const blueprint = new Blueprint(table, "create");
2420
+ callback(blueprint);
2421
+ this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
2422
+ return this;
2423
+ }
2424
+ table(table, callback) {
2425
+ const blueprint = new Blueprint(table, "alter");
2426
+ callback(blueprint);
2427
+ this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
2428
+ return this;
2429
+ }
2430
+ drop(table) {
2431
+ const blueprint = new Blueprint(table, "drop");
2432
+ this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
2433
+ return this;
2434
+ }
2435
+ toSql() {
2436
+ return [...this.#statements];
2437
+ }
2438
+ async execute(db2) {
2439
+ for (const statement of this.#statements) {
2440
+ await db2.unsafe(statement);
2441
+ }
2442
+ }
2443
+ }
2444
+
2445
+ class Schema {
2446
+ static builder(driver) {
2447
+ return new SchemaBuilder(driver ?? resolveDatabaseDriver());
2448
+ }
2449
+ static async run(db2, driver, callback) {
2450
+ const schema = Schema.builder(driver);
2451
+ await callback(schema);
2452
+ await schema.execute(db2);
2453
+ }
2454
+ }
2455
+ function createSchemaBuilder(db2, driver) {
2456
+ const builder = Schema.builder(driver);
2457
+ return Object.assign(builder, {
2458
+ async commit() {
2459
+ await builder.execute(db2);
2460
+ }
2461
+ });
2462
+ }
2463
+ // ../../src/core/database/seeders/runner.ts
2464
+ import { readdir as readdir2 } from "fs/promises";
2465
+ import { join as join2 } from "path";
2466
+ import { pathToFileURL as pathToFileURL2 } from "url";
2467
+ async function loadSeedersFromDirectory(directory) {
2468
+ const entries = await readdir2(directory);
2469
+ const seederFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
2470
+ const loadedSeeders = await Promise.all(seederFiles.map(async (fileName) => {
2471
+ const moduleUrl = pathToFileURL2(join2(directory, fileName)).href;
2472
+ const module = await import(moduleUrl);
2473
+ return module.default;
2474
+ }));
2475
+ return loadedSeeders.filter((seeder) => seeder?.name !== undefined);
2476
+ }
2477
+ async function runSeedersFromDirectory(directory, db2, options) {
2478
+ const seeders = await loadSeedersFromDirectory(directory);
2479
+ if (seeders.length === 0) {
2480
+ return 0;
2481
+ }
2482
+ for (const seeder of seeders) {
2483
+ options?.onSeeder?.(seeder.name);
2484
+ await seeder.run(db2);
2485
+ }
2486
+ return seeders.length;
2487
+ }
1194
2488
  // ../../src/core/database/table.ts
1195
2489
  function defineTable(definition) {
1196
2490
  return definition;
1197
2491
  }
2492
+ // ../../src/core/database/connection.ts
2493
+ function createDatabaseConnection2(source) {
2494
+ return {
2495
+ async unsafe(query, params = []) {
2496
+ return await source.unsafe(query, params);
2497
+ }
2498
+ };
2499
+ }
2500
+
2501
+ // ../../src/core/database/transaction.ts
2502
+ async function runInTransaction(operation) {
2503
+ return await connection_default.begin(async (transaction) => {
2504
+ return await operation(createDatabaseConnection2(transaction));
2505
+ });
2506
+ }
1198
2507
  // ../../src/core/logging/logger.ts
1199
2508
  class Logger {
1200
2509
  channel;
@@ -1235,6 +2544,8 @@ var appLogger = new Logger("app");
1235
2544
  var CORE_QUEUE_TOKEN = "core.queue";
1236
2545
  var CORE_POLICY_GATE_TOKEN = "core.policyGate";
1237
2546
  var CORE_AUTH_TOKEN = "core.auth";
2547
+ var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
2548
+ var DEFAULT_QUEUE_DRIVER = "sync";
1238
2549
 
1239
2550
  // ../../src/bootstrap/applicationRegistry.ts
1240
2551
  var activeContext;
@@ -1262,6 +2573,9 @@ function resolveApplicationConfig() {
1262
2573
  function resolveApplicationLogger() {
1263
2574
  return appLogger;
1264
2575
  }
2576
+ function resolveApplicationDependencies() {
2577
+ return requireActiveApplicationContext().dependencies;
2578
+ }
1265
2579
 
1266
2580
  // ../../src/core/mail/mailer.ts
1267
2581
  function resolveSmtpConfig() {
@@ -1446,7 +2760,7 @@ function mailer() {
1446
2760
  // ../../src/core/storage/storage.ts
1447
2761
  var {S3Client } = globalThis.Bun;
1448
2762
  import { mkdir, readFile, unlink, writeFile } from "fs/promises";
1449
- import { dirname, join as join2 } from "path";
2763
+ import { dirname, join as join3 } from "path";
1450
2764
 
1451
2765
  class LocalStorageDriver {
1452
2766
  rootDirectory;
@@ -1454,7 +2768,7 @@ class LocalStorageDriver {
1454
2768
  this.rootDirectory = rootDirectory;
1455
2769
  }
1456
2770
  resolvePath(path) {
1457
- return join2(this.rootDirectory, path.replace(/^\/+/, ""));
2771
+ return join3(this.rootDirectory, path.replace(/^\/+/, ""));
1458
2772
  }
1459
2773
  async put(path, contents) {
1460
2774
  const absolutePath = this.resolvePath(path);
@@ -1585,14 +2899,466 @@ function mail() {
1585
2899
  function storageFacade() {
1586
2900
  return storage();
1587
2901
  }
1588
- // ../../src/config/frontend.ts
1589
- function readFrontendMode() {
1590
- const mode = (process.env.FRONTEND_MODE ?? "api").trim();
1591
- if (mode === "server-htmx") {
1592
- return "server-htmx";
1593
- }
1594
- if (mode === "spa-react") {
1595
- return "spa-react";
2902
+ // ../../src/core/http/bodySizeLimitMiddleware.ts
2903
+ var DEFAULT_MAX_BODY_BYTES = 1048576;
2904
+ function resolveMaxBodyBytes() {
2905
+ const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
2906
+ if (!raw) {
2907
+ return DEFAULT_MAX_BODY_BYTES;
2908
+ }
2909
+ const parsed = Number.parseInt(raw, 10);
2910
+ if (!Number.isInteger(parsed) || parsed <= 0) {
2911
+ return DEFAULT_MAX_BODY_BYTES;
2912
+ }
2913
+ return parsed;
2914
+ }
2915
+ function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
2916
+ return async (request, next) => {
2917
+ const contentLength = request.headers.get("content-length");
2918
+ if (contentLength) {
2919
+ const bytes = Number.parseInt(contentLength, 10);
2920
+ if (Number.isInteger(bytes) && bytes > maxBytes) {
2921
+ const error = new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
2922
+ return Response.json({ error: error.message }, { status: error.status });
2923
+ }
2924
+ }
2925
+ return await next();
2926
+ };
2927
+ }
2928
+ // ../../src/core/http/csrfToken.ts
2929
+ import { createHmac, randomBytes, timingSafeEqual } from "crypto";
2930
+
2931
+ // ../../src/core/http/requestMetaContext.ts
2932
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
2933
+ var requestMetaContext = new AsyncLocalStorage3;
2934
+ function runWithRequestMeta(meta, callback) {
2935
+ return requestMetaContext.run(meta, callback);
2936
+ }
2937
+ function currentRequestMeta() {
2938
+ return requestMetaContext.getStore() ?? {
2939
+ ipAddress: null,
2940
+ userAgent: null
2941
+ };
2942
+ }
2943
+
2944
+ // ../../src/core/http/csrfToken.ts
2945
+ var CSRF_COOKIE = "workhub_csrf";
2946
+ var CSRF_TTL_MS = 60 * 60 * 1000;
2947
+ function resolveCsrfSecret() {
2948
+ return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
2949
+ }
2950
+ function signCsrfToken(token, issuedAt) {
2951
+ const payload = `${token}.${issuedAt}`;
2952
+ const signature = createHmac("sha256", resolveCsrfSecret()).update(payload).digest("hex");
2953
+ return `${payload}.${signature}`;
2954
+ }
2955
+ function readCsrfCookie(request) {
2956
+ const cookieHeader = request.headers.get("cookie");
2957
+ if (!cookieHeader) {
2958
+ return null;
2959
+ }
2960
+ for (const part of cookieHeader.split(";")) {
2961
+ const [name, ...rest] = part.trim().split("=");
2962
+ if (name === CSRF_COOKIE) {
2963
+ return decodeURIComponent(rest.join("="));
2964
+ }
2965
+ }
2966
+ return null;
2967
+ }
2968
+ function parseSignedCsrfValue(cookieValue) {
2969
+ const parts = cookieValue.split(".");
2970
+ if (parts.length !== 3) {
2971
+ return null;
2972
+ }
2973
+ const [token, issuedAtRaw, cookieSignature] = parts;
2974
+ if (!token || !issuedAtRaw || !cookieSignature) {
2975
+ return null;
2976
+ }
2977
+ const issuedAt = Number.parseInt(issuedAtRaw, 10);
2978
+ if (!Number.isFinite(issuedAt)) {
2979
+ return null;
2980
+ }
2981
+ if (Date.now() - issuedAt > CSRF_TTL_MS) {
2982
+ return null;
2983
+ }
2984
+ const expectedSignature = signCsrfToken(token, issuedAt).split(".").pop();
2985
+ if (!expectedSignature) {
2986
+ return null;
2987
+ }
2988
+ const expectedBuffer = Buffer.from(expectedSignature);
2989
+ const actualBuffer = Buffer.from(cookieSignature);
2990
+ if (expectedBuffer.length !== actualBuffer.length) {
2991
+ return null;
2992
+ }
2993
+ if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
2994
+ return null;
2995
+ }
2996
+ return { token, issuedAt };
2997
+ }
2998
+ function createCsrfTokenCookie() {
2999
+ const token = randomBytes(24).toString("hex");
3000
+ const issuedAt = Date.now();
3001
+ const value = signCsrfToken(token, issuedAt);
3002
+ return {
3003
+ token,
3004
+ cookie: `${CSRF_COOKIE}=${encodeURIComponent(value)}; Path=/; SameSite=Lax; Max-Age=3600`
3005
+ };
3006
+ }
3007
+ function resolveCsrfToken(request) {
3008
+ const cookieValue = readCsrfCookie(request);
3009
+ if (cookieValue) {
3010
+ const parsed = parseSignedCsrfValue(cookieValue);
3011
+ if (parsed) {
3012
+ return { token: parsed.token };
3013
+ }
3014
+ }
3015
+ return createCsrfTokenCookie();
3016
+ }
3017
+ function readSubmittedCsrfToken(request) {
3018
+ const headerToken = request.headers.get("x-csrf-token")?.trim();
3019
+ if (headerToken) {
3020
+ return headerToken;
3021
+ }
3022
+ return null;
3023
+ }
3024
+ async function readSubmittedCsrfTokenFromBody(request) {
3025
+ const headerToken = readSubmittedCsrfToken(request);
3026
+ if (headerToken) {
3027
+ return headerToken;
3028
+ }
3029
+ const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
3030
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
3031
+ const formData = await request.clone().formData();
3032
+ const field = formData.get("_token");
3033
+ if (typeof field === "string" && field.trim().length > 0) {
3034
+ return field.trim();
3035
+ }
3036
+ }
3037
+ return null;
3038
+ }
3039
+ function verifyCsrfToken(request, submittedToken) {
3040
+ if (!submittedToken) {
3041
+ return false;
3042
+ }
3043
+ const cookieValue = readCsrfCookie(request);
3044
+ if (!cookieValue) {
3045
+ return false;
3046
+ }
3047
+ const parsed = parseSignedCsrfValue(cookieValue);
3048
+ if (!parsed) {
3049
+ return false;
3050
+ }
3051
+ const submittedBuffer = Buffer.from(submittedToken);
3052
+ const expectedBuffer = Buffer.from(parsed.token);
3053
+ if (submittedBuffer.length !== expectedBuffer.length) {
3054
+ return false;
3055
+ }
3056
+ return timingSafeEqual(submittedBuffer, expectedBuffer);
3057
+ }
3058
+ function resolveCsrfTokenForRequest(request) {
3059
+ const metaToken = currentRequestMeta().csrfToken;
3060
+ if (metaToken) {
3061
+ return metaToken;
3062
+ }
3063
+ return resolveCsrfToken(request).token;
3064
+ }
3065
+
3066
+ // ../../src/core/http/csrfMiddleware.ts
3067
+ var MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
3068
+ function appendSetCookie(response, cookie) {
3069
+ const headers = new Headers(response.headers);
3070
+ headers.append("set-cookie", cookie);
3071
+ return new Response(response.body, {
3072
+ status: response.status,
3073
+ statusText: response.statusText,
3074
+ headers
3075
+ });
3076
+ }
3077
+ function createCsrfMiddleware() {
3078
+ return async (request, next) => {
3079
+ const method = request.method.toUpperCase();
3080
+ if (!MUTATING_METHODS.has(method)) {
3081
+ const csrf = resolveCsrfToken(request);
3082
+ const meta = currentRequestMeta();
3083
+ meta.csrfToken = csrf.token;
3084
+ const response = await next();
3085
+ if (!csrf.cookie) {
3086
+ return response;
3087
+ }
3088
+ return appendSetCookie(response, csrf.cookie);
3089
+ }
3090
+ const submitted = await readSubmittedCsrfTokenFromBody(request);
3091
+ if (!verifyCsrfToken(request, submitted)) {
3092
+ throw new ForbiddenError("Invalid or missing CSRF token.");
3093
+ }
3094
+ return await next();
3095
+ };
3096
+ }
3097
+ // ../../src/core/http/etag.ts
3098
+ import { createHash } from "crypto";
3099
+ function isEtagEnabled() {
3100
+ return (process.env.FEATURE_ETAG ?? "true") !== "false";
3101
+ }
3102
+ function formatWeakEtag(digest) {
3103
+ return `W/"${digest}"`;
3104
+ }
3105
+ function computeEtagFromJson(data) {
3106
+ const digest = createHash("sha256").update(JSON.stringify(data)).digest("hex").slice(0, 32);
3107
+ return formatWeakEtag(digest);
3108
+ }
3109
+ function etagFromResource(resource) {
3110
+ const version = resource.updated_at ?? resource.created_at ?? "";
3111
+ const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
3112
+ const digest = createHash("sha256").update(`${String(resource.id ?? "0")}:${versionText}`).digest("hex").slice(0, 32);
3113
+ return formatWeakEtag(digest);
3114
+ }
3115
+ function normalizeEtag(value) {
3116
+ return value.trim();
3117
+ }
3118
+ function etagValuesMatch(left, right) {
3119
+ return normalizeEtag(left) === normalizeEtag(right);
3120
+ }
3121
+ function parseEtagList(header) {
3122
+ if (!header) {
3123
+ return [];
3124
+ }
3125
+ return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
3126
+ }
3127
+ function ifNoneMatchSatisfied(request, etag) {
3128
+ const header = request.headers.get("if-none-match");
3129
+ if (!header) {
3130
+ return false;
3131
+ }
3132
+ if (header.trim() === "*") {
3133
+ return true;
3134
+ }
3135
+ return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
3136
+ }
3137
+ function ifMatchSatisfied(request, etag) {
3138
+ const header = request.headers.get("if-match");
3139
+ if (!header) {
3140
+ return false;
3141
+ }
3142
+ if (header.trim() === "*") {
3143
+ return true;
3144
+ }
3145
+ return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
3146
+ }
3147
+ function assertIfMatch(request, etag, options = {}) {
3148
+ const header = request.headers.get("if-match");
3149
+ if (!header) {
3150
+ if (options.required) {
3151
+ throw new PreconditionFailedError("If-Match header is required.");
3152
+ }
3153
+ return;
3154
+ }
3155
+ if (!ifMatchSatisfied(request, etag)) {
3156
+ throw new PreconditionFailedError("Resource ETag does not match If-Match.");
3157
+ }
3158
+ }
3159
+ function applyEtagHeaders(headers, etag) {
3160
+ const next = new Headers(headers);
3161
+ next.set("ETag", etag);
3162
+ next.set("Cache-Control", "private, must-revalidate");
3163
+ next.append("Vary", "Authorization");
3164
+ next.append("Vary", "X-Tenant-Id");
3165
+ return next;
3166
+ }
3167
+ function notModifiedResponse(etag) {
3168
+ return new Response(null, {
3169
+ status: 304,
3170
+ headers: applyEtagHeaders(new Headers, etag)
3171
+ });
3172
+ }
3173
+ function applyConditionalGet(request, response, etag) {
3174
+ if (!isEtagEnabled()) {
3175
+ return response;
3176
+ }
3177
+ if (ifNoneMatchSatisfied(request, etag)) {
3178
+ return notModifiedResponse(etag);
3179
+ }
3180
+ const headers = applyEtagHeaders(new Headers(response.headers), etag);
3181
+ return new Response(response.body, {
3182
+ status: response.status,
3183
+ statusText: response.statusText,
3184
+ headers
3185
+ });
3186
+ }
3187
+ // ../../src/core/tenant/tenantContext.ts
3188
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
3189
+ var tenantContext = new AsyncLocalStorage4;
3190
+ function runWithTenant(tenant, callback) {
3191
+ return tenantContext.run(tenant, callback);
3192
+ }
3193
+ function currentTenant() {
3194
+ return tenantContext.getStore() ?? null;
3195
+ }
3196
+ function currentTenantId() {
3197
+ return currentTenant()?.id ?? 1;
3198
+ }
3199
+ function rateLimitMultiplierForPlan(plan) {
3200
+ switch (plan) {
3201
+ case "enterprise":
3202
+ return 4;
3203
+ case "pro":
3204
+ return 2;
3205
+ default:
3206
+ return 1;
3207
+ }
3208
+ }
3209
+
3210
+ // ../../src/core/http/validation.ts
3211
+ function buildRequestCacheKey(fallbackPath, request) {
3212
+ if (!request) {
3213
+ return fallbackPath;
3214
+ }
3215
+ const url = new URL(request.url);
3216
+ const user = currentAuthUser();
3217
+ const authScope = user ? `u:${user.id}` : "guest";
3218
+ const tenantScope = `t:${currentTenantId()}`;
3219
+ return `${authScope}|${tenantScope}|${url.pathname}${url.search}`;
3220
+ }
3221
+ function getQueryParams(request) {
3222
+ if (!request) {
3223
+ return new URLSearchParams;
3224
+ }
3225
+ return new URL(request.url).searchParams;
3226
+ }
3227
+ function parseOptionalPositiveIntQueryParam(params, name) {
3228
+ const value = params.get(name);
3229
+ if (value === null || value.trim() === "") {
3230
+ return;
3231
+ }
3232
+ const parsed = Number.parseInt(value, 10);
3233
+ if (!Number.isInteger(parsed) || parsed <= 0) {
3234
+ throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
3235
+ }
3236
+ return parsed;
3237
+ }
3238
+ function parseOptionalBooleanQueryParam(params, name) {
3239
+ const value = params.get(name);
3240
+ if (value === null || value.trim() === "") {
3241
+ return;
3242
+ }
3243
+ switch (value.toLowerCase()) {
3244
+ case "true":
3245
+ case "1":
3246
+ return true;
3247
+ case "false":
3248
+ case "0":
3249
+ return false;
3250
+ default:
3251
+ throw new BadRequestError(`Invalid query parameter "${name}". Expected a boolean.`);
3252
+ }
3253
+ }
3254
+ function parseOptionalEnumQueryParam(params, name, allowedValues) {
3255
+ const value = params.get(name);
3256
+ if (value === null || value.trim() === "") {
3257
+ return;
3258
+ }
3259
+ if (!allowedValues.includes(value)) {
3260
+ throw new BadRequestError(`Invalid query parameter "${name}". Expected one of: ${allowedValues.join(", ")}.`);
3261
+ }
3262
+ return value;
3263
+ }
3264
+ function expectObject(value, label = "request body") {
3265
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
3266
+ throw new BadRequestError(`${label} must be a JSON object.`);
3267
+ }
3268
+ return value;
3269
+ }
3270
+ async function parseJsonBody(request, validator) {
3271
+ let payload;
3272
+ try {
3273
+ payload = await request.json();
3274
+ } catch {
3275
+ throw new BadRequestError("Request body must be valid JSON.");
3276
+ }
3277
+ return validator(payload);
3278
+ }
3279
+ function readRequiredString(payload, field, options = {}) {
3280
+ const value = payload[field];
3281
+ if (typeof value !== "string" || value.trim() === "") {
3282
+ throw new BadRequestError(`"${field}" is required and must be a string.`);
3283
+ }
3284
+ const trimmed = value.trim();
3285
+ if (options.minLength !== undefined && trimmed.length < options.minLength) {
3286
+ throw new BadRequestError(`"${field}" must be at least ${options.minLength} characters.`);
3287
+ }
3288
+ if (options.maxLength !== undefined && trimmed.length > options.maxLength) {
3289
+ throw new BadRequestError(`"${field}" must be at most ${options.maxLength} characters.`);
3290
+ }
3291
+ if (options.pattern && !options.pattern.test(trimmed)) {
3292
+ throw new BadRequestError(`"${field}" has an invalid format.`);
3293
+ }
3294
+ return trimmed;
3295
+ }
3296
+ function readOptionalString(payload, field, options = {}) {
3297
+ if (!(field in payload) || payload[field] === undefined) {
3298
+ return;
3299
+ }
3300
+ return readRequiredString(payload, field, options);
3301
+ }
3302
+ function readRequiredEnum(payload, field, allowedValues) {
3303
+ const value = readRequiredString(payload, field);
3304
+ if (!allowedValues.includes(value)) {
3305
+ throw new BadRequestError(`"${field}" must be one of: ${allowedValues.join(", ")}.`);
3306
+ }
3307
+ return value;
3308
+ }
3309
+ function readOptionalEnum(payload, field, allowedValues) {
3310
+ if (!(field in payload) || payload[field] === undefined) {
3311
+ return;
3312
+ }
3313
+ return readRequiredEnum(payload, field, allowedValues);
3314
+ }
3315
+ function readRequiredPositiveInt(payload, field) {
3316
+ const value = payload[field];
3317
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
3318
+ throw new BadRequestError(`"${field}" is required and must be a positive integer.`);
3319
+ }
3320
+ return value;
3321
+ }
3322
+ function readOptionalPositiveInt(payload, field) {
3323
+ if (!(field in payload) || payload[field] === undefined) {
3324
+ return;
3325
+ }
3326
+ return readRequiredPositiveInt(payload, field);
3327
+ }
3328
+ function parsePositiveIntParam(value, name = "id") {
3329
+ const parsed = Number.parseInt(value, 10);
3330
+ if (!Number.isInteger(parsed) || parsed <= 0) {
3331
+ throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
3332
+ }
3333
+ return parsed;
3334
+ }
3335
+
3336
+ // ../../src/core/http/formRequest.ts
3337
+ class FormRequest {
3338
+ authorize(_request) {
3339
+ return true;
3340
+ }
3341
+ async validate(request) {
3342
+ if (!await this.authorize(request)) {
3343
+ throw new ForbiddenError;
3344
+ }
3345
+ return await parseJsonBody(request, (payload) => this.parse(payload));
3346
+ }
3347
+ }
3348
+
3349
+ class QueryFormRequest {
3350
+ validate(request) {
3351
+ return this.parseQuery(request);
3352
+ }
3353
+ }
3354
+ // ../../src/config/frontend.ts
3355
+ function readFrontendMode() {
3356
+ const mode = (process.env.FRONTEND_MODE ?? "api").trim();
3357
+ if (mode === "server-htmx") {
3358
+ return "server-htmx";
3359
+ }
3360
+ if (mode === "spa-react") {
3361
+ return "spa-react";
1596
3362
  }
1597
3363
  return "api";
1598
3364
  }
@@ -1601,9 +3367,37 @@ function isViewsEnabled() {
1601
3367
  }
1602
3368
 
1603
3369
  // ../../src/core/view/etaViewEngine.ts
1604
- import { join as join3 } from "path";
3370
+ import { join as join4 } from "path";
1605
3371
  import { Eta } from "eta";
1606
- var DEFAULT_VIEWS_DIRECTORY = join3(process.cwd(), "resources/views");
3372
+ var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
3373
+ var DEFAULT_LAYOUT = "layouts/app.eta";
3374
+
3375
+ class EtaViewEngine {
3376
+ eta;
3377
+ resolveLayoutData;
3378
+ constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
3379
+ this.eta = new Eta({
3380
+ views: viewsDirectory,
3381
+ autoTrim: false
3382
+ });
3383
+ this.resolveLayoutData = resolveLayoutData;
3384
+ }
3385
+ async render(name, data = {}, options = {}) {
3386
+ const template = name.endsWith(".eta") ? name : `${name}.eta`;
3387
+ const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
3388
+ const mergedData = { ...layoutData, ...data };
3389
+ const body = await this.eta.renderAsync(template, mergedData);
3390
+ const layout = options.layout ?? DEFAULT_LAYOUT;
3391
+ if (layout === false) {
3392
+ return body;
3393
+ }
3394
+ const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
3395
+ return await this.eta.renderAsync(layoutTemplate, {
3396
+ ...mergedData,
3397
+ body
3398
+ });
3399
+ }
3400
+ }
1607
3401
  // ../../src/core/view/htmlResponse.ts
1608
3402
  function htmlResponse(html, init = {}) {
1609
3403
  return new Response(html, {
@@ -1614,6 +3408,145 @@ function htmlResponse(html, init = {}) {
1614
3408
  }
1615
3409
  });
1616
3410
  }
3411
+ function isHtmxRequest(request) {
3412
+ return request.headers.get("HX-Request") === "true";
3413
+ }
3414
+ // ../../src/core/auth/oauth/oidcProvider.ts
3415
+ class OidcProvider {
3416
+ options;
3417
+ name;
3418
+ constructor(options) {
3419
+ this.options = options;
3420
+ this.name = options.name;
3421
+ }
3422
+ getAuthorizationUrl(state) {
3423
+ const params = new URLSearchParams({
3424
+ client_id: this.options.clientId,
3425
+ redirect_uri: this.options.redirectUri,
3426
+ response_type: "code",
3427
+ scope: (this.options.scopes ?? ["openid", "email", "profile"]).join(" "),
3428
+ state
3429
+ });
3430
+ return `${this.options.issuer.replace(/\/$/, "")}/authorize?${params.toString()}`;
3431
+ }
3432
+ async exchangeCode(code) {
3433
+ const tokenResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/token`, {
3434
+ method: "POST",
3435
+ headers: { "content-type": "application/x-www-form-urlencoded" },
3436
+ body: new URLSearchParams({
3437
+ grant_type: "authorization_code",
3438
+ code,
3439
+ redirect_uri: this.options.redirectUri,
3440
+ client_id: this.options.clientId,
3441
+ client_secret: this.options.clientSecret
3442
+ })
3443
+ });
3444
+ const tokenBody = await tokenResponse.json();
3445
+ if (!tokenBody.access_token) {
3446
+ throw new Error("OIDC token exchange failed.");
3447
+ }
3448
+ const profileResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/userinfo`, {
3449
+ headers: { authorization: `Bearer ${tokenBody.access_token}` }
3450
+ });
3451
+ const profile = await profileResponse.json();
3452
+ return {
3453
+ providerUserId: profile.sub,
3454
+ email: profile.email ?? `${profile.sub}@oidc.local`,
3455
+ name: profile.name ?? profile.sub
3456
+ };
3457
+ }
3458
+ }
3459
+
3460
+ // ../../src/core/auth/oauth/providers.ts
3461
+ class GitHubOAuthProvider {
3462
+ options;
3463
+ name = "github";
3464
+ constructor(options) {
3465
+ this.options = options;
3466
+ }
3467
+ getAuthorizationUrl(state) {
3468
+ const params = new URLSearchParams({
3469
+ client_id: this.options.clientId,
3470
+ redirect_uri: this.options.redirectUri,
3471
+ scope: "read:user user:email",
3472
+ state
3473
+ });
3474
+ return `https://github.com/login/oauth/authorize?${params.toString()}`;
3475
+ }
3476
+ async exchangeCode(code) {
3477
+ const tokenResponse = await fetch("https://github.com/login/oauth/access_token", {
3478
+ method: "POST",
3479
+ headers: {
3480
+ accept: "application/json",
3481
+ "content-type": "application/json"
3482
+ },
3483
+ body: JSON.stringify({
3484
+ client_id: this.options.clientId,
3485
+ client_secret: this.options.clientSecret,
3486
+ code,
3487
+ redirect_uri: this.options.redirectUri
3488
+ })
3489
+ });
3490
+ const tokenBody = await tokenResponse.json();
3491
+ if (!tokenBody.access_token) {
3492
+ throw new Error("GitHub OAuth token exchange failed.");
3493
+ }
3494
+ const profileResponse = await fetch("https://api.github.com/user", {
3495
+ headers: {
3496
+ authorization: `Bearer ${tokenBody.access_token}`,
3497
+ accept: "application/json",
3498
+ "user-agent": "workhub"
3499
+ }
3500
+ });
3501
+ const profile = await profileResponse.json();
3502
+ return {
3503
+ providerUserId: String(profile.id),
3504
+ email: profile.email ?? `${profile.login}@users.noreply.github.com`,
3505
+ name: profile.name ?? profile.login
3506
+ };
3507
+ }
3508
+ }
3509
+
3510
+ class MockOAuthProvider {
3511
+ profile;
3512
+ name = "mock";
3513
+ constructor(profile) {
3514
+ this.profile = profile;
3515
+ }
3516
+ getAuthorizationUrl(state) {
3517
+ return `https://mock.oauth/authorize?state=${encodeURIComponent(state)}`;
3518
+ }
3519
+ async exchangeCode(code) {
3520
+ if (code !== "valid-code") {
3521
+ throw new Error("Invalid OAuth code.");
3522
+ }
3523
+ return this.profile;
3524
+ }
3525
+ }
3526
+
3527
+ // ../../src/core/auth/oauth/samlProvider.ts
3528
+ class SamlProvider {
3529
+ loginUrl;
3530
+ name = "saml";
3531
+ constructor(loginUrl) {
3532
+ this.loginUrl = loginUrl;
3533
+ }
3534
+ getAuthorizationUrl(state) {
3535
+ return `${this.loginUrl}?state=${encodeURIComponent(state)}`;
3536
+ }
3537
+ async exchangeCode(code) {
3538
+ if (!code.startsWith("saml:")) {
3539
+ throw new Error("Invalid SAML assertion reference.");
3540
+ }
3541
+ const [, email, name] = code.split(":");
3542
+ return {
3543
+ providerUserId: email ?? "saml-user",
3544
+ email: email ?? "saml-user@workhub.test",
3545
+ name: name ?? "SAML User"
3546
+ };
3547
+ }
3548
+ }
3549
+
1617
3550
  // ../../src/config/features.ts
1618
3551
  function readFeatureFlags() {
1619
3552
  return {
@@ -1652,12 +3585,18 @@ var apiTokenTable = defineTable({
1652
3585
  });
1653
3586
 
1654
3587
  // ../../src/core/auth/password.ts
3588
+ async function hashPassword(password) {
3589
+ return await Bun.password.hash(password, {
3590
+ algorithm: "bcrypt",
3591
+ cost: 10
3592
+ });
3593
+ }
1655
3594
  async function verifyPassword(password, passwordHash) {
1656
3595
  return await Bun.password.verify(password, passwordHash);
1657
3596
  }
1658
3597
 
1659
3598
  // ../../src/core/crypto/fieldEncryption.ts
1660
- import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
3599
+ import { createCipheriv, createDecipheriv, createHmac as createHmac2, randomBytes as randomBytes2 } from "crypto";
1661
3600
  var ENCRYPTION_PREFIX = "enc:v1:";
1662
3601
  var IV_LENGTH = 12;
1663
3602
  var TAG_LENGTH = 16;
@@ -1685,6 +3624,14 @@ function isFieldEncryptionEnabled() {
1685
3624
  }
1686
3625
  return (process.env.APP_ENV ?? "local") === "production";
1687
3626
  }
3627
+ function encryptField(plaintext, key) {
3628
+ const iv = randomBytes2(IV_LENGTH);
3629
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
3630
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
3631
+ const tag = cipher.getAuthTag();
3632
+ const payload = Buffer.concat([iv, encrypted, tag]).toString("base64");
3633
+ return `${ENCRYPTION_PREFIX}${payload}`;
3634
+ }
1688
3635
  function decryptField(value, key) {
1689
3636
  if (!value.startsWith(ENCRYPTION_PREFIX)) {
1690
3637
  return value;
@@ -1697,8 +3644,47 @@ function decryptField(value, key) {
1697
3644
  decipher.setAuthTag(tag);
1698
3645
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
1699
3646
  }
3647
+ function hashLookupValue(normalizedValue, key) {
3648
+ return createHmac2("sha256", key).update(normalizedValue).digest("hex");
3649
+ }
3650
+ function normalizeEmail(email) {
3651
+ return email.trim().toLowerCase();
3652
+ }
3653
+ function protectEmail(email) {
3654
+ const normalized = normalizeEmail(email);
3655
+ const key = resolveEncryptionKey();
3656
+ if (!key || !isFieldEncryptionEnabled()) {
3657
+ return { storedEmail: normalized, emailLookup: normalized };
3658
+ }
3659
+ return {
3660
+ storedEmail: encryptField(normalized, key),
3661
+ emailLookup: hashLookupValue(normalized, key)
3662
+ };
3663
+ }
3664
+ function revealEmail(storedEmail) {
3665
+ const key = resolveEncryptionKey();
3666
+ if (!key || !storedEmail.startsWith(ENCRYPTION_PREFIX)) {
3667
+ return storedEmail;
3668
+ }
3669
+ return decryptField(storedEmail, key);
3670
+ }
3671
+ function emailLookupForQuery(email) {
3672
+ const normalized = normalizeEmail(email);
3673
+ const key = resolveEncryptionKey();
3674
+ if (!key || !isFieldEncryptionEnabled()) {
3675
+ return normalized;
3676
+ }
3677
+ return hashLookupValue(normalized, key);
3678
+ }
1700
3679
 
1701
3680
  // ../../src/core/crypto/mfaSecret.ts
3681
+ function protectMfaSecret(secret) {
3682
+ const key = resolveEncryptionKey();
3683
+ if (!isFieldEncryptionEnabled() || !key) {
3684
+ return secret;
3685
+ }
3686
+ return encryptField(secret, key);
3687
+ }
1702
3688
  function revealMfaSecret(stored) {
1703
3689
  if (!stored) {
1704
3690
  return null;
@@ -1710,16 +3696,6 @@ function revealMfaSecret(stored) {
1710
3696
  return decryptField(stored, key);
1711
3697
  }
1712
3698
 
1713
- // ../../src/core/http/requestMetaContext.ts
1714
- import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
1715
- var requestMetaContext = new AsyncLocalStorage3;
1716
- function currentRequestMeta() {
1717
- return requestMetaContext.getStore() ?? {
1718
- ipAddress: null,
1719
- userAgent: null
1720
- };
1721
- }
1722
-
1723
3699
  // ../../src/core/security/securityEvents.ts
1724
3700
  function logSecurityEvent(event, details = {}) {
1725
3701
  const meta = currentRequestMeta();
@@ -1749,7 +3725,7 @@ function resolveDefaultTokenExpiryDays() {
1749
3725
  }
1750
3726
 
1751
3727
  // ../../src/core/security/totp.ts
1752
- import { createHmac as createHmac2 } from "crypto";
3728
+ import { createHmac as createHmac3 } from "crypto";
1753
3729
  function decodeBase32(input) {
1754
3730
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
1755
3731
  const normalized = input.replace(/=+$/u, "").toUpperCase();
@@ -1771,7 +3747,7 @@ function generateTotp(secret, counter, digits = 6) {
1771
3747
  const key = decodeBase32(secret);
1772
3748
  const buffer = Buffer.alloc(8);
1773
3749
  buffer.writeBigUInt64BE(BigInt(counter));
1774
- const digest = createHmac2("sha1", key).update(buffer).digest();
3750
+ const digest = createHmac3("sha1", key).update(buffer).digest();
1775
3751
  const lastByte = digest[digest.length - 1] ?? 0;
1776
3752
  const offset = lastByte & 15;
1777
3753
  const b0 = digest[offset] ?? 0;
@@ -1795,16 +3771,6 @@ function verifyTotp(secret, token, window = 1) {
1795
3771
  return false;
1796
3772
  }
1797
3773
 
1798
- // ../../src/core/tenant/tenantContext.ts
1799
- import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
1800
- var tenantContext = new AsyncLocalStorage4;
1801
- function currentTenant() {
1802
- return tenantContext.getStore() ?? null;
1803
- }
1804
- function currentTenantId() {
1805
- return currentTenant()?.id ?? 1;
1806
- }
1807
-
1808
3774
  // ../../src/domain/abilities.ts
1809
3775
  var MEMBER_ABILITIES = [
1810
3776
  "organizations:read",
@@ -1972,11 +3938,160 @@ var userTable = defineTable({
1972
3938
  defaultOrderBy: { column: "id", direction: "ASC" }
1973
3939
  });
1974
3940
 
1975
- // ../../src/core/http/csrfToken.ts
1976
- var CSRF_TTL_MS = 60 * 60 * 1000;
3941
+ // ../../src/core/auth/tokenHash.ts
3942
+ import { createHash as createHash2, createHmac as createHmac4 } from "crypto";
3943
+ function resolveTokenPepper() {
3944
+ return process.env.TOKEN_HASH_PEPPER?.trim() ?? "workhub-dev-token-pepper";
3945
+ }
3946
+ function hashApiToken(token) {
3947
+ const pepper = resolveTokenPepper();
3948
+ if (pepper && pepper !== "workhub-dev-token-pepper") {
3949
+ return createHmac4("sha256", pepper).update(token).digest("hex");
3950
+ }
3951
+ return createHash2("sha256").update(token).digest("hex");
3952
+ }
3953
+
3954
+ // ../../src/modules/user/provider.ts
3955
+ var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
1977
3956
 
1978
3957
  // ../../src/core/http/flashSession.ts
3958
+ import { createHmac as createHmac5, timingSafeEqual as timingSafeEqual2 } from "crypto";
3959
+ var FLASH_COOKIE = "workhub_flash";
1979
3960
  var FLASH_TTL_MS = 60 * 1000;
3961
+ function resolveFlashSecret() {
3962
+ return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
3963
+ }
3964
+ function signFlashPayload(payload, issuedAt) {
3965
+ const signature = createHmac5("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
3966
+ return `${payload}.${issuedAt}.${signature}`;
3967
+ }
3968
+ function readFlashCookie(request) {
3969
+ const cookieHeader = request.headers.get("cookie");
3970
+ if (!cookieHeader) {
3971
+ return null;
3972
+ }
3973
+ for (const part of cookieHeader.split(";")) {
3974
+ const [name, ...rest] = part.trim().split("=");
3975
+ if (name === FLASH_COOKIE) {
3976
+ return decodeURIComponent(rest.join("="));
3977
+ }
3978
+ }
3979
+ return null;
3980
+ }
3981
+ function parseFlashCookie(cookieValue) {
3982
+ const parts = cookieValue.split(".");
3983
+ if (parts.length < 3) {
3984
+ return null;
3985
+ }
3986
+ const signature = parts.pop();
3987
+ const issuedAtRaw = parts.pop();
3988
+ const payload = parts.join(".");
3989
+ if (!signature || !issuedAtRaw || !payload) {
3990
+ return null;
3991
+ }
3992
+ const issuedAt = Number.parseInt(issuedAtRaw, 10);
3993
+ if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
3994
+ return null;
3995
+ }
3996
+ const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
3997
+ if (!expectedSignature) {
3998
+ return null;
3999
+ }
4000
+ const expectedBuffer = Buffer.from(expectedSignature);
4001
+ const actualBuffer = Buffer.from(signature);
4002
+ if (expectedBuffer.length !== actualBuffer.length) {
4003
+ return null;
4004
+ }
4005
+ if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
4006
+ return null;
4007
+ }
4008
+ try {
4009
+ const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
4010
+ if (!parsed?.message || typeof parsed.message !== "string") {
4011
+ return null;
4012
+ }
4013
+ if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
4014
+ return null;
4015
+ }
4016
+ return parsed;
4017
+ } catch {
4018
+ return null;
4019
+ }
4020
+ }
4021
+ function createFlashCookie(message) {
4022
+ const payload = Buffer.from(JSON.stringify(message), "utf8").toString("base64url");
4023
+ const issuedAt = Date.now();
4024
+ const value = signFlashPayload(payload, issuedAt);
4025
+ return `${FLASH_COOKIE}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=60`;
4026
+ }
4027
+ function clearFlashCookie() {
4028
+ return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
4029
+ }
4030
+ function pullFlash(request) {
4031
+ const cookieValue = readFlashCookie(request);
4032
+ if (!cookieValue) {
4033
+ return null;
4034
+ }
4035
+ return parseFlashCookie(cookieValue);
4036
+ }
4037
+ function flashResponse(response, message) {
4038
+ const headers = new Headers(response.headers);
4039
+ headers.append("set-cookie", createFlashCookie(message));
4040
+ return new Response(response.body, {
4041
+ status: response.status,
4042
+ statusText: response.statusText,
4043
+ headers
4044
+ });
4045
+ }
4046
+ function withFlashClear(response) {
4047
+ const headers = new Headers(response.headers);
4048
+ headers.append("set-cookie", clearFlashCookie());
4049
+ return new Response(response.body, {
4050
+ status: response.status,
4051
+ statusText: response.statusText,
4052
+ headers
4053
+ });
4054
+ }
4055
+
4056
+ // ../../src/core/view/webLayoutData.ts
4057
+ async function resolveWebLayoutData(container, request) {
4058
+ const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
4059
+ const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
4060
+ const authUser = currentAuthUser();
4061
+ if (!authUser) {
4062
+ return { authUser: null, csrfToken, flash };
4063
+ }
4064
+ const userId = Number(authUser.id);
4065
+ if (!Number.isInteger(userId) || userId <= 0) {
4066
+ return { authUser: null, csrfToken, flash };
4067
+ }
4068
+ if (!container.has(tokenServiceToken)) {
4069
+ return {
4070
+ authUser: {
4071
+ id: userId,
4072
+ email: "",
4073
+ role: authUser.role ?? "member"
4074
+ },
4075
+ csrfToken,
4076
+ flash
4077
+ };
4078
+ }
4079
+ const tokenService = container.resolve(tokenServiceToken);
4080
+ try {
4081
+ const user = await tokenService.findByIdOrThrow(userId);
4082
+ return {
4083
+ authUser: {
4084
+ id: userId,
4085
+ email: user.email ?? "",
4086
+ role: authUser.role ?? user.role ?? "member"
4087
+ },
4088
+ csrfToken,
4089
+ flash
4090
+ };
4091
+ } catch {
4092
+ return { authUser: null, csrfToken, flash };
4093
+ }
4094
+ }
1980
4095
  // ../../src/core/http/contentNegotiation.ts
1981
4096
  function requestPrefersJson(request) {
1982
4097
  if (!request) {
@@ -2036,127 +4151,123 @@ function webErrorResponse(error, request) {
2036
4151
  status: mappedError.status
2037
4152
  });
2038
4153
  }
2039
- // ../../src/core/http/etag.ts
2040
- import { createHash } from "crypto";
2041
- function isEtagEnabled() {
2042
- return (process.env.FEATURE_ETAG ?? "true") !== "false";
2043
- }
2044
- function formatWeakEtag(digest) {
2045
- return `W/"${digest}"`;
2046
- }
2047
- function etagFromResource(resource) {
2048
- const version = resource.updated_at ?? resource.created_at ?? "";
2049
- const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
2050
- const digest = createHash("sha256").update(`${String(resource.id ?? "0")}:${versionText}`).digest("hex").slice(0, 32);
2051
- return formatWeakEtag(digest);
2052
- }
2053
- function normalizeEtag(value) {
2054
- return value.trim();
2055
- }
2056
- function etagValuesMatch(left, right) {
2057
- return normalizeEtag(left) === normalizeEtag(right);
2058
- }
2059
- function parseEtagList(header) {
2060
- if (!header) {
2061
- return [];
2062
- }
2063
- return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
2064
- }
2065
- function ifNoneMatchSatisfied(request, etag) {
2066
- const header = request.headers.get("if-none-match");
2067
- if (!header) {
2068
- return false;
2069
- }
2070
- if (header.trim() === "*") {
2071
- return true;
2072
- }
2073
- return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
2074
- }
2075
- function ifMatchSatisfied(request, etag) {
2076
- const header = request.headers.get("if-match");
2077
- if (!header) {
2078
- return false;
2079
- }
2080
- if (header.trim() === "*") {
2081
- return true;
2082
- }
2083
- return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
4154
+ // ../../src/core/http/authMiddleware.ts
4155
+ function createAuthMiddleware(auth2) {
4156
+ return async (request, next) => {
4157
+ const user = await auth2.resolve(request);
4158
+ return await runWithAuthUser(user, async () => {
4159
+ const response = await next();
4160
+ if (user) {
4161
+ const headers = new Headers(response.headers);
4162
+ headers.set("x-authenticated-user-id", String(user.id));
4163
+ return new Response(response.body, {
4164
+ status: response.status,
4165
+ statusText: response.statusText,
4166
+ headers
4167
+ });
4168
+ }
4169
+ return response;
4170
+ });
4171
+ };
2084
4172
  }
2085
- function assertIfMatch(request, etag, options = {}) {
2086
- const header = request.headers.get("if-match");
2087
- if (!header) {
2088
- if (options.required) {
2089
- throw new PreconditionFailedError("If-Match header is required.");
4173
+ // ../../src/core/http/authorizeMiddleware.ts
4174
+ function createAuthorizeMiddleware(gate, auth2, resource, action) {
4175
+ return async (request, next) => {
4176
+ const user = await auth2.resolve(request);
4177
+ if (!gate.allows(resource, action, user)) {
4178
+ const error = new ForbiddenError;
4179
+ return Response.json({ error: error.message }, { status: error.status });
2090
4180
  }
2091
- return;
2092
- }
2093
- if (!ifMatchSatisfied(request, etag)) {
2094
- throw new PreconditionFailedError("Resource ETag does not match If-Match.");
2095
- }
2096
- }
2097
- function applyEtagHeaders(headers, etag) {
2098
- const next = new Headers(headers);
2099
- next.set("ETag", etag);
2100
- next.set("Cache-Control", "private, must-revalidate");
2101
- next.append("Vary", "Authorization");
2102
- next.append("Vary", "X-Tenant-Id");
2103
- return next;
4181
+ return await next();
4182
+ };
2104
4183
  }
2105
- function notModifiedResponse(etag) {
2106
- return new Response(null, {
2107
- status: 304,
2108
- headers: applyEtagHeaders(new Headers, etag)
4184
+ // ../../src/core/http/conditionalResponse.ts
4185
+ function jsonResponse(data, init = {}) {
4186
+ return Response.json(data, {
4187
+ status: init.status ?? 200,
4188
+ headers: init.headers
2109
4189
  });
2110
4190
  }
2111
- function applyConditionalGet(request, response, etag) {
2112
- if (!isEtagEnabled()) {
2113
- return response;
4191
+ function conditionalJsonResponse(request, data, init = {}) {
4192
+ if (!request || !isEtagEnabled()) {
4193
+ return jsonResponse(data, init);
2114
4194
  }
4195
+ const etag = computeEtagFromJson(data);
2115
4196
  if (ifNoneMatchSatisfied(request, etag)) {
2116
4197
  return notModifiedResponse(etag);
2117
4198
  }
2118
- const headers = applyEtagHeaders(new Headers(response.headers), etag);
4199
+ const response = jsonResponse(data, init);
4200
+ const headers = new Headers(response.headers);
4201
+ headers.set("ETag", etag);
4202
+ headers.set("Cache-Control", "private, must-revalidate");
4203
+ headers.append("Vary", "Authorization");
4204
+ headers.append("Vary", "X-Tenant-Id");
2119
4205
  return new Response(response.body, {
2120
4206
  status: response.status,
2121
4207
  statusText: response.statusText,
2122
4208
  headers
2123
4209
  });
2124
4210
  }
2125
- // ../../src/core/http/validation.ts
2126
- function getQueryParams(request) {
2127
- if (!request) {
2128
- return new URLSearchParams;
2129
- }
2130
- return new URL(request.url).searchParams;
4211
+ // ../../src/core/http/middleware.ts
4212
+ function isRouteHandler(value) {
4213
+ return typeof value === "function";
2131
4214
  }
2132
- async function parseJsonBody(request, validator) {
2133
- let payload;
2134
- try {
2135
- payload = await request.json();
2136
- } catch {
2137
- throw new BadRequestError("Request body must be valid JSON.");
4215
+ function isMethodRouteMap(value) {
4216
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
4217
+ return false;
2138
4218
  }
2139
- return validator(payload);
4219
+ const entries = Object.entries(value);
4220
+ return entries.length > 0 && entries.every(([, handler]) => isRouteHandler(handler));
2140
4221
  }
2141
- function parsePositiveIntParam(value, name = "id") {
2142
- const parsed = Number.parseInt(value, 10);
2143
- if (!Number.isInteger(parsed) || parsed <= 0) {
2144
- throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
2145
- }
2146
- return parsed;
4222
+ function composeMiddleware(...middleware) {
4223
+ return (handler) => {
4224
+ return async (request) => {
4225
+ let index = 0;
4226
+ const dispatch = async () => {
4227
+ if (index >= middleware.length) {
4228
+ return await handler(request);
4229
+ }
4230
+ const current = middleware[index];
4231
+ index += 1;
4232
+ if (!current) {
4233
+ return await handler(request);
4234
+ }
4235
+ return await current(request, dispatch);
4236
+ };
4237
+ return await dispatch();
4238
+ };
4239
+ };
2147
4240
  }
2148
-
2149
- // ../../src/core/http/formRequest.ts
2150
- class FormRequest {
2151
- authorize(_request) {
2152
- return true;
2153
- }
2154
- async validate(request) {
2155
- if (!await this.authorize(request)) {
2156
- throw new ForbiddenError;
4241
+ async function requestIdMiddleware(request, next) {
4242
+ const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
4243
+ const response = await next();
4244
+ const headers = new Headers(response.headers);
4245
+ headers.set("x-request-id", requestId);
4246
+ return new Response(response.body, {
4247
+ status: response.status,
4248
+ statusText: response.statusText,
4249
+ headers
4250
+ });
4251
+ }
4252
+ function wrapRouteHandler(handler, middleware) {
4253
+ if (isMethodRouteMap(handler)) {
4254
+ const wrapped = {};
4255
+ for (const [method, routeHandler] of Object.entries(handler)) {
4256
+ wrapped[method] = composeMiddleware(...middleware)(routeHandler);
2157
4257
  }
2158
- return await parseJsonBody(request, (payload) => this.parse(payload));
4258
+ return wrapped;
4259
+ }
4260
+ if (isRouteHandler(handler)) {
4261
+ return composeMiddleware(...middleware)(handler);
4262
+ }
4263
+ return handler;
4264
+ }
4265
+ function applyMiddlewareToRoutes(routes, middleware) {
4266
+ const wrapped = {};
4267
+ for (const [path, routeHandler] of Object.entries(routes)) {
4268
+ wrapped[path] = wrapRouteHandler(routeHandler, middleware);
2159
4269
  }
4270
+ return wrapped;
2160
4271
  }
2161
4272
  // ../../src/core/http/pagination.ts
2162
4273
  var DEFAULT_PER_PAGE = 15;
@@ -2189,14 +4300,74 @@ function parsePaginationQuery(request) {
2189
4300
  function paginatedResponse(data, meta, init = {}) {
2190
4301
  return Response.json({ data, meta }, init);
2191
4302
  }
4303
+ // ../../src/core/http/requireAuthMiddleware.ts
4304
+ function createRequireAuthMiddleware(auth2) {
4305
+ return async (request, next) => {
4306
+ if (!await auth2.check(request)) {
4307
+ const error = new UnauthorizedError;
4308
+ return Response.json({ error: error.message }, { status: error.status });
4309
+ }
4310
+ return await next();
4311
+ };
4312
+ }
4313
+ // ../../src/core/http/resources.ts
4314
+ function serializeDate(value) {
4315
+ return value instanceof Date ? value.toISOString() : value;
4316
+ }
4317
+ function toResourceCollection(items, transformer) {
4318
+ return items.map(transformer);
4319
+ }
4320
+ function toPaginatedResourceCollection(items, meta, transformer) {
4321
+ return {
4322
+ data: toResourceCollection(items, transformer),
4323
+ meta
4324
+ };
4325
+ }
4326
+ // ../../src/core/http/routeMiddleware.ts
4327
+ function withMiddleware(...middleware) {
4328
+ const wrap = composeMiddleware(...middleware);
4329
+ return (handler) => {
4330
+ return wrap(handler);
4331
+ };
4332
+ }
4333
+ // ../../src/core/http/routeModelBinding.ts
4334
+ function bindRouteModel(param, resolver, handler) {
4335
+ return async (request) => {
4336
+ const id = parsePositiveIntParam(String(request.params[param]), String(param));
4337
+ const model = await resolver(id, request);
4338
+ return await handler(request, model);
4339
+ };
4340
+ }
2192
4341
  // ../../src/core/http/securedRouteModelBinding.ts
2193
4342
  function isMutatingPolicyAction(action) {
2194
4343
  return action === "update" || action === "delete";
2195
4344
  }
2196
- function securedBindRouteModel(param, resolver, authorization, handler) {
4345
+ function securedBindRouteModel(param, resolver, authorization, handler) {
4346
+ return async (request) => {
4347
+ const id = parsePositiveIntParam(String(request.params[param]), String(param));
4348
+ const model = await resolver(id, request);
4349
+ const gate = resolveApplicationPolicyGate();
4350
+ const auth2 = resolveApplicationAuth();
4351
+ gate.authorize(authorization.resource, authorization.action, await auth2.resolve(request), model);
4352
+ if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
4353
+ assertIfMatch(request, etagFromResource(model), {
4354
+ required: authorization.requireIfMatch ?? true
4355
+ });
4356
+ }
4357
+ const response = await handler(request, model);
4358
+ if (isEtagEnabled() && authorization.action === "view") {
4359
+ return applyConditionalGet(request, response, etagFromResource(model));
4360
+ }
4361
+ return response;
4362
+ };
4363
+ }
4364
+ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
2197
4365
  return async (request) => {
2198
- const id = parsePositiveIntParam(String(request.params[param]), String(param));
2199
- const model = await resolver(id, request);
4366
+ const key = String(request.params[param] ?? "").trim();
4367
+ if (!key) {
4368
+ throw new BadRequestError(`Missing route parameter "${String(param)}".`);
4369
+ }
4370
+ const model = await resolver(key, request);
2200
4371
  const gate = resolveApplicationPolicyGate();
2201
4372
  const auth2 = resolveApplicationAuth();
2202
4373
  gate.authorize(authorization.resource, authorization.action, await auth2.resolve(request), model);
@@ -2226,16 +4397,79 @@ var ALLOWED_UPLOAD_MIME_TYPES = new Set([
2226
4397
  "text/plain",
2227
4398
  "text/csv"
2228
4399
  ]);
4400
+ function resolveMaxUploadBytes() {
4401
+ const raw = process.env.MAX_UPLOAD_BYTES?.trim() ?? process.env.MAX_REQUEST_BODY_BYTES?.trim();
4402
+ if (!raw) {
4403
+ return DEFAULT_MAX_UPLOAD_BYTES;
4404
+ }
4405
+ const parsed = Number.parseInt(raw, 10);
4406
+ if (!Number.isInteger(parsed) || parsed <= 0) {
4407
+ return DEFAULT_MAX_UPLOAD_BYTES;
4408
+ }
4409
+ return parsed;
4410
+ }
4411
+ function normalizeMimeType(mimeType) {
4412
+ return mimeType.split(";")[0]?.trim().toLowerCase() ?? "";
4413
+ }
4414
+ function isAllowedMimeType(mimeType) {
4415
+ const normalized = normalizeMimeType(mimeType);
4416
+ if (!normalized || normalized === "application/octet-stream") {
4417
+ return true;
4418
+ }
4419
+ return ALLOWED_UPLOAD_MIME_TYPES.has(normalized);
4420
+ }
4421
+
4422
+ // ../../src/core/http/parseMultipartUpload.ts
4423
+ function normalizeMimeType2(mimeType) {
4424
+ return mimeType.split(";")[0]?.trim().toLowerCase() || "application/octet-stream";
4425
+ }
4426
+ function sanitizeUploadFileName(name) {
4427
+ const base = name.split(/[/\\]/).pop()?.trim() ?? "upload";
4428
+ const sanitized = base.replace(/[^\w.\-()+ ]+/g, "_").slice(0, 200);
4429
+ return sanitized.length > 0 ? sanitized : "upload";
4430
+ }
4431
+ async function parseMultipartUpload(request, fieldName = "file") {
4432
+ const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
4433
+ if (!contentType.includes("multipart/form-data")) {
4434
+ throw new BadRequestError("Expected multipart form data.");
4435
+ }
4436
+ const formData = await request.formData();
4437
+ const value = formData.get(fieldName);
4438
+ if (!(value instanceof File)) {
4439
+ throw new BadRequestError(`Missing upload field "${fieldName}".`);
4440
+ }
4441
+ if (value.size <= 0) {
4442
+ throw new BadRequestError("Uploaded file is empty.");
4443
+ }
4444
+ const maxBytes = resolveMaxUploadBytes();
4445
+ if (value.size > maxBytes) {
4446
+ throw new PayloadTooLargeError(`Upload exceeds the ${maxBytes} byte limit.`);
4447
+ }
4448
+ const mimeType = normalizeMimeType2(value.type.trim() || "application/octet-stream");
4449
+ if (!isAllowedMimeType(mimeType)) {
4450
+ throw new BadRequestError(`File type "${mimeType}" is not allowed.`);
4451
+ }
4452
+ return {
4453
+ fileName: sanitizeUploadFileName(value.name),
4454
+ mimeType,
4455
+ size: value.size,
4456
+ contents: new Uint8Array(await value.arrayBuffer())
4457
+ };
4458
+ }
4459
+ // ../../src/core/http/route.ts
4460
+ function getRouteParams(request) {
4461
+ return request.params;
4462
+ }
2229
4463
 
2230
4464
  // ../../src/core/http/index.ts
2231
- function jsonResponse(data, init = {}) {
4465
+ function jsonResponse2(data, init = {}) {
2232
4466
  return Response.json(data, {
2233
4467
  status: init.status ?? 200,
2234
4468
  headers: init.headers
2235
4469
  });
2236
4470
  }
2237
4471
  function createdResponse(data, init = {}) {
2238
- return jsonResponse(data, { ...init, status: init.status ?? 201 });
4472
+ return jsonResponse2(data, { ...init, status: init.status ?? 201 });
2239
4473
  }
2240
4474
  function noContentResponse() {
2241
4475
  return new Response(null, { status: 204 });
@@ -2261,6 +4495,73 @@ function withErrorHandling(handler) {
2261
4495
  }
2262
4496
  };
2263
4497
  }
4498
+ // ../../src/core/http/loginThrottleMiddleware.ts
4499
+ var {RedisClient } = globalThis.Bun;
4500
+ function resolveLoginIdentity(request) {
4501
+ return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
4502
+ }
4503
+ async function resolveLoginEmail(request) {
4504
+ const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
4505
+ try {
4506
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
4507
+ const formData = await request.clone().formData();
4508
+ const email = formData.get("email");
4509
+ return typeof email === "string" ? email.trim().toLowerCase() : "unknown";
4510
+ }
4511
+ const payload = await request.clone().json();
4512
+ return typeof payload.email === "string" ? payload.email.trim().toLowerCase() : "unknown";
4513
+ } catch {
4514
+ return "unknown";
4515
+ }
4516
+ }
4517
+ function createLoginThrottleMiddleware(options) {
4518
+ const client = new RedisClient(options.redisUrl);
4519
+ const prefix = options.keyPrefix ?? "workhub:login-throttle:";
4520
+ return async (request, next) => {
4521
+ const identity = resolveLoginIdentity(request);
4522
+ const email = await resolveLoginEmail(request);
4523
+ const throttleKey = `${prefix}${identity}:${email}`;
4524
+ const attempts = Number(await client.incr(throttleKey));
4525
+ if (attempts === 1) {
4526
+ await client.expire(throttleKey, options.decaySeconds);
4527
+ }
4528
+ if (attempts > options.maxAttempts) {
4529
+ return Response.json({ error: "Too many login attempts. Try again later." }, {
4530
+ status: 429,
4531
+ headers: {
4532
+ "retry-after": String(options.decaySeconds)
4533
+ }
4534
+ });
4535
+ }
4536
+ return await next();
4537
+ };
4538
+ }
4539
+ // ../../src/core/http/memoryThrottleMiddleware.ts
4540
+ var buckets = new Map;
4541
+ function createMemoryThrottleMiddleware(options) {
4542
+ const prefix = options.keyPrefix ?? "workhub:memory-throttle:";
4543
+ return async (request, next) => {
4544
+ const path = new URL(request.url).pathname;
4545
+ const identity = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("authorization")?.slice(0, 32) ?? "unknown";
4546
+ const key = `${prefix}${identity}:${path}`;
4547
+ const now = Date.now();
4548
+ const existing = buckets.get(key);
4549
+ if (!existing || existing.resetAt <= now) {
4550
+ buckets.set(key, { count: 1, resetAt: now + options.decaySeconds * 1000 });
4551
+ return await next();
4552
+ }
4553
+ existing.count += 1;
4554
+ if (existing.count > options.maxAttempts) {
4555
+ return Response.json({ error: "Too many requests." }, {
4556
+ status: 429,
4557
+ headers: {
4558
+ "retry-after": String(options.decaySeconds)
4559
+ }
4560
+ });
4561
+ }
4562
+ return await next();
4563
+ };
4564
+ }
2264
4565
  // ../../src/core/metrics/prometheus.ts
2265
4566
  class PrometheusRegistry {
2266
4567
  httpRequestsTotal = new Map;
@@ -2347,6 +4648,128 @@ function createMetricsMiddleware() {
2347
4648
  return response;
2348
4649
  };
2349
4650
  }
4651
+ // ../../src/core/http/requireWebAuthMiddleware.ts
4652
+ function createRequireWebAuthMiddleware(auth2) {
4653
+ return async (request, next) => {
4654
+ const user = await auth2.resolve(request);
4655
+ if (user) {
4656
+ return await next();
4657
+ }
4658
+ if (requestPrefersJson(request)) {
4659
+ throw new UnauthorizedError;
4660
+ }
4661
+ const redirectTarget = encodeURIComponent(new URL(request.url).pathname);
4662
+ return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
4663
+ };
4664
+ }
4665
+ // ../../src/config/app.ts
4666
+ var appConfig = {
4667
+ name: "WorkHub",
4668
+ env: process.env.APP_ENV ?? "local",
4669
+ debug: (process.env.APP_DEBUG ?? "true") !== "false",
4670
+ url: process.env.APP_URL ?? "http://localhost:3000",
4671
+ apiPrefix: process.env.API_PREFIX ?? "/api/v1"
4672
+ };
4673
+
4674
+ // ../../src/config/contentSecurityPolicy.ts
4675
+ function strictApiContentSecurityPolicy() {
4676
+ return "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
4677
+ }
4678
+ function serverHtmxContentSecurityPolicy() {
4679
+ return [
4680
+ "default-src 'self'",
4681
+ "script-src 'self' https://unpkg.com",
4682
+ "style-src 'self'",
4683
+ "connect-src 'self'",
4684
+ "img-src 'self'",
4685
+ "font-src 'self'",
4686
+ "form-action 'self'",
4687
+ "frame-ancestors 'none'",
4688
+ "base-uri 'self'"
4689
+ ].join("; ");
4690
+ }
4691
+ function spaContentSecurityPolicy() {
4692
+ return [
4693
+ "default-src 'self'",
4694
+ "script-src 'self'",
4695
+ "style-src 'self'",
4696
+ "connect-src 'self'",
4697
+ "img-src 'self'",
4698
+ "font-src 'self'",
4699
+ "frame-ancestors 'none'",
4700
+ "base-uri 'self'"
4701
+ ].join("; ");
4702
+ }
4703
+ function resolveContentSecurityPolicy(response) {
4704
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
4705
+ if (!contentType.includes("text/html")) {
4706
+ return strictApiContentSecurityPolicy();
4707
+ }
4708
+ const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
4709
+ if (frontendMode === "server-htmx") {
4710
+ return serverHtmxContentSecurityPolicy();
4711
+ }
4712
+ if (frontendMode === "spa-react") {
4713
+ return spaContentSecurityPolicy();
4714
+ }
4715
+ return strictApiContentSecurityPolicy();
4716
+ }
4717
+
4718
+ // ../../src/core/http/securityHeadersMiddleware.ts
4719
+ function createSecurityHeadersMiddleware() {
4720
+ return async (_request, next) => {
4721
+ const response = await next();
4722
+ const headers = new Headers(response.headers);
4723
+ headers.set("X-Content-Type-Options", "nosniff");
4724
+ headers.set("X-Frame-Options", "DENY");
4725
+ headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
4726
+ headers.set("X-XSS-Protection", "0");
4727
+ headers.set("Content-Security-Policy", resolveContentSecurityPolicy(response));
4728
+ if (appConfig.env === "production") {
4729
+ headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
4730
+ }
4731
+ return new Response(response.body, {
4732
+ status: response.status,
4733
+ statusText: response.statusText,
4734
+ headers
4735
+ });
4736
+ };
4737
+ }
4738
+ // ../../src/core/http/throttleMiddleware.ts
4739
+ var {RedisClient: RedisClient2 } = globalThis.Bun;
4740
+ function resolveThrottleIdentity(request) {
4741
+ const user = currentAuthUser();
4742
+ if (user?.tokenId !== undefined) {
4743
+ return `token:${user.tokenId}`;
4744
+ }
4745
+ if (user) {
4746
+ return `user:${user.id}`;
4747
+ }
4748
+ return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
4749
+ }
4750
+ function createThrottleMiddleware(options) {
4751
+ const client = new RedisClient2(options.redisUrl);
4752
+ const prefix = options.keyPrefix ?? "workhub:throttle:";
4753
+ return async (request, next) => {
4754
+ const identity = resolveThrottleIdentity(request);
4755
+ const path = new URL(request.url).pathname;
4756
+ const throttleKey = `${prefix}${identity}:${path}`;
4757
+ const attempts = Number(await client.incr(throttleKey));
4758
+ if (attempts === 1) {
4759
+ await client.expire(throttleKey, options.decaySeconds);
4760
+ }
4761
+ const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
4762
+ if (attempts > maxAttempts) {
4763
+ return Response.json({ error: "Too many requests." }, {
4764
+ status: 429,
4765
+ headers: {
4766
+ "retry-after": String(options.decaySeconds)
4767
+ }
4768
+ });
4769
+ }
4770
+ return await next();
4771
+ };
4772
+ }
2350
4773
  // ../../src/core/http/parseFormBody.ts
2351
4774
  async function parseFormBody(request) {
2352
4775
  const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
@@ -2388,6 +4811,49 @@ class WebFormRequest {
2388
4811
  }
2389
4812
  }
2390
4813
  }
4814
+ // ../../src/core/lifecycle/gracefulShutdown.ts
4815
+ var shutdownHandlers = new Map;
4816
+ var shutdownInstalled = false;
4817
+ var shuttingDown = false;
4818
+ function registerShutdownHandler(name, handler) {
4819
+ shutdownHandlers.set(name, handler);
4820
+ return () => {
4821
+ shutdownHandlers.delete(name);
4822
+ };
4823
+ }
4824
+ async function runGracefulShutdown(signal) {
4825
+ if (shuttingDown) {
4826
+ return;
4827
+ }
4828
+ shuttingDown = true;
4829
+ console.log(`[shutdown] Received ${signal}, draining ${shutdownHandlers.size} handler(s)...`);
4830
+ for (const [name, handler] of shutdownHandlers) {
4831
+ try {
4832
+ await handler();
4833
+ console.log(`[shutdown] Completed ${name}`);
4834
+ } catch (error) {
4835
+ console.error(`[shutdown] Failed ${name}:`, error);
4836
+ }
4837
+ }
4838
+ }
4839
+ function installGracefulShutdownSignals(signals = ["SIGINT", "SIGTERM"]) {
4840
+ if (shutdownInstalled) {
4841
+ return;
4842
+ }
4843
+ shutdownInstalled = true;
4844
+ for (const signal of signals) {
4845
+ process.on(signal, () => {
4846
+ runGracefulShutdown(signal).finally(() => {
4847
+ process.exit(0);
4848
+ });
4849
+ });
4850
+ }
4851
+ }
4852
+ function resetGracefulShutdownForTests() {
4853
+ shutdownHandlers.clear();
4854
+ shutdownInstalled = false;
4855
+ shuttingDown = false;
4856
+ }
2391
4857
  // ../../src/core/queue/index.ts
2392
4858
  class Job {
2393
4859
  maxAttempts;
@@ -2413,6 +4879,333 @@ class AsyncQueue {
2413
4879
  function createQueue(driver) {
2414
4880
  return driver === "async" ? new AsyncQueue : new SyncQueue;
2415
4881
  }
4882
+ // ../../src/core/queue/failedJobTable.ts
4883
+ var failedJobTable = defineTable({
4884
+ name: "failed_job",
4885
+ primaryKey: "id",
4886
+ columns: ["id", "job_name", "payload", "exception", "failed_at"],
4887
+ defaultOrderBy: { column: "failed_at", direction: "DESC" }
4888
+ });
4889
+
4890
+ // ../../src/core/queue/failedJobRepository.ts
4891
+ class FailedJobRepository extends baseRepository_default {
4892
+ constructor() {
4893
+ super(failedJobTable);
4894
+ }
4895
+ }
4896
+ var failedJobRepository_default = FailedJobRepository;
4897
+
4898
+ // ../../src/core/queue/failedJobService.ts
4899
+ class FailedJobService {
4900
+ repository;
4901
+ constructor(repository) {
4902
+ this.repository = repository;
4903
+ }
4904
+ async recordFailure(input) {
4905
+ return await this.repository.create({
4906
+ job_name: input.jobName,
4907
+ payload: input.payload,
4908
+ exception: input.exception,
4909
+ failed_at: new Date
4910
+ });
4911
+ }
4912
+ listRecent(limit = 50) {
4913
+ return this.repository.findAll({
4914
+ limit,
4915
+ orderBy: { column: "failed_at", direction: "DESC" }
4916
+ });
4917
+ }
4918
+ async retry(id) {
4919
+ const failedJob = await this.repository.findByIdOrThrow(id, (jobId) => new Error(`Failed job ${jobId} not found.`));
4920
+ await this.repository.deleteById(id);
4921
+ return failedJob;
4922
+ }
4923
+ async flush() {
4924
+ const jobs = await this.repository.findAll();
4925
+ let deleted = 0;
4926
+ for (const job of jobs) {
4927
+ if (await this.repository.deleteById(job.id)) {
4928
+ deleted += 1;
4929
+ }
4930
+ }
4931
+ return deleted;
4932
+ }
4933
+ }
4934
+ var failedJobService_default = FailedJobService;
4935
+
4936
+ // ../../src/core/queue/jobRegistry.ts
4937
+ class JobRegistry {
4938
+ constructor() {}
4939
+ factories = new Map;
4940
+ instances = new WeakMap;
4941
+ register(name, factory) {
4942
+ this.factories.set(name, factory);
4943
+ }
4944
+ resolveName(job) {
4945
+ return this.instances.get(job);
4946
+ }
4947
+ track(name, job) {
4948
+ this.instances.set(job, name);
4949
+ return job;
4950
+ }
4951
+ create(name) {
4952
+ const factory = this.factories.get(name);
4953
+ if (!factory) {
4954
+ return;
4955
+ }
4956
+ return factory();
4957
+ }
4958
+ names() {
4959
+ return [...this.factories.keys()];
4960
+ }
4961
+ }
4962
+ var jobRegistry = new JobRegistry;
4963
+
4964
+ // ../../src/core/queue/redisQueue.ts
4965
+ var {RedisClient: RedisClient3 } = globalThis.Bun;
4966
+
4967
+ // ../../src/config/queue.ts
4968
+ var queueConfig = {
4969
+ driver: process.env.QUEUE_DRIVER === "redis" || process.env.QUEUE_DRIVER === "async" || process.env.QUEUE_DRIVER === "sync" ? process.env.QUEUE_DRIVER : DEFAULT_QUEUE_DRIVER,
4970
+ maxAttempts: Number(process.env.QUEUE_MAX_ATTEMPTS ?? "3"),
4971
+ backoffMs: Number(process.env.QUEUE_BACKOFF_MS ?? "1000")
4972
+ };
4973
+
4974
+ // ../../src/core/queue/jobRunner.ts
4975
+ async function runQueueJob(envelope, failedJobs) {
4976
+ const job = jobRegistry.create(envelope.name);
4977
+ if (!job) {
4978
+ throw new Error(`Unknown job "${envelope.name}".`);
4979
+ }
4980
+ const attempts = envelope.attempts ?? 0;
4981
+ try {
4982
+ await job.handle(envelope.payload);
4983
+ } catch (error) {
4984
+ const nextAttempt = attempts + 1;
4985
+ const maxAttempts = job.maxAttempts ?? queueConfig.maxAttempts;
4986
+ if (nextAttempt < maxAttempts) {
4987
+ const backoffMs = job.backoffMs ?? queueConfig.backoffMs;
4988
+ await new Promise((resolve) => setTimeout(resolve, backoffMs * nextAttempt));
4989
+ await runQueueJob({
4990
+ ...envelope,
4991
+ attempts: nextAttempt
4992
+ }, failedJobs);
4993
+ return;
4994
+ }
4995
+ await failedJobs.recordFailure({
4996
+ jobName: envelope.name,
4997
+ payload: envelope.payload,
4998
+ exception: error instanceof Error ? error.stack ?? error.message : String(error)
4999
+ });
5000
+ throw error;
5001
+ }
5002
+ }
5003
+
5004
+ // ../../src/core/queue/redisQueue.ts
5005
+ var QUEUE_LIST_KEY = "workhub:queue:default";
5006
+ var QUEUE_HIGH_KEY = "workhub:queue:high";
5007
+ var QUEUE_LOW_KEY = "workhub:queue:low";
5008
+ var QUEUE_KEYS = [QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY];
5009
+ function queueKeyForPriority(priority = "default") {
5010
+ switch (priority) {
5011
+ case "high":
5012
+ return QUEUE_HIGH_KEY;
5013
+ case "low":
5014
+ return QUEUE_LOW_KEY;
5015
+ default:
5016
+ return QUEUE_LIST_KEY;
5017
+ }
5018
+ }
5019
+ function parseQueueJobEnvelope(rawPayload) {
5020
+ let parsed;
5021
+ try {
5022
+ parsed = JSON.parse(rawPayload);
5023
+ } catch {
5024
+ console.error("[QueueWorker] Ignoring malformed queue payload");
5025
+ return null;
5026
+ }
5027
+ if (!parsed || typeof parsed !== "object") {
5028
+ console.error("[QueueWorker] Ignoring non-object queue payload");
5029
+ return null;
5030
+ }
5031
+ const envelope = parsed;
5032
+ if (typeof envelope.name !== "string" || envelope.name.length === 0) {
5033
+ console.error("[QueueWorker] Ignoring queue payload without job name");
5034
+ return null;
5035
+ }
5036
+ if (!jobRegistry.create(envelope.name)) {
5037
+ console.error(`[QueueWorker] Ignoring unknown job name: ${envelope.name}`);
5038
+ return null;
5039
+ }
5040
+ if (envelope.payload !== undefined && (typeof envelope.payload !== "object" || envelope.payload === null)) {
5041
+ console.error("[QueueWorker] Ignoring queue payload with invalid payload object");
5042
+ return null;
5043
+ }
5044
+ return {
5045
+ name: envelope.name,
5046
+ payload: envelope.payload ?? {},
5047
+ attempts: typeof envelope.attempts === "number" ? envelope.attempts : 0
5048
+ };
5049
+ }
5050
+
5051
+ class RedisQueue {
5052
+ client;
5053
+ constructor(redisUrl) {
5054
+ this.client = new RedisClient3(redisUrl);
5055
+ }
5056
+ async dispatch(job, payload) {
5057
+ const name = jobRegistry.resolveName(job);
5058
+ if (!name) {
5059
+ throw new Error("Job is not registered with the queue worker registry.");
5060
+ }
5061
+ const envelope = {
5062
+ name,
5063
+ payload,
5064
+ attempts: 0
5065
+ };
5066
+ const queueKey = queueKeyForPriority(job.priority);
5067
+ await this.client.lpush(queueKey, JSON.stringify(envelope));
5068
+ }
5069
+ }
5070
+
5071
+ class QueueWorker {
5072
+ failedJobs;
5073
+ timeoutSeconds;
5074
+ running = false;
5075
+ stopping = false;
5076
+ client;
5077
+ constructor(redisUrl, failedJobs, timeoutSeconds = 5) {
5078
+ this.failedJobs = failedJobs;
5079
+ this.timeoutSeconds = timeoutSeconds;
5080
+ this.client = new RedisClient3(redisUrl);
5081
+ }
5082
+ requestStop() {
5083
+ this.stopping = true;
5084
+ }
5085
+ isRunning() {
5086
+ return this.running;
5087
+ }
5088
+ async processNext() {
5089
+ let result = null;
5090
+ for (const queueKey of QUEUE_KEYS) {
5091
+ result = await this.client.brpop(queueKey, 1);
5092
+ if (result) {
5093
+ break;
5094
+ }
5095
+ }
5096
+ if (!result) {
5097
+ result = await this.client.brpop(QUEUE_LIST_KEY, this.timeoutSeconds);
5098
+ }
5099
+ if (!result) {
5100
+ return false;
5101
+ }
5102
+ const [, rawPayload] = result;
5103
+ const envelope = parseQueueJobEnvelope(rawPayload);
5104
+ if (!envelope) {
5105
+ return true;
5106
+ }
5107
+ try {
5108
+ await runQueueJob(envelope, this.failedJobs);
5109
+ } catch (error) {
5110
+ console.error("[QueueWorker] Job failed:", error);
5111
+ }
5112
+ return true;
5113
+ }
5114
+ async run() {
5115
+ this.running = true;
5116
+ while (!this.stopping) {
5117
+ await this.processNext();
5118
+ }
5119
+ this.running = false;
5120
+ }
5121
+ close() {
5122
+ this.client.close();
5123
+ }
5124
+ }
5125
+
5126
+ // ../../src/core/queue/resilientQueue.ts
5127
+ class ResilientQueue {
5128
+ failedJobs;
5129
+ asyncDispatch;
5130
+ constructor(failedJobs, asyncDispatch = false) {
5131
+ this.failedJobs = failedJobs;
5132
+ this.asyncDispatch = asyncDispatch;
5133
+ }
5134
+ async dispatch(job, payload) {
5135
+ const name = jobRegistry.resolveName(job);
5136
+ if (!name) {
5137
+ throw new Error("Job is not registered with the queue worker registry.");
5138
+ }
5139
+ const envelope = {
5140
+ name,
5141
+ payload,
5142
+ attempts: 0
5143
+ };
5144
+ if (this.asyncDispatch) {
5145
+ setTimeout(() => {
5146
+ runQueueJob(envelope, this.failedJobs).catch((error) => {
5147
+ console.error("[ResilientQueue] Job failed:", error);
5148
+ });
5149
+ }, 0);
5150
+ return;
5151
+ }
5152
+ await runQueueJob(envelope, this.failedJobs);
5153
+ }
5154
+ }
5155
+
5156
+ // ../../src/core/queue/publicQueue.ts
5157
+ function createFailedJobService() {
5158
+ return new failedJobService_default(new failedJobRepository_default);
5159
+ }
5160
+ function createTrackedJob(name, job) {
5161
+ return jobRegistry.track(name, job);
5162
+ }
5163
+ function createProductionQueue(driver, options = {}) {
5164
+ options.registerJobs?.();
5165
+ const failedJobs = options.failedJobs ?? createFailedJobService();
5166
+ if (driver === "redis") {
5167
+ if (!options.redisUrl) {
5168
+ throw new Error('QUEUE_DRIVER="redis" requires REDIS_URL to be set.');
5169
+ }
5170
+ return new RedisQueue(options.redisUrl);
5171
+ }
5172
+ return new ResilientQueue(failedJobs, driver === "async");
5173
+ }
5174
+ function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
5175
+ return new QueueWorker(redisUrl, failedJobs);
5176
+ }
5177
+ // ../../src/core/scheduler/schedule.ts
5178
+ class Schedule {
5179
+ tasks = [];
5180
+ command(expression, name, run) {
5181
+ this.tasks.push({ expression, name, run });
5182
+ return this;
5183
+ }
5184
+ dueTasks(now = new Date) {
5185
+ const minute = now.getMinutes();
5186
+ return this.tasks.filter((task) => {
5187
+ if (task.expression === "* * * * *") {
5188
+ return true;
5189
+ }
5190
+ if (task.expression.startsWith("*/")) {
5191
+ const interval = Number.parseInt(task.expression.slice(2), 10);
5192
+ return Number.isInteger(interval) && interval > 0 && minute % interval === 0;
5193
+ }
5194
+ return false;
5195
+ });
5196
+ }
5197
+ tasksList() {
5198
+ return [...this.tasks];
5199
+ }
5200
+ }
5201
+ var appSchedule = new Schedule;
5202
+ async function runDueScheduledTasks(schedule = appSchedule, now = new Date) {
5203
+ const due = schedule.dueTasks(now);
5204
+ for (const task of due) {
5205
+ await task.run();
5206
+ }
5207
+ return due.length;
5208
+ }
2416
5209
  // ../../src/core/validation/rules.ts
2417
5210
  function required() {
2418
5211
  return (field, value) => {
@@ -2455,6 +5248,45 @@ function maxLength(maximum) {
2455
5248
  return;
2456
5249
  };
2457
5250
  }
5251
+ function pattern(expression) {
5252
+ return (field, value) => {
5253
+ if (typeof value !== "string") {
5254
+ return;
5255
+ }
5256
+ if (!expression.test(value.trim())) {
5257
+ return `"${field}" has an invalid format.`;
5258
+ }
5259
+ return;
5260
+ };
5261
+ }
5262
+ function enumRule(allowedValues) {
5263
+ return (field, value) => {
5264
+ if (typeof value !== "string") {
5265
+ return;
5266
+ }
5267
+ if (!allowedValues.includes(value)) {
5268
+ return `"${field}" must be one of: ${allowedValues.join(", ")}.`;
5269
+ }
5270
+ return;
5271
+ };
5272
+ }
5273
+ function optional() {
5274
+ return () => {
5275
+ return;
5276
+ };
5277
+ }
5278
+ function integerRule() {
5279
+ return (field, value) => {
5280
+ if (value === undefined || value === null || value === "") {
5281
+ return;
5282
+ }
5283
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
5284
+ if (!Number.isInteger(parsed)) {
5285
+ return `"${field}" must be an integer.`;
5286
+ }
5287
+ return;
5288
+ };
5289
+ }
2458
5290
  function emailRule() {
2459
5291
  return (field, value) => {
2460
5292
  if (typeof value !== "string") {
@@ -2467,6 +5299,40 @@ function emailRule() {
2467
5299
  return;
2468
5300
  };
2469
5301
  }
5302
+ function confirmed(fieldName) {
5303
+ return (field, value, payload) => {
5304
+ const confirmationKey = `${fieldName}_confirmation`;
5305
+ const confirmation = payload[confirmationKey];
5306
+ if (value !== confirmation) {
5307
+ return `"${field}" confirmation does not match.`;
5308
+ }
5309
+ return;
5310
+ };
5311
+ }
5312
+ function positiveIntegerRule() {
5313
+ return (field, value) => {
5314
+ if (value === undefined || value === null || value === "") {
5315
+ return;
5316
+ }
5317
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
5318
+ if (!Number.isInteger(parsed) || parsed <= 0) {
5319
+ return `"${field}" must be a positive integer.`;
5320
+ }
5321
+ return;
5322
+ };
5323
+ }
5324
+ function integerRange(minimum, maximum) {
5325
+ return (field, value) => {
5326
+ if (value === undefined || value === null || value === "") {
5327
+ return;
5328
+ }
5329
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
5330
+ if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
5331
+ return `"${field}" must be an integer between ${minimum} and ${maximum}.`;
5332
+ }
5333
+ return;
5334
+ };
5335
+ }
2470
5336
  function validateObject(payload, schema) {
2471
5337
  if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
2472
5338
  throw new ValidationError("Request body must be a JSON object.");
@@ -2491,15 +5357,29 @@ function validateObject(payload, schema) {
2491
5357
  return output;
2492
5358
  }
2493
5359
  export {
5360
+ withMigrationLock,
5361
+ withMiddleware,
2494
5362
  withErrorHandling,
2495
5363
  validateObject,
5364
+ toResourceCollection,
5365
+ toPaginatedResourceCollection,
2496
5366
  stringRule,
2497
5367
  storageFacade as storage,
5368
+ serializeDate,
5369
+ securedBindRouteModelByKey,
2498
5370
  securedBindRouteModel,
5371
+ runWithDatabaseConnection,
2499
5372
  runWithAuthUser,
5373
+ runSeedersFromDirectory,
5374
+ runInTransaction,
5375
+ runGracefulShutdown,
5376
+ runDueScheduledTasks,
2500
5377
  rollbackDatabase,
5378
+ resolveWebLayoutData,
2501
5379
  resolveService,
5380
+ resolveDatabaseDriver,
2502
5381
  required,
5382
+ registerShutdownHandler,
2503
5383
  registerModelRepository,
2504
5384
  queue,
2505
5385
  prometheusRegistry,
@@ -2514,53 +5394,105 @@ export {
2514
5394
  mailer,
2515
5395
  mail,
2516
5396
  log,
5397
+ loadSeedersFromDirectory,
2517
5398
  loadMigrationsFromDirectory,
2518
- jsonResponse,
5399
+ jsonResponse2 as jsonResponse,
5400
+ jobRegistry,
5401
+ isHtmxRequest,
2519
5402
  isEtagEnabled,
5403
+ installGracefulShutdownSignals,
5404
+ inferReferencedTable,
5405
+ indexHasOneRelation,
2520
5406
  indexHasManyRelation,
2521
5407
  indexBelongsToRelation,
5408
+ indexBelongsToManyRelation,
5409
+ hydrateValue,
5410
+ htmlResponse,
5411
+ hasOne,
2522
5412
  hasMany,
5413
+ grammarForDriver,
2523
5414
  getMigrationStatus,
5415
+ getActiveDatabaseConnection,
2524
5416
  freshDatabase,
5417
+ filterMassAssignable,
2525
5418
  events,
2526
5419
  etagFromResource,
2527
5420
  emailRule,
5421
+ dehydrateValue,
2528
5422
  defineTable,
2529
5423
  currentAuthUser,
2530
5424
  createdResponse,
5425
+ createTrackedJob,
5426
+ createThrottleMiddleware,
5427
+ createSecurityHeadersMiddleware,
5428
+ createSchemaBuilder,
5429
+ createRequireWebAuthMiddleware,
5430
+ createRequireAuthMiddleware,
5431
+ createQueueWorker,
2531
5432
  createQueue,
5433
+ createProductionQueue,
2532
5434
  createMetricsMiddleware,
5435
+ createMemoryThrottleMiddleware,
5436
+ createLoginThrottleMiddleware,
5437
+ createFailedJobService,
5438
+ createCsrfMiddleware,
5439
+ createBodySizeLimitMiddleware,
5440
+ createAuthorizeMiddleware,
5441
+ createAuthMiddleware,
2533
5442
  config,
5443
+ composeMiddleware,
5444
+ compileBlueprint,
2534
5445
  cache,
2535
- bindDatabaseConnection,
5446
+ bindDatabaseConnection2 as bindDatabaseConnection,
5447
+ belongsToMany,
2536
5448
  belongsTo,
2537
5449
  auth,
2538
5450
  assertIfMatch,
5451
+ applyMiddlewareToRoutes,
5452
+ applyCasts,
5453
+ appSchedule,
5454
+ WhereBuilder,
2539
5455
  WebFormRequest,
2540
5456
  ValidationError,
5457
+ UnsupportedSchemaFeatureError,
2541
5458
  UnprocessableEntityError,
2542
5459
  UnauthorizedError,
2543
5460
  SyncQueue,
2544
5461
  StorageManager,
5462
+ SqliteGrammar,
2545
5463
  ServiceContainer,
5464
+ Schema,
5465
+ Schedule,
5466
+ ResilientQueue,
2546
5467
  RepositoryQuery,
5468
+ RedisQueue,
5469
+ QueueWorker,
2547
5470
  PrometheusRegistry,
2548
5471
  PreconditionFailedError,
5472
+ PostgresGrammar,
2549
5473
  PolicyGate,
2550
5474
  Policy,
2551
5475
  NotFoundError,
5476
+ MySqlGrammar,
2552
5477
  Model,
2553
5478
  Mailer,
2554
5479
  LogMailDriver,
2555
5480
  LocalStorageDriver,
2556
5481
  Job,
2557
5482
  FormRequest,
5483
+ ForeignIdColumnDefinition,
2558
5484
  ForbiddenError,
5485
+ failedJobService_default as FailedJobService,
5486
+ failedJobRepository_default as FailedJobRepository,
2559
5487
  EventBus,
5488
+ EtaViewEngine,
5489
+ DEFAULT_VIEWS_DIRECTORY,
2560
5490
  ConflictError,
2561
5491
  ConfigStore,
5492
+ ColumnDefinition,
2562
5493
  repository_default as CacheRepository,
2563
5494
  CACHE_TAGS,
5495
+ Blueprint,
2564
5496
  baseRepository_default as BaseRepository,
2565
5497
  BadRequestError,
2566
5498
  AsyncQueue