@open-mercato/shared 0.6.7-develop.6860.1.f796b22e80 → 0.6.7

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 (199) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +6 -57
  3. package/build.mjs +4 -2
  4. package/dist/lib/auth/server.js +2 -15
  5. package/dist/lib/auth/server.js.map +2 -2
  6. package/dist/lib/bootstrap/dynamicLoader.js +43 -117
  7. package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
  8. package/dist/lib/commands/command-bus.js +1 -5
  9. package/dist/lib/commands/command-bus.js.map +2 -2
  10. package/dist/lib/commands/command-interceptor-runner.js +2 -2
  11. package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
  12. package/dist/lib/crud/enricher-runner.js +11 -2
  13. package/dist/lib/crud/enricher-runner.js.map +2 -2
  14. package/dist/lib/crud/factory.js +4 -63
  15. package/dist/lib/crud/factory.js.map +2 -2
  16. package/dist/lib/crud/interceptor-runner.js +2 -2
  17. package/dist/lib/crud/interceptor-runner.js.map +2 -2
  18. package/dist/lib/crud/mutation-guard-registry.js +2 -2
  19. package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
  20. package/dist/lib/crud/types.js +0 -4
  21. package/dist/lib/crud/types.js.map +3 -3
  22. package/dist/lib/data/engine.js +11 -37
  23. package/dist/lib/data/engine.js.map +2 -2
  24. package/dist/lib/db/mikro.js +12 -3
  25. package/dist/lib/db/mikro.js.map +2 -2
  26. package/dist/lib/db/pg-errors.js +0 -42
  27. package/dist/lib/db/pg-errors.js.map +2 -2
  28. package/dist/lib/di/container.js +6 -48
  29. package/dist/lib/di/container.js.map +2 -2
  30. package/dist/lib/email/send.js +0 -56
  31. package/dist/lib/email/send.js.map +2 -2
  32. package/dist/lib/encryption/subscriber.js +10 -2
  33. package/dist/lib/encryption/subscriber.js.map +2 -2
  34. package/dist/lib/encryption/tenantDataEncryptionService.js +17 -52
  35. package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
  36. package/dist/lib/i18n/config.js +1 -1
  37. package/dist/lib/i18n/config.js.map +2 -2
  38. package/dist/lib/i18n/server.js +4 -4
  39. package/dist/lib/i18n/server.js.map +3 -3
  40. package/dist/lib/logger/index.js +3 -47
  41. package/dist/lib/logger/index.js.map +3 -3
  42. package/dist/lib/modules/registry.js +1 -5
  43. package/dist/lib/modules/registry.js.map +2 -2
  44. package/dist/lib/query/engine.js +40 -54
  45. package/dist/lib/query/engine.js.map +2 -2
  46. package/dist/lib/query/types.js.map +1 -1
  47. package/dist/lib/redis/connection.js +2 -7
  48. package/dist/lib/redis/connection.js.map +2 -2
  49. package/dist/lib/search/config.js +2 -61
  50. package/dist/lib/search/config.js.map +2 -2
  51. package/dist/lib/search/tokenize.js +11 -23
  52. package/dist/lib/search/tokenize.js.map +2 -2
  53. package/dist/lib/version.js +1 -1
  54. package/dist/lib/version.js.map +1 -1
  55. package/dist/lib/webhooks/index.js +1 -13
  56. package/dist/lib/webhooks/index.js.map +2 -2
  57. package/dist/lib/webhooks/verify.js +7 -12
  58. package/dist/lib/webhooks/verify.js.map +2 -2
  59. package/dist/modules/analytics.js.map +2 -2
  60. package/dist/modules/overrides.js +1 -50
  61. package/dist/modules/overrides.js.map +2 -2
  62. package/dist/modules/payment_gateways/types.js +1 -8
  63. package/dist/modules/payment_gateways/types.js.map +2 -2
  64. package/dist/modules/registry.js +0 -1
  65. package/dist/modules/registry.js.map +2 -2
  66. package/dist/security/enabledModulesRegistry.js +11 -56
  67. package/dist/security/enabledModulesRegistry.js.map +2 -2
  68. package/jest.config.cjs +1 -2
  69. package/package.json +9 -19
  70. package/src/lib/ai/llm-provider.ts +0 -33
  71. package/src/lib/auth/__tests__/server.test.ts +0 -23
  72. package/src/lib/auth/server.ts +2 -23
  73. package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +8 -63
  74. package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +1 -21
  75. package/src/lib/bootstrap/dynamicLoader.ts +65 -213
  76. package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +0 -28
  77. package/src/lib/commands/command-bus.ts +1 -5
  78. package/src/lib/commands/command-interceptor-runner.ts +2 -2
  79. package/src/lib/crud/__tests__/crud-factory.test.ts +0 -298
  80. package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +0 -26
  81. package/src/lib/crud/enricher-runner.ts +11 -2
  82. package/src/lib/crud/factory.ts +1 -69
  83. package/src/lib/crud/interceptor-runner.ts +2 -2
  84. package/src/lib/crud/mutation-guard-registry.ts +2 -2
  85. package/src/lib/crud/types.ts +0 -3
  86. package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +3 -18
  87. package/src/lib/data/engine.ts +16 -50
  88. package/src/lib/db/__tests__/mikro.test.ts +25 -0
  89. package/src/lib/db/mikro.ts +38 -13
  90. package/src/lib/db/pg-errors.ts +0 -58
  91. package/src/lib/di/container.ts +8 -72
  92. package/src/lib/email/__tests__/send.test.ts +1 -37
  93. package/src/lib/email/send.ts +0 -79
  94. package/src/lib/encryption/__tests__/subscriber.test.ts +2 -54
  95. package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +0 -58
  96. package/src/lib/encryption/subscriber.ts +14 -2
  97. package/src/lib/encryption/tenantDataEncryptionService.ts +18 -64
  98. package/src/lib/i18n/config.ts +2 -2
  99. package/src/lib/i18n/server.ts +2 -6
  100. package/src/lib/logger/__tests__/logger.test.ts +0 -77
  101. package/src/lib/logger/index.ts +1 -59
  102. package/src/lib/modules/registry.ts +0 -10
  103. package/src/lib/query/__tests__/engine.test.ts +13 -65
  104. package/src/lib/query/engine.ts +58 -75
  105. package/src/lib/query/types.ts +5 -4
  106. package/src/lib/redis/__tests__/connection.test.ts +0 -16
  107. package/src/lib/redis/connection.ts +7 -10
  108. package/src/lib/search/__tests__/config.test.ts +0 -177
  109. package/src/lib/search/config.ts +8 -106
  110. package/src/lib/search/tokenize.ts +11 -33
  111. package/src/lib/webhooks/__tests__/verify.test.ts +1 -20
  112. package/src/lib/webhooks/index.ts +1 -19
  113. package/src/lib/webhooks/verify.ts +9 -17
  114. package/src/modules/__tests__/registry.test.ts +2 -10
  115. package/src/modules/analytics.ts +0 -7
  116. package/src/modules/customer-auth.ts +0 -1
  117. package/src/modules/encryption.ts +0 -3
  118. package/src/modules/navigation/backendChrome.ts +0 -15
  119. package/src/modules/overrides.ts +0 -103
  120. package/src/modules/payment_gateways/types.ts +0 -41
  121. package/src/modules/registry.ts +0 -3
  122. package/src/security/enabledModulesRegistry.ts +12 -64
  123. package/dist/lib/ai/safety-identifier.js +0 -32
  124. package/dist/lib/ai/safety-identifier.js.map +0 -7
  125. package/dist/lib/auth/organizationScope.js +0 -34
  126. package/dist/lib/auth/organizationScope.js.map +0 -7
  127. package/dist/lib/bootstrap/clientOnlyModules.js +0 -55
  128. package/dist/lib/bootstrap/clientOnlyModules.js.map +0 -7
  129. package/dist/lib/data/consistency.js +0 -19
  130. package/dist/lib/data/consistency.js.map +0 -7
  131. package/dist/lib/db/entityMetadata.js +0 -26
  132. package/dist/lib/db/entityMetadata.js.map +0 -7
  133. package/dist/lib/logger/extension.js +0 -29
  134. package/dist/lib/logger/extension.js.map +0 -7
  135. package/dist/lib/modules/surfaceFingerprint.js +0 -47
  136. package/dist/lib/modules/surfaceFingerprint.js.map +0 -7
  137. package/dist/lib/query/ciphertext-search-warning.js +0 -45
  138. package/dist/lib/query/ciphertext-search-warning.js.map +0 -7
  139. package/dist/lib/search/auto-indexing.js +0 -14
  140. package/dist/lib/search/auto-indexing.js.map +0 -7
  141. package/dist/lib/search/availability.js +0 -107
  142. package/dist/lib/search/availability.js.map +0 -7
  143. package/dist/lib/search/tokenLookup.js +0 -46
  144. package/dist/lib/search/tokenLookup.js.map +0 -7
  145. package/dist/lib/telemetry/runtime.js +0 -35
  146. package/dist/lib/telemetry/runtime.js.map +0 -7
  147. package/dist/lib/webhooks/body.js +0 -57
  148. package/dist/lib/webhooks/body.js.map +0 -7
  149. package/dist/lib/webhooks/inbound-types.js +0 -1
  150. package/dist/lib/webhooks/inbound-types.js.map +0 -7
  151. package/dist/modules/widgets/extension-points.js +0 -96
  152. package/dist/modules/widgets/extension-points.js.map +0 -7
  153. package/dist/security/featurePolicy.js +0 -62
  154. package/dist/security/featurePolicy.js.map +0 -7
  155. package/scripts/versionSource.cjs +0 -65
  156. package/src/lib/__tests__/versionSource.test.ts +0 -46
  157. package/src/lib/ai/__tests__/llm-provider-contract.test.ts +0 -59
  158. package/src/lib/ai/__tests__/safety-identifier.test.ts +0 -71
  159. package/src/lib/ai/safety-identifier.ts +0 -73
  160. package/src/lib/auth/__tests__/organizationScope.test.ts +0 -107
  161. package/src/lib/auth/organizationScope.ts +0 -65
  162. package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +0 -189
  163. package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +0 -98
  164. package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +0 -211
  165. package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +0 -192
  166. package/src/lib/bootstrap/clientOnlyModules.ts +0 -85
  167. package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +0 -171
  168. package/src/lib/data/__tests__/consistency.test.ts +0 -38
  169. package/src/lib/data/consistency.ts +0 -17
  170. package/src/lib/db/__tests__/entityMetadata.test.ts +0 -38
  171. package/src/lib/db/__tests__/pg-errors.test.ts +0 -48
  172. package/src/lib/db/entityMetadata.ts +0 -43
  173. package/src/lib/di/__tests__/container-app-di-absent.test.ts +0 -155
  174. package/src/lib/di/__tests__/container-app-di.test.ts +0 -173
  175. package/src/lib/di/__tests__/registrar-error-log.test.ts +0 -154
  176. package/src/lib/i18n/__tests__/config.test.ts +0 -8
  177. package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +0 -30
  178. package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +0 -29
  179. package/src/lib/logger/extension.ts +0 -59
  180. package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +0 -122
  181. package/src/lib/modules/surfaceFingerprint.ts +0 -87
  182. package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +0 -178
  183. package/src/lib/query/ciphertext-search-warning.ts +0 -95
  184. package/src/lib/search/__tests__/availability.test.ts +0 -211
  185. package/src/lib/search/__tests__/tokenLookup.test.ts +0 -206
  186. package/src/lib/search/__tests__/tokenize.test.ts +0 -49
  187. package/src/lib/search/auto-indexing.ts +0 -22
  188. package/src/lib/search/availability.ts +0 -232
  189. package/src/lib/search/tokenLookup.ts +0 -133
  190. package/src/lib/telemetry/runtime.ts +0 -72
  191. package/src/lib/webhooks/__tests__/body.test.ts +0 -95
  192. package/src/lib/webhooks/body.ts +0 -70
  193. package/src/lib/webhooks/inbound-types.ts +0 -125
  194. package/src/modules/__tests__/nav-group-order-override.test.ts +0 -183
  195. package/src/modules/payment_gateways/__tests__/types.test.ts +0 -37
  196. package/src/modules/widgets/__tests__/extension-points.test.ts +0 -101
  197. package/src/modules/widgets/extension-points.ts +0 -524
  198. package/src/security/__tests__/featurePolicy.test.ts +0 -166
  199. package/src/security/featurePolicy.ts +0 -89
@@ -10,7 +10,6 @@ import {
10
10
  registerOptimisticLockReaders,
11
11
  } from '@open-mercato/shared/lib/crud/optimistic-lock-store'
12
12
  import { loadCustomFieldDefinitionIndex } from '@open-mercato/shared/lib/crud/custom-fields'
13
- import { registerMutationGuards } from '@open-mercato/shared/lib/crud/mutation-guard-store'
14
13
  import { z } from 'zod'
15
14
 
16
15
  // Keep the real custom-field helpers but spy on the definition loader so we can
@@ -202,7 +201,6 @@ describe('CRUD Factory', () => {
202
201
  }
203
202
  crudMutationGuardService = null
204
203
  registerApiInterceptors([])
205
- registerMutationGuards([])
206
204
  })
207
205
 
208
206
  const querySchema = z.object({
@@ -545,7 +543,6 @@ describe('CRUD Factory', () => {
545
543
  const data = await res.json()
546
544
  expect(data.id).toBeDefined()
547
545
  // CF saved
548
- expect(mockDataEngine.setCustomFields).toHaveBeenCalledWith(expect.objectContaining({ notify: false }))
549
546
  expect(setRecordCustomFields).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ entityId: 'example.todo', values: { priority: 3 } }))
550
547
  // Event + indexer delegated to data engine
551
548
  expect(mockDataEngine.emitOrmEntityEvent).toHaveBeenCalledTimes(1)
@@ -572,7 +569,6 @@ describe('CRUD Factory', () => {
572
569
  await em.persist(created).flush()
573
570
  const res = await route.PUT(new Request('http://x/api/example/todos', { method: 'PUT', body: JSON.stringify({ id: created.id, title: 'X2', cf_priority: 5 }), headers: { 'content-type': 'application/json' } }))
574
571
  expect(res.status).toBe(200)
575
- expect(mockDataEngine.setCustomFields).toHaveBeenCalledWith(expect.objectContaining({ notify: false }))
576
572
  expect(setRecordCustomFields).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ values: { priority: 5 } }))
577
573
  expect(mockDataEngine.emitOrmEntityEvent).toHaveBeenCalledTimes(1)
578
574
  const updatedCall = mockDataEngine.emitOrmEntityEvent.mock.calls.at(0)
@@ -594,34 +590,6 @@ describe('CRUD Factory', () => {
594
590
  expect(mockDataEngine.emitOrmEntityEvent).not.toHaveBeenCalled()
595
591
  })
596
592
 
597
- it('returns a retryable 503 when a handler hits a transient DB connection failure', async () => {
598
- setRecordCustomFields.mockImplementationOnce(async () => {
599
- throw Object.assign(new Error('sorry, too many clients already'), { code: '53300' })
600
- })
601
- const res = await route.POST(new Request('http://x/api/example/todos', { method: 'POST', body: JSON.stringify({ title: 'Exhausted', is_done: true, cf_priority: 3 }), headers: { 'content-type': 'application/json' } }))
602
- expect(res.status).toBe(503)
603
- expect(res.headers.get('Retry-After')).toBe('2')
604
- // The failed write is still rolled back — no created event/index leaks out.
605
- expect(Object.values(db)).toHaveLength(0)
606
- expect(mockDataEngine.emitOrmEntityEvent).not.toHaveBeenCalled()
607
- })
608
-
609
- it('POST surfaces CRUD side-effect failures after custom field writes', async () => {
610
- mockDataEngine.emitOrmEntityEvent.mockImplementationOnce(async () => {
611
- throw new Error('index write failed')
612
- })
613
-
614
- const res = await route.POST(new Request('http://x/api/example/todos', {
615
- method: 'POST',
616
- body: JSON.stringify({ title: 'Indexed', is_done: true, cf_priority: 3 }),
617
- headers: { 'content-type': 'application/json' },
618
- }))
619
-
620
- expect(res.status).toBe(500)
621
- expect(mockDataEngine.setCustomFields).toHaveBeenCalledWith(expect.objectContaining({ notify: false }))
622
- expect(mockDataEngine.emitOrmEntityEvent).toHaveBeenCalledTimes(1)
623
- })
624
-
625
593
  it('PUT rolls back the entity update when the custom field write fails', async () => {
626
594
  const created = em.create(Todo, { title: 'Before', organizationId: defaultOrganizationId, tenantId: defaultTenantId }) as Rec
627
595
  created.id = '123e4567-e89b-12d3-a456-426614174003'
@@ -753,193 +721,6 @@ describe('CRUD Factory', () => {
753
721
  expect(mockDataEngine.emitOrmEntityEvent).not.toHaveBeenCalled()
754
722
  })
755
723
 
756
- it('POST command route runs mutation guards before executing the command', async () => {
757
- const guardValidate = jest.fn(async (_input: any) => ({ ok: false, status: 403, message: 'Blocked by test guard' }))
758
- registerMutationGuards([{ moduleId: 'example', guards: [{
759
- id: 'example.block-command-create',
760
- targetEntity: 'example.todo',
761
- operations: ['create'],
762
- validate: guardValidate,
763
- }] }])
764
- const commandRoute = makeCrudRoute({
765
- metadata: { POST: { requireAuth: true } },
766
- orm: { entity: Todo, idField: 'id', orgField: 'organizationId', tenantField: 'tenantId', softDeleteField: 'deletedAt' },
767
- indexer: { entityType: 'example.todo' },
768
- actions: {
769
- create: {
770
- commandId: 'example.todo.create',
771
- schema: createSchema,
772
- response: () => ({ ok: true }),
773
- },
774
- },
775
- })
776
-
777
- const res = await commandRoute.POST(new Request('http://x/api/example/todos/command', {
778
- method: 'POST',
779
- body: JSON.stringify({ title: 'A' }),
780
- headers: { 'content-type': 'application/json' },
781
- }))
782
-
783
- expect(res.status).toBe(403)
784
- expect(guardValidate).toHaveBeenCalledWith(expect.objectContaining({
785
- resourceKind: 'example.todo',
786
- resourceId: null,
787
- operation: 'create',
788
- mutationPayload: expect.objectContaining({ title: 'A' }),
789
- }))
790
- expect(commandBus.execute).not.toHaveBeenCalled()
791
- })
792
-
793
- it('POST command route merges guard modifiedPayload and runs afterSuccess with the command result id', async () => {
794
- commandBus.execute.mockResolvedValue({ result: { id: 'cmd-created-1' }, logEntry: { id: 'log-1' } })
795
- const guardAfterSuccess = jest.fn(async () => {})
796
- registerMutationGuards([{ moduleId: 'example', guards: [{
797
- id: 'example.rewrite-command-create',
798
- targetEntity: 'example.todo',
799
- operations: ['create'],
800
- validate: async (_input: any) => ({ ok: true, modifiedPayload: { title: 'FROM-GUARD' }, shouldRunAfterSuccess: true }),
801
- afterSuccess: guardAfterSuccess,
802
- }] }])
803
- const commandRoute = makeCrudRoute({
804
- metadata: { POST: { requireAuth: true } },
805
- orm: { entity: Todo, idField: 'id', orgField: 'organizationId', tenantField: 'tenantId', softDeleteField: 'deletedAt' },
806
- indexer: { entityType: 'example.todo' },
807
- actions: {
808
- create: {
809
- commandId: 'example.todo.create',
810
- schema: createSchema,
811
- response: () => ({ ok: true }),
812
- },
813
- },
814
- })
815
-
816
- const res = await commandRoute.POST(new Request('http://x/api/example/todos/command', {
817
- method: 'POST',
818
- body: JSON.stringify({ title: 'A' }),
819
- headers: { 'content-type': 'application/json' },
820
- }))
821
-
822
- expect(res.status).toBe(201)
823
- expect(commandBus.execute).toHaveBeenCalledWith('example.todo.create', expect.objectContaining({
824
- input: expect.objectContaining({ title: 'FROM-GUARD' }),
825
- }))
826
- expect(guardAfterSuccess).toHaveBeenCalledWith(expect.objectContaining({
827
- resourceId: 'cmd-created-1',
828
- operation: 'create',
829
- }))
830
- })
831
-
832
- it('POST command route falls back to the response payload id for guard afterSuccess', async () => {
833
- commandBus.execute.mockResolvedValue({ result: { lineId: 'line-42' }, logEntry: { id: 'log-1' } })
834
- const guardAfterSuccess = jest.fn(async () => {})
835
- registerMutationGuards([{ moduleId: 'example', guards: [{
836
- id: 'example.after-command-create',
837
- targetEntity: 'example.todo',
838
- operations: ['create'],
839
- validate: async (_input: any) => ({ ok: true, shouldRunAfterSuccess: true }),
840
- afterSuccess: guardAfterSuccess,
841
- }] }])
842
- const commandRoute = makeCrudRoute({
843
- metadata: { POST: { requireAuth: true } },
844
- orm: { entity: Todo, idField: 'id', orgField: 'organizationId', tenantField: 'tenantId', softDeleteField: 'deletedAt' },
845
- indexer: { entityType: 'example.todo' },
846
- actions: {
847
- create: {
848
- commandId: 'example.todo.create',
849
- schema: createSchema,
850
- response: ({ result }: any) => ({ id: result.lineId }),
851
- },
852
- },
853
- })
854
-
855
- const res = await commandRoute.POST(new Request('http://x/api/example/todos/command', {
856
- method: 'POST',
857
- body: JSON.stringify({ title: 'A' }),
858
- headers: { 'content-type': 'application/json' },
859
- }))
860
-
861
- expect(res.status).toBe(201)
862
- expect(guardAfterSuccess).toHaveBeenCalledWith(expect.objectContaining({
863
- resourceId: 'line-42',
864
- operation: 'create',
865
- }))
866
- })
867
-
868
- // Commands whose mapInput wraps the payload (e.g. `{ body }`) null the factory
869
- // candidateId and thereby OPT OUT of row-level mutation guards, leaving the
870
- // command-level optimistic-lock check as the sole guard — a documented contract
871
- // (apps/docs/docs/framework/data-integrity/concurrency-locking.mdx) that sales
872
- // line/adjustment routes rely on.
873
- it('PUT command route without a top-level id keeps the documented row-level guard opt-out', async () => {
874
- crudMutationGuardService = {
875
- validateMutation: jest.fn().mockResolvedValue({ ok: true, shouldRunAfterSuccess: false }),
876
- afterMutationSuccess: jest.fn().mockResolvedValue(undefined),
877
- }
878
- const guardValidate = jest.fn(async (_input: any) => ({ ok: false, status: 409, message: 'must not run' }))
879
- registerMutationGuards([{ moduleId: 'example', guards: [{
880
- id: 'example.idless-update-opt-out',
881
- targetEntity: 'example.todo',
882
- operations: ['update'],
883
- validate: guardValidate,
884
- }] }])
885
- const commandRoute = makeCrudRoute({
886
- metadata: { PUT: { requireAuth: true } },
887
- orm: { entity: Todo, idField: 'id', orgField: 'organizationId', tenantField: 'tenantId', softDeleteField: 'deletedAt' },
888
- indexer: { entityType: 'example.todo' },
889
- actions: {
890
- update: {
891
- commandId: 'example.todo.update',
892
- schema: z.object({ title: z.string() }),
893
- mapInput: ({ parsed }: any) => ({ body: parsed }),
894
- response: () => ({ ok: true }),
895
- },
896
- },
897
- })
898
-
899
- const res = await commandRoute.PUT(new Request('http://x/api/example/todos/command', {
900
- method: 'PUT',
901
- body: JSON.stringify({ title: 'nested id shape' }),
902
- headers: { 'content-type': 'application/json' },
903
- }))
904
-
905
- expect(res.status).toBe(200)
906
- expect(guardValidate).not.toHaveBeenCalled()
907
- expect(crudMutationGuardService.validateMutation).not.toHaveBeenCalled()
908
- expect(commandBus.execute).toHaveBeenCalledWith('example.todo.update', expect.anything())
909
- })
910
-
911
- it('DELETE command route without any id keeps the documented row-level guard opt-out', async () => {
912
- const guardValidate = jest.fn(async (_input: any) => ({ ok: false, status: 403, message: 'must not run' }))
913
- registerMutationGuards([{ moduleId: 'example', guards: [{
914
- id: 'example.idless-delete-opt-out',
915
- targetEntity: 'example.todo',
916
- operations: ['delete'],
917
- validate: guardValidate,
918
- }] }])
919
- const commandRoute = makeCrudRoute({
920
- metadata: { DELETE: { requireAuth: true } },
921
- orm: { entity: Todo, idField: 'id', orgField: 'organizationId', tenantField: 'tenantId', softDeleteField: 'deletedAt' },
922
- indexer: { entityType: 'example.todo' },
923
- actions: {
924
- delete: {
925
- commandId: 'example.todo.delete',
926
- schema: z.any(),
927
- response: () => ({ ok: true }),
928
- },
929
- },
930
- })
931
-
932
- const res = await commandRoute.DELETE(new Request('http://x/api/example/todos/command', {
933
- method: 'DELETE',
934
- body: JSON.stringify({}),
935
- headers: { 'content-type': 'application/json' },
936
- }))
937
-
938
- expect(res.status).toBe(200)
939
- expect(guardValidate).not.toHaveBeenCalled()
940
- expect(commandBus.execute).toHaveBeenCalledWith('example.todo.delete', expect.anything())
941
- })
942
-
943
724
  it('POST is blocked by interceptor before hook', async () => {
944
725
  registerApiInterceptors([
945
726
  {
@@ -1003,85 +784,6 @@ describe('CRUD Factory', () => {
1003
784
  const body = await res.json()
1004
785
  expect(body._interceptor).toEqual({ ok: true, count: 1 })
1005
786
  })
1006
-
1007
- // The command DELETE path used to hand before-interceptors the body only, so a
1008
- // guard reading `?id=` saw nothing to object to and the delete went through
1009
- // with a 200 (issue #4842).
1010
- it('DELETE command route passes the query id to interceptor before hooks', async () => {
1011
- const lockedId = '123e4567-e89b-12d3-a456-426614174010'
1012
- const seenQueries: Array<Record<string, unknown> | undefined> = []
1013
- registerApiInterceptors([
1014
- {
1015
- moduleId: 'example',
1016
- interceptors: [
1017
- {
1018
- id: 'example.block-locked-delete',
1019
- targetRoute: 'example/todos/command',
1020
- methods: ['DELETE'],
1021
- async before(request) {
1022
- seenQueries.push(request.query)
1023
- const queryId = request.query?.id
1024
- if (typeof queryId === 'string' && queryId === lockedId) {
1025
- return { ok: false, statusCode: 409, message: 'Record is locked' }
1026
- }
1027
- return { ok: true }
1028
- },
1029
- },
1030
- ],
1031
- },
1032
- ])
1033
- const commandRoute = makeCrudRoute({
1034
- metadata: { DELETE: { requireAuth: true } },
1035
- orm: { entity: Todo, idField: 'id', orgField: 'organizationId', tenantField: 'tenantId', softDeleteField: 'deletedAt' },
1036
- indexer: { entityType: 'example.todo' },
1037
- actions: {
1038
- delete: {
1039
- commandId: 'example.todo.delete',
1040
- schema: z.any(),
1041
- response: () => ({ ok: true }),
1042
- },
1043
- },
1044
- })
1045
-
1046
- const res = await commandRoute.DELETE(new Request(`http://x/api/example/todos/command?id=${lockedId}`, {
1047
- method: 'DELETE',
1048
- body: JSON.stringify({}),
1049
- headers: { 'content-type': 'application/json' },
1050
- }))
1051
-
1052
- expect(res.status).toBe(409)
1053
- expect(await res.json()).toMatchObject({ error: 'Record is locked' })
1054
- expect(seenQueries).toEqual([{ id: lockedId }])
1055
- expect(commandBus.execute).not.toHaveBeenCalled()
1056
- })
1057
-
1058
- it('DELETE route passes the whole query string to interceptor before hooks', async () => {
1059
- const created = em.create(Todo, { title: 'Z', organizationId: defaultOrganizationId, tenantId: defaultTenantId }) as Rec
1060
- created.id = '123e4567-e89b-12d3-a456-426614174011'
1061
- await em.persist(created).flush()
1062
- let seenQuery: Record<string, unknown> | undefined
1063
- registerApiInterceptors([
1064
- {
1065
- moduleId: 'example',
1066
- interceptors: [
1067
- {
1068
- id: 'example.capture-delete-query',
1069
- targetRoute: 'example/todos',
1070
- methods: ['DELETE'],
1071
- async before(request) {
1072
- seenQuery = request.query
1073
- return { ok: true }
1074
- },
1075
- },
1076
- ],
1077
- },
1078
- ])
1079
-
1080
- const res = await route.DELETE(new Request(`http://x/api/example/todos?id=${created.id}&reason=cleanup`, { method: 'DELETE' }))
1081
-
1082
- expect(res.status).toBe(200)
1083
- expect(seenQuery).toEqual({ id: created.id, reason: 'cleanup' })
1084
- })
1085
787
  })
1086
788
 
1087
789
  describe('CRUD Factory — optimistic-lock auto-registration', () => {
@@ -1,9 +1,5 @@
1
1
  import { matchesEntity, runMutationGuards } from '../mutation-guard-registry'
2
2
  import type { MutationGuard, MutationGuardInput } from '../mutation-guard-registry'
3
- import {
4
- applyAclFeatureOverrides,
5
- resetModuleContractOverridesForTests,
6
- } from '../../../modules/overrides'
7
3
 
8
4
  describe('matchesEntity', () => {
9
5
  it('matches wildcard "*" against any entity', () => {
@@ -28,10 +24,6 @@ describe('matchesEntity', () => {
28
24
  })
29
25
 
30
26
  describe('runMutationGuards', () => {
31
- afterEach(() => {
32
- resetModuleContractOverridesForTests()
33
- })
34
-
35
27
  const baseInput: MutationGuardInput = {
36
28
  tenantId: 'tenant-1',
37
29
  organizationId: 'org-1',
@@ -167,24 +159,6 @@ describe('runMutationGuards', () => {
167
159
  expect(resultWithWildcard.errorBody).toEqual({ error: 'Blocked by wildcard', guardId: 'g1' })
168
160
  })
169
161
 
170
- it('does not run a guard gated by a nulled ACL feature', async () => {
171
- applyAclFeatureOverrides({ 'premium.locks': null })
172
- const guard = makeGuard({
173
- id: 'g1',
174
- features: ['premium.locks'],
175
- validate: jest.fn().mockResolvedValue({ ok: false }),
176
- })
177
-
178
- const result = await runMutationGuards(
179
- [guard],
180
- baseInput,
181
- { userFeatures: ['*', 'premium.locks'] },
182
- )
183
-
184
- expect(result.ok).toBe(true)
185
- expect(guard.validate).not.toHaveBeenCalled()
186
- })
187
-
188
162
  it('uses custom error body when provided', async () => {
189
163
  const guard = makeGuard({
190
164
  id: 'g1',
@@ -15,7 +15,6 @@ import type {
15
15
  import { getEnrichersForEntity } from './enricher-registry'
16
16
  import { logEnricherTiming } from '../umes/enricher-timing'
17
17
  import { createLogger } from '../logger'
18
- import { authorizeFeatures } from '../../security/featurePolicy'
19
18
 
20
19
  const logger = createLogger('shared').child({ component: 'umes' })
21
20
 
@@ -36,7 +35,17 @@ function hasRequiredFeatures(
36
35
  ): boolean {
37
36
  if (!enricher.features || enricher.features.length === 0) return true
38
37
  if (!userFeatures) return false
39
- return authorizeFeatures(enricher.features, { grantedFeatures: userFeatures })
38
+ const hasFeature = (required: string): boolean => {
39
+ for (const granted of userFeatures) {
40
+ if (granted === '*' || granted === required) return true
41
+ if (granted.endsWith('.*')) {
42
+ const prefix = granted.slice(0, -1)
43
+ if (required.startsWith(prefix)) return true
44
+ }
45
+ }
46
+ return false
47
+ }
48
+ return enricher.features.every((feature) => hasFeature(feature))
40
49
  }
41
50
 
42
51
  function filterByACLAndTenant(
@@ -57,7 +57,6 @@ import {
57
57
  isCrudCacheEnabled,
58
58
  normalizeIdentifierValue,
59
59
  normalizeTagSegment,
60
- pickFirstIdentifier,
61
60
  resolveCrudCache,
62
61
  } from './cache'
63
62
  import { deriveCrudSegmentTag } from './cache-stats'
@@ -73,7 +72,6 @@ import { parseExtensionHeaders } from '../umes/extension-headers'
73
72
  import { createGenericOptimisticLockReader } from './optimistic-lock'
74
73
  import { registerOptimisticLockReaderIfAbsent } from './optimistic-lock-store'
75
74
  import { createLogger } from '../logger'
76
- import { isTransientDbError } from '../db/pg-errors'
77
75
 
78
76
  type RbacServiceLike = {
79
77
  getGrantedFeatures: (userId: string, opts: { tenantId: string | null; organizationId: string | null }) => Promise<string[]>
@@ -556,18 +554,6 @@ function handleError(err: unknown): Response {
556
554
  if (err instanceof Response) return err
557
555
  if (isCrudHttpError(err)) return json(err.body, { status: err.status })
558
556
  if (err instanceof z.ZodError) return json({ error: 'Invalid input', details: err.issues }, { status: 400 })
559
- if (isTransientDbError(err)) {
560
- // Transient DB unavailability (pool exhausted, `max_connections` reached, DB
561
- // restarting) is retryable — surface a 503 with a Retry-After hint instead of
562
- // a generic 500 so clients back off and retry once the DB recovers.
563
- logger.warn('Transient DB failure during CRUD handler', {
564
- message: err instanceof Error ? err.message : undefined,
565
- })
566
- return json(
567
- { error: 'Service temporarily unavailable' },
568
- { status: 503, headers: { 'Retry-After': '2' } },
569
- )
570
- }
571
557
 
572
558
  const message = err instanceof Error ? err.message : undefined
573
559
  const stack = err instanceof Error ? err.stack : undefined
@@ -929,12 +915,6 @@ function buildCrudCacheKey(
929
915
  `tenant:${normalizeTagSegment(ctx.auth?.tenantId ?? null)}`,
930
916
  `selectedOrg:${normalizeTagSegment(ctx.selectedOrganizationId ?? null)}`,
931
917
  `scope:${scopeSegment}`,
932
- // List payloads can vary per caller identity beyond tenant/org scope:
933
- // buildFilters may narrow by ctx.auth (e.g. ?mine=true), before-interceptor
934
- // query rewrites are feature-gated per user, and afterList/after-interceptor
935
- // output is embedded in the stored payload — so entries MUST be partitioned
936
- // per actor (API key or user), never shared across identities.
937
- `user:${normalizeTagSegment((ctx.auth?.keyId ?? ctx.auth?.sub) ?? null)}`,
938
918
  `query:${serializeSearchParams(url.searchParams)}`,
939
919
  ]
940
920
  // The cached list payload already embeds enricher output (enrichment runs before
@@ -2160,31 +2140,6 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
2160
2140
  }
2161
2141
  }
2162
2142
 
2163
- // Mutation guard registry — command path (mirrors the direct create branch)
2164
- const createCmdUserFeatures = await resolveUserFeatures(ctx)
2165
- const { allGuards: createCmdAllGuards } = collectAndRunGuards(ctx.container)
2166
- let createCmdGuardAfterCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }> = []
2167
- if (createCmdAllGuards.length && ctx.auth.tenantId) {
2168
- const guardResult = await runMutationGuards(createCmdAllGuards, {
2169
- tenantId: ctx.auth.tenantId,
2170
- organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null,
2171
- userId: ctx.auth.sub,
2172
- resourceKind,
2173
- resourceId: null,
2174
- operation: 'create',
2175
- requestMethod: request.method,
2176
- requestHeaders: request.headers,
2177
- mutationPayload: input && typeof input === 'object' ? (input as Record<string, unknown>) : null,
2178
- }, { userFeatures: createCmdUserFeatures ?? [] })
2179
- if (!guardResult.ok) {
2180
- return json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })
2181
- }
2182
- if (guardResult.modifiedPayload && typeof input === 'object' && input) {
2183
- input = { ...input as Record<string, unknown>, ...guardResult.modifiedPayload }
2184
- }
2185
- createCmdGuardAfterCallbacks = guardResult.afterSuccessCallbacks
2186
- }
2187
-
2188
2143
  const baseMetadata: CommandLogMetadata = {
2189
2144
  tenantId: ctx.auth?.tenantId ?? null,
2190
2145
  organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null,
@@ -2227,22 +2182,6 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
2227
2182
  const status = action.status ?? 201
2228
2183
  const response = json(resolvedPayload, { status })
2229
2184
  attachOperationHeader(response, logEntry)
2230
- const commandResultId = pickFirstIdentifier(
2231
- (result as Record<string, unknown> | null | undefined)?.id,
2232
- (resolvedPayload as Record<string, unknown> | null | undefined)?.id,
2233
- )
2234
- if (createCmdGuardAfterCallbacks.length && ctx.auth.tenantId && commandResultId) {
2235
- await runGuardAfterSuccessCallbacks(createCmdGuardAfterCallbacks, {
2236
- tenantId: ctx.auth.tenantId,
2237
- organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null,
2238
- userId: ctx.auth.sub,
2239
- resourceKind,
2240
- resourceId: commandResultId,
2241
- operation: 'create',
2242
- requestMethod: request.method,
2243
- requestHeaders: request.headers,
2244
- })
2245
- }
2246
2185
  // Note: side effects (events + indexing) are already flushed by CommandBus.execute()
2247
2186
  // via flushCrudSideEffects(). Calling markCommandResultForIndexing here would cause
2248
2187
  // duplicate event emissions.
@@ -2346,7 +2285,6 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
2346
2285
  organizationId: targetOrgId,
2347
2286
  tenantId: writeTenantId,
2348
2287
  values,
2349
- notify: false,
2350
2288
  })
2351
2289
  }
2352
2290
  }
@@ -2484,10 +2422,6 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
2484
2422
  const updateUserFeatures = await resolveUserFeatures(ctx)
2485
2423
  const { allGuards: updateAllGuards } = collectAndRunGuards(ctx.container)
2486
2424
  let cmdUpdateGuardAfterCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }> = []
2487
- // Commands whose mapInput wraps the payload (e.g. `{ body }`) intentionally
2488
- // null candidateId and OPT OUT of row-level guards, leaving the command-level
2489
- // optimistic-lock check as the sole guard — a documented contract, see
2490
- // apps/docs/docs/framework/data-integrity/concurrency-locking.mdx.
2491
2425
  if (updateAllGuards.length && ctx.auth.tenantId && candidateId) {
2492
2426
  const guardResult = await runMutationGuards(updateAllGuards, {
2493
2427
  tenantId: ctx.auth.tenantId,
@@ -2684,7 +2618,6 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
2684
2618
  organizationId: targetOrgId,
2685
2619
  tenantId: writeTenantId,
2686
2620
  values,
2687
- notify: false,
2688
2621
  })
2689
2622
  }
2690
2623
  }
@@ -2788,7 +2721,6 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
2788
2721
  request,
2789
2722
  method: 'DELETE',
2790
2723
  body: interceptorInput,
2791
- query: raw.query,
2792
2724
  })
2793
2725
  if (beforeInterceptors.errorResponse) return beforeInterceptors.errorResponse
2794
2726
  interceptorRequestPayload = beforeInterceptors.requestPayload
@@ -2913,7 +2845,7 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
2913
2845
  request,
2914
2846
  method: 'DELETE',
2915
2847
  body: idFrom === 'query' ? undefined : ({ id } as Record<string, unknown>),
2916
- query: idFrom === 'query' ? Object.fromEntries(url.searchParams.entries()) : undefined,
2848
+ query: idFrom === 'query' ? ({ id } as Record<string, unknown>) : undefined,
2917
2849
  })
2918
2850
  if (beforeInterceptors.errorResponse) return beforeInterceptors.errorResponse
2919
2851
  interceptorRequestPayload = beforeInterceptors.requestPayload
@@ -6,7 +6,7 @@ import type {
6
6
  InterceptorBeforeResult,
7
7
  } from './api-interceptor'
8
8
  import { getApiInterceptorsForRoute } from './interceptor-registry'
9
- import { authorizeFeatures } from '../../security/featurePolicy'
9
+ import { hasAllFeatures } from '../../security/features'
10
10
  import { logInterceptorActivity } from '../umes/interceptor-activity'
11
11
 
12
12
  const DEFAULT_TIMEOUT_MS = 5000
@@ -39,7 +39,7 @@ function sanitizeObject(input?: Record<string, unknown>): Record<string, unknown
39
39
  }
40
40
 
41
41
  function hasRequiredFeatures(features: string[] | undefined, userFeatures: string[] | undefined): boolean {
42
- return authorizeFeatures(features ?? [], { grantedFeatures: userFeatures ?? [] })
42
+ return hasAllFeatures(userFeatures, features)
43
43
  }
44
44
 
45
45
  function timeoutPromise(ms: number, interceptorId: string): Promise<never> {
@@ -1,5 +1,5 @@
1
1
  import type { AwilixContainer } from 'awilix'
2
- import { authorizeFeatures } from '../../security/featurePolicy'
2
+ import { hasAllFeatures } from '../../security/features'
3
3
  import { resolveCrudMutationGuardService } from './mutation-guard-service'
4
4
 
5
5
  // ---------------------------------------------------------------------------
@@ -101,7 +101,7 @@ export async function runMutationGuards(
101
101
  const matching = guards
102
102
  .filter((g) => matchesEntity(g.targetEntity, input.resourceKind))
103
103
  .filter((g) => g.operations.includes(input.operation))
104
- .filter((g) => authorizeFeatures(g.features ?? [], { grantedFeatures: context.userFeatures }))
104
+ .filter((g) => hasAllFeatures(context.userFeatures, g.features))
105
105
  .sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50))
106
106
 
107
107
  let payload = input.mutationPayload
@@ -1,8 +1,5 @@
1
1
  export type CrudEventAction = 'created' | 'updated' | 'deleted'
2
2
 
3
- /** Internal payload marker: the data engine owns this CRUD event's query-index decision. */
4
- export const CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY = '__omQueryIndexManaged' as const
5
-
6
3
  export type CrudEntityIdentifiers = {
7
4
  id: string
8
5
  organizationId: string | null
@@ -1,11 +1,7 @@
1
1
  import type { AwilixContainer } from 'awilix'
2
2
  import type { EntityManager } from '@mikro-orm/postgresql'
3
3
  import { DefaultDataEngine } from '../engine'
4
- import {
5
- CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY,
6
- type CrudEventsConfig,
7
- type CrudIndexerConfig,
8
- } from '../../crud/types'
4
+ import type { CrudEventsConfig, CrudIndexerConfig } from '../../crud/types'
9
5
 
10
6
  // The bulk-import deferral (`suppress`) must gate the two per-record side effects `emitOrmEntityEvent`
11
7
  // fans out — the `<module>.<entity>.<action>` domain event and the inline `query_index.upsert_one`
@@ -29,15 +25,6 @@ function buildEngine() {
29
25
  return { engine, emitEvent, emittedNames }
30
26
  }
31
27
 
32
- function expectManagedDomainPayload(emitEvent: jest.Mock, eventName: string): void {
33
- const call = emitEvent.mock.calls.find(([name]) => name === eventName)
34
- expect(call).toBeDefined()
35
- const payload = call?.[1] as Record<string, unknown>
36
- expect(payload[CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY]).toBe(true)
37
- expect(Object.keys(payload)).not.toContain(CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY)
38
- expect(JSON.stringify(payload)).not.toContain(CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY)
39
- }
40
-
41
28
  describe('DefaultDataEngine bulk-import suppression', () => {
42
29
  // The test events are intentionally not registered in the event registry; silence the
43
30
  // one-time "undeclared event" warning so it doesn't clutter the suite output.
@@ -46,10 +33,9 @@ describe('DefaultDataEngine bulk-import suppression', () => {
46
33
  afterAll(() => { warnSpy.mockRestore() })
47
34
 
48
35
  it('emits both the domain event and the reindex when unsuppressed', async () => {
49
- const { engine, emitEvent, emittedNames } = buildEngine()
36
+ const { engine, emittedNames } = buildEngine()
50
37
  await engine.emitOrmEntityEvent({ action: 'created', entity: {}, events: EVENTS, indexer: INDEXER, identifiers: IDENTIFIERS })
51
38
  expect(emittedNames()).toEqual(expect.arrayContaining(['sales.order.created', 'query_index.upsert_one']))
52
- expectManagedDomainPayload(emitEvent, 'sales.order.created')
53
39
  })
54
40
 
55
41
  it('skips the domain event but keeps the reindex with skipEvents', async () => {
@@ -61,12 +47,11 @@ describe('DefaultDataEngine bulk-import suppression', () => {
61
47
  })
62
48
 
63
49
  it('skips the reindex but keeps the domain event with skipReindex', async () => {
64
- const { engine, emitEvent, emittedNames } = buildEngine()
50
+ const { engine, emittedNames } = buildEngine()
65
51
  await engine.emitOrmEntityEvent({ action: 'created', entity: {}, events: EVENTS, indexer: INDEXER, identifiers: IDENTIFIERS, suppress: { skipReindex: true } })
66
52
  const names = emittedNames()
67
53
  expect(names).toContain('sales.order.created')
68
54
  expect(names).not.toContain('query_index.upsert_one')
69
- expectManagedDomainPayload(emitEvent, 'sales.order.created')
70
55
  })
71
56
 
72
57
  it('emits nothing when both are suppressed', async () => {