@open-mercato/shared 0.4.2-canary-c02407ff85

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 (324) hide show
  1. package/build.mjs +101 -0
  2. package/dist/index.js +1 -0
  3. package/dist/index.js.map +7 -0
  4. package/dist/lib/api/crud.js +47 -0
  5. package/dist/lib/api/crud.js.map +7 -0
  6. package/dist/lib/api/scoped.js +140 -0
  7. package/dist/lib/api/scoped.js.map +7 -0
  8. package/dist/lib/auth/jwt.js +34 -0
  9. package/dist/lib/auth/jwt.js.map +7 -0
  10. package/dist/lib/auth/server.js +157 -0
  11. package/dist/lib/auth/server.js.map +7 -0
  12. package/dist/lib/boolean.js +22 -0
  13. package/dist/lib/boolean.js.map +7 -0
  14. package/dist/lib/bootstrap/appResolver.js +43 -0
  15. package/dist/lib/bootstrap/appResolver.js.map +7 -0
  16. package/dist/lib/bootstrap/dynamicLoader.js +108 -0
  17. package/dist/lib/bootstrap/dynamicLoader.js.map +7 -0
  18. package/dist/lib/bootstrap/factory.js +59 -0
  19. package/dist/lib/bootstrap/factory.js.map +7 -0
  20. package/dist/lib/bootstrap/index.js +11 -0
  21. package/dist/lib/bootstrap/index.js.map +7 -0
  22. package/dist/lib/bootstrap/types.js +1 -0
  23. package/dist/lib/bootstrap/types.js.map +7 -0
  24. package/dist/lib/cache/segments.js +36 -0
  25. package/dist/lib/cache/segments.js.map +7 -0
  26. package/dist/lib/cli/progress.js +46 -0
  27. package/dist/lib/cli/progress.js.map +7 -0
  28. package/dist/lib/commands/command-bus.js +285 -0
  29. package/dist/lib/commands/command-bus.js.map +7 -0
  30. package/dist/lib/commands/customFieldSnapshots.js +66 -0
  31. package/dist/lib/commands/customFieldSnapshots.js.map +7 -0
  32. package/dist/lib/commands/helpers.js +98 -0
  33. package/dist/lib/commands/helpers.js.map +7 -0
  34. package/dist/lib/commands/index.js +8 -0
  35. package/dist/lib/commands/index.js.map +7 -0
  36. package/dist/lib/commands/operationMetadata.js +32 -0
  37. package/dist/lib/commands/operationMetadata.js.map +7 -0
  38. package/dist/lib/commands/registry.js +43 -0
  39. package/dist/lib/commands/registry.js.map +7 -0
  40. package/dist/lib/commands/scope.js +44 -0
  41. package/dist/lib/commands/scope.js.map +7 -0
  42. package/dist/lib/commands/types.js +8 -0
  43. package/dist/lib/commands/types.js.map +7 -0
  44. package/dist/lib/crud/cache-stats.js +98 -0
  45. package/dist/lib/crud/cache-stats.js.map +7 -0
  46. package/dist/lib/crud/cache.js +175 -0
  47. package/dist/lib/crud/cache.js.map +7 -0
  48. package/dist/lib/crud/custom-fields-client.js +52 -0
  49. package/dist/lib/crud/custom-fields-client.js.map +7 -0
  50. package/dist/lib/crud/custom-fields.js +467 -0
  51. package/dist/lib/crud/custom-fields.js.map +7 -0
  52. package/dist/lib/crud/errors.js +24 -0
  53. package/dist/lib/crud/errors.js.map +7 -0
  54. package/dist/lib/crud/exporters.js +154 -0
  55. package/dist/lib/crud/exporters.js.map +7 -0
  56. package/dist/lib/crud/factory.js +1311 -0
  57. package/dist/lib/crud/factory.js.map +7 -0
  58. package/dist/lib/crud/types.js +1 -0
  59. package/dist/lib/crud/types.js.map +7 -0
  60. package/dist/lib/custom-fields/normalize.js +36 -0
  61. package/dist/lib/custom-fields/normalize.js.map +7 -0
  62. package/dist/lib/data/engine.js +396 -0
  63. package/dist/lib/data/engine.js.map +7 -0
  64. package/dist/lib/db/escapeLikePattern.js +5 -0
  65. package/dist/lib/db/escapeLikePattern.js.map +7 -0
  66. package/dist/lib/db/mikro.js +82 -0
  67. package/dist/lib/db/mikro.js.map +7 -0
  68. package/dist/lib/di/container.js +94 -0
  69. package/dist/lib/di/container.js.map +7 -0
  70. package/dist/lib/email/send.js +12 -0
  71. package/dist/lib/email/send.js.map +7 -0
  72. package/dist/lib/encryption/aes.js +58 -0
  73. package/dist/lib/encryption/aes.js.map +7 -0
  74. package/dist/lib/encryption/customFieldValues.js +49 -0
  75. package/dist/lib/encryption/customFieldValues.js.map +7 -0
  76. package/dist/lib/encryption/entityFields.js +26 -0
  77. package/dist/lib/encryption/entityFields.js.map +7 -0
  78. package/dist/lib/encryption/entityIds.js +80 -0
  79. package/dist/lib/encryption/entityIds.js.map +7 -0
  80. package/dist/lib/encryption/find.js +45 -0
  81. package/dist/lib/encryption/find.js.map +7 -0
  82. package/dist/lib/encryption/indexDoc.js +69 -0
  83. package/dist/lib/encryption/indexDoc.js.map +7 -0
  84. package/dist/lib/encryption/kms.js +282 -0
  85. package/dist/lib/encryption/kms.js.map +7 -0
  86. package/dist/lib/encryption/subscriber.js +330 -0
  87. package/dist/lib/encryption/subscriber.js.map +7 -0
  88. package/dist/lib/encryption/tenantDataEncryptionService.js +252 -0
  89. package/dist/lib/encryption/tenantDataEncryptionService.js.map +7 -0
  90. package/dist/lib/encryption/toggles.js +18 -0
  91. package/dist/lib/encryption/toggles.js.map +7 -0
  92. package/dist/lib/entities/naming.js +9 -0
  93. package/dist/lib/entities/naming.js.map +7 -0
  94. package/dist/lib/entities/system-entities.js +43 -0
  95. package/dist/lib/entities/system-entities.js.map +7 -0
  96. package/dist/lib/frontend/organizationEvents.js +41 -0
  97. package/dist/lib/frontend/organizationEvents.js.map +7 -0
  98. package/dist/lib/frontend/useOrganizationScope.js +32 -0
  99. package/dist/lib/frontend/useOrganizationScope.js.map +7 -0
  100. package/dist/lib/hotkeys/index.js +128 -0
  101. package/dist/lib/hotkeys/index.js.map +7 -0
  102. package/dist/lib/i18n/app-dictionaries.js +17 -0
  103. package/dist/lib/i18n/app-dictionaries.js.map +7 -0
  104. package/dist/lib/i18n/config.js +7 -0
  105. package/dist/lib/i18n/config.js.map +7 -0
  106. package/dist/lib/i18n/context.js +50 -0
  107. package/dist/lib/i18n/context.js.map +7 -0
  108. package/dist/lib/i18n/server.js +68 -0
  109. package/dist/lib/i18n/server.js.map +7 -0
  110. package/dist/lib/i18n/translate.js +45 -0
  111. package/dist/lib/i18n/translate.js.map +7 -0
  112. package/dist/lib/indexers/error-log.js +82 -0
  113. package/dist/lib/indexers/error-log.js.map +7 -0
  114. package/dist/lib/indexers/status-log.js +80 -0
  115. package/dist/lib/indexers/status-log.js.map +7 -0
  116. package/dist/lib/lib/auth/jwt.js +34 -0
  117. package/dist/lib/lib/auth/jwt.js.map +7 -0
  118. package/dist/lib/lib/auth/server.js +77 -0
  119. package/dist/lib/lib/auth/server.js.map +7 -0
  120. package/dist/lib/lib/email/send.js +12 -0
  121. package/dist/lib/lib/email/send.js.map +7 -0
  122. package/dist/lib/lib/i18n/config.js +7 -0
  123. package/dist/lib/lib/i18n/config.js.map +7 -0
  124. package/dist/lib/lib/i18n/context.js +31 -0
  125. package/dist/lib/lib/i18n/context.js.map +7 -0
  126. package/dist/lib/lib/utils.js +9 -0
  127. package/dist/lib/lib/utils.js.map +7 -0
  128. package/dist/lib/location/countries.js +68 -0
  129. package/dist/lib/location/countries.js.map +7 -0
  130. package/dist/lib/modules/index.js +6 -0
  131. package/dist/lib/modules/index.js.map +7 -0
  132. package/dist/lib/modules/registry.js +18 -0
  133. package/dist/lib/modules/registry.js.map +7 -0
  134. package/dist/lib/openapi/crud.js +137 -0
  135. package/dist/lib/openapi/crud.js.map +7 -0
  136. package/dist/lib/openapi/generator.js +1131 -0
  137. package/dist/lib/openapi/generator.js.map +7 -0
  138. package/dist/lib/openapi/index.js +10 -0
  139. package/dist/lib/openapi/index.js.map +7 -0
  140. package/dist/lib/openapi/sanitize.js +110 -0
  141. package/dist/lib/openapi/sanitize.js.map +7 -0
  142. package/dist/lib/openapi/types.js +1 -0
  143. package/dist/lib/openapi/types.js.map +7 -0
  144. package/dist/lib/profiler/index.js +258 -0
  145. package/dist/lib/profiler/index.js.map +7 -0
  146. package/dist/lib/query/engine.js +729 -0
  147. package/dist/lib/query/engine.js.map +7 -0
  148. package/dist/lib/query/join-utils.js +195 -0
  149. package/dist/lib/query/join-utils.js.map +7 -0
  150. package/dist/lib/query/types.js +9 -0
  151. package/dist/lib/query/types.js.map +7 -0
  152. package/dist/lib/search/config.js +32 -0
  153. package/dist/lib/search/config.js.map +7 -0
  154. package/dist/lib/search/tokenize.js +34 -0
  155. package/dist/lib/search/tokenize.js.map +7 -0
  156. package/dist/lib/slugify.js +24 -0
  157. package/dist/lib/slugify.js.map +7 -0
  158. package/dist/lib/testing/bootstrap.js +51 -0
  159. package/dist/lib/testing/bootstrap.js.map +7 -0
  160. package/dist/lib/testing/index.js +17 -0
  161. package/dist/lib/testing/index.js.map +7 -0
  162. package/dist/lib/testing/renderWithProviders.js +15 -0
  163. package/dist/lib/testing/renderWithProviders.js.map +7 -0
  164. package/dist/lib/url.js +12 -0
  165. package/dist/lib/url.js.map +7 -0
  166. package/dist/lib/utils.js +13 -0
  167. package/dist/lib/utils.js.map +7 -0
  168. package/dist/lib/version.js +7 -0
  169. package/dist/lib/version.js.map +7 -0
  170. package/dist/modules/dashboard/widgets.js +1 -0
  171. package/dist/modules/dashboard/widgets.js.map +7 -0
  172. package/dist/modules/dsl.js +30 -0
  173. package/dist/modules/dsl.js.map +7 -0
  174. package/dist/modules/entities/kinds.js +22 -0
  175. package/dist/modules/entities/kinds.js.map +7 -0
  176. package/dist/modules/entities/options.js +26 -0
  177. package/dist/modules/entities/options.js.map +7 -0
  178. package/dist/modules/entities/validation.js +102 -0
  179. package/dist/modules/entities/validation.js.map +7 -0
  180. package/dist/modules/entities/validators.js +88 -0
  181. package/dist/modules/entities/validators.js.map +7 -0
  182. package/dist/modules/entities.js +1 -0
  183. package/dist/modules/entities.js.map +7 -0
  184. package/dist/modules/navigation/sidebarPreferences.js +50 -0
  185. package/dist/modules/navigation/sidebarPreferences.js.map +7 -0
  186. package/dist/modules/perspectives/types.js +1 -0
  187. package/dist/modules/perspectives/types.js.map +7 -0
  188. package/dist/modules/registry.js +96 -0
  189. package/dist/modules/registry.js.map +7 -0
  190. package/dist/modules/search.js +15 -0
  191. package/dist/modules/search.js.map +7 -0
  192. package/dist/modules/vector.js +1 -0
  193. package/dist/modules/vector.js.map +7 -0
  194. package/dist/modules/widgets/injection-loader.js +180 -0
  195. package/dist/modules/widgets/injection-loader.js.map +7 -0
  196. package/dist/modules/widgets/injection.js +1 -0
  197. package/dist/modules/widgets/injection.js.map +7 -0
  198. package/dist/security/features.js +23 -0
  199. package/dist/security/features.js.map +7 -0
  200. package/dist/types/pg.d.js +1 -0
  201. package/dist/types/pg.d.js.map +7 -0
  202. package/dist/types/react-email.d.js +1 -0
  203. package/dist/types/react-email.d.js.map +7 -0
  204. package/dist/types/resend.d.js +1 -0
  205. package/dist/types/resend.d.js.map +7 -0
  206. package/jest.config.cjs +22 -0
  207. package/package.json +88 -0
  208. package/src/index.ts +0 -0
  209. package/src/lib/api/__tests__/scoped.test.ts +38 -0
  210. package/src/lib/api/crud.ts +59 -0
  211. package/src/lib/api/scoped.ts +239 -0
  212. package/src/lib/auth/jwt.ts +39 -0
  213. package/src/lib/auth/server.ts +199 -0
  214. package/src/lib/boolean.ts +17 -0
  215. package/src/lib/bootstrap/appResolver.ts +85 -0
  216. package/src/lib/bootstrap/dynamicLoader.ts +177 -0
  217. package/src/lib/bootstrap/factory.ts +108 -0
  218. package/src/lib/bootstrap/index.ts +23 -0
  219. package/src/lib/bootstrap/types.ts +31 -0
  220. package/src/lib/cache/segments.ts +56 -0
  221. package/src/lib/cli/progress.ts +55 -0
  222. package/src/lib/commands/__tests__/command-bus.test.ts +84 -0
  223. package/src/lib/commands/__tests__/helpers.test.ts +42 -0
  224. package/src/lib/commands/command-bus.ts +349 -0
  225. package/src/lib/commands/customFieldSnapshots.ts +86 -0
  226. package/src/lib/commands/helpers.ts +143 -0
  227. package/src/lib/commands/index.ts +4 -0
  228. package/src/lib/commands/operationMetadata.ts +40 -0
  229. package/src/lib/commands/registry.ts +46 -0
  230. package/src/lib/commands/scope.ts +59 -0
  231. package/src/lib/commands/types.ts +63 -0
  232. package/src/lib/crud/__tests__/crud-factory.test.ts +333 -0
  233. package/src/lib/crud/__tests__/custom-fields.test.ts +150 -0
  234. package/src/lib/crud/cache-stats.ts +127 -0
  235. package/src/lib/crud/cache.ts +205 -0
  236. package/src/lib/crud/custom-fields-client.ts +54 -0
  237. package/src/lib/crud/custom-fields.ts +607 -0
  238. package/src/lib/crud/errors.ts +23 -0
  239. package/src/lib/crud/exporters.ts +188 -0
  240. package/src/lib/crud/factory.ts +1622 -0
  241. package/src/lib/crud/types.ts +29 -0
  242. package/src/lib/custom-fields/normalize.ts +45 -0
  243. package/src/lib/data/engine.ts +562 -0
  244. package/src/lib/db/escapeLikePattern.ts +2 -0
  245. package/src/lib/db/mikro.ts +100 -0
  246. package/src/lib/di/container.ts +105 -0
  247. package/src/lib/email/send.ts +18 -0
  248. package/src/lib/encryption/__tests__/customFieldValues.test.ts +63 -0
  249. package/src/lib/encryption/__tests__/indexDoc.test.ts +115 -0
  250. package/src/lib/encryption/aes.ts +64 -0
  251. package/src/lib/encryption/customFieldValues.ts +67 -0
  252. package/src/lib/encryption/entityFields.ts +39 -0
  253. package/src/lib/encryption/entityIds.ts +107 -0
  254. package/src/lib/encryption/find.ts +81 -0
  255. package/src/lib/encryption/indexDoc.ts +104 -0
  256. package/src/lib/encryption/kms.ts +337 -0
  257. package/src/lib/encryption/subscriber.ts +416 -0
  258. package/src/lib/encryption/tenantDataEncryptionService.ts +313 -0
  259. package/src/lib/encryption/toggles.ts +15 -0
  260. package/src/lib/entities/naming.ts +6 -0
  261. package/src/lib/entities/system-entities.ts +43 -0
  262. package/src/lib/frontend/organizationEvents.ts +55 -0
  263. package/src/lib/frontend/useOrganizationScope.ts +30 -0
  264. package/src/lib/hotkeys/index.ts +168 -0
  265. package/src/lib/i18n/app-dictionaries.ts +18 -0
  266. package/src/lib/i18n/config.ts +4 -0
  267. package/src/lib/i18n/context.tsx +66 -0
  268. package/src/lib/i18n/server.ts +74 -0
  269. package/src/lib/i18n/translate.ts +54 -0
  270. package/src/lib/indexers/error-log.ts +106 -0
  271. package/src/lib/indexers/status-log.ts +119 -0
  272. package/src/lib/lib/auth/jwt.ts +39 -0
  273. package/src/lib/lib/auth/server.ts +94 -0
  274. package/src/lib/lib/email/send.ts +18 -0
  275. package/src/lib/lib/i18n/config.ts +4 -0
  276. package/src/lib/lib/i18n/context.tsx +38 -0
  277. package/src/lib/lib/utils.ts +6 -0
  278. package/src/lib/location/countries.ts +97 -0
  279. package/src/lib/modules/index.ts +1 -0
  280. package/src/lib/modules/registry.ts +18 -0
  281. package/src/lib/openapi/crud.ts +218 -0
  282. package/src/lib/openapi/generator.ts +1311 -0
  283. package/src/lib/openapi/index.ts +4 -0
  284. package/src/lib/openapi/sanitize.ts +137 -0
  285. package/src/lib/openapi/types.ts +79 -0
  286. package/src/lib/profiler/index.ts +371 -0
  287. package/src/lib/query/__tests__/engine.test.ts +274 -0
  288. package/src/lib/query/engine.ts +837 -0
  289. package/src/lib/query/join-utils.ts +238 -0
  290. package/src/lib/query/types.ts +121 -0
  291. package/src/lib/search/config.ts +49 -0
  292. package/src/lib/search/tokenize.ts +45 -0
  293. package/src/lib/slugify.ts +28 -0
  294. package/src/lib/testing/bootstrap.ts +124 -0
  295. package/src/lib/testing/index.ts +15 -0
  296. package/src/lib/testing/renderWithProviders.tsx +31 -0
  297. package/src/lib/url.ts +12 -0
  298. package/src/lib/utils.ts +17 -0
  299. package/src/lib/version.ts +5 -0
  300. package/src/modules/__tests__/dsl.test.ts +35 -0
  301. package/src/modules/__tests__/registry.test.ts +300 -0
  302. package/src/modules/dashboard/widgets.ts +57 -0
  303. package/src/modules/dsl.ts +32 -0
  304. package/src/modules/entities/__tests__/validation.test.ts +52 -0
  305. package/src/modules/entities/kinds.ts +20 -0
  306. package/src/modules/entities/options.ts +36 -0
  307. package/src/modules/entities/validation.ts +118 -0
  308. package/src/modules/entities/validators.ts +93 -0
  309. package/src/modules/entities.ts +102 -0
  310. package/src/modules/navigation/sidebarPreferences.ts +62 -0
  311. package/src/modules/perspectives/types.ts +40 -0
  312. package/src/modules/registry.ts +249 -0
  313. package/src/modules/search.ts +325 -0
  314. package/src/modules/vector.ts +122 -0
  315. package/src/modules/widgets/__tests__/injection.test.ts +48 -0
  316. package/src/modules/widgets/injection-loader.ts +235 -0
  317. package/src/modules/widgets/injection.ts +120 -0
  318. package/src/security/features.ts +22 -0
  319. package/src/types/pg.d.ts +2 -0
  320. package/src/types/react-email.d.ts +2 -0
  321. package/src/types/resend.d.ts +2 -0
  322. package/tsconfig.build.json +11 -0
  323. package/tsconfig.json +9 -0
  324. package/watch.mjs +6 -0
