@getstrata/core 0.4.0 → 0.5.1

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