@open-mercato/shared 0.9.1-develop.7574.1.837288eec1 → 0.10.1-develop.7576.1.21d5b50435

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 (125) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +4 -4
  3. package/dist/lib/auth/server.js +1 -1
  4. package/dist/lib/auth/server.js.map +2 -2
  5. package/dist/lib/bootstrap/dynamicLoader.js +6 -46
  6. package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
  7. package/dist/lib/bootstrap/factory.js +3 -4
  8. package/dist/lib/bootstrap/factory.js.map +2 -2
  9. package/dist/lib/catalog-visibility/index.js +0 -4
  10. package/dist/lib/catalog-visibility/index.js.map +2 -2
  11. package/dist/lib/commands/command-bus.js +13 -45
  12. package/dist/lib/commands/command-bus.js.map +2 -2
  13. package/dist/lib/commands/types.js.map +2 -2
  14. package/dist/lib/crud/exporters.js +1 -1
  15. package/dist/lib/crud/exporters.js.map +2 -2
  16. package/dist/lib/crud/ids.js +1 -13
  17. package/dist/lib/crud/ids.js.map +2 -2
  18. package/dist/lib/db/pg-errors.js +0 -14
  19. package/dist/lib/db/pg-errors.js.map +2 -2
  20. package/dist/lib/localization/index.js +1 -9
  21. package/dist/lib/localization/index.js.map +2 -2
  22. package/dist/lib/localization/translatable-fields.js +0 -26
  23. package/dist/lib/localization/translatable-fields.js.map +2 -2
  24. package/dist/lib/number.js +0 -6
  25. package/dist/lib/number.js.map +2 -2
  26. package/dist/lib/openapi/generator.js +67 -84
  27. package/dist/lib/openapi/generator.js.map +2 -2
  28. package/dist/lib/query/engine.js +10 -45
  29. package/dist/lib/query/engine.js.map +2 -2
  30. package/dist/lib/query/join-utils.js +1 -1
  31. package/dist/lib/query/join-utils.js.map +2 -2
  32. package/dist/lib/query/types.js.map +2 -2
  33. package/dist/lib/url.js +0 -24
  34. package/dist/lib/url.js.map +2 -2
  35. package/dist/lib/version.js +1 -1
  36. package/dist/lib/version.js.map +1 -1
  37. package/dist/lib/webhooks/body.js +3 -7
  38. package/dist/lib/webhooks/body.js.map +2 -2
  39. package/dist/lib/webhooks/index.js +0 -2
  40. package/dist/lib/webhooks/index.js.map +2 -2
  41. package/dist/modules/integrations/types.js.map +2 -2
  42. package/dist/modules/notifications/types.js +0 -4
  43. package/dist/modules/notifications/types.js.map +3 -3
  44. package/dist/modules/payment_gateways/types.js +1 -14
  45. package/dist/modules/payment_gateways/types.js.map +2 -2
  46. package/dist/modules/registry.js.map +2 -2
  47. package/dist/modules/search.js.map +2 -2
  48. package/dist/modules/widgets/injection-loader.js +3 -14
  49. package/dist/modules/widgets/injection-loader.js.map +2 -2
  50. package/package.json +3 -10
  51. package/src/lib/__tests__/number.test.ts +1 -24
  52. package/src/lib/auth/__tests__/server.test.ts +0 -14
  53. package/src/lib/auth/server.ts +1 -1
  54. package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +2 -2
  55. package/src/lib/bootstrap/__tests__/dynamicLoader.appModuleOverrides.test.ts +2 -2
  56. package/src/lib/bootstrap/__tests__/dynamicLoader.cacheRecovery.test.ts +2 -2
  57. package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +2 -2
  58. package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +2 -2
  59. package/src/lib/bootstrap/__tests__/factory.test.ts +6 -18
  60. package/src/lib/bootstrap/dynamicLoader.ts +5 -63
  61. package/src/lib/bootstrap/factory.ts +3 -8
  62. package/src/lib/bootstrap/types.ts +1 -5
  63. package/src/lib/catalog-visibility/index.ts +0 -2
  64. package/src/lib/commands/__tests__/command-bus.test.ts +1 -100
  65. package/src/lib/commands/command-bus.ts +10 -42
  66. package/src/lib/commands/command-interceptor.ts +0 -4
  67. package/src/lib/commands/types.ts +0 -14
  68. package/src/lib/crud/__tests__/exporters.test.ts +0 -18
  69. package/src/lib/crud/__tests__/ids.test.ts +1 -34
  70. package/src/lib/crud/exporters.ts +1 -1
  71. package/src/lib/crud/ids.ts +0 -24
  72. package/src/lib/db/__tests__/pg-errors.test.ts +1 -68
  73. package/src/lib/db/pg-errors.ts +0 -35
  74. package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +0 -23
  75. package/src/lib/localization/__tests__/translatable-fields.test.ts +0 -17
  76. package/src/lib/localization/index.ts +1 -8
  77. package/src/lib/localization/translatable-fields.ts +0 -46
  78. package/src/lib/number.ts +0 -13
  79. package/src/lib/openapi/generator.ts +77 -106
  80. package/src/lib/query/engine.ts +8 -58
  81. package/src/lib/query/join-utils.ts +1 -1
  82. package/src/lib/query/types.ts +1 -13
  83. package/src/lib/url.ts +0 -30
  84. package/src/lib/webhooks/__tests__/body.test.ts +0 -48
  85. package/src/lib/webhooks/body.ts +4 -11
  86. package/src/lib/webhooks/index.ts +0 -1
  87. package/src/modules/integrations/types.ts +0 -6
  88. package/src/modules/notifications/types.ts +0 -7
  89. package/src/modules/payment_gateways/__tests__/types.test.ts +0 -44
  90. package/src/modules/payment_gateways/types.ts +6 -52
  91. package/src/modules/registry.ts +0 -5
  92. package/src/modules/search.ts +0 -39
  93. package/src/modules/widgets/injection-loader.ts +3 -33
  94. package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js +0 -6
  95. package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js.map +0 -7
  96. package/dist/lib/bootstrap/next-subpath-resolve-hook.js +0 -62
  97. package/dist/lib/bootstrap/next-subpath-resolve-hook.js.map +0 -7
  98. package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js +0 -33
  99. package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js.map +0 -7
  100. package/dist/lib/catalog-visibility/hashEffectiveScope.js +0 -9
  101. package/dist/lib/catalog-visibility/hashEffectiveScope.js.map +0 -7
  102. package/dist/lib/db/advisoryLock.js +0 -172
  103. package/dist/lib/db/advisoryLock.js.map +0 -7
  104. package/dist/lib/query/overlap.js +0 -58
  105. package/dist/lib/query/overlap.js.map +0 -7
  106. package/dist/modules/notifications/safeHref.js +0 -20
  107. package/dist/modules/notifications/safeHref.js.map +0 -7
  108. package/src/lib/__tests__/url.cross-site.test.ts +0 -140
  109. package/src/lib/bootstrap/__tests__/dynamicLoader.externalSubpath.test.ts +0 -141
  110. package/src/lib/bootstrap/__tests__/next-subpath-resolve-hook.test.ts +0 -174
  111. package/src/lib/bootstrap/next-subpath-resolve-hook-worker.ts +0 -3
  112. package/src/lib/bootstrap/next-subpath-resolve-hook.ts +0 -130
  113. package/src/lib/catalog-visibility/__tests__/hashEffectiveScope.test.ts +0 -121
  114. package/src/lib/catalog-visibility/canonicalizeEffectiveScope.ts +0 -41
  115. package/src/lib/catalog-visibility/hashEffectiveScope.ts +0 -8
  116. package/src/lib/db/__tests__/advisoryLock.integration.test.ts +0 -343
  117. package/src/lib/db/__tests__/advisoryLock.test.ts +0 -564
  118. package/src/lib/db/advisoryLock.ts +0 -271
  119. package/src/lib/openapi/__tests__/optional-path-params.test.ts +0 -102
  120. package/src/lib/query/__tests__/empty-ids-filter.pg.test.ts +0 -89
  121. package/src/lib/query/__tests__/engine.empty-in-filter.test.ts +0 -79
  122. package/src/lib/query/__tests__/overlap.test.ts +0 -219
  123. package/src/lib/query/overlap.ts +0 -106
  124. package/src/modules/notifications/safeHref.ts +0 -19
  125. package/src/modules/widgets/__tests__/injection-loader.partitioned-registration.test.ts +0 -78