@@ -0,0 +1,239 @@
1
+ import type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'
2
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
3
+ import { splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'
4
+ import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
5
+ import type { z } from 'zod'
6
+
7
+ export type ScopedContext = (CommandRuntimeContext | CrudCtx) & {
8
+ auth: { tenantId?: string | null; orgId?: string | null } | null
9
+ selectedOrganizationId?: string | null
10
+ }
11
+
12
+ export type TranslateFn = (key: string, fallback?: string) => string
13
+
14
+ export type ScopedMessage = {
15
+ key: string
16
+ fallback: string
17
+ }
18
+
19
+ export type ScopedPayloadMessages = {
20
+ tenantRequired?: ScopedMessage
21
+ organizationRequired?: ScopedMessage
22
+ idRequired?: ScopedMessage
23
+ tenantForbidden?: ScopedMessage
24
+ }
25
+
26
+ export type ScopedPayloadOptions = {
27
+ requireOrganization?: boolean
28
+ messages?: ScopedPayloadMessages
29
+ }
30
+
31
+ const DEFAULT_MESSAGES: Required<ScopedPayloadMessages> = {
32
+ tenantRequired: { key: 'errors.tenant_required', fallback: 'Tenant context is required.' },
33
+ organizationRequired: { key: 'errors.organization_required', fallback: 'Organization context is required.' },
34
+ idRequired: { key: 'errors.id_required', fallback: 'Record identifier is required.' },
35
+ tenantForbidden: { key: 'errors.tenant_forbidden', fallback: 'You are not allowed to target this tenant.' },
36
+ }
37
+
38
+ function resolveMessage(messages: ScopedPayloadMessages | undefined, key: keyof ScopedPayloadMessages): ScopedMessage {
39
+ const override = messages?.[key]
40
+ if (override && typeof override.key === 'string' && override.key.length > 0) {
41
+ return {
42
+ key: override.key,
43
+ fallback: override.fallback ?? DEFAULT_MESSAGES[key]!.fallback,
44
+ }
45
+ }
46
+ return DEFAULT_MESSAGES[key]!
47
+ }
48
+
49
+ export function withScopedPayload<T extends Record<string, unknown>>(
50
+ payload: T | null | undefined,
51
+ ctx: ScopedContext,
52
+ translate: TranslateFn,
53
+ options: ScopedPayloadOptions = {}
54
+ ): T & { tenantId: string; organizationId?: string } {
55
+ const requireOrganization = options.requireOrganization !== false
56
+ const hasGlobalOrgAccess = ctx.organizationScope?.allowedIds === null
57
+ const source = payload ? { ...payload } : {}
58
+ const tenantId = (source as { tenantId?: string })?.tenantId ?? ctx.auth?.tenantId ?? null
59
+ if (!tenantId) {
60
+ const msg = resolveMessage(options.messages, 'tenantRequired')
61
+ throw new CrudHttpError(400, { error: translate(msg.key, msg.fallback) })
62
+ }
63
+
64
+ const resolvedOrg =
65
+ (source as { organizationId?: string })?.organizationId ??
66
+ ctx.selectedOrganizationId ??
67
+ ctx.auth?.orgId ??
68
+ null
69
+
70
+ if (requireOrganization && !hasGlobalOrgAccess && !resolvedOrg) {
71
+ const msg = resolveMessage(options.messages, 'organizationRequired')
72
+ throw new CrudHttpError(400, { error: translate(msg.key, msg.fallback) })
73
+ }
74
+
75
+ const scoped = {
76
+ ...source,
77
+ tenantId,
78
+ } as T & { tenantId: string; organizationId?: string }
79
+
80
+ if (resolvedOrg) scoped.organizationId = resolvedOrg
81
+
82
+ return scoped
83
+ }
84
+
85
+ export function parseScopedCommandInput<TSchema extends z.ZodTypeAny>(
86
+ schema: TSchema,
87
+ payload: unknown,
88
+ ctx: ScopedContext,
89
+ translate: TranslateFn,
90
+ options: ScopedPayloadOptions = {}
91
+ ): z.infer<TSchema> & { customFields?: Record<string, unknown> } {
92
+ const scoped = withScopedPayload(
93
+ (payload && typeof payload === 'object' ? payload : {}) as Record<string, unknown>,
94
+ ctx,
95
+ translate,
96
+ options
97
+ )
98
+ const actorTenantId = normalizeTenant(ctx.auth?.tenantId)
99
+ const requestedTenantId = normalizeTenant(scoped.tenantId)
100
+ const isSuperAdmin = authIsSuperAdmin(ctx.auth)
101
+ if (!isSuperAdmin) {
102
+ if (actorTenantId) {
103
+ if (!requestedTenantId || requestedTenantId !== actorTenantId) {
104
+ const msg = resolveMessage(options.messages, 'tenantForbidden')
105
+ throw new CrudHttpError(403, { error: translate(msg.key, msg.fallback) })
106
+ }
107
+ } else if (requestedTenantId) {
108
+ const msg = resolveMessage(options.messages, 'tenantForbidden')
109
+ throw new CrudHttpError(403, { error: translate(msg.key, msg.fallback) })
110
+ }
111
+ }
112
+ const { base, custom } = splitCustomFieldPayload(scoped)
113
+ const hasCustomFields = custom && Object.keys(custom).length > 0
114
+ const candidates: Array<Record<string, unknown>> = hasCustomFields
115
+ ? [base, { ...base, customFields: custom }]
116
+ : [base]
117
+
118
+ let parsed: z.infer<TSchema> | undefined
119
+ let lastError: unknown
120
+ for (const candidate of candidates) {
121
+ try {
122
+ parsed = schema.parse(candidate) as z.infer<TSchema>
123
+ break
124
+ } catch (err) {
125
+ lastError = err
126
+ }
127
+ }
128
+ if (!parsed) {
129
+ if (lastError instanceof Error) throw lastError
130
+ throw new CrudHttpError(400, { error: translate('errors.invalid_input', 'Invalid input') })
131
+ }
132
+
133
+ const parsedWithCustom = hasCustomFields
134
+ ? Object.assign({}, parsed, { customFields: custom })
135
+ : parsed
136
+
137
+ return parsedWithCustom as z.infer<TSchema> & { customFields?: Record<string, unknown> }
138
+ }
139
+
140
+ function normalizeTenant(candidate: unknown): string | null {
141
+ if (typeof candidate === 'string' && candidate.trim().length > 0) return candidate.trim()
142
+ return null
143
+ }
144
+
145
+ function authIsSuperAdmin(auth: ScopedContext['auth']): boolean {
146
+ if (!auth) return false
147
+ return (auth as Record<string, unknown>).isSuperAdmin === true
148
+ }
149
+
150
+ export function requireRecordId(
151
+ candidate: unknown,
152
+ ctx: ScopedContext,
153
+ translate: TranslateFn,
154
+ options: ScopedPayloadOptions = {}
155
+ ): string {
156
+ const fieldName = 'id'
157
+ const id =
158
+ typeof candidate === 'string'
159
+ ? candidate.trim()
160
+ : candidate && typeof candidate === 'object'
161
+ ? typeof (candidate as Record<string, unknown>)[fieldName] === 'string'
162
+ ? String((candidate as Record<string, unknown>)[fieldName])
163
+ : null
164
+ : null
165
+ if (id && id.length > 0) return id
166
+ const msg = resolveMessage(options.messages, 'idRequired')
167
+ throw new CrudHttpError(400, { error: translate(msg.key, msg.fallback) })
168
+ }
169
+
170
+ export function resolveCrudRecordId(
171
+ parsed: unknown,
172
+ ctx: ScopedContext,
173
+ translate: TranslateFn,
174
+ options: ScopedPayloadOptions & { fieldName?: string; queryParam?: string } = {}
175
+ ): string {
176
+ const fieldName = options.fieldName ?? 'id'
177
+ const queryParam = options.queryParam ?? fieldName
178
+
179
+ const tryRequire = (value: unknown): string | null => {
180
+ try {
181
+ return requireRecordId(value, ctx, translate, options)
182
+ } catch {
183
+ return null
184
+ }
185
+ }
186
+
187
+ if (parsed && typeof parsed === 'object') {
188
+ const body = (parsed as Record<string, unknown>).body
189
+ const fromBody = body && typeof body === 'object' ? tryRequire(body) : null
190
+ if (fromBody) return fromBody
191
+
192
+ const fallback = tryRequire(parsed)
193
+ if (fallback) return fallback
194
+
195
+ const query = (parsed as Record<string, unknown>).query
196
+ if (query && typeof query === 'object') {
197
+ const candidate = (query as Record<string, unknown>)[queryParam]
198
+ if (typeof candidate === 'string' && candidate.trim().length > 0) return candidate.trim()
199
+ }
200
+ }
201
+
202
+ if (ctx.request instanceof Request) {
203
+ const value = new URL(ctx.request.url).searchParams.get(queryParam)
204
+ if (value && value.trim().length > 0) return value.trim()
205
+ }
206
+
207
+ const msg = resolveMessage(options.messages, 'idRequired')
208
+ throw new CrudHttpError(400, { error: translate(msg.key, msg.fallback) })
209
+ }
210
+
211
+ export function createScopedApiHelpers(baseOptions?: ScopedPayloadOptions) {
212
+ return {
213
+ withScopedPayload: <T extends Record<string, unknown>>(
214
+ payload: T | null | undefined,
215
+ ctx: ScopedContext,
216
+ translate: TranslateFn,
217
+ options: ScopedPayloadOptions = {}
218
+ ) => withScopedPayload(payload, ctx, translate, { ...baseOptions, ...options }),
219
+ parseScopedCommandInput: <TSchema extends z.ZodTypeAny>(
220
+ schema: TSchema,
221
+ payload: unknown,
222
+ ctx: ScopedContext,
223
+ translate: TranslateFn,
224
+ options: ScopedPayloadOptions = {}
225
+ ) => parseScopedCommandInput(schema, payload, ctx, translate, { ...baseOptions, ...options }),
226
+ requireRecordId: (
227
+ candidate: unknown,
228
+ ctx: ScopedContext,
229
+ translate: TranslateFn,
230
+ options: ScopedPayloadOptions = {}
231
+ ) => requireRecordId(candidate, ctx, translate, { ...baseOptions, ...options }),
232
+ resolveCrudRecordId: (
233
+ parsed: unknown,
234
+ ctx: ScopedContext,
235
+ translate: TranslateFn,
236
+ options: ScopedPayloadOptions & { fieldName?: string; queryParam?: string } = {}
237
+ ) => resolveCrudRecordId(parsed, ctx, translate, { ...baseOptions, ...options }),
238
+ }
239
+ }
@@ -0,0 +1,39 @@
1
+ import crypto from 'node:crypto'
2
+
3
+ function base64url(input: Buffer | string) {
4
+ return (typeof input === 'string' ? Buffer.from(input) : input)
5
+ .toString('base64')
6
+ .replace(/=/g, '')
7
+ .replace(/\+/g, '-')
8
+ .replace(/\//g, '_')
9
+ }
10
+
11
+ export type JwtPayload = Record<string, any>
12
+
13
+ export function signJwt(payload: JwtPayload, secret = process.env.JWT_SECRET!, expiresInSec = 60 * 60 * 8) {
14
+ if (!secret) throw new Error('JWT_SECRET is not set')
15
+ const header = { alg: 'HS256', typ: 'JWT' }
16
+ const now = Math.floor(Date.now() / 1000)
17
+ const body = { iat: now, exp: now + expiresInSec, ...payload }
18
+ const encHeader = base64url(JSON.stringify(header))
19
+ const encBody = base64url(JSON.stringify(body))
20
+ const data = `${encHeader}.${encBody}`
21
+ const sig = crypto.createHmac('sha256', secret).update(data).digest()
22
+ const encSig = base64url(sig)
23
+ return `${data}.${encSig}`
24
+ }
25
+
26
+ export function verifyJwt(token: string, secret = process.env.JWT_SECRET!) {
27
+ if (!secret) throw new Error('JWT_SECRET is not set')
28
+ const parts = token.split('.')
29
+ if (parts.length !== 3) return null
30
+ const [h, p, s] = parts
31
+ const data = `${h}.${p}`
32
+ const expected = base64url(crypto.createHmac('sha256', secret).update(data).digest())
33
+ if (!crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected))) return null
34
+ const payload = JSON.parse(Buffer.from(p, 'base64').toString('utf8'))
35
+ const now = Math.floor(Date.now() / 1000)
36
+ if (payload.exp && now > payload.exp) return null
37
+ return payload
38
+ }
39
+
@@ -0,0 +1,199 @@
1
+ import { cookies } from 'next/headers'
2
+ import type { EntityManager } from '@mikro-orm/postgresql'
3
+ import { verifyJwt } from './jwt'
4
+
5
+ const TENANT_COOKIE_NAME = 'om_selected_tenant'
6
+ const ORGANIZATION_COOKIE_NAME = 'om_selected_org'
7
+ const ALL_ORGANIZATIONS_COOKIE_VALUE = '__all__'
8
+ const SUPERADMIN_ROLE = 'superadmin'
9
+
10
+ export type AuthContext = {
11
+ sub: string
12
+ tenantId: string | null
13
+ orgId: string | null
14
+ email?: string
15
+ roles?: string[]
16
+ isApiKey?: boolean
17
+ keyId?: string
18
+ keyName?: string
19
+ [k: string]: unknown
20
+ } | null
21
+
22
+ type CookieOverride = { applied: boolean; value: string | null }
23
+
24
+ function decodeCookieValue(raw: string | undefined): string | null {
25
+ if (raw === undefined) return null
26
+ try {
27
+ const decoded = decodeURIComponent(raw)
28
+ return decoded ?? null
29
+ } catch {
30
+ return raw ?? null
31
+ }
32
+ }
33
+
34
+ function readCookieFromHeader(header: string | null | undefined, name: string): string | undefined {
35
+ if (!header) return undefined
36
+ const parts = header.split(';')
37
+ for (const part of parts) {
38
+ const trimmed = part.trim()
39
+ if (trimmed.startsWith(`${name}=`)) {
40
+ return trimmed.slice(name.length + 1)
41
+ }
42
+ }
43
+ return undefined
44
+ }
45
+
46
+ function resolveTenantOverride(raw: string | undefined): CookieOverride {
47
+ if (raw === undefined) return { applied: false, value: null }
48
+ const decoded = decodeCookieValue(raw)
49
+ if (!decoded) return { applied: true, value: null }
50
+ const trimmed = decoded.trim()
51
+ if (!trimmed) return { applied: true, value: null }
52
+ return { applied: true, value: trimmed }
53
+ }
54
+
55
+ function resolveOrganizationOverride(raw: string | undefined): CookieOverride {
56
+ if (raw === undefined) return { applied: false, value: null }
57
+ const decoded = decodeCookieValue(raw)
58
+ if (!decoded || decoded === ALL_ORGANIZATIONS_COOKIE_VALUE) {
59
+ return { applied: true, value: null }
60
+ }
61
+ const trimmed = decoded.trim()
62
+ if (!trimmed || trimmed === ALL_ORGANIZATIONS_COOKIE_VALUE) {
63
+ return { applied: true, value: null }
64
+ }
65
+ return { applied: true, value: trimmed }
66
+ }
67
+
68
+ function isSuperAdminAuth(auth: AuthContext | null | undefined): boolean {
69
+ if (!auth) return false
70
+ if ((auth as Record<string, unknown>).isSuperAdmin === true) return true
71
+ const roles = Array.isArray(auth?.roles) ? auth.roles : []
72
+ return roles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)
73
+ }
74
+
75
+ function applySuperAdminScope(
76
+ auth: AuthContext,
77
+ tenantCookie: string | undefined,
78
+ orgCookie: string | undefined
79
+ ): AuthContext {
80
+ if (!auth || !isSuperAdminAuth(auth)) return auth
81
+
82
+ const tenantOverride = resolveTenantOverride(tenantCookie)
83
+ const orgOverride = resolveOrganizationOverride(orgCookie)
84
+ if (!tenantOverride.applied && !orgOverride.applied) return auth
85
+
86
+ type MutableAuthContext = Exclude<AuthContext, null> & {
87
+ actorTenantId?: string | null
88
+ actorOrgId?: string | null
89
+ }
90
+ const baseAuth = auth as Exclude<AuthContext, null>
91
+ const next: MutableAuthContext = { ...baseAuth }
92
+ if (tenantOverride.applied) {
93
+ if (!('actorTenantId' in next)) next.actorTenantId = auth?.tenantId ?? null
94
+ next.tenantId = tenantOverride.value
95
+ }
96
+ if (orgOverride.applied) {
97
+ if (!('actorOrgId' in next)) next.actorOrgId = auth?.orgId ?? null
98
+ next.orgId = orgOverride.value
99
+ }
100
+ next.isSuperAdmin = true
101
+ const existingRoles = Array.isArray(next.roles) ? next.roles : []
102
+ if (!existingRoles.some((role) => typeof role === 'string' && role.trim().toLowerCase() === SUPERADMIN_ROLE)) {
103
+ next.roles = [...existingRoles, 'superadmin']
104
+ }
105
+ return next
106
+ }
107
+
108
+ async function resolveApiKeyAuth(secret: string): Promise<AuthContext> {
109
+ if (!secret) return null
110
+ try {
111
+ const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')
112
+ const container = await createRequestContainer()
113
+ const em = (container.resolve('em') as EntityManager)
114
+ const { findApiKeyBySecret } = await import('@open-mercato/core/modules/api_keys/services/apiKeyService')
115
+ const { Role } = await import('@open-mercato/core/modules/auth/data/entities')
116
+
117
+ const record = await findApiKeyBySecret(em, secret)
118
+ if (!record) return null
119
+
120
+ const roleIds = Array.isArray(record.rolesJson)
121
+ ? record.rolesJson.filter((value): value is string => typeof value === 'string' && value.length > 0)
122
+ : []
123
+ const roles = roleIds.length
124
+ ? await em.find(Role, { id: { $in: roleIds } })
125
+ : []
126
+ const roleNames = roles.map((role) => role.name).filter((name): name is string => typeof name === 'string' && name.length > 0)
127
+
128
+ try {
129
+ record.lastUsedAt = new Date()
130
+ await em.persistAndFlush(record)
131
+ } catch {
132
+ // best-effort update; ignore write failures
133
+ }
134
+
135
+ return {
136
+ sub: `api_key:${record.id}`,
137
+ tenantId: record.tenantId ?? null,
138
+ orgId: record.organizationId ?? null,
139
+ roles: roleNames,
140
+ isApiKey: true,
141
+ keyId: record.id,
142
+ keyName: record.name,
143
+ }
144
+ } catch {
145
+ return null
146
+ }
147
+ }
148
+
149
+ function extractApiKey(req: Request): string | null {
150
+ const header = (req.headers.get('x-api-key') || '').trim()
151
+ if (header) return header
152
+ const authHeader = (req.headers.get('authorization') || '').trim()
153
+ if (authHeader.toLowerCase().startsWith('apikey ')) {
154
+ return authHeader.slice(7).trim()
155
+ }
156
+ return null
157
+ }
158
+
159
+ export async function getAuthFromCookies(): Promise<AuthContext> {
160
+ const cookieStore = await cookies()
161
+ const token = cookieStore.get('auth_token')?.value
162
+ if (!token) return null
163
+ try {
164
+ const payload = verifyJwt(token) as AuthContext
165
+ if (!payload) return null
166
+ const tenantCookie = cookieStore.get(TENANT_COOKIE_NAME)?.value
167
+ const orgCookie = cookieStore.get(ORGANIZATION_COOKIE_NAME)?.value
168
+ return applySuperAdminScope(payload, tenantCookie, orgCookie)
169
+ } catch {
170
+ return null
171
+ }
172
+ }
173
+
174
+ export async function getAuthFromRequest(req: Request): Promise<AuthContext> {
175
+ const cookieHeader = req.headers.get('cookie') || ''
176
+ const tenantCookie = readCookieFromHeader(cookieHeader, TENANT_COOKIE_NAME)
177
+ const orgCookie = readCookieFromHeader(cookieHeader, ORGANIZATION_COOKIE_NAME)
178
+ const authHeader = (req.headers.get('authorization') || '').trim()
179
+ let token: string | undefined
180
+ if (authHeader.toLowerCase().startsWith('bearer ')) token = authHeader.slice(7).trim()
181
+ if (!token) {
182
+ const match = cookieHeader.match(/(?:^|;\s*)auth_token=([^;]+)/)
183
+ if (match) token = decodeURIComponent(match[1])
184
+ }
185
+ if (token) {
186
+ try {
187
+ const payload = verifyJwt(token) as AuthContext
188
+ if (payload) return applySuperAdminScope(payload, tenantCookie, orgCookie)
189
+ } catch {
190
+ // fall back to API key detection
191
+ }
192
+ }
193
+
194
+ const apiKey = extractApiKey(req)
195
+ if (!apiKey) return null
196
+ const apiAuth = await resolveApiKeyAuth(apiKey)
197
+ if (!apiAuth) return null
198
+ return applySuperAdminScope(apiAuth, tenantCookie, orgCookie)
199
+ }
@@ -0,0 +1,17 @@
1
+ export const TRUE_VALUES = new Set(['1', 'true', 'yes', 'y', 'on', 'enable', 'enabled'])
2
+ export const FALSE_VALUES = new Set(['0', 'false', 'no', 'n', 'off', 'disable', 'disabled'])
3
+
4
+ export function parseBooleanToken(raw: string | null | undefined): boolean | null {
5
+ if (typeof raw !== 'string') return null
6
+ const trimmed = raw.trim()
7
+ if (!trimmed) return null
8
+ const normalized = trimmed.toLowerCase()
9
+ if (TRUE_VALUES.has(normalized)) return true
10
+ if (FALSE_VALUES.has(normalized)) return false
11
+ return null
12
+ }
13
+
14
+ export function parseBooleanWithDefault(raw: string | null | undefined, fallback: boolean): boolean {
15
+ const parsed = parseBooleanToken(raw)
16
+ return parsed === null ? fallback : parsed
17
+ }
@@ -0,0 +1,85 @@
1
+ import path from 'node:path'
2
+ import fs from 'node:fs'
3
+
4
+ export interface AppRoot {
5
+ appDir: string
6
+ mercatoDir: string
7
+ generatedDir: string
8
+ }
9
+
10
+ /**
11
+ * Find the Next.js app root by searching for next.config.ts/js/mjs.
12
+ *
13
+ * Starts from the given directory (defaults to cwd) and walks up the
14
+ * directory tree until it finds a Next.js config file with a .mercato/generated
15
+ * directory.
16
+ *
17
+ * @param startDir - Directory to start searching from (defaults to process.cwd())
18
+ * @returns The resolved app root paths, or null if not found
19
+ */
20
+ export function findAppRoot(startDir: string = process.cwd()): AppRoot | null {
21
+ let current = startDir
22
+
23
+ while (current !== path.dirname(current)) {
24
+ const configTs = path.join(current, 'next.config.ts')
25
+ const configJs = path.join(current, 'next.config.js')
26
+ const configMjs = path.join(current, 'next.config.mjs')
27
+
28
+ if (fs.existsSync(configTs) || fs.existsSync(configJs) || fs.existsSync(configMjs)) {
29
+ const mercatoDir = path.join(current, '.mercato')
30
+ const generatedDir = path.join(mercatoDir, 'generated')
31
+
32
+ // Only return if .mercato/generated exists
33
+ if (fs.existsSync(generatedDir)) {
34
+ return { appDir: current, mercatoDir, generatedDir }
35
+ }
36
+
37
+ // Found Next.js config but no .mercato/generated - return anyway for generate command
38
+ // The caller can decide whether to create the directory
39
+ return { appDir: current, mercatoDir, generatedDir }
40
+ }
41
+
42
+ current = path.dirname(current)
43
+ }
44
+
45
+ return null
46
+ }
47
+
48
+ /**
49
+ * Find all apps with .mercato directories in a monorepo.
50
+ *
51
+ * Scans the apps/ directory for Next.js apps with .mercato/generated directories.
52
+ *
53
+ * @param rootDir - The monorepo root directory
54
+ * @returns Array of app root paths
55
+ */
56
+ export function findAllApps(rootDir: string): AppRoot[] {
57
+ const appsDir = path.join(rootDir, 'apps')
58
+ if (!fs.existsSync(appsDir)) return []
59
+
60
+ const apps: AppRoot[] = []
61
+ const entries = fs.readdirSync(appsDir, { withFileTypes: true })
62
+
63
+ for (const entry of entries) {
64
+ if (!entry.isDirectory()) continue
65
+
66
+ const appDir = path.join(appsDir, entry.name)
67
+
68
+ // Check for Next.js config
69
+ const hasNextConfig =
70
+ fs.existsSync(path.join(appDir, 'next.config.ts')) ||
71
+ fs.existsSync(path.join(appDir, 'next.config.js')) ||
72
+ fs.existsSync(path.join(appDir, 'next.config.mjs'))
73
+
74
+ if (!hasNextConfig) continue
75
+
76
+ const mercatoDir = path.join(appDir, '.mercato')
77
+ const generatedDir = path.join(mercatoDir, 'generated')
78
+
79
+ if (fs.existsSync(generatedDir)) {
80
+ apps.push({ appDir, mercatoDir, generatedDir })
81
+ }
82
+ }
83
+
84
+ return apps
85
+ }