@open-mercato/shared 0.6.7 → 0.6.8-canary.320.1.bf889f49e4

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 (224) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +62 -6
  3. package/build.mjs +2 -4
  4. package/dist/lib/ai/safety-identifier.js +32 -0
  5. package/dist/lib/ai/safety-identifier.js.map +7 -0
  6. package/dist/lib/auth/organizationScope.js +34 -0
  7. package/dist/lib/auth/organizationScope.js.map +7 -0
  8. package/dist/lib/auth/server.js +15 -2
  9. package/dist/lib/auth/server.js.map +2 -2
  10. package/dist/lib/bootstrap/clientOnlyModules.js +55 -0
  11. package/dist/lib/bootstrap/clientOnlyModules.js.map +7 -0
  12. package/dist/lib/bootstrap/dynamicLoader.js +190 -70
  13. package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
  14. package/dist/lib/bootstrap/factory.js +18 -13
  15. package/dist/lib/bootstrap/factory.js.map +2 -2
  16. package/dist/lib/commands/command-bus.js +9 -3
  17. package/dist/lib/commands/command-bus.js.map +2 -2
  18. package/dist/lib/commands/command-interceptor-runner.js +9 -4
  19. package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
  20. package/dist/lib/commands/errors.js +25 -4
  21. package/dist/lib/commands/errors.js.map +2 -2
  22. package/dist/lib/commands/index.js +7 -1
  23. package/dist/lib/commands/index.js.map +2 -2
  24. package/dist/lib/crud/enricher-runner.js +2 -11
  25. package/dist/lib/crud/enricher-runner.js.map +2 -2
  26. package/dist/lib/crud/factory.js +104 -23
  27. package/dist/lib/crud/factory.js.map +2 -2
  28. package/dist/lib/crud/interceptor-runner.js +2 -2
  29. package/dist/lib/crud/interceptor-runner.js.map +2 -2
  30. package/dist/lib/crud/mutation-guard-registry.js +2 -2
  31. package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
  32. package/dist/lib/crud/types.js +4 -0
  33. package/dist/lib/crud/types.js.map +3 -3
  34. package/dist/lib/data/consistency.js +19 -0
  35. package/dist/lib/data/consistency.js.map +7 -0
  36. package/dist/lib/data/engine.js +37 -11
  37. package/dist/lib/data/engine.js.map +2 -2
  38. package/dist/lib/db/entityMetadata.js +26 -0
  39. package/dist/lib/db/entityMetadata.js.map +7 -0
  40. package/dist/lib/db/pg-errors.js +42 -0
  41. package/dist/lib/db/pg-errors.js.map +2 -2
  42. package/dist/lib/di/container.js +48 -6
  43. package/dist/lib/di/container.js.map +2 -2
  44. package/dist/lib/email/send.js +56 -0
  45. package/dist/lib/email/send.js.map +2 -2
  46. package/dist/lib/encryption/find.js +22 -0
  47. package/dist/lib/encryption/find.js.map +2 -2
  48. package/dist/lib/encryption/likeFilterWarning.js +66 -0
  49. package/dist/lib/encryption/likeFilterWarning.js.map +7 -0
  50. package/dist/lib/encryption/subscriber.js +2 -10
  51. package/dist/lib/encryption/subscriber.js.map +2 -2
  52. package/dist/lib/encryption/tenantDataEncryptionService.js +52 -17
  53. package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
  54. package/dist/lib/i18n/config.js +1 -1
  55. package/dist/lib/i18n/config.js.map +2 -2
  56. package/dist/lib/i18n/dictionary-cache.js +5 -0
  57. package/dist/lib/i18n/dictionary-cache.js.map +2 -2
  58. package/dist/lib/i18n/server.js +4 -4
  59. package/dist/lib/i18n/server.js.map +3 -3
  60. package/dist/lib/logger/extension.js +29 -0
  61. package/dist/lib/logger/extension.js.map +7 -0
  62. package/dist/lib/logger/index.js +47 -3
  63. package/dist/lib/logger/index.js.map +3 -3
  64. package/dist/lib/modules/registry.js +36 -6
  65. package/dist/lib/modules/registry.js.map +2 -2
  66. package/dist/lib/modules/surfaceFingerprint.js +47 -0
  67. package/dist/lib/modules/surfaceFingerprint.js.map +7 -0
  68. package/dist/lib/query/ciphertext-search-warning.js +51 -0
  69. package/dist/lib/query/ciphertext-search-warning.js.map +7 -0
  70. package/dist/lib/query/engine.js +60 -43
  71. package/dist/lib/query/engine.js.map +2 -2
  72. package/dist/lib/query/types.js.map +1 -1
  73. package/dist/lib/redis/connection.js +7 -2
  74. package/dist/lib/redis/connection.js.map +2 -2
  75. package/dist/lib/search/auto-indexing.js +14 -0
  76. package/dist/lib/search/auto-indexing.js.map +7 -0
  77. package/dist/lib/search/availability.js +107 -0
  78. package/dist/lib/search/availability.js.map +7 -0
  79. package/dist/lib/search/config.js +61 -2
  80. package/dist/lib/search/config.js.map +2 -2
  81. package/dist/lib/search/tokenLookup.js +46 -0
  82. package/dist/lib/search/tokenLookup.js.map +7 -0
  83. package/dist/lib/search/tokenize.js +23 -11
  84. package/dist/lib/search/tokenize.js.map +2 -2
  85. package/dist/lib/telemetry/runtime.js +35 -0
  86. package/dist/lib/telemetry/runtime.js.map +7 -0
  87. package/dist/lib/version.js +1 -1
  88. package/dist/lib/version.js.map +1 -1
  89. package/dist/lib/webhooks/body.js +57 -0
  90. package/dist/lib/webhooks/body.js.map +7 -0
  91. package/dist/lib/webhooks/inbound-types.js +1 -0
  92. package/dist/lib/webhooks/inbound-types.js.map +7 -0
  93. package/dist/lib/webhooks/index.js +13 -1
  94. package/dist/lib/webhooks/index.js.map +2 -2
  95. package/dist/lib/webhooks/verify.js +12 -7
  96. package/dist/lib/webhooks/verify.js.map +2 -2
  97. package/dist/modules/analytics.js.map +2 -2
  98. package/dist/modules/overrides.js +50 -1
  99. package/dist/modules/overrides.js.map +2 -2
  100. package/dist/modules/payment_gateways/types.js +8 -1
  101. package/dist/modules/payment_gateways/types.js.map +2 -2
  102. package/dist/modules/registry.js +1 -0
  103. package/dist/modules/registry.js.map +2 -2
  104. package/dist/modules/widgets/extension-points.js +96 -0
  105. package/dist/modules/widgets/extension-points.js.map +7 -0
  106. package/dist/security/enabledModulesRegistry.js +56 -11
  107. package/dist/security/enabledModulesRegistry.js.map +2 -2
  108. package/dist/security/featurePolicy.js +62 -0
  109. package/dist/security/featurePolicy.js.map +7 -0
  110. package/jest.config.cjs +20 -10
  111. package/package.json +19 -9
  112. package/scripts/versionSource.cjs +65 -0
  113. package/src/lib/__tests__/versionSource.test.ts +64 -0
  114. package/src/lib/ai/__tests__/llm-provider-contract.test.ts +59 -0
  115. package/src/lib/ai/__tests__/safety-identifier.test.ts +71 -0
  116. package/src/lib/ai/llm-provider.ts +33 -0
  117. package/src/lib/ai/safety-identifier.ts +73 -0
  118. package/src/lib/auth/__tests__/organizationScope.test.ts +107 -0
  119. package/src/lib/auth/__tests__/server.test.ts +23 -0
  120. package/src/lib/auth/organizationScope.ts +65 -0
  121. package/src/lib/auth/server.ts +23 -2
  122. package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +189 -0
  123. package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +98 -0
  124. package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +211 -0
  125. package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +63 -8
  126. package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +256 -0
  127. package/src/lib/bootstrap/__tests__/dynamicLoader.test.ts +126 -0
  128. package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +21 -1
  129. package/src/lib/bootstrap/__tests__/factory.test.ts +78 -0
  130. package/src/lib/bootstrap/clientOnlyModules.ts +85 -0
  131. package/src/lib/bootstrap/dynamicLoader.ts +346 -96
  132. package/src/lib/bootstrap/factory.ts +21 -15
  133. package/src/lib/bootstrap/types.ts +6 -0
  134. package/src/lib/commands/__tests__/command-bus.test.ts +63 -0
  135. package/src/lib/commands/__tests__/command-interceptor-error.test.ts +91 -1
  136. package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +101 -0
  137. package/src/lib/commands/command-bus.ts +9 -3
  138. package/src/lib/commands/command-interceptor-runner.ts +27 -6
  139. package/src/lib/commands/command-interceptor.ts +8 -0
  140. package/src/lib/commands/errors.ts +67 -2
  141. package/src/lib/commands/index.ts +7 -1
  142. package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +171 -0
  143. package/src/lib/crud/__tests__/crud-factory.test.ts +564 -0
  144. package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +26 -0
  145. package/src/lib/crud/enricher-runner.ts +2 -11
  146. package/src/lib/crud/factory.ts +157 -22
  147. package/src/lib/crud/interceptor-runner.ts +2 -2
  148. package/src/lib/crud/mutation-guard-registry.ts +2 -2
  149. package/src/lib/crud/types.ts +3 -0
  150. package/src/lib/data/__tests__/consistency.test.ts +38 -0
  151. package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +18 -3
  152. package/src/lib/data/consistency.ts +17 -0
  153. package/src/lib/data/engine.ts +50 -16
  154. package/src/lib/db/__tests__/entityMetadata.test.ts +38 -0
  155. package/src/lib/db/__tests__/pg-errors.test.ts +48 -0
  156. package/src/lib/db/entityMetadata.ts +43 -0
  157. package/src/lib/db/pg-errors.ts +58 -0
  158. package/src/lib/di/__tests__/container-app-di-absent.test.ts +155 -0
  159. package/src/lib/di/__tests__/container-app-di.test.ts +173 -0
  160. package/src/lib/di/__tests__/registrar-error-log.test.ts +154 -0
  161. package/src/lib/di/container.ts +72 -8
  162. package/src/lib/email/__tests__/send.test.ts +37 -1
  163. package/src/lib/email/send.ts +79 -0
  164. package/src/lib/encryption/__tests__/likeFilterWarning.test.ts +167 -0
  165. package/src/lib/encryption/__tests__/subscriber.test.ts +54 -2
  166. package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +58 -0
  167. package/src/lib/encryption/find.ts +22 -0
  168. package/src/lib/encryption/likeFilterWarning.ts +102 -0
  169. package/src/lib/encryption/subscriber.ts +2 -14
  170. package/src/lib/encryption/tenantDataEncryptionService.ts +64 -18
  171. package/src/lib/i18n/__tests__/config.test.ts +8 -0
  172. package/src/lib/i18n/__tests__/dictionary-cache.test.ts +23 -0
  173. package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +30 -0
  174. package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +29 -0
  175. package/src/lib/i18n/config.ts +2 -2
  176. package/src/lib/i18n/dictionary-cache.ts +5 -0
  177. package/src/lib/i18n/server.ts +6 -2
  178. package/src/lib/logger/__tests__/logger.test.ts +77 -0
  179. package/src/lib/logger/extension.ts +59 -0
  180. package/src/lib/logger/index.ts +59 -1
  181. package/src/lib/modules/__tests__/registry.test.ts +26 -0
  182. package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +122 -0
  183. package/src/lib/modules/registry.ts +46 -7
  184. package/src/lib/modules/surfaceFingerprint.ts +87 -0
  185. package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +178 -0
  186. package/src/lib/query/__tests__/engine.scope-and-or.test.ts +67 -0
  187. package/src/lib/query/__tests__/engine.test.ts +213 -13
  188. package/src/lib/query/ciphertext-search-warning.ts +106 -0
  189. package/src/lib/query/engine.ts +102 -61
  190. package/src/lib/query/types.ts +4 -5
  191. package/src/lib/redis/__tests__/connection.test.ts +16 -0
  192. package/src/lib/redis/connection.ts +10 -7
  193. package/src/lib/search/__tests__/availability.test.ts +211 -0
  194. package/src/lib/search/__tests__/config.test.ts +177 -0
  195. package/src/lib/search/__tests__/tokenLookup.test.ts +206 -0
  196. package/src/lib/search/__tests__/tokenize.test.ts +49 -0
  197. package/src/lib/search/auto-indexing.ts +22 -0
  198. package/src/lib/search/availability.ts +232 -0
  199. package/src/lib/search/config.ts +106 -8
  200. package/src/lib/search/tokenLookup.ts +133 -0
  201. package/src/lib/search/tokenize.ts +33 -11
  202. package/src/lib/telemetry/runtime.ts +72 -0
  203. package/src/lib/webhooks/__tests__/body.test.ts +95 -0
  204. package/src/lib/webhooks/__tests__/verify.test.ts +20 -1
  205. package/src/lib/webhooks/body.ts +70 -0
  206. package/src/lib/webhooks/inbound-types.ts +125 -0
  207. package/src/lib/webhooks/index.ts +19 -1
  208. package/src/lib/webhooks/verify.ts +17 -9
  209. package/src/modules/__tests__/nav-group-order-override.test.ts +183 -0
  210. package/src/modules/__tests__/registry.test.ts +10 -2
  211. package/src/modules/analytics.ts +7 -0
  212. package/src/modules/customer-auth.ts +1 -0
  213. package/src/modules/encryption.ts +3 -0
  214. package/src/modules/entities.ts +15 -0
  215. package/src/modules/navigation/backendChrome.ts +15 -0
  216. package/src/modules/overrides.ts +103 -0
  217. package/src/modules/payment_gateways/__tests__/types.test.ts +37 -0
  218. package/src/modules/payment_gateways/types.ts +41 -0
  219. package/src/modules/registry.ts +3 -0
  220. package/src/modules/widgets/__tests__/extension-points.test.ts +101 -0
  221. package/src/modules/widgets/extension-points.ts +524 -0
  222. package/src/security/__tests__/featurePolicy.test.ts +166 -0
  223. package/src/security/enabledModulesRegistry.ts +64 -12
  224. package/src/security/featurePolicy.ts +89 -0