@@ -1,4 +1,4 @@
1
- import { formatLocaleNumberInput, parseLocaleNumber, parseNumberWithDefault, resolveLocaleNumberSeparators } from '../number'
1
+ import { parseLocaleNumber, parseNumberWithDefault, resolveLocaleNumberSeparators } from '../number'
2
2
 
3
3
  describe('parseNumberWithDefault', () => {
4
4
  it('returns the fallback when raw is missing or blank', () => {
@@ -121,26 +121,3 @@ describe('parseLocaleNumber', () => {
121
121
  expect(parseLocaleNumber('110.70', undefined)).toBe(110.7)
122
122
  })
123
123
  })
124
-
125
- describe('formatLocaleNumberInput (issue #6309)', () => {
126
- it('uses the locale decimal separator instead of the dot String() always emits', () => {
127
- expect(formatLocaleNumberInput(2.5, 'pl-PL')).toBe('2,5')
128
- expect(formatLocaleNumberInput(-2.5, 'pl-PL')).toBe('-2,5')
129
- expect(formatLocaleNumberInput(1234.56, 'pl-PL')).toBe('1234,56')
130
- expect(formatLocaleNumberInput(2.5, 'en-US')).toBe('2.5')
131
- })
132
-
133
- it('leaves integers untouched and applies no grouping', () => {
134
- expect(formatLocaleNumberInput(1500, 'pl-PL')).toBe('1500')
135
- expect(formatLocaleNumberInput(1500, 'en-US')).toBe('1500')
136
- expect(formatLocaleNumberInput(0, 'pl-PL')).toBe('0')
137
- })
138
-
139
- it('round-trips through parseLocaleNumber', () => {
140
- for (const value of [2.5, -0.125, 1234.56, 0.1 + 0.2, 7]) {
141
- for (const locale of ['pl-PL', 'de-DE', 'en-US', 'fr-FR']) {
142
- expect(parseLocaleNumber(formatLocaleNumberInput(value, locale), locale)).toBe(value)
143
- }
144
- }
145
- })
146
- })
@@ -122,20 +122,6 @@ describe('auth server integrity checks', () => {
122
122
  await expect(resolveAuthFromRequestDetailed(request)).resolves.toEqual({ auth: null, status: 'invalid' })
123
123
  })
124
124
 
125
- it('treats a malformed %-escape auth_token as an invalid token instead of 500ing (issue #7382)', async () => {
126
- const { resolveAuthFromRequestDetailed } = await import('@open-mercato/shared/lib/auth/server')
127
- verifyJwt.mockImplementation(() => {
128
- throw new Error('malformed token')
129
- })
130
-
131
- const request = new Request('https://example.test/api/test', {
132
- headers: { cookie: 'auth_token=%zz' },
133
- })
134
-
135
- await expect(resolveAuthFromRequestDetailed(request)).resolves.toEqual({ auth: null, status: 'invalid' })
136
- expect(verifyJwt).toHaveBeenCalledWith('%zz')
137
- })
138
-
139
125
  it('replaces stale JWT roles with canonical roles from the database', async () => {
140
126
  const { getAuthFromRequest } = await import('@open-mercato/shared/lib/auth/server')
141
127
  const jwtAuth = {
@@ -413,7 +413,7 @@ export async function resolveAuthFromRequestDetailed(req: Request): Promise<Auth
413
413
  if (authHeader.toLowerCase().startsWith('bearer ')) token = authHeader.slice(7).trim()
414
414
  if (!token) {
415
415
  const match = cookieHeader.match(/(?:^|;\s*)auth_token=([^;]+)/)
416
- if (match) token = decodeCookieValue(match[1]) ?? undefined
416
+ if (match) token = decodeURIComponent(match[1])
417
417
  }
418
418
  if (token) {
419
419
  try {
@@ -91,9 +91,9 @@ function writeCompiledPair(
91
91
  fs.writeFileSync(compiledPath, source.compiled)
92
92
  const sourceRelativePath = path.relative(appRoot, sourcePath).split(path.sep).join('/')
93
93
  fs.writeFileSync(`${compiledPath}.cache.json`, JSON.stringify({
94
- version: 5,
94
+ version: 4,
95
95
  inputHash: hash(JSON.stringify({
96
- version: 5,
96
+ version: 4,
97
97
  sourceHash: hash(source.ts),
98
98
  tsconfigHashes: {
99
99
  'tsconfig.json': hash(APP_TSCONFIG),
@@ -132,9 +132,9 @@ function writeCompiledPair(
132
132
  fs.writeFileSync(compiledPath, source.compiled)
133
133
  const sourceRelativePath = path.relative(appRoot, sourcePath).split(path.sep).join('/')
134
134
  fs.writeFileSync(`${compiledPath}.cache.json`, JSON.stringify({
135
- version: 5,
135
+ version: 4,
136
136
  inputHash: hash(JSON.stringify({
137
- version: 5,
137
+ version: 4,
138
138
  sourceHash: hash(source.ts),
139
139
  tsconfigHashes: {
140
140
  'tsconfig.json': hash(APP_TSCONFIG),
@@ -77,7 +77,7 @@ function writeGeneratedModule(generatedDir: string, baseName: string, source: {
77
77
  function writeCompiledSibling(generatedDir: string, baseName: string, compiled: string) {
78
78
  const source = fs.readFileSync(path.join(generatedDir, `${baseName}.ts`), 'utf8')
79
79
  const inputHash = hash(JSON.stringify({
80
- version: 5,
80
+ version: 4,
81
81
  sourceHash: hash(source),
82
82
  tsconfigHashes: {
83
83
  'tsconfig.json': hash(APP_TSCONFIG),
@@ -90,7 +90,7 @@ function writeCompiledSibling(generatedDir: string, baseName: string, compiled:
90
90
  const compiledPath = path.join(generatedDir, `${baseName}.mjs`)
91
91
  fs.writeFileSync(compiledPath, compiled)
92
92
  fs.writeFileSync(`${compiledPath}.cache.json`, JSON.stringify({
93
- version: 5,
93
+ version: 4,
94
94
  inputHash,
95
95
  outputHash: hash(compiled),
96
96
  dependencies: {
@@ -77,9 +77,9 @@ function writeGeneratedModule(generatedDir: string, baseName: string, source: {
77
77
  fs.writeFileSync(sourcePath, source.ts)
78
78
  fs.writeFileSync(compiledPath, source.compiled)
79
79
  fs.writeFileSync(`${compiledPath}.cache.json`, JSON.stringify({
80
- version: 5,
80
+ version: 4,
81
81
  inputHash: hash(JSON.stringify({
82
- version: 5,
82
+ version: 4,
83
83
  sourceHash: hash(source.ts),
84
84
  tsconfigHashes: {
85
85
  'tsconfig.json': hash(APP_TSCONFIG),
@@ -133,7 +133,7 @@ describe('dynamic loader app tsconfig and content-addressed cache', () => {
133
133
  loadBootstrapDataInNode(appRoot)
134
134
  const after = readCacheMetadata(appRoot, 'entities')
135
135
 
136
- expect(after.version).toBe(5)
136
+ expect(after.version).toBe(4)
137
137
  expect(after.inputHash).not.toBe(before.inputHash)
138
138
  })
139
139
 
@@ -287,6 +287,6 @@ describe('dynamic loader app tsconfig and content-addressed cache', () => {
287
287
 
288
288
  loadBootstrapDataInNode(appRoot)
289
289
 
290
- expect(readCacheMetadata(appRoot, 'entities').version).toBe(5)
290
+ expect(readCacheMetadata(appRoot, 'entities').version).toBe(4)
291
291
  })
292
292
  })
@@ -62,8 +62,7 @@ describe('partitioned bootstrap registration', () => {
62
62
  })
63
63
 
64
64
  it('keeps API-only bootstrap from replacing core injection widgets', async () => {
65
- const entry = { moduleId: 'example', key: 'example:widget', source: 'app' as const, loader: async () => ({}) as never }
66
- const apiBootstrap = createBootstrap({ ...emptyBootstrapData, injectionWidgetEntries: [entry] }, {
65
+ const apiBootstrap = createBootstrap(emptyBootstrapData, {
67
66
  registrationKey: 'api-only',
68
67
  skipUiRegistries: true,
69
68
  skipCoreInjectionWidgets: true,
@@ -72,22 +71,11 @@ describe('partitioned bootstrap registration', () => {
72
71
  apiBootstrap()
73
72
  await waitForAsyncRegistration()
74
73
 
75
- // The API partition still contributes its entries — module API routes resolve widget
76
- // contributions on the request path — but in `merge` mode, so it can never shrink a
77
- // registry the full page bootstrap already published in the same process.
78
- expect(registerCoreInjectionWidgetsMock).toHaveBeenCalledWith([entry], { mode: 'merge' })
79
- // The raw widget entries also travel with the tables so a `key`-spelled injection
80
- // override resolves to the `widgetId` the slots reference (#5152).
81
- expect(registerCoreInjectionTablesMock).toHaveBeenCalledWith([], [entry])
74
+ expect(registerCoreInjectionWidgetsMock).not.toHaveBeenCalled()
75
+ // The raw widget entries still travel with the tables so a `key`-spelled injection
76
+ // override resolves to the `widgetId` the slots reference (#5152), even though this
77
+ // bootstrap deliberately skips registering the widgets themselves.
78
+ expect(registerCoreInjectionTablesMock).toHaveBeenCalledWith([], [])
82
79
  expect(registerEnabledModuleIdsMock).toHaveBeenCalledTimes(1)
83
80
  })
84
-
85
- it('lets the full bootstrap replace the registry outright', async () => {
86
- const fullBootstrap = createBootstrap(emptyBootstrapData, { registrationKey: 'full-only' })
87
-
88
- fullBootstrap()
89
- await waitForAsyncRegistration()
90
-
91
- expect(registerCoreInjectionWidgetsMock).toHaveBeenCalledWith([], undefined)
92
- })
93
81
  })
@@ -16,7 +16,7 @@ import { CLIENT_ONLY_STUB_NAMESPACE, createClientOnlyStubPlugin } from './client
16
16
  import path from 'node:path'
17
17
  import fs from 'node:fs'
18
18
  import crypto from 'node:crypto'
19
- import { createRequire, isBuiltin } from 'node:module'
19
+ import { createRequire } from 'node:module'
20
20
  import { pathToFileURL } from 'node:url'
21
21
 
22
22
  let activeBootstrapLoads = 0
@@ -77,44 +77,6 @@ class GeneratedFileNotFoundError extends Error {
77
77
  }
78
78
  }
79
79
 
80
- function splitBareSpecifier(specifier: string): { packageName: string; subpath: string } | null {
81
- const segments = specifier.split('/')
82
- const nameLength = specifier.startsWith('@') ? 2 : 1
83
- if (segments.length <= nameLength || segments.slice(0, nameLength).some((segment) => !segment)) return null
84
- return {
85
- packageName: segments.slice(0, nameLength).join('/'),
86
- subpath: segments.slice(nameLength).join('/'),
87
- }
88
- }
89
-
90
- /**
91
- * Rewrites an externalized `pkg/subpath` specifier to the file Node's ESM resolver can load
92
- * when `pkg` publishes no `exports` map (#6993). Builtins, packages with `exports`, subpaths
93
- * that already carry an extension and anything that fails to resolve keep the original
94
- * specifier, so the bundle never gets worse than before.
95
- */
96
- function rewriteLegacySubpathSpecifier(
97
- specifier: string,
98
- appRequire: NodeRequire,
99
- onRewrite: (manifestPath: string) => void,
100
- ): string {
101
- if (isBuiltin(specifier)) return specifier
102
- const parts = splitBareSpecifier(specifier)
103
- if (!parts || path.posix.extname(parts.subpath)) return specifier
104
- try {
105
- const manifestPath = appRequire.resolve(`${parts.packageName}/package.json`)
106
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as { exports?: unknown }
107
- if (manifest.exports != null) return specifier
108
- const resolvedFile = appRequire.resolve(specifier)
109
- const relativeFile = path.relative(path.dirname(manifestPath), resolvedFile)
110
- if (!relativeFile || relativeFile.startsWith('..') || path.isAbsolute(relativeFile)) return specifier
111
- onRewrite(manifestPath)
112
- return `${parts.packageName}/${relativeFile.split(path.sep).join('/')}`
113
- } catch {
114
- return specifier
115
- }
116
- }
117
-
118
80
  /**
119
81
  * esbuild plugins for the CLI bundle, in resolution order. The client-only stub must come
120
82
  * first so it wins over the alias and external plugins for `*.client` dynamic imports.
@@ -123,10 +85,7 @@ function rewriteLegacySubpathSpecifier(
123
85
  * `createClientOnlyStubPlugin` in isolation stays green if the plugin is dropped from this
124
86
  * list, which would silently reintroduce #4623.
125
87
  */
126
- export function createCliBundlePlugins(
127
- appRoot: string,
128
- options: { onLegacySubpathRewrite?: (manifestPath: string) => void } = {},
129
- ): import('esbuild').Plugin[] {
88
+ export function createCliBundlePlugins(appRoot: string): import('esbuild').Plugin[] {
130
89
  // Plugin to resolve the @/ alias the way the app tsconfig maps it:
131
90
  // `@/.mercato/*` to the app root, every other `@/*` to the app's src/ directory.
132
91
  const aliasPlugin: import('esbuild').Plugin = {
@@ -154,19 +113,6 @@ export function createCliBundlePlugins(
154
113
  },
155
114
  }
156
115
 
157
- // Node's ESM resolver only maps an extensionless package subpath (`next/link`) when the
158
- // package publishes an `exports` map; legacy packages need the real file path instead.
159
- const appRequire = createRequire(path.join(appRoot, 'package.json'))
160
- const externalSpecifierCache = new Map<string, string>()
161
- const onLegacySubpathRewrite = options.onLegacySubpathRewrite ?? (() => undefined)
162
- const resolveExternalSpecifier = (specifier: string): string => {
163
- const cached = externalSpecifierCache.get(specifier)
164
- if (cached !== undefined) return cached
165
- const resolved = rewriteLegacySubpathSpecifier(specifier, appRequire, onLegacySubpathRewrite)
166
- externalSpecifierCache.set(specifier, resolved)
167
- return resolved
168
- }
169
-
170
116
  // Plugin to mark non-JSON package imports as external
171
117
  const externalNonJsonPlugin: import('esbuild').Plugin = {
172
118
  name: 'external-non-json',
@@ -183,7 +129,7 @@ export function createCliBundlePlugins(
183
129
  return null // Let esbuild handle it
184
130
  }
185
131
  // Otherwise mark as external
186
- return { path: resolveExternalSpecifier(args.path), external: true }
132
+ return { path: args.path, external: true }
187
133
  })
188
134
  },
189
135
  }
@@ -191,7 +137,7 @@ export function createCliBundlePlugins(
191
137
  return [createClientOnlyStubPlugin(), aliasPlugin, externalNonJsonPlugin]
192
138
  }
193
139
 
194
- const DYNAMIC_LOADER_CACHE_VERSION = 5
140
+ const DYNAMIC_LOADER_CACHE_VERSION = 4
195
141
 
196
142
  type DynamicLoaderCacheMetadata = {
197
143
  version: number
@@ -487,7 +433,6 @@ async function compileAppSourceFileWithActiveEsbuild(
487
433
  // Dynamically import esbuild only when needed
488
434
  const esbuild = await getEsbuildRuntime()
489
435
 
490
- const rewrittenPackageManifests = new Set<string>()
491
436
  // Use esbuild.build with bundling to handle JSON imports
492
437
  const result = await esbuild.build({
493
438
  entryPoints: [tsPath],
@@ -499,9 +444,7 @@ async function compileAppSourceFileWithActiveEsbuild(
499
444
  platform: 'node',
500
445
  target: 'node18',
501
446
  tsconfig: appTsconfig,
502
- plugins: createCliBundlePlugins(appRoot, {
503
- onLegacySubpathRewrite: (manifestPath) => rewrittenPackageManifests.add(manifestPath),
504
- }),
447
+ plugins: createCliBundlePlugins(appRoot),
505
448
  // Allow JSON imports
506
449
  loader: { '.json': 'json' },
507
450
  })
@@ -512,7 +455,6 @@ async function compileAppSourceFileWithActiveEsbuild(
512
455
  dependencies: {
513
456
  ...collectDependencyHashes(appRoot, result.metafile.inputs),
514
457
  ...hashFilesRelativeTo(appRoot, tsconfigPaths),
515
- ...hashFilesRelativeTo(appRoot, [...rewrittenPackageManifests]),
516
458
  },
517
459
  }
518
460
  fs.writeFileSync(metadataPath, JSON.stringify(metadata))
@@ -143,14 +143,9 @@ async function registerWidgetsAndOptionalPackages(data: BootstrapData, options:
143
143
  // browser-facing UI registries. API-only bootstraps avoid loading @open-mercato/ui.
144
144
  try {
145
145
  const coreInjection = await import('@open-mercato/core/modules/widgets/lib/injection')
146
- // A partitioned runtime still needs its own entries — server-side resolvers read this
147
- // registry on request paths — it just must not shrink what a fuller partition already
148
- // published in the same process. Merge does both; skipping registration altogether left
149
- // API-only processes with an unregistered registry that throws on first read.
150
- coreInjection.registerCoreInjectionWidgets(
151
- data.injectionWidgetEntries,
152
- options.skipCoreInjectionWidgets ? { mode: 'merge' } : undefined,
153
- )
146
+ if (!options.skipCoreInjectionWidgets) {
147
+ coreInjection.registerCoreInjectionWidgets(data.injectionWidgetEntries)
148
+ }
154
149
  coreInjection.registerCoreInjectionTables(data.injectionTables, data.injectionWidgetEntries)
155
150
  coreInjection.registerEnabledModuleIds(
156
151
  data.modules.map((module) => module.id).filter((id): id is string => typeof id === 'string' && id.length > 0),
@@ -73,11 +73,7 @@ export interface BootstrapOptions {
73
73
  registrationKey?: string
74
74
  /** Skip browser-facing dashboard/injection registries in API-only runtimes. */
75
75
  skipUiRegistries?: boolean
76
- /**
77
- * Keep an existing core injection-widget registry intact in partitioned runtimes: this
78
- * bootstrap's entries are merged in rather than published as the whole registry, so a
79
- * partition that registers second never shrinks what the first one already exposed.
80
- */
76
+ /** Keep an existing core injection-widget registry intact in partitioned runtimes. */
81
77
  skipCoreInjectionWidgets?: boolean
82
78
  onRegistrationComplete?: () => void
83
79
  appDiRegistrar?: AppDiRegistrar
@@ -3,5 +3,3 @@ export { matchesOne } from './matchesOne'
3
3
  export { matchesScope } from './matchesScope'
4
4
  export { unionScopes } from './unionScopes'
5
5
  export { intersectScopes } from './intersectScopes'
6
- export { canonicalizeEffectiveScope } from './canonicalizeEffectiveScope'
7
- export { hashEffectiveScope } from './hashEffectiveScope'
@@ -7,7 +7,7 @@ import {
7
7
  isCommandInterceptorError,
8
8
  } from '@open-mercato/shared/lib/commands'
9
9
  import { registerCommandInterceptors } from '@open-mercato/shared/lib/commands/command-interceptor-store'
10
- import type { CommandInterceptor, CommandInterceptorContext } from '@open-mercato/shared/lib/commands/command-interceptor'
10
+ import type { CommandInterceptor } from '@open-mercato/shared/lib/commands/command-interceptor'
11
11
 
12
12
  describe('CommandBus', () => {
13
13
  afterEach(() => {
@@ -296,57 +296,6 @@ describe('CommandBus', () => {
296
296
  )
297
297
  })
298
298
 
299
- it('defers opted-in before snapshots until after the transactional write guard', async () => {
300
- const calls: string[] = []
301
- const transactionalEm = { id: 'transaction-em' }
302
- const buildLogMock = jest.fn(() => ({
303
- actionLabel: 'Transaction snapshot',
304
- resourceKind: 'test',
305
- resourceId: 'transactional',
306
- }))
307
- const prepare = jest.fn(async (_input, ctx) => {
308
- calls.push('prepare')
309
- expect(ctx.transactionalEm).toBe(transactionalEm)
310
- return { before: { state: 'locked-before' } }
311
- })
312
-
313
- registerCommand({
314
- id: 'test.command.transactional-snapshot',
315
- prepare,
316
- prepareSnapshotInsideTransaction: true,
317
- execute: jest.fn(async (_input, ctx) => {
318
- calls.push('execute')
319
- await ctx.beforeTransactionalWrite?.(transactionalEm as never)
320
- calls.push('mutate')
321
- return { ok: true }
322
- }),
323
- buildLog: buildLogMock,
324
- })
325
-
326
- const logMock = jest.fn(async () => ({ id: 'transaction-log' }))
327
- const container = createContainer({ injectionMode: InjectionMode.CLASSIC })
328
- container.register({ actionLogService: asValue({ log: logMock }) })
329
- const bus = new CommandBus()
330
- const ctx = {
331
- container,
332
- auth: { sub: 'user-transaction', tenantId: 'tenant-transaction', orgId: null },
333
- organizationScope: null,
334
- selectedOrganizationId: null,
335
- organizationIds: null,
336
- beforeTransactionalWrite: jest.fn(async () => {
337
- calls.push('lock')
338
- }),
339
- }
340
-
341
- await bus.execute('test.command.transactional-snapshot', { input: {}, ctx })
342
-
343
- expect(calls).toEqual(['execute', 'lock', 'prepare', 'mutate'])
344
- expect(prepare).toHaveBeenCalledTimes(1)
345
- expect(buildLogMock).toHaveBeenCalledWith(expect.objectContaining({
346
- snapshots: { before: { state: 'locked-before' }, after: undefined },
347
- }))
348
- })
349
-
350
299
  it('loads a command file lazily before execution', async () => {
351
300
  const execute = jest.fn(async () => ({ ok: true }))
352
301
  registerCommandLoaders([
@@ -589,54 +538,6 @@ describe('CommandBus', () => {
589
538
  })
590
539
 
591
540
  describe('interceptor rejections', () => {
592
- it('passes the original request to beforeExecute for preflight version checks', async () => {
593
- registerCommand({ id: 'test.request-context', execute: jest.fn(async () => ({ ok: true })) })
594
- const beforeExecute = jest.fn(async (_input: unknown, context: { request?: Request | null }) => {
595
- expect(context.request?.headers.get('x-om-ext-optimistic-lock-expected-updated-at')).toBe('2026-09-29T11:00:00.000Z')
596
- return { ok: false, status: 422 }
597
- })
598
- registerCommandInterceptors([{ moduleId: 'test', interceptors: [{
599
- id: 'test.request-context-interceptor', targetCommand: 'test.request-context', beforeExecute,
600
- }] }])
601
- const request = new Request('http://localhost/test', { headers: { 'x-om-ext-optimistic-lock-expected-updated-at': '2026-09-29T11:00:00.000Z' } })
602
- const container = createContainer({ injectionMode: InjectionMode.CLASSIC })
603
- const bus = new CommandBus()
604
- await expect(bus.execute('test.request-context', { input: {}, ctx: {
605
- container,
606
- auth: { sub: 'user-1', tenantId: 'tenant-1', orgId: null },
607
- organizationScope: null,
608
- selectedOrganizationId: null,
609
- organizationIds: null,
610
- request,
611
- } })).rejects.toMatchObject({ status: 422 })
612
- expect(beforeExecute).toHaveBeenCalledTimes(1)
613
- })
614
-
615
- it('passes request and organization scope to both execution lifecycle hooks', async () => {
616
- registerCommand({ id: 'test.lifecycle-context', execute: async () => ({ ok: true }) })
617
- const request = new Request('http://localhost/test')
618
- const organizationScope = { selectedId: 'parent', filterIds: ['parent', 'child'], allowedIds: ['parent', 'child'], tenantId: 'tenant-1' }
619
- const beforeExecute = jest.fn(async (_input: unknown, context: CommandInterceptorContext) => {
620
- expect(context.request).toBe(request)
621
- expect(context.organizationScope).toBe(organizationScope)
622
- return { ok: true }
623
- })
624
- const afterExecute = jest.fn(async (_input: unknown, _result: unknown, context: CommandInterceptorContext) => {
625
- expect(context.request).toBe(request)
626
- expect(context.organizationScope).toBe(organizationScope)
627
- })
628
- registerCommandInterceptors([{ moduleId: 'test', interceptors: [{
629
- id: 'test.lifecycle-context-interceptor', targetCommand: 'test.lifecycle-context', beforeExecute, afterExecute,
630
- }] }])
631
- await new CommandBus().execute('test.lifecycle-context', { input: {}, ctx: {
632
- container: createContainer({ injectionMode: InjectionMode.CLASSIC }),
633
- auth: { sub: 'user-1', tenantId: 'tenant-1', orgId: 'parent' },
634
- organizationScope, selectedOrganizationId: 'parent', organizationIds: ['parent', 'child'], request,
635
- } })
636
- expect(beforeExecute).toHaveBeenCalledTimes(1)
637
- expect(afterExecute).toHaveBeenCalledTimes(1)
638
- })
639
-
640
541
  const blockingInterceptor = (result: Record<string, unknown>): CommandInterceptor => ({
641
542
  id: 'test.block',
642
543
  targetCommand: 'test.*',
@@ -297,8 +297,6 @@ export class CommandBus {
297
297
  auth: baseOptions.ctx.auth ?? null,
298
298
  selectedOrganizationId: baseOptions.ctx.selectedOrganizationId ?? baseOptions.ctx.auth?.orgId ?? null,
299
299
  container: baseOptions.ctx.container,
300
- organizationScope: baseOptions.ctx.organizationScope ?? null,
301
- request: baseOptions.ctx.request ?? null,
302
300
  }
303
301
  const beforeResult = await runCommandInterceptorsBefore(
304
302
  allInterceptors, commandId, baseOptions.input, interceptorCtx, resolvedUserFeatures,
@@ -340,40 +338,16 @@ export class CommandBus {
340
338
  }
341
339
 
342
340
  const executeCore = async (coreOptions: CommandExecutionOptions<TInput>) => {
343
- const snapshots: { before?: unknown } = {}
344
- let executionOptions = coreOptions
345
- if (handler.prepareSnapshotInsideTransaction && handler.prepare) {
346
- const prepare = handler.prepare
347
- const originalCtx = coreOptions.ctx
348
- const originalBeforeTransactionalWrite = originalCtx.beforeTransactionalWrite
349
- let prepared = false
350
- let transactionalCtx: CommandRuntimeContext
351
- transactionalCtx = {
352
- ...originalCtx,
353
- beforeTransactionalWrite: async (em, input) => {
354
- await originalBeforeTransactionalWrite?.(em, input)
355
- if (prepared) return
356
- Object.assign(
357
- snapshots,
358
- (await prepare(coreOptions.input, { ...transactionalCtx, transactionalEm: em })) ?? {},
359
- )
360
- prepared = true
361
- },
362
- }
363
- executionOptions = { ...coreOptions, ctx: transactionalCtx }
364
- } else {
365
- Object.assign(snapshots, await this.prepareSnapshots(handler, coreOptions))
366
- }
367
- effectiveOptions = executionOptions
368
- const sourceLog = executionOptions.redoLogEntry ?? null
341
+ const sourceLog = coreOptions.redoLogEntry ?? null
342
+ const snapshots = await this.prepareSnapshots(handler, coreOptions)
369
343
  const result =
370
344
  sourceLog && typeof handler.redo === 'function'
371
- ? await handler.redo({ input: executionOptions.input, ctx: executionOptions.ctx, logEntry: sourceLog })
372
- : await handler.execute(executionOptions.input, executionOptions.ctx)
373
- const afterSnapshot = await this.captureAfter(handler, executionOptions, result)
345
+ ? await handler.redo({ input: coreOptions.input, ctx: coreOptions.ctx, logEntry: sourceLog })
346
+ : await handler.execute(coreOptions.input, coreOptions.ctx)
347
+ const afterSnapshot = await this.captureAfter(handler, coreOptions, result)
374
348
  const snapshotsWithAfter = { ...snapshots, after: afterSnapshot }
375
- const logMeta = await this.buildLog(handler, executionOptions, result, snapshotsWithAfter)
376
- let mergedMeta = this.mergeMetadata(executionOptions.metadata, logMeta)
349
+ const logMeta = await this.buildLog(handler, coreOptions, result, snapshotsWithAfter)
350
+ let mergedMeta = this.mergeMetadata(coreOptions.metadata, logMeta)
377
351
  // Interceptors opt into audit-log enrichment with a reserved `logContext` key rather
378
352
  // than the generic `context` one, so the metadata an interceptor already passes to its
379
353
  // own afterExecute hook is never silently promoted into audit storage.
@@ -388,7 +362,7 @@ export class CommandBus {
388
362
  ...logContextRecord,
389
363
  }
390
364
  }
391
- const baseContext = asRecord(executionOptions.metadata?.context) ?? {}
365
+ const baseContext = asRecord(coreOptions.metadata?.context) ?? {}
392
366
  const logMetaContext = asRecord(logMeta?.context) ?? {}
393
367
  if (Object.keys(interceptorContextMerged).length > 0 || Object.keys(baseContext).length > 0 || Object.keys(logMetaContext).length > 0) {
394
368
  mergedMeta = mergedMeta ?? {}
@@ -402,7 +376,7 @@ export class CommandBus {
402
376
  if (undoable && mergedMeta?.replayable !== false) {
403
377
  mergedMeta = mergedMeta ?? {}
404
378
  if (!mergedMeta.undoToken) mergedMeta.undoToken = defaultUndoToken()
405
- if (mergedMeta.actorUserId === undefined) mergedMeta.actorUserId = executionOptions.ctx.auth?.sub ?? null
379
+ if (mergedMeta.actorUserId === undefined) mergedMeta.actorUserId = coreOptions.ctx.auth?.sub ?? null
406
380
  } else if (mergedMeta?.replayable === false) {
407
381
  mergedMeta.undoToken = null
408
382
  }
@@ -431,7 +405,7 @@ export class CommandBus {
431
405
  if (inferred) mergedMeta.changes = inferred
432
406
  }
433
407
  }
434
- const logEntry = await this.persistLog(commandId, executionOptions, mergedMeta)
408
+ const logEntry = await this.persistLog(commandId, coreOptions, mergedMeta)
435
409
  return { result, mergedMeta, logEntry }
436
410
  }
437
411
 
@@ -500,8 +474,6 @@ export class CommandBus {
500
474
  auth: effectiveOptions.ctx.auth ?? null,
501
475
  selectedOrganizationId: effectiveOptions.ctx.selectedOrganizationId ?? effectiveOptions.ctx.auth?.orgId ?? null,
502
476
  container: effectiveOptions.ctx.container,
503
- organizationScope: effectiveOptions.ctx.organizationScope ?? null,
504
- request: effectiveOptions.ctx.request ?? null,
505
477
  }
506
478
  const afterResult = await runCommandInterceptorsAfter(
507
479
  allInterceptors, commandId, effectiveOptions.input, result, interceptorCtx,
@@ -573,8 +545,6 @@ export class CommandBus {
573
545
  auth: replayCtx.auth ?? null,
574
546
  selectedOrganizationId: replayCtx.selectedOrganizationId ?? replayCtx.auth?.orgId ?? null,
575
547
  container: replayCtx.container,
576
- organizationScope: replayCtx.organizationScope ?? null,
577
- request: replayCtx.request ?? null,
578
548
  }
579
549
  const beforeResult = await runCommandInterceptorsBeforeUndo(
580
550
  allInterceptors, log.commandId, undoCtx, interceptorCtx, resolvedUserFeatures,
@@ -689,8 +659,6 @@ export class CommandBus {
689
659
  auth: ctx.auth ?? null,
690
660
  selectedOrganizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
691
661
  container: ctx.container,
692
- organizationScope: ctx.organizationScope ?? null,
693
- request: ctx.request ?? null,
694
662
  }
695
663
  await runCommandInterceptorsAfterUndo(
696
664
  allInterceptors, log.commandId, undoCtx, interceptorCtx,
@@ -1,5 +1,4 @@
1
1
  import type { AwilixContainer } from 'awilix'
2
- import type { OrganizationScope } from '@open-mercato/shared/lib/auth/principal-service'
3
2
  import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
4
3
 
5
4
  export interface CommandInterceptor {
@@ -48,11 +47,8 @@ export interface CommandInterceptorContext {
48
47
  auth: AuthContext | null
49
48
  /** Selected organization ID */
50
49
  selectedOrganizationId: string | null
51
- organizationScope?: OrganizationScope | null
52
50
  /** DI container (read-only usage recommended) */
53
51
  container: AwilixContainer
54
- /** Original request, when the command was called through HTTP. */
55
- request?: Request | null
56
52
  /** Metadata passthrough from beforeExecute to afterExecute (or beforeUndo to afterUndo) */
57
53
  metadata?: Record<string, unknown>
58
54
  }
@@ -67,14 +67,6 @@ export type CommandRuntimeContext = {
67
67
  * surrounding work as a single atomic, single-locked operation.
68
68
  */
69
69
  transactionalEm?: EntityManager
70
- /**
71
- * Optional server-side guard that supported command handlers invoke after
72
- * opening their write transaction and before reading or mutating records.
73
- * The guard receives that same transaction-bound EntityManager and, when the
74
- * handler provides it, the final input after command interceptors. The input
75
- * stays optional so existing handlers and callers remain source-compatible.
76
- */
77
- beforeTransactionalWrite?: (em: EntityManager, input?: unknown) => Promise<void>
78
70
  /**
79
71
  * Identifies the owner of an already-active {@link transactionalEm} lifetime.
80
72
  * Atomic replay accepts an ambient EntityManager only when this is the exact
@@ -251,12 +243,6 @@ export interface CommandHandler<TInput = unknown, TResult = unknown> {
251
243
  */
252
244
  readonly outputSchema?: ZodTypeAny
253
245
  prepare?(input: TInput, ctx: CommandRuntimeContext): Promise<{ before?: unknown } | null> | { before?: unknown } | null
254
- /**
255
- * Opt in to taking the undo snapshot inside the write transaction. The bus
256
- * defers `prepare()` into `ctx.beforeTransactionalWrite`, after the handler's
257
- * transaction guard and before its first read or mutation.
258
- */
259
- readonly prepareSnapshotInsideTransaction?: boolean
260
246
  execute(input: TInput, ctx: CommandRuntimeContext): Promise<TResult> | TResult
261
247
  buildLog?(args: CommandLogBuilderArgs<TInput, TResult>): Promise<CommandLogMetadata | null | undefined> | CommandLogMetadata | null | undefined
262
248
  captureAfter?(input: TInput, result: TResult, ctx: CommandRuntimeContext): Promise<unknown> | unknown
@@ -28,22 +28,4 @@ describe('CRUD export serializers', () => {
28
28
  '"\'\t=HYPERLINK(""https://example.invalid"")","\'\r+1+1",\'-7',
29
29
  ])
30
30
  })
31
-
32
- it('escapes and neutralizes CSV header cells the same way as data cells', () => {
33
- const serialized = serializeExport({
34
- columns: [
35
- { field: 'company', header: 'Name, Inc.' },
36
- { field: 'formula', header: '=HEADER' },
37
- { field: 'plain', header: 'Balance' },
38
- ],
39
- rows: [
40
- { company: 'Acme', formula: 'ok', plain: 1 },
41
- ],
42
- }, 'csv')
43
-
44
- expect(serialized.body.split('\n')).toEqual([
45
- '"Name, Inc.",\'=HEADER,Balance',
46
- 'Acme,ok,1',
47
- ])
48
- })
49
31
  })