@@ -5,7 +5,49 @@ function isUniqueViolation(err) {
5
5
  const message = err.message;
6
6
  return typeof message === "string" && /duplicate key value|unique constraint/i.test(message);
7
7
  }
8
+ const TRANSIENT_CONNECTION_SQLSTATES = /* @__PURE__ */ new Set([
9
+ "53300",
10
+ // too_many_connections
11
+ "53400",
12
+ // configuration_limit_exceeded
13
+ "57P01",
14
+ // admin_shutdown
15
+ "57P02",
16
+ // crash_shutdown
17
+ "57P03",
18
+ // cannot_connect_now (db starting up)
19
+ "08000",
20
+ // connection_exception
21
+ "08001",
22
+ // sqlclient_unable_to_establish_sqlconnection
23
+ "08003",
24
+ // connection_does_not_exist
25
+ "08006"
26
+ // connection_failure
27
+ ]);
28
+ const TRANSIENT_DB_MESSAGE_PATTERNS = [
29
+ /too many clients already/i,
30
+ /unable to acquire a connection/i,
31
+ /timeout acquiring a connection/i,
32
+ /connection terminated/i,
33
+ /the database system is (starting up|shutting down|in recovery)/i
34
+ ];
35
+ function isTransientDbError(err) {
36
+ if (!err || typeof err !== "object") {
37
+ return false;
38
+ }
39
+ const code = err.code;
40
+ if (typeof code === "string" && TRANSIENT_CONNECTION_SQLSTATES.has(code)) {
41
+ return true;
42
+ }
43
+ const message = err.message;
44
+ if (typeof message === "string" && TRANSIENT_DB_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))) {
45
+ return true;
46
+ }
47
+ return false;
48
+ }
8
49
  export {
50
+ isTransientDbError,
9
51
  isUniqueViolation
10
52
  };
11
53
  //# sourceMappingURL=pg-errors.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/db/pg-errors.ts"],
4
- "sourcesContent": ["/**\n * Detect a Postgres unique-constraint violation (SQLSTATE 23505) regardless of\n * the ORM/driver layer that surfaces it. Shared across modules so duplicate-insert\n * handling stays consistent platform-wide.\n */\nexport function isUniqueViolation(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false\n const code = (err as { code?: string }).code\n if (code === '23505') return true // Postgres unique_violation\n const message = (err as { message?: string }).message\n return typeof message === 'string' && /duplicate key value|unique constraint/i.test(message)\n}\n"],
5
- "mappings": "AAKO,SAAS,kBAAkB,KAAuB;AACvD,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,OAAQ,IAA0B;AACxC,MAAI,SAAS,QAAS,QAAO;AAC7B,QAAM,UAAW,IAA6B;AAC9C,SAAO,OAAO,YAAY,YAAY,yCAAyC,KAAK,OAAO;AAC7F;",
4
+ "sourcesContent": ["/**\n * Detect a Postgres unique-constraint violation (SQLSTATE 23505) regardless of\n * the ORM/driver layer that surfaces it. Shared across modules so duplicate-insert\n * handling stays consistent platform-wide.\n */\nexport function isUniqueViolation(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false\n const code = (err as { code?: string }).code\n if (code === '23505') return true // Postgres unique_violation\n const message = (err as { message?: string }).message\n return typeof message === 'string' && /duplicate key value|unique constraint/i.test(message)\n}\n\n/**\n * Postgres SQLSTATEs for transient connection / availability failures \u2014 the\n * database (or its connection pool) is temporarily unreachable and the request\n * can succeed on retry. Deliberately scoped to connection/availability codes;\n * query-level conflicts (deadlock 40P01, serialization 40001, lock_not_available\n * 55P03) are NOT included because they do not mean \"service unavailable\".\n */\nconst TRANSIENT_CONNECTION_SQLSTATES = new Set([\n '53300', // too_many_connections\n '53400', // configuration_limit_exceeded\n '57P01', // admin_shutdown\n '57P02', // crash_shutdown\n '57P03', // cannot_connect_now (db starting up)\n '08000', // connection_exception\n '08001', // sqlclient_unable_to_establish_sqlconnection\n '08003', // connection_does_not_exist\n '08006', // connection_failure\n])\n\n/**\n * Postgres-driver / connection-pool messages that describe a transient DB\n * connection failure when the SQLSTATE is dropped by the ORM wrapper. These are\n * intentionally DB-specific phrases only. Bare socket codes (ECONNREFUSED,\n * ETIMEDOUT, \u2026) are deliberately NOT matched: they can originate from any\n * outbound socket (HTTP, cache, queue), so keying off them would falsely\n * attribute unrelated failures to the database.\n */\nconst TRANSIENT_DB_MESSAGE_PATTERNS = [\n /too many clients already/i,\n /unable to acquire a connection/i,\n /timeout acquiring a connection/i,\n /connection terminated/i,\n /the database system is (starting up|shutting down|in recovery)/i,\n]\n\n/**\n * Detect a transient Postgres connection / availability failure (pool exhaustion,\n * `max_connections` reached, DB restarting) via its SQLSTATE or a DB-specific\n * driver message. Gates retryable 503 responses so callers do not report a\n * temporary infrastructure blip as an auth failure (401) or an unexpected server\n * error (500). Scoped to unambiguous DB signals so generic socket errors from\n * non-DB calls are never misclassified.\n */\nexport function isTransientDbError(err: unknown): boolean {\n if (!err || typeof err !== 'object') {\n return false\n }\n const code = (err as { code?: string }).code\n if (typeof code === 'string' && TRANSIENT_CONNECTION_SQLSTATES.has(code)) {\n return true\n }\n const message = (err as { message?: string }).message\n if (typeof message === 'string' && TRANSIENT_DB_MESSAGE_PATTERNS.some((pattern) => pattern.test(message))) {\n return true\n }\n return false\n}\n"],
5
+ "mappings": "AAKO,SAAS,kBAAkB,KAAuB;AACvD,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,OAAQ,IAA0B;AACxC,MAAI,SAAS,QAAS,QAAO;AAC7B,QAAM,UAAW,IAA6B;AAC9C,SAAO,OAAO,YAAY,YAAY,yCAAyC,KAAK,OAAO;AAC7F;AASA,MAAM,iCAAiC,oBAAI,IAAI;AAAA,EAC7C;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AAAA,EACA;AAAA;AACF,CAAC;AAUD,MAAM,gCAAgC;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAUO,SAAS,mBAAmB,KAAuB;AACxD,MAAI,CAAC,OAAO,OAAO,QAAQ,UAAU;AACnC,WAAO;AAAA,EACT;AACA,QAAM,OAAQ,IAA0B;AACxC,MAAI,OAAO,SAAS,YAAY,+BAA+B,IAAI,IAAI,GAAG;AACxE,WAAO;AAAA,EACT;AACA,QAAM,UAAW,IAA6B;AAC9C,MAAI,OAAO,YAAY,YAAY,8BAA8B,KAAK,CAAC,YAAY,QAAQ,KAAK,OAAO,CAAC,GAAG;AACzG,WAAO;AAAA,EACT;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -12,6 +12,9 @@ import { createLogger } from "../logger/index.js";
12
12
  const logger = createLogger("shared").child({ component: "di" });
13
13
  const GLOBAL_KEY = "__openMercatoDiRegistrars__";
14
14
  const APP_DI_REGISTRAR_KEY = "__openMercatoAppDiRegistrar__";
15
+ const APP_DI_LOAD_WARNING_KEY = "__openMercatoAppDiLoadWarningEmitted__";
16
+ const APP_DI_REGISTER_WARNING_KEY = "__openMercatoAppDiRegisterWarningEmitted__";
17
+ const APP_DI_MODULE_UNRESOLVABLE_KEY = "__openMercatoAppDiModuleUnresolvable__";
15
18
  const BOOTSTRAP_CACHE_KEY = "__openMercatoBootstrapCache__";
16
19
  const ENCRYPTION_ENABLED_KEY = "__openMercatoEncryptionEnabledCache__";
17
20
  const BOOTSTRAP_CACHE_KEYS = [
@@ -77,6 +80,7 @@ function registerDiRegistrars(registrars) {
77
80
  setGlobalRegistrars(registrars);
78
81
  globalThis[BOOTSTRAP_CACHE_KEY] = null;
79
82
  globalThis[ENCRYPTION_ENABLED_KEY] = void 0;
83
+ globalThis[APP_DI_MODULE_UNRESOLVABLE_KEY] = void 0;
80
84
  }
81
85
  function getDiRegistrars() {
82
86
  const registrars = getGlobalRegistrars();
@@ -96,6 +100,29 @@ function registerAppDiRegistrar(registrar) {
96
100
  function resetBootstrapCache() {
97
101
  globalThis[BOOTSTRAP_CACHE_KEY] = null;
98
102
  globalThis[ENCRYPTION_ENABLED_KEY] = void 0;
103
+ globalThis[APP_DI_LOAD_WARNING_KEY] = void 0;
104
+ globalThis[APP_DI_REGISTER_WARNING_KEY] = void 0;
105
+ globalThis[APP_DI_MODULE_UNRESOLVABLE_KEY] = void 0;
106
+ }
107
+ function isAppDiModuleUnresolvable() {
108
+ return globalThis[APP_DI_MODULE_UNRESOLVABLE_KEY] === true;
109
+ }
110
+ function markAppDiModuleUnresolvable() {
111
+ ;
112
+ globalThis[APP_DI_MODULE_UNRESOLVABLE_KEY] = true;
113
+ }
114
+ function isAppDiModuleNotFound(error) {
115
+ if (!error || typeof error !== "object") return false;
116
+ const { code, message } = error;
117
+ const text = typeof message === "string" ? message : "";
118
+ const moduleNotFound = code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND" || text.startsWith("Cannot find module") || text.startsWith("Cannot find package");
119
+ return moduleNotFound && /(?:module|package) ['"]@\/di['"]/.test(text);
120
+ }
121
+ function warnAppDiFailureOnce(key, message, error) {
122
+ const globalScope = globalThis;
123
+ if (globalScope[key] === true) return;
124
+ globalScope[key] = true;
125
+ logger.warn(message, { err: error });
99
126
  }
100
127
  function isAwilixResolver(value) {
101
128
  return Boolean(value && typeof value === "object" && typeof value.resolve === "function");
@@ -151,10 +178,11 @@ async function createRequestContainer() {
151
178
  () => createCommandOptimisticLockGuardService()
152
179
  ).scoped()
153
180
  });
154
- for (const reg of diRegistrars) {
181
+ for (const [registrarIndex, reg] of diRegistrars.entries()) {
155
182
  try {
156
183
  reg?.(container);
157
- } catch {
184
+ } catch (error) {
185
+ logger.error("Module DI registrar failed", { registrarIndex, err: error });
158
186
  }
159
187
  }
160
188
  const alreadyBootstrappedOnThisContainer = !!container.registrations?.eventBus;
@@ -184,18 +212,32 @@ async function createRequestContainer() {
184
212
  } catch (error) {
185
213
  logger.error("App-level DI registrar failed", { err: error });
186
214
  }
187
- } else {
215
+ } else if (!isAppDiModuleUnresolvable()) {
188
216
  try {
189
217
  const appDi = await import("@/di");
190
218
  if (appDi?.register) {
191
219
  try {
192
220
  const maybe = appDi.register(container);
193
221
  if (maybe && typeof maybe.then === "function") await maybe;
194
- } catch (error) {
195
- logger.error("App-level DI registrar failed", { err: error });
222
+ } catch (err) {
223
+ warnAppDiFailureOnce(
224
+ APP_DI_REGISTER_WARNING_KEY,
225
+ "App-level DI override (src/di.ts register()) threw; its registrations are skipped",
226
+ err
227
+ );
196
228
  }
197
229
  }
198
- } catch {
230
+ } catch (err) {
231
+ if (isAppDiModuleNotFound(err)) {
232
+ markAppDiModuleUnresolvable();
233
+ logger.debug("App-level DI override module (@/di) not resolvable; skipping", { err });
234
+ } else {
235
+ warnAppDiFailureOnce(
236
+ APP_DI_LOAD_WARNING_KEY,
237
+ "App-level DI override module (@/di) failed to load; its registrations are skipped",
238
+ err
239
+ );
240
+ }
199
241
  }
200
242
  }
201
243
  applyDiOverridesToContainer({
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/di/container.ts"],
4
- "sourcesContent": ["import { asFunction, createContainer, asValue, AwilixContainer, InjectionMode, type Resolver } from 'awilix'\nimport { RequestContext } from '@mikro-orm/core'\nimport { getOrm } from '@open-mercato/shared/lib/db/mikro'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { BasicQueryEngine } from '@open-mercato/shared/lib/query/engine'\nimport { DefaultDataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { commandRegistry, CommandBus } from '@open-mercato/shared/lib/commands'\nimport { applyDiOverridesToContainer } from '@open-mercato/shared/modules/overrides'\nimport { createOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock'\nimport { getAllOptimisticLockReaders } from '@open-mercato/shared/lib/crud/optimistic-lock-store'\nimport { createCommandOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'di' })\n\ntype DynamicCradle = Record<string, any>\n\nexport type AppContainer = AwilixContainer<DynamicCradle>\nexport type DiRegistrar = (container: AppContainer) => void\nexport type AppDiRegistrar = (container: AppContainer) => void | Promise<void>\n\n// Registration pattern for publishable packages\n// Use globalThis to survive tsx/esbuild module duplication issue where the same\n// file can be loaded as multiple module instances when mixing dynamic and static imports\nconst GLOBAL_KEY = '__openMercatoDiRegistrars__'\nconst APP_DI_REGISTRAR_KEY = '__openMercatoAppDiRegistrar__'\n// Phase 5 \u2014 process-scoped bootstrap cache. The cache/event-bus/encryption\n// services bootstrap() creates are inherently process-scoped (they hold\n// state across requests). Caching them on globalThis after the first\n// successful bootstrap call lets every subsequent request skip the\n// `await bootstrap(container)` body and just re-register the cached\n// instances. Same globalThis pattern as registerDiRegistrars so HMR\n// keeps working.\nconst BOOTSTRAP_CACHE_KEY = '__openMercatoBootstrapCache__'\nconst ENCRYPTION_ENABLED_KEY = '__openMercatoEncryptionEnabledCache__'\n\nconst BOOTSTRAP_CACHE_KEYS = [\n 'cache',\n 'eventBus',\n 'kmsService',\n 'tenantEncryptionService',\n 'rateLimiterService',\n 'searchModuleConfigs',\n 'searchIndexer',\n] as const\n\ntype BootstrapCacheEntry = Partial<Record<(typeof BOOTSTRAP_CACHE_KEYS)[number], unknown>>\n\n// Phase 5 is opt-in. Some bootstrap services close over per-request state\n// (e.g. tenantEncryptionService captures the first request's `em.fork`, the\n// event-bus's resolver closes over the first container) so naively replaying\n// them on later requests yields stale references \u2014 observed as a 500 from\n// CRUD list endpoints in `next start`. Default OFF preserves develop's\n// per-request bootstrap. Set `OM_BOOTSTRAP_CACHE=1` to opt in once each\n// cached service is verified safe for cross-request reuse.\nfunction isBootstrapCacheEnabled(): boolean {\n const raw = process.env.OM_BOOTSTRAP_CACHE\n if (raw === undefined) return false\n const normalized = raw.trim().toLowerCase()\n if (!normalized.length) return false\n if (normalized === '0' || normalized === 'off' || normalized === 'false' || normalized === 'no') return false\n return true\n}\n\nfunction getBootstrapCache(): BootstrapCacheEntry | null {\n if (!isBootstrapCacheEnabled()) return null\n const existing = (globalThis as any)[BOOTSTRAP_CACHE_KEY]\n return existing && typeof existing === 'object' ? (existing as BootstrapCacheEntry) : null\n}\n\nfunction setBootstrapCache(entry: BootstrapCacheEntry): void {\n if (!isBootstrapCacheEnabled()) return\n ;(globalThis as any)[BOOTSTRAP_CACHE_KEY] = entry\n}\n\nfunction harvestBootstrapCache(container: AwilixContainer): BootstrapCacheEntry {\n const entry: BootstrapCacheEntry = {}\n for (const key of BOOTSTRAP_CACHE_KEYS) {\n try {\n const value: unknown = container.resolve(key as never)\n if (value !== undefined && value !== null) entry[key] = value\n } catch {\n // not registered \u2014 skip\n }\n }\n return entry\n}\n\ntype EncryptionEnabledProbe = { isEnabled?: () => boolean } | null | undefined\n\nfunction getCachedEncryptionEnabled(service: EncryptionEnabledProbe): boolean | null {\n if (!service || typeof service.isEnabled !== 'function') return false\n const cached = (globalThis as Record<string, unknown>)[ENCRYPTION_ENABLED_KEY]\n if (typeof cached === 'boolean') return cached\n try {\n const result = !!service.isEnabled()\n ;(globalThis as Record<string, unknown>)[ENCRYPTION_ENABLED_KEY] = result\n return result\n } catch {\n return null\n }\n}\n\nfunction getGlobalRegistrars(): DiRegistrar[] | null {\n return (globalThis as any)[GLOBAL_KEY] ?? null\n}\n\nfunction setGlobalRegistrars(registrars: DiRegistrar[]): void {\n (globalThis as any)[GLOBAL_KEY] = registrars\n}\n\nexport function registerDiRegistrars(registrars: DiRegistrar[]) {\n const existing = getGlobalRegistrars()\n if (existing !== null && process.env.NODE_ENV === 'development') {\n logger.debug('DI registrars re-registered (this may occur during HMR)')\n }\n setGlobalRegistrars(registrars)\n // Force re-bootstrap on HMR \u2014 module subscribers may have changed.\n ;(globalThis as any)[BOOTSTRAP_CACHE_KEY] = null\n ;(globalThis as any)[ENCRYPTION_ENABLED_KEY] = undefined\n}\n\nexport function getDiRegistrars(): DiRegistrar[] {\n const registrars = getGlobalRegistrars()\n if (!registrars) {\n throw new Error('[Bootstrap] DI registrars not registered. Call registerDiRegistrars() at bootstrap.')\n }\n return registrars\n}\n\nfunction getAppDiRegistrar(): AppDiRegistrar | null {\n const registrar = (globalThis as Record<string, unknown>)[APP_DI_REGISTRAR_KEY]\n return typeof registrar === 'function' ? registrar as AppDiRegistrar : null\n}\n\nexport function registerAppDiRegistrar(registrar: AppDiRegistrar | null): void {\n ;(globalThis as Record<string, unknown>)[APP_DI_REGISTRAR_KEY] = registrar\n}\n\n/** Test-only helper to drop the process-scoped bootstrap cache. */\nexport function resetBootstrapCache(): void {\n (globalThis as any)[BOOTSTRAP_CACHE_KEY] = null\n ;(globalThis as any)[ENCRYPTION_ENABLED_KEY] = undefined\n}\n\nfunction isAwilixResolver(value: unknown): value is Resolver<unknown> {\n return Boolean(value && typeof value === 'object' && typeof (value as { resolve?: unknown }).resolve === 'function')\n}\n\nfunction toAwilixRegistrations(registrations: Record<string, unknown>): Record<string, Resolver<any>> {\n return Object.fromEntries(\n Object.entries(registrations).map(([key, value]) => [\n key,\n isAwilixResolver(value) ? value : asValue(value),\n ]),\n )\n}\n\nexport async function createRequestContainer(): Promise<AppContainer> {\n const diRegistrars = getDiRegistrars()\n const orm = await getOrm()\n // Use a fresh event manager so request-level subscribers (e.g., encryption) don't pile up globally\n const baseEm = (RequestContext.getEntityManager() as any) ?? orm.em\n const em = baseEm.fork({ clear: true, freshEventManager: true, useContext: true }) as unknown as EntityManager\n const container = createContainer<DynamicCradle>({ injectionMode: InjectionMode.CLASSIC })\n // Core registrations\n container.register({\n em: asValue(em),\n queryEngine: asValue(new BasicQueryEngine(em, undefined, () => {\n try { return container.resolve('tenantEncryptionService') as any } catch { return null }\n })),\n dataEngine: asValue(new DefaultDataEngine(em, container as any)),\n commandRegistry: asValue(commandRegistry),\n commandBus: asValue(new CommandBus()),\n // Default OSS optimistic-lock guard. Reads from the global reader store\n // (populated by `makeCrudRoute` auto-registration + any module-DI\n // hand-wired calls to `registerOptimisticLockReaders`). Service is\n // strictly additive: when `OM_OPTIMISTIC_LOCK=off` (or no header is\n // sent) it short-circuits at validateMutation. Module-level di.ts\n // registrations override this default via Awilix replace semantics \u2014\n // see the enterprise `record_locks` module for the canonical override.\n // Spec: .ai/specs/implemented/2026-05-25-oss-optimistic-locking.md\n crudMutationGuardService: asFunction((em: EntityManager) =>\n createOptimisticLockGuardService({\n getEm: () => em,\n readers: getAllOptimisticLockReaders(),\n }),\n ).scoped(),\n // Default OSS command-level optimistic-lock guard, awaited by\n // `enforceCommandOptimisticLockWithGuards` for Command-pattern writes.\n // Header/explicit-token compare only (no `resolveExpected`), so it is\n // behaviourally identical to calling `enforceCommandOptimisticLock`\n // directly. The enterprise `record_locks` module overrides this DI key\n // with a lock-backed `resolveExpected` via Awilix replace semantics.\n // Spec: .ai/specs/enterprise/2026-06-09-record-locks-unified-coverage.md (Phase 0)\n commandOptimisticLockGuardService: asFunction(() =>\n createCommandOptimisticLockGuardService(),\n ).scoped(),\n })\n // Allow modules to override/extend\n for (const reg of diRegistrars) {\n try { reg?.(container) } catch {}\n }\n // Core bootstrap (cache, event bus, encryption subscriber/KMS, module subscribers)\n // Phase 5 \u2014 process-scoped once-guard. The first request runs the full\n // bootstrap() body; later requests re-register the cached services\n // directly on this request's container without re-importing or\n // re-initializing anything. HMR clears the cache (see\n // registerDiRegistrars). Skippable if a caller already wired eventBus.\n const alreadyBootstrappedOnThisContainer = !!container.registrations?.eventBus\n if (!alreadyBootstrappedOnThisContainer) {\n const cached = getBootstrapCache()\n if (cached) {\n const replay: Record<string, any> = {}\n for (const [key, value] of Object.entries(cached)) {\n if (value !== undefined && value !== null) replay[key] = asValue(value)\n }\n if (Object.keys(replay).length > 0) container.register(replay)\n } else {\n try {\n const { bootstrap } = await import('@open-mercato/core/bootstrap') as any\n if (bootstrap && typeof bootstrap === 'function') {\n await bootstrap(container)\n setBootstrapCache(harvestBootstrapCache(container))\n }\n } catch { /* optional */ }\n }\n }\n // App-level DI override. Scaffolded apps register this callback explicitly\n // from their own bootstrap module so app aliases resolve in app context.\n const appDiRegistrar = getAppDiRegistrar()\n if (appDiRegistrar) {\n try {\n await appDiRegistrar(container)\n } catch (error) {\n logger.error('App-level DI registrar failed', { err: error })\n }\n } else {\n // Backward-compatible fallback for apps that have not adopted explicit wiring.\n try {\n // @ts-ignore - @/di only exists in app context, not in packages\n const appDi = await import('@/di') as any\n if (appDi?.register) {\n try {\n const maybe = appDi.register(container)\n if (maybe && typeof maybe.then === 'function') await maybe\n } catch (error) {\n logger.error('App-level DI registrar failed', { err: error })\n }\n }\n } catch {}\n }\n applyDiOverridesToContainer({\n register: (registrations) => container.register(toAwilixRegistrations(registrations)),\n unregister: (key) => container.register({ [key]: asValue(undefined) }),\n })\n // Ensure tenant encryption subscriber is always registered on the fresh request-scoped EM\n // Phase 5 \u2014 cache `tenantEncryptionService.isEnabled()` for the process\n // lifetime. The result depends only on config that does not change at\n // runtime, so reading it once skips a config lookup per request.\n try {\n const emForEnc = container.resolve('em') as any\n const tenantEncryptionService = container.hasRegistration('tenantEncryptionService')\n ? (container.resolve('tenantEncryptionService') as any)\n : null\n if (emForEnc && tenantEncryptionService && getCachedEncryptionEnabled(tenantEncryptionService) === true) {\n const { registerTenantEncryptionSubscriber } = await import('@open-mercato/shared/lib/encryption/subscriber')\n registerTenantEncryptionSubscriber(emForEnc, tenantEncryptionService)\n }\n } catch {\n // best-effort; do not block container creation\n }\n return container\n}\ntry {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n require('server-only')\n} catch {\n // allow CLI/generator usage where Next server-only is not present\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { asFunction, createContainer, asValue, AwilixContainer, InjectionMode, type Resolver } from 'awilix'\nimport { RequestContext } from '@mikro-orm/core'\nimport { getOrm } from '@open-mercato/shared/lib/db/mikro'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { BasicQueryEngine } from '@open-mercato/shared/lib/query/engine'\nimport { DefaultDataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { commandRegistry, CommandBus } from '@open-mercato/shared/lib/commands'\nimport { applyDiOverridesToContainer } from '@open-mercato/shared/modules/overrides'\nimport { createOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock'\nimport { getAllOptimisticLockReaders } from '@open-mercato/shared/lib/crud/optimistic-lock-store'\nimport { createCommandOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'di' })\n\ntype DynamicCradle = Record<string, any>\n\nexport type AppContainer = AwilixContainer<DynamicCradle>\nexport type DiRegistrar = (container: AppContainer) => void\nexport type AppDiRegistrar = (container: AppContainer) => void | Promise<void>\n\n// Registration pattern for publishable packages\n// Use globalThis to survive tsx/esbuild module duplication issue where the same\n// file can be loaded as multiple module instances when mixing dynamic and static imports\nconst GLOBAL_KEY = '__openMercatoDiRegistrars__'\nconst APP_DI_REGISTRAR_KEY = '__openMercatoAppDiRegistrar__'\nconst APP_DI_LOAD_WARNING_KEY = '__openMercatoAppDiLoadWarningEmitted__'\nconst APP_DI_REGISTER_WARNING_KEY = '__openMercatoAppDiRegisterWarningEmitted__'\n// Set once the legacy `@/di` fallback proves the specifier is unresolvable in this process.\n// Worker/CLI processes run against built package output where the app's `@/` alias does not\n// exist, so retrying the import per request container only repeats a failed module resolution\n// (and its log line) once per job.\nconst APP_DI_MODULE_UNRESOLVABLE_KEY = '__openMercatoAppDiModuleUnresolvable__'\n// Phase 5 \u2014 process-scoped bootstrap cache. The cache/event-bus/encryption\n// services bootstrap() creates are inherently process-scoped (they hold\n// state across requests). Caching them on globalThis after the first\n// successful bootstrap call lets every subsequent request skip the\n// `await bootstrap(container)` body and just re-register the cached\n// instances. Same globalThis pattern as registerDiRegistrars so HMR\n// keeps working.\nconst BOOTSTRAP_CACHE_KEY = '__openMercatoBootstrapCache__'\nconst ENCRYPTION_ENABLED_KEY = '__openMercatoEncryptionEnabledCache__'\n\nconst BOOTSTRAP_CACHE_KEYS = [\n 'cache',\n 'eventBus',\n 'kmsService',\n 'tenantEncryptionService',\n 'rateLimiterService',\n 'searchModuleConfigs',\n 'searchIndexer',\n] as const\n\ntype BootstrapCacheEntry = Partial<Record<(typeof BOOTSTRAP_CACHE_KEYS)[number], unknown>>\n\n// Phase 5 is opt-in. Some bootstrap services close over per-request state\n// (e.g. tenantEncryptionService captures the first request's `em.fork`, the\n// event-bus's resolver closes over the first container) so naively replaying\n// them on later requests yields stale references \u2014 observed as a 500 from\n// CRUD list endpoints in `next start`. Default OFF preserves develop's\n// per-request bootstrap. Set `OM_BOOTSTRAP_CACHE=1` to opt in once each\n// cached service is verified safe for cross-request reuse.\nfunction isBootstrapCacheEnabled(): boolean {\n const raw = process.env.OM_BOOTSTRAP_CACHE\n if (raw === undefined) return false\n const normalized = raw.trim().toLowerCase()\n if (!normalized.length) return false\n if (normalized === '0' || normalized === 'off' || normalized === 'false' || normalized === 'no') return false\n return true\n}\n\nfunction getBootstrapCache(): BootstrapCacheEntry | null {\n if (!isBootstrapCacheEnabled()) return null\n const existing = (globalThis as any)[BOOTSTRAP_CACHE_KEY]\n return existing && typeof existing === 'object' ? (existing as BootstrapCacheEntry) : null\n}\n\nfunction setBootstrapCache(entry: BootstrapCacheEntry): void {\n if (!isBootstrapCacheEnabled()) return\n ;(globalThis as any)[BOOTSTRAP_CACHE_KEY] = entry\n}\n\nfunction harvestBootstrapCache(container: AwilixContainer): BootstrapCacheEntry {\n const entry: BootstrapCacheEntry = {}\n for (const key of BOOTSTRAP_CACHE_KEYS) {\n try {\n const value: unknown = container.resolve(key as never)\n if (value !== undefined && value !== null) entry[key] = value\n } catch {\n // not registered \u2014 skip\n }\n }\n return entry\n}\n\ntype EncryptionEnabledProbe = { isEnabled?: () => boolean } | null | undefined\n\nfunction getCachedEncryptionEnabled(service: EncryptionEnabledProbe): boolean | null {\n if (!service || typeof service.isEnabled !== 'function') return false\n const cached = (globalThis as Record<string, unknown>)[ENCRYPTION_ENABLED_KEY]\n if (typeof cached === 'boolean') return cached\n try {\n const result = !!service.isEnabled()\n ;(globalThis as Record<string, unknown>)[ENCRYPTION_ENABLED_KEY] = result\n return result\n } catch {\n return null\n }\n}\n\nfunction getGlobalRegistrars(): DiRegistrar[] | null {\n return (globalThis as any)[GLOBAL_KEY] ?? null\n}\n\nfunction setGlobalRegistrars(registrars: DiRegistrar[]): void {\n (globalThis as any)[GLOBAL_KEY] = registrars\n}\n\nexport function registerDiRegistrars(registrars: DiRegistrar[]) {\n const existing = getGlobalRegistrars()\n if (existing !== null && process.env.NODE_ENV === 'development') {\n logger.debug('DI registrars re-registered (this may occur during HMR)')\n }\n setGlobalRegistrars(registrars)\n // Force re-bootstrap on HMR \u2014 module subscribers may have changed.\n ;(globalThis as any)[BOOTSTRAP_CACHE_KEY] = null\n ;(globalThis as any)[ENCRYPTION_ENABLED_KEY] = undefined\n // An app that gains a src/di.ts mid-session reloads through here, so the negative\n // resolution result must not outlive the reload.\n ;(globalThis as Record<string, unknown>)[APP_DI_MODULE_UNRESOLVABLE_KEY] = undefined\n}\n\nexport function getDiRegistrars(): DiRegistrar[] {\n const registrars = getGlobalRegistrars()\n if (!registrars) {\n throw new Error('[Bootstrap] DI registrars not registered. Call registerDiRegistrars() at bootstrap.')\n }\n return registrars\n}\n\nfunction getAppDiRegistrar(): AppDiRegistrar | null {\n const registrar = (globalThis as Record<string, unknown>)[APP_DI_REGISTRAR_KEY]\n return typeof registrar === 'function' ? registrar as AppDiRegistrar : null\n}\n\nexport function registerAppDiRegistrar(registrar: AppDiRegistrar | null): void {\n ;(globalThis as Record<string, unknown>)[APP_DI_REGISTRAR_KEY] = registrar\n}\n\n/** Test-only helper to drop process-scoped request-container state. */\nexport function resetBootstrapCache(): void {\n (globalThis as any)[BOOTSTRAP_CACHE_KEY] = null\n ;(globalThis as any)[ENCRYPTION_ENABLED_KEY] = undefined\n ;(globalThis as Record<string, unknown>)[APP_DI_LOAD_WARNING_KEY] = undefined\n ;(globalThis as Record<string, unknown>)[APP_DI_REGISTER_WARNING_KEY] = undefined\n ;(globalThis as Record<string, unknown>)[APP_DI_MODULE_UNRESOLVABLE_KEY] = undefined\n}\n\nfunction isAppDiModuleUnresolvable(): boolean {\n return (globalThis as Record<string, unknown>)[APP_DI_MODULE_UNRESOLVABLE_KEY] === true\n}\n\nfunction markAppDiModuleUnresolvable(): void {\n ;(globalThis as Record<string, unknown>)[APP_DI_MODULE_UNRESOLVABLE_KEY] = true\n}\n\nfunction isAppDiModuleNotFound(error: unknown): boolean {\n if (!error || typeof error !== 'object') return false\n const { code, message } = error as { code?: unknown; message?: unknown }\n const text = typeof message === 'string' ? message : ''\n const moduleNotFound =\n code === 'MODULE_NOT_FOUND'\n || code === 'ERR_MODULE_NOT_FOUND'\n || text.startsWith('Cannot find module')\n || text.startsWith('Cannot find package')\n return moduleNotFound && /(?:module|package) ['\"]@\\/di['\"]/.test(text)\n}\n\nfunction warnAppDiFailureOnce(\n key: typeof APP_DI_LOAD_WARNING_KEY | typeof APP_DI_REGISTER_WARNING_KEY,\n message: string,\n error: unknown,\n): void {\n const globalScope = globalThis as Record<string, unknown>\n if (globalScope[key] === true) return\n globalScope[key] = true\n logger.warn(message, { err: error })\n}\n\nfunction isAwilixResolver(value: unknown): value is Resolver<unknown> {\n return Boolean(value && typeof value === 'object' && typeof (value as { resolve?: unknown }).resolve === 'function')\n}\n\nfunction toAwilixRegistrations(registrations: Record<string, unknown>): Record<string, Resolver<any>> {\n return Object.fromEntries(\n Object.entries(registrations).map(([key, value]) => [\n key,\n isAwilixResolver(value) ? value : asValue(value),\n ]),\n )\n}\n\nexport async function createRequestContainer(): Promise<AppContainer> {\n const diRegistrars = getDiRegistrars()\n const orm = await getOrm()\n // Use a fresh event manager so request-level subscribers (e.g., encryption) don't pile up globally\n const baseEm = (RequestContext.getEntityManager() as any) ?? orm.em\n const em = baseEm.fork({ clear: true, freshEventManager: true, useContext: true }) as unknown as EntityManager\n const container = createContainer<DynamicCradle>({ injectionMode: InjectionMode.CLASSIC })\n // Core registrations\n container.register({\n em: asValue(em),\n queryEngine: asValue(new BasicQueryEngine(em, undefined, () => {\n try { return container.resolve('tenantEncryptionService') as any } catch { return null }\n })),\n dataEngine: asValue(new DefaultDataEngine(em, container as any)),\n commandRegistry: asValue(commandRegistry),\n commandBus: asValue(new CommandBus()),\n // Default OSS optimistic-lock guard. Reads from the global reader store\n // (populated by `makeCrudRoute` auto-registration + any module-DI\n // hand-wired calls to `registerOptimisticLockReaders`). Service is\n // strictly additive: when `OM_OPTIMISTIC_LOCK=off` (or no header is\n // sent) it short-circuits at validateMutation. Module-level di.ts\n // registrations override this default via Awilix replace semantics \u2014\n // see the enterprise `record_locks` module for the canonical override.\n // Spec: .ai/specs/implemented/2026-05-25-oss-optimistic-locking.md\n crudMutationGuardService: asFunction((em: EntityManager) =>\n createOptimisticLockGuardService({\n getEm: () => em,\n readers: getAllOptimisticLockReaders(),\n }),\n ).scoped(),\n // Default OSS command-level optimistic-lock guard, awaited by\n // `enforceCommandOptimisticLockWithGuards` for Command-pattern writes.\n // Header/explicit-token compare only (no `resolveExpected`), so it is\n // behaviourally identical to calling `enforceCommandOptimisticLock`\n // directly. The enterprise `record_locks` module overrides this DI key\n // with a lock-backed `resolveExpected` via Awilix replace semantics.\n // Spec: .ai/specs/enterprise/2026-06-09-record-locks-unified-coverage.md (Phase 0)\n commandOptimisticLockGuardService: asFunction(() =>\n createCommandOptimisticLockGuardService(),\n ).scoped(),\n })\n // Allow modules to override/extend. Fail-open by design (one broken module's\n // di.ts must not take down every request container), but never silently: the\n // module's services would otherwise vanish with no trace until an unrelated\n // Awilix resolution error surfaces much later.\n for (const [registrarIndex, reg] of diRegistrars.entries()) {\n try { reg?.(container) } catch (error) {\n logger.error('Module DI registrar failed', { registrarIndex, err: error })\n }\n }\n // Core bootstrap (cache, event bus, encryption subscriber/KMS, module subscribers)\n // Phase 5 \u2014 process-scoped once-guard. The first request runs the full\n // bootstrap() body; later requests re-register the cached services\n // directly on this request's container without re-importing or\n // re-initializing anything. HMR clears the cache (see\n // registerDiRegistrars). Skippable if a caller already wired eventBus.\n const alreadyBootstrappedOnThisContainer = !!container.registrations?.eventBus\n if (!alreadyBootstrappedOnThisContainer) {\n const cached = getBootstrapCache()\n if (cached) {\n const replay: Record<string, any> = {}\n for (const [key, value] of Object.entries(cached)) {\n if (value !== undefined && value !== null) replay[key] = asValue(value)\n }\n if (Object.keys(replay).length > 0) container.register(replay)\n } else {\n try {\n const { bootstrap } = await import('@open-mercato/core/bootstrap') as any\n if (bootstrap && typeof bootstrap === 'function') {\n await bootstrap(container)\n setBootstrapCache(harvestBootstrapCache(container))\n }\n } catch { /* optional */ }\n }\n }\n // App-level DI override. Scaffolded apps register this callback explicitly\n // from their own bootstrap module so app aliases resolve in app context.\n const appDiRegistrar = getAppDiRegistrar()\n if (appDiRegistrar) {\n try {\n await appDiRegistrar(container)\n } catch (error) {\n logger.error('App-level DI registrar failed', { err: error })\n }\n } else if (!isAppDiModuleUnresolvable()) {\n // Backward-compatible fallback for apps that have not adopted explicit wiring.\n try {\n // @ts-ignore - @/di only exists in app context, not in packages\n const appDi = await import('@/di') as any\n if (appDi?.register) {\n try {\n const maybe = appDi.register(container)\n if (maybe && typeof maybe.then === 'function') await maybe\n } catch (err) {\n warnAppDiFailureOnce(\n APP_DI_REGISTER_WARNING_KEY,\n 'App-level DI override (src/di.ts register()) threw; its registrations are skipped',\n err,\n )\n }\n }\n } catch (err) {\n if (isAppDiModuleNotFound(err)) {\n markAppDiModuleUnresolvable()\n logger.debug('App-level DI override module (@/di) not resolvable; skipping', { err })\n } else {\n warnAppDiFailureOnce(\n APP_DI_LOAD_WARNING_KEY,\n 'App-level DI override module (@/di) failed to load; its registrations are skipped',\n err,\n )\n }\n }\n }\n applyDiOverridesToContainer({\n register: (registrations) => container.register(toAwilixRegistrations(registrations)),\n unregister: (key) => container.register({ [key]: asValue(undefined) }),\n })\n // Ensure tenant encryption subscriber is always registered on the fresh request-scoped EM\n // Phase 5 \u2014 cache `tenantEncryptionService.isEnabled()` for the process\n // lifetime. The result depends only on config that does not change at\n // runtime, so reading it once skips a config lookup per request.\n try {\n const emForEnc = container.resolve('em') as any\n const tenantEncryptionService = container.hasRegistration('tenantEncryptionService')\n ? (container.resolve('tenantEncryptionService') as any)\n : null\n if (emForEnc && tenantEncryptionService && getCachedEncryptionEnabled(tenantEncryptionService) === true) {\n const { registerTenantEncryptionSubscriber } = await import('@open-mercato/shared/lib/encryption/subscriber')\n registerTenantEncryptionSubscriber(emForEnc, tenantEncryptionService)\n }\n } catch {\n // best-effort; do not block container creation\n }\n return container\n}\ntry {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n require('server-only')\n} catch {\n // allow CLI/generator usage where Next server-only is not present\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["em"]
7
7
  }
@@ -1,8 +1,64 @@
1
1
  import { Resend } from "resend";
2
+ import React from "react";
3
+ import { appendFile, mkdir } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { tmpdir } from "node:os";
2
6
  import { parseBooleanWithDefault } from "../boolean.js";
3
7
  import { resolveDefaultEmailFromAddress } from "./config.js";
8
+ const DEFAULT_TEST_EMAIL_CAPTURE_PATH = join(tmpdir(), "open-mercato-email-capture.jsonl");
9
+ function resolveTestEmailCapturePath() {
10
+ return process.env.OM_TEST_EMAIL_CAPTURE_PATH?.trim() || DEFAULT_TEST_EMAIL_CAPTURE_PATH;
11
+ }
12
+ function readElementProps(node) {
13
+ return node.props;
14
+ }
15
+ function collectEmailLinks(node, links = []) {
16
+ if (node == null || typeof node === "boolean") return links;
17
+ if (Array.isArray(node)) {
18
+ for (const child of node) collectEmailLinks(child, links);
19
+ return links;
20
+ }
21
+ if (React.isValidElement(node)) {
22
+ const props = readElementProps(node);
23
+ if (typeof props.href === "string" && props.href.length > 0) links.push(props.href);
24
+ collectEmailLinks(props.children, links);
25
+ }
26
+ return links;
27
+ }
28
+ function collectEmailText(node, parts = []) {
29
+ if (node == null || typeof node === "boolean") return parts;
30
+ if (typeof node === "string" || typeof node === "number") {
31
+ parts.push(String(node));
32
+ return parts;
33
+ }
34
+ if (Array.isArray(node)) {
35
+ for (const child of node) collectEmailText(child, parts);
36
+ return parts;
37
+ }
38
+ if (React.isValidElement(node)) {
39
+ collectEmailText(readElementProps(node).children, parts);
40
+ }
41
+ return parts;
42
+ }
43
+ async function captureEmailForTests(options) {
44
+ if (!parseBooleanWithDefault(process.env.OM_TEST_MODE, false)) return;
45
+ const capturePath = resolveTestEmailCapturePath();
46
+ const record = {
47
+ to: options.to,
48
+ subject: options.subject,
49
+ from: options.from ?? resolveDefaultEmailFromAddress() ?? null,
50
+ replyTo: options.replyTo ?? null,
51
+ links: collectEmailLinks(options.react),
52
+ text: collectEmailText(options.react).join(" ").replace(/\s+/g, " ").trim(),
53
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
54
+ };
55
+ await mkdir(dirname(capturePath), { recursive: true });
56
+ await appendFile(capturePath, `${JSON.stringify(record)}
57
+ `, "utf8");
58
+ }
4
59
  async function sendEmail({ to, subject, react, from, replyTo, attachments }) {
5
60
  const emailDisabled = parseBooleanWithDefault(process.env.OM_DISABLE_EMAIL_DELIVERY, false) || parseBooleanWithDefault(process.env.OM_TEST_MODE, false);
61
+ await captureEmailForTests({ to, subject, react, from, replyTo, attachments });
6
62
  if (emailDisabled) return;
7
63
  const apiKey = process.env.RESEND_API_KEY;
8
64
  if (!apiKey) throw new Error("RESEND_API_KEY is not set");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/email/send.ts"],
4
- "sourcesContent": ["import { Resend } from 'resend'\nimport React from 'react'\nimport { parseBooleanWithDefault } from '../boolean'\nimport { resolveDefaultEmailFromAddress } from './config'\n\nexport type SendEmailOptions = {\n to: string\n subject: string\n react: React.ReactElement\n from?: string\n replyTo?: string\n attachments?: Array<{\n filename: string\n content: string\n contentType?: string\n }>\n}\n\nexport async function sendEmail({ to, subject, react, from, replyTo, attachments }: SendEmailOptions) {\n const emailDisabled =\n parseBooleanWithDefault(process.env.OM_DISABLE_EMAIL_DELIVERY, false) ||\n parseBooleanWithDefault(process.env.OM_TEST_MODE, false)\n if (emailDisabled) return\n\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) throw new Error('RESEND_API_KEY is not set')\n const resend = new Resend(apiKey)\n const fromAddr = from || resolveDefaultEmailFromAddress()\n if (!fromAddr) {\n throw new Error('EMAIL_FROM_NOT_CONFIGURED: set NOTIFICATIONS_EMAIL_FROM, EMAIL_FROM, or ADMIN_EMAIL')\n }\n const payload = {\n to,\n subject,\n from: fromAddr,\n react,\n ...(replyTo ? { reply_to: replyTo } : {}),\n ...(attachments?.length ? { attachments } : {}),\n }\n const result = await resend.emails.send(payload)\n const errorMessage =\n typeof (result as any)?.error === 'string'\n ? (result as any).error\n : typeof (result as any)?.error?.message === 'string'\n ? (result as any).error.message\n : null\n if (errorMessage) {\n throw new Error(`RESEND_SEND_FAILED: ${errorMessage}`)\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,cAAc;AAEvB,SAAS,+BAA+B;AACxC,SAAS,sCAAsC;AAe/C,eAAsB,UAAU,EAAE,IAAI,SAAS,OAAO,MAAM,SAAS,YAAY,GAAqB;AACpG,QAAM,gBACJ,wBAAwB,QAAQ,IAAI,2BAA2B,KAAK,KACpE,wBAAwB,QAAQ,IAAI,cAAc,KAAK;AACzD,MAAI,cAAe;AAEnB,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,2BAA2B;AACxD,QAAM,SAAS,IAAI,OAAO,MAAM;AAChC,QAAM,WAAW,QAAQ,+BAA+B;AACxD,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,qFAAqF;AAAA,EACvG;AACA,QAAM,UAAU;AAAA,IACd;AAAA,IACA;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA,GAAI,UAAU,EAAE,UAAU,QAAQ,IAAI,CAAC;AAAA,IACvC,GAAI,aAAa,SAAS,EAAE,YAAY,IAAI,CAAC;AAAA,EAC/C;AACA,QAAM,SAAS,MAAM,OAAO,OAAO,KAAK,OAAO;AAC/C,QAAM,eACJ,OAAQ,QAAgB,UAAU,WAC7B,OAAe,QAChB,OAAQ,QAAgB,OAAO,YAAY,WACxC,OAAe,MAAM,UACtB;AACR,MAAI,cAAc;AAChB,UAAM,IAAI,MAAM,uBAAuB,YAAY,EAAE;AAAA,EACvD;AACF;",
4
+ "sourcesContent": ["import { Resend } from 'resend'\nimport React from 'react'\nimport { appendFile, mkdir } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { parseBooleanWithDefault } from '../boolean'\nimport { resolveDefaultEmailFromAddress } from './config'\n\nexport type SendEmailOptions = {\n to: string\n subject: string\n react: React.ReactElement\n from?: string\n replyTo?: string\n attachments?: Array<{\n filename: string\n content: string\n contentType?: string\n }>\n}\n\ntype CapturedEmail = {\n to: string\n subject: string\n from: string | null\n replyTo: string | null\n links: string[]\n text: string\n capturedAt: string\n}\n\ntype ReactElementProps = {\n href?: unknown\n children?: unknown\n}\n\nconst DEFAULT_TEST_EMAIL_CAPTURE_PATH = join(tmpdir(), 'open-mercato-email-capture.jsonl')\n\nfunction resolveTestEmailCapturePath(): string {\n return process.env.OM_TEST_EMAIL_CAPTURE_PATH?.trim() || DEFAULT_TEST_EMAIL_CAPTURE_PATH\n}\n\nfunction readElementProps(node: React.ReactElement): ReactElementProps {\n return node.props as ReactElementProps\n}\n\nfunction collectEmailLinks(node: unknown, links: string[] = []): string[] {\n if (node == null || typeof node === 'boolean') return links\n if (Array.isArray(node)) {\n for (const child of node) collectEmailLinks(child, links)\n return links\n }\n if (React.isValidElement(node)) {\n const props = readElementProps(node)\n if (typeof props.href === 'string' && props.href.length > 0) links.push(props.href)\n collectEmailLinks(props.children, links)\n }\n return links\n}\n\nfunction collectEmailText(node: unknown, parts: string[] = []): string[] {\n if (node == null || typeof node === 'boolean') return parts\n if (typeof node === 'string' || typeof node === 'number') {\n parts.push(String(node))\n return parts\n }\n if (Array.isArray(node)) {\n for (const child of node) collectEmailText(child, parts)\n return parts\n }\n if (React.isValidElement(node)) {\n collectEmailText(readElementProps(node).children, parts)\n }\n return parts\n}\n\nasync function captureEmailForTests(options: SendEmailOptions): Promise<void> {\n if (!parseBooleanWithDefault(process.env.OM_TEST_MODE, false)) return\n\n const capturePath = resolveTestEmailCapturePath()\n const record: CapturedEmail = {\n to: options.to,\n subject: options.subject,\n from: options.from ?? resolveDefaultEmailFromAddress() ?? null,\n replyTo: options.replyTo ?? null,\n links: collectEmailLinks(options.react),\n text: collectEmailText(options.react).join(' ').replace(/\\s+/g, ' ').trim(),\n capturedAt: new Date().toISOString(),\n }\n\n await mkdir(dirname(capturePath), { recursive: true })\n await appendFile(capturePath, `${JSON.stringify(record)}\\n`, 'utf8')\n}\n\nexport async function sendEmail({ to, subject, react, from, replyTo, attachments }: SendEmailOptions) {\n const emailDisabled =\n parseBooleanWithDefault(process.env.OM_DISABLE_EMAIL_DELIVERY, false) ||\n parseBooleanWithDefault(process.env.OM_TEST_MODE, false)\n\n await captureEmailForTests({ to, subject, react, from, replyTo, attachments })\n\n if (emailDisabled) return\n\n const apiKey = process.env.RESEND_API_KEY\n if (!apiKey) throw new Error('RESEND_API_KEY is not set')\n const resend = new Resend(apiKey)\n const fromAddr = from || resolveDefaultEmailFromAddress()\n if (!fromAddr) {\n throw new Error('EMAIL_FROM_NOT_CONFIGURED: set NOTIFICATIONS_EMAIL_FROM, EMAIL_FROM, or ADMIN_EMAIL')\n }\n const payload = {\n to,\n subject,\n from: fromAddr,\n react,\n ...(replyTo ? { reply_to: replyTo } : {}),\n ...(attachments?.length ? { attachments } : {}),\n }\n const result = await resend.emails.send(payload)\n const errorMessage =\n typeof (result as any)?.error === 'string'\n ? (result as any).error\n : typeof (result as any)?.error?.message === 'string'\n ? (result as any).error.message\n : null\n if (errorMessage) {\n throw new Error(`RESEND_SEND_FAILED: ${errorMessage}`)\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,13 @@
1
+ import { warnOnEncryptedLikeFilter } from "./likeFilterWarning.js";
1
2
  import { decryptEntitiesWithFallbackScope } from "./subscriber.js";
2
3
  async function findWithDecryption(em, entityName, where, options, scope) {
4
+ await warnOnEncryptedLikeFilter({
5
+ em,
6
+ entityName,
7
+ where,
8
+ tenantId: scope?.tenantId,
9
+ encryptionService: scope?.encryptionService
10
+ });
3
11
  const records = await em.find(entityName, where, options);
4
12
  if (!Array.isArray(records) || records.length === 0) return records ?? [];
5
13
  await decryptEntitiesWithFallbackScope(records, {
@@ -11,6 +19,13 @@ async function findWithDecryption(em, entityName, where, options, scope) {
11
19
  return records;
12
20
  }
13
21
  async function findOneWithDecryption(em, entityName, where, options, scope) {
22
+ await warnOnEncryptedLikeFilter({
23
+ em,
24
+ entityName,
25
+ where,
26
+ tenantId: scope?.tenantId,
27
+ encryptionService: scope?.encryptionService
28
+ });
14
29
  const record = await em.findOne(entityName, where, options);
15
30
  if (!record) return record;
16
31
  await decryptEntitiesWithFallbackScope(record, {
@@ -22,6 +37,13 @@ async function findOneWithDecryption(em, entityName, where, options, scope) {
22
37
  return record;
23
38
  }
24
39
  async function findAndCountWithDecryption(em, entityName, where, options, scope) {
40
+ await warnOnEncryptedLikeFilter({
41
+ em,
42
+ entityName,
43
+ where,
44
+ tenantId: scope?.tenantId,
45
+ encryptionService: scope?.encryptionService
46
+ });
25
47
  const [recordsRaw, count] = await em.findAndCount(
26
48
  entityName,
27
49
  where,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/encryption/find.ts"],
4
- "sourcesContent": ["import type {\n EntityManager,\n EntityName,\n FilterQuery,\n FindOneOptions,\n FindOptions,\n} from '@mikro-orm/postgresql'\nimport { decryptEntitiesWithFallbackScope } from './subscriber'\nimport type { TenantDataEncryptionService } from './tenantDataEncryptionService'\n\nexport type DecryptionScope = {\n tenantId?: string | null\n organizationId?: string | null\n encryptionService?: TenantDataEncryptionService | null\n}\n\ntype AnyFindOptions<Entity extends object, Hint extends string = any> = FindOptions<Entity, Hint, any, any>\ntype AnyFindOneOptions<Entity extends object, Hint extends string = any> = FindOneOptions<Entity, Hint, any, any>\n\nexport async function findWithDecryption<Entity extends object, Hint extends string = any>(\n em: EntityManager,\n entityName: EntityName<Entity>,\n where: FilterQuery<Entity>,\n options?: AnyFindOptions<Entity, Hint>,\n scope?: DecryptionScope,\n): Promise<Entity[]> {\n const records = (await em.find<Entity, Hint, any, any>(entityName as any, where as any, options as any)) as any as\n | Entity[]\n | undefined\n if (!Array.isArray(records) || records.length === 0) return records ?? []\n await decryptEntitiesWithFallbackScope(records, {\n em,\n tenantId: scope?.tenantId ?? null,\n organizationId: scope?.organizationId ?? null,\n encryptionService: scope?.encryptionService ?? null,\n })\n return records\n}\n\nexport async function findOneWithDecryption<Entity extends object, Hint extends string = any>( // NOSONAR \u2014 returns null or decrypted entity; same variable name, different values\n em: EntityManager,\n entityName: EntityName<Entity>,\n where: FilterQuery<Entity>,\n options?: AnyFindOneOptions<Entity, Hint>,\n scope?: DecryptionScope,\n): Promise<Entity | null> {\n const record = (await em.findOne<Entity, Hint, any, any>(entityName as any, where as any, options as any)) as any as\n | Entity\n | null\n if (!record) return record\n await decryptEntitiesWithFallbackScope(record, {\n em,\n tenantId: scope?.tenantId ?? null,\n organizationId: scope?.organizationId ?? null,\n encryptionService: scope?.encryptionService ?? null,\n })\n return record\n}\n\nexport async function findAndCountWithDecryption<Entity extends object, Hint extends string = any>(\n em: EntityManager,\n entityName: EntityName<Entity>,\n where: FilterQuery<Entity>,\n options?: AnyFindOptions<Entity, Hint>,\n scope?: DecryptionScope,\n): Promise<[Entity[], number]> {\n const [recordsRaw, count] = await em.findAndCount<Entity, Hint, any, any>(\n entityName as any,\n where as any,\n options as any,\n ) as any as [Entity[] | undefined, number]\n const records = Array.isArray(recordsRaw) ? recordsRaw : []\n if (!records.length) return [records, count]\n await decryptEntitiesWithFallbackScope(records, {\n em,\n tenantId: scope?.tenantId ?? null,\n organizationId: scope?.organizationId ?? null,\n encryptionService: scope?.encryptionService ?? null,\n })\n return [records, count]\n}\n"],
5
- "mappings": "AAOA,SAAS,wCAAwC;AAYjD,eAAsB,mBACpB,IACA,YACA,OACA,SACA,OACmB;AACnB,QAAM,UAAW,MAAM,GAAG,KAA6B,YAAmB,OAAc,OAAc;AAGtG,MAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,QAAQ,WAAW,EAAG,QAAO,WAAW,CAAC;AACxE,QAAM,iCAAiC,SAAS;AAAA,IAC9C;AAAA,IACA,UAAU,OAAO,YAAY;AAAA,IAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IACzC,mBAAmB,OAAO,qBAAqB;AAAA,EACjD,CAAC;AACD,SAAO;AACT;AAEA,eAAsB,sBACpB,IACA,YACA,OACA,SACA,OACwB;AACxB,QAAM,SAAU,MAAM,GAAG,QAAgC,YAAmB,OAAc,OAAc;AAGxG,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,iCAAiC,QAAQ;AAAA,IAC7C;AAAA,IACA,UAAU,OAAO,YAAY;AAAA,IAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IACzC,mBAAmB,OAAO,qBAAqB;AAAA,EACjD,CAAC;AACD,SAAO;AACT;AAEA,eAAsB,2BACpB,IACA,YACA,OACA,SACA,OAC6B;AAC7B,QAAM,CAAC,YAAY,KAAK,IAAI,MAAM,GAAG;AAAA,IACnC;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,UAAU,MAAM,QAAQ,UAAU,IAAI,aAAa,CAAC;AAC1D,MAAI,CAAC,QAAQ,OAAQ,QAAO,CAAC,SAAS,KAAK;AAC3C,QAAM,iCAAiC,SAAS;AAAA,IAC9C;AAAA,IACA,UAAU,OAAO,YAAY;AAAA,IAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IACzC,mBAAmB,OAAO,qBAAqB;AAAA,EACjD,CAAC;AACD,SAAO,CAAC,SAAS,KAAK;AACxB;",
4
+ "sourcesContent": ["import type {\n EntityManager,\n EntityName,\n FilterQuery,\n FindOneOptions,\n FindOptions,\n} from '@mikro-orm/postgresql'\nimport { warnOnEncryptedLikeFilter } from './likeFilterWarning'\nimport { decryptEntitiesWithFallbackScope } from './subscriber'\nimport type { TenantDataEncryptionService } from './tenantDataEncryptionService'\n\nexport type DecryptionScope = {\n tenantId?: string | null\n organizationId?: string | null\n encryptionService?: TenantDataEncryptionService | null\n}\n\ntype AnyFindOptions<Entity extends object, Hint extends string = any> = FindOptions<Entity, Hint, any, any>\ntype AnyFindOneOptions<Entity extends object, Hint extends string = any> = FindOneOptions<Entity, Hint, any, any>\n\nexport async function findWithDecryption<Entity extends object, Hint extends string = any>(\n em: EntityManager,\n entityName: EntityName<Entity>,\n where: FilterQuery<Entity>,\n options?: AnyFindOptions<Entity, Hint>,\n scope?: DecryptionScope,\n): Promise<Entity[]> {\n await warnOnEncryptedLikeFilter({\n em,\n entityName,\n where,\n tenantId: scope?.tenantId,\n encryptionService: scope?.encryptionService,\n })\n const records = (await em.find<Entity, Hint, any, any>(entityName as any, where as any, options as any)) as any as\n | Entity[]\n | undefined\n if (!Array.isArray(records) || records.length === 0) return records ?? []\n await decryptEntitiesWithFallbackScope(records, {\n em,\n tenantId: scope?.tenantId ?? null,\n organizationId: scope?.organizationId ?? null,\n encryptionService: scope?.encryptionService ?? null,\n })\n return records\n}\n\nexport async function findOneWithDecryption<Entity extends object, Hint extends string = any>( // NOSONAR \u2014 returns null or decrypted entity; same variable name, different values\n em: EntityManager,\n entityName: EntityName<Entity>,\n where: FilterQuery<Entity>,\n options?: AnyFindOneOptions<Entity, Hint>,\n scope?: DecryptionScope,\n): Promise<Entity | null> {\n await warnOnEncryptedLikeFilter({\n em,\n entityName,\n where,\n tenantId: scope?.tenantId,\n encryptionService: scope?.encryptionService,\n })\n const record = (await em.findOne<Entity, Hint, any, any>(entityName as any, where as any, options as any)) as any as\n | Entity\n | null\n if (!record) return record\n await decryptEntitiesWithFallbackScope(record, {\n em,\n tenantId: scope?.tenantId ?? null,\n organizationId: scope?.organizationId ?? null,\n encryptionService: scope?.encryptionService ?? null,\n })\n return record\n}\n\nexport async function findAndCountWithDecryption<Entity extends object, Hint extends string = any>(\n em: EntityManager,\n entityName: EntityName<Entity>,\n where: FilterQuery<Entity>,\n options?: AnyFindOptions<Entity, Hint>,\n scope?: DecryptionScope,\n): Promise<[Entity[], number]> {\n await warnOnEncryptedLikeFilter({\n em,\n entityName,\n where,\n tenantId: scope?.tenantId,\n encryptionService: scope?.encryptionService,\n })\n const [recordsRaw, count] = await em.findAndCount<Entity, Hint, any, any>(\n entityName as any,\n where as any,\n options as any,\n ) as any as [Entity[] | undefined, number]\n const records = Array.isArray(recordsRaw) ? recordsRaw : []\n if (!records.length) return [records, count]\n await decryptEntitiesWithFallbackScope(records, {\n em,\n tenantId: scope?.tenantId ?? null,\n organizationId: scope?.organizationId ?? null,\n encryptionService: scope?.encryptionService ?? null,\n })\n return [records, count]\n}\n"],
5
+ "mappings": "AAOA,SAAS,iCAAiC;AAC1C,SAAS,wCAAwC;AAYjD,eAAsB,mBACpB,IACA,YACA,OACA,SACA,OACmB;AACnB,QAAM,0BAA0B;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,OAAO;AAAA,IACjB,mBAAmB,OAAO;AAAA,EAC5B,CAAC;AACD,QAAM,UAAW,MAAM,GAAG,KAA6B,YAAmB,OAAc,OAAc;AAGtG,MAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,QAAQ,WAAW,EAAG,QAAO,WAAW,CAAC;AACxE,QAAM,iCAAiC,SAAS;AAAA,IAC9C;AAAA,IACA,UAAU,OAAO,YAAY;AAAA,IAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IACzC,mBAAmB,OAAO,qBAAqB;AAAA,EACjD,CAAC;AACD,SAAO;AACT;AAEA,eAAsB,sBACpB,IACA,YACA,OACA,SACA,OACwB;AACxB,QAAM,0BAA0B;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,OAAO;AAAA,IACjB,mBAAmB,OAAO;AAAA,EAC5B,CAAC;AACD,QAAM,SAAU,MAAM,GAAG,QAAgC,YAAmB,OAAc,OAAc;AAGxG,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,iCAAiC,QAAQ;AAAA,IAC7C;AAAA,IACA,UAAU,OAAO,YAAY;AAAA,IAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IACzC,mBAAmB,OAAO,qBAAqB;AAAA,EACjD,CAAC;AACD,SAAO;AACT;AAEA,eAAsB,2BACpB,IACA,YACA,OACA,SACA,OAC6B;AAC7B,QAAM,0BAA0B;AAAA,IAC9B;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,OAAO;AAAA,IACjB,mBAAmB,OAAO;AAAA,EAC5B,CAAC;AACD,QAAM,CAAC,YAAY,KAAK,IAAI,MAAM,GAAG;AAAA,IACnC;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,UAAU,MAAM,QAAQ,UAAU,IAAI,aAAa,CAAC;AAC1D,MAAI,CAAC,QAAQ,OAAQ,QAAO,CAAC,SAAS,KAAK;AAC3C,QAAM,iCAAiC,SAAS;AAAA,IAC9C;AAAA,IACA,UAAU,OAAO,YAAY;AAAA,IAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IACzC,mBAAmB,OAAO,qBAAqB;AAAA,EACjD,CAAC;AACD,SAAO,CAAC,SAAS,KAAK;AACxB;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,66 @@
1
+ import { warnOnCiphertextLikeFallback } from "../query/ciphertext-search-warning.js";
2
+ import { resolveTenantEncryptionService } from "./customFieldValues.js";
3
+ import { resolveEntityIdFromMetadata } from "./entityIds.js";
4
+ const LIKE_OPERATORS = /* @__PURE__ */ new Set(["$like", "$ilike", "$re", "$fulltext"]);
5
+ const LOGICAL_OPERATORS = /* @__PURE__ */ new Set(["$and", "$or", "$not", "$every", "$some", "$none"]);
6
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
7
+ const isOperatorObject = (value) => {
8
+ if (!isPlainObject(value)) return false;
9
+ const keys = Object.keys(value);
10
+ return keys.length > 0 && keys.every((key) => key.startsWith("$"));
11
+ };
12
+ function walkFilter(node, field, out) {
13
+ if (Array.isArray(node)) {
14
+ for (const item of node) walkFilter(item, field, out);
15
+ return;
16
+ }
17
+ if (!isPlainObject(node)) return;
18
+ for (const [key, value] of Object.entries(node)) {
19
+ if (LIKE_OPERATORS.has(key)) {
20
+ if (field) out.add(field);
21
+ continue;
22
+ }
23
+ if (LOGICAL_OPERATORS.has(key)) {
24
+ walkFilter(value, field, out);
25
+ continue;
26
+ }
27
+ if (key.startsWith("$")) continue;
28
+ if (isOperatorObject(value)) walkFilter(value, key, out);
29
+ }
30
+ }
31
+ function collectLikeFilterFields(where) {
32
+ const found = /* @__PURE__ */ new Set();
33
+ walkFilter(where, null, found);
34
+ return Array.from(found);
35
+ }
36
+ const isWarningEnabled = () => process.env.NODE_ENV !== "production";
37
+ function resolveEntityId(em, entityName) {
38
+ const metadata = em?.getMetadata?.();
39
+ if (!metadata || typeof metadata.find !== "function") return null;
40
+ const name = typeof entityName === "string" ? entityName : entityName?.name;
41
+ if (!name) return null;
42
+ return resolveEntityIdFromMetadata(metadata.find(name) ?? void 0);
43
+ }
44
+ async function warnOnEncryptedLikeFilter(params) {
45
+ if (!isWarningEnabled()) return;
46
+ const fields = collectLikeFilterFields(params.where);
47
+ if (!fields.length) return;
48
+ try {
49
+ const entity = resolveEntityId(params.em, params.entityName);
50
+ if (!entity) return;
51
+ const service = params.encryptionService ?? resolveTenantEncryptionService(params.em);
52
+ await warnOnCiphertextLikeFallback({
53
+ entity,
54
+ fields,
55
+ tenantId: params.tenantId ?? null,
56
+ reason: "raw-orm-filter",
57
+ service
58
+ });
59
+ } catch {
60
+ }
61
+ }
62
+ export {
63
+ collectLikeFilterFields,
64
+ warnOnEncryptedLikeFilter
65
+ };
66
+ //# sourceMappingURL=likeFilterWarning.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/encryption/likeFilterWarning.ts"],
4
+ "sourcesContent": ["import type { EntityManager, EntityName } from '@mikro-orm/postgresql'\nimport { warnOnCiphertextLikeFallback } from '../query/ciphertext-search-warning'\nimport { resolveTenantEncryptionService } from './customFieldValues'\nimport { resolveEntityIdFromMetadata } from './entityIds'\nimport type { TenantDataEncryptionService } from './tenantDataEncryptionService'\n\nconst LIKE_OPERATORS = new Set(['$like', '$ilike', '$re', '$fulltext'])\nconst LOGICAL_OPERATORS = new Set(['$and', '$or', '$not', '$every', '$some', '$none'])\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)\n\n// An operator object holds only `$`-prefixed keys. Anything else is a nested\n// relation filter, whose property names belong to another entity's encryption\n// map \u2014 attributing them to the root entity would produce false warnings.\nconst isOperatorObject = (value: unknown): value is Record<string, unknown> => {\n if (!isPlainObject(value)) return false\n const keys = Object.keys(value)\n return keys.length > 0 && keys.every((key) => key.startsWith('$'))\n}\n\nfunction walkFilter(node: unknown, field: string | null, out: Set<string>): void {\n if (Array.isArray(node)) {\n for (const item of node) walkFilter(item, field, out)\n return\n }\n if (!isPlainObject(node)) return\n for (const [key, value] of Object.entries(node)) {\n if (LIKE_OPERATORS.has(key)) {\n if (field) out.add(field)\n continue\n }\n if (LOGICAL_OPERATORS.has(key)) {\n walkFilter(value, field, out)\n continue\n }\n if (key.startsWith('$')) continue\n if (isOperatorObject(value)) walkFilter(value, key, out)\n }\n}\n\n/**\n * Lists the root-entity properties a filter targets with a string-matching\n * operator (`$like`, `$ilike`, `$re`, `$fulltext`), including inside\n * `$and`/`$or`/`$not` branches.\n */\nexport function collectLikeFilterFields(where: unknown): string[] {\n const found = new Set<string>()\n walkFilter(where, null, found)\n return Array.from(found)\n}\n\n// The map lookup behind the warning issues an uncached read against\n// `encryption_maps`, and this path runs on every matching query rather than in a\n// rare fallback branch \u2014 so the check stays out of production entirely.\nconst isWarningEnabled = (): boolean => process.env.NODE_ENV !== 'production'\n\nfunction resolveEntityId(em: EntityManager, entityName: EntityName<any>): string | null {\n const metadata = (em as any)?.getMetadata?.()\n if (!metadata || typeof metadata.find !== 'function') return null\n const name = typeof entityName === 'string' ? entityName : (entityName as any)?.name\n if (!name) return null\n return resolveEntityIdFromMetadata(metadata.find(name) ?? undefined)\n}\n\n/**\n * Warn, outside production, when a `where` clause matches an encrypted-at-rest\n * column with a string operator.\n *\n * Decryption happens on load, not on filter, so the predicate compares a\n * plaintext pattern against ciphertext: it matches nothing and does not raise,\n * which is indistinguishable from a genuine empty result. The query engines\n * already warn about their own `ILIKE` fallback; this covers the raw-ORM path\n * that never reaches them. Issue #5051, related to #2990.\n *\n * Never throws and never blocks the query.\n */\nexport async function warnOnEncryptedLikeFilter(params: {\n em: EntityManager\n entityName: EntityName<any>\n where: unknown\n tenantId?: string | null\n encryptionService?: TenantDataEncryptionService | null\n}): Promise<void> {\n if (!isWarningEnabled()) return\n const fields = collectLikeFilterFields(params.where)\n if (!fields.length) return\n try {\n const entity = resolveEntityId(params.em, params.entityName)\n if (!entity) return\n const service = params.encryptionService ?? resolveTenantEncryptionService(params.em)\n await warnOnCiphertextLikeFallback({\n entity,\n fields,\n tenantId: params.tenantId ?? null,\n reason: 'raw-orm-filter',\n service,\n })\n } catch {\n // A diagnostic must never break the query it is diagnosing.\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,oCAAoC;AAC7C,SAAS,sCAAsC;AAC/C,SAAS,mCAAmC;AAG5C,MAAM,iBAAiB,oBAAI,IAAI,CAAC,SAAS,UAAU,OAAO,WAAW,CAAC;AACtE,MAAM,oBAAoB,oBAAI,IAAI,CAAC,QAAQ,OAAO,QAAQ,UAAU,SAAS,OAAO,CAAC;AAErF,MAAM,gBAAgB,CAAC,UACrB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK,KAAK,EAAE,iBAAiB;AAK7F,MAAM,mBAAmB,CAAC,UAAqD;AAC7E,MAAI,CAAC,cAAc,KAAK,EAAG,QAAO;AAClC,QAAM,OAAO,OAAO,KAAK,KAAK;AAC9B,SAAO,KAAK,SAAS,KAAK,KAAK,MAAM,CAAC,QAAQ,IAAI,WAAW,GAAG,CAAC;AACnE;AAEA,SAAS,WAAW,MAAe,OAAsB,KAAwB;AAC/E,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,eAAW,QAAQ,KAAM,YAAW,MAAM,OAAO,GAAG;AACpD;AAAA,EACF;AACA,MAAI,CAAC,cAAc,IAAI,EAAG;AAC1B,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC/C,QAAI,eAAe,IAAI,GAAG,GAAG;AAC3B,UAAI,MAAO,KAAI,IAAI,KAAK;AACxB;AAAA,IACF;AACA,QAAI,kBAAkB,IAAI,GAAG,GAAG;AAC9B,iBAAW,OAAO,OAAO,GAAG;AAC5B;AAAA,IACF;AACA,QAAI,IAAI,WAAW,GAAG,EAAG;AACzB,QAAI,iBAAiB,KAAK,EAAG,YAAW,OAAO,KAAK,GAAG;AAAA,EACzD;AACF;AAOO,SAAS,wBAAwB,OAA0B;AAChE,QAAM,QAAQ,oBAAI,IAAY;AAC9B,aAAW,OAAO,MAAM,KAAK;AAC7B,SAAO,MAAM,KAAK,KAAK;AACzB;AAKA,MAAM,mBAAmB,MAAe,QAAQ,IAAI,aAAa;AAEjE,SAAS,gBAAgB,IAAmB,YAA4C;AACtF,QAAM,WAAY,IAAY,cAAc;AAC5C,MAAI,CAAC,YAAY,OAAO,SAAS,SAAS,WAAY,QAAO;AAC7D,QAAM,OAAO,OAAO,eAAe,WAAW,aAAc,YAAoB;AAChF,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,4BAA4B,SAAS,KAAK,IAAI,KAAK,MAAS;AACrE;AAcA,eAAsB,0BAA0B,QAM9B;AAChB,MAAI,CAAC,iBAAiB,EAAG;AACzB,QAAM,SAAS,wBAAwB,OAAO,KAAK;AACnD,MAAI,CAAC,OAAO,OAAQ;AACpB,MAAI;AACF,UAAM,SAAS,gBAAgB,OAAO,IAAI,OAAO,UAAU;AAC3D,QAAI,CAAC,OAAQ;AACb,UAAM,UAAU,OAAO,qBAAqB,+BAA+B,OAAO,EAAE;AACpF,UAAM,6BAA6B;AAAA,MACjC;AAAA,MACA;AAAA,MACA,UAAU,OAAO,YAAY;AAAA,MAC7B,QAAQ;AAAA,MACR;AAAA,IACF,CAAC;AAAA,EACH,QAAQ;AAAA,EAER;AACF;",
6
+ "names": []
7
+ }
@@ -5,6 +5,7 @@ import { isTenantDataEncryptionEnabled } from "./toggles.js";
5
5
  import { isEncryptionDebugEnabled } from "./toggles.js";
6
6
  import { resolveTenantEncryptionService } from "./customFieldValues.js";
7
7
  import { createLogger } from "../logger/index.js";
8
+ import { listEntityMetadataFromRegistry } from "../db/entityMetadata.js";
8
9
  const logger = createLogger("shared").child({ component: "encryption" });
9
10
  function resolveScope(entity) {
10
11
  const tenantId = entity.tenantId ?? entity.tenant_id ?? entity.tenant?.id ?? null;
@@ -70,9 +71,8 @@ class TenantEncryptionSubscriber {
70
71
  return registry.get?.(ctor);
71
72
  } catch {
72
73
  }
73
- const all = typeof registry.getAll === "function" && registry.getAll() || (Array.isArray(registry.metadata) ? registry.metadata : void 0) || registry.metadata || {};
74
74
  try {
75
- const entries = Array.isArray(all) ? all : Object.values(all);
75
+ const entries = listEntityMetadataFromRegistry(registry);
76
76
  const match = entries.find(
77
77
  (m) => m?.className === name || m?.name === name || m?.entityName === name || m?.collection === ctor?.prototype?.__meta?.tableName || m?.tableName === ctor?.prototype?.__meta?.tableName
78
78
  );
@@ -174,10 +174,6 @@ class TenantEncryptionSubscriber {
174
174
  return;
175
175
  }
176
176
  const { tenantId, organizationId } = resolveScope(target);
177
- if (!tenantId) {
178
- debug("\u26AA\uFE0F subscriber.skip", { reason: "no-tenant", entityId });
179
- return;
180
- }
181
177
  const encrypted = await this.service.encryptEntityPayload(entityId, target, tenantId, organizationId);
182
178
  const metaProps = resolvedMeta?.properties && typeof resolvedMeta.properties === "object" ? resolvedMeta.properties : {};
183
179
  const payloadObj = changeSet && typeof changeSet === "object" ? typeof changeSet.payload === "object" && changeSet.payload ? changeSet.payload : changeSet.payload = {} : null;
@@ -255,10 +251,6 @@ class TenantEncryptionSubscriber {
255
251
  const { tenantId, organizationId } = resolveScope(target);
256
252
  const scopedTenantId = tenantId ?? fallbackScope?.tenantId ?? null;
257
253
  const scopedOrgId = organizationId ?? fallbackScope?.organizationId ?? null;
258
- if (!scopedTenantId) {
259
- debug("\u26AA\uFE0F subscriber.skip", { reason: "no-tenant", entityId });
260
- return;
261
- }
262
254
  const hadPendingChanges = syncOriginal ? this.hasPendingChanges(target, resolvedMeta, em) : false;
263
255
  const decrypted = await this.service.decryptEntityPayload(entityId, target, scopedTenantId, scopedOrgId);
264
256
  Object.assign(target, decrypted);