@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
@@ -1,524 +0,0 @@
1
- export type ExtensionHostFamily =
2
- | 'generic'
3
- | 'menu'
4
- | 'data-table'
5
- | 'crud-form'
6
- | 'detail'
7
- | 'portal-page'
8
- | 'component-handle'
9
- | 'entity'
10
- | 'api-route'
11
- | 'command'
12
- | 'event'
13
- | 'query-lifecycle'
14
- | 'dashboard'
15
- | 'notification'
16
- | 'integration'
17
- | 'specialized-registry'
18
- | 'module-override'
19
-
20
- export type ExtensionHostCapability =
21
- | 'render-widget'
22
- | 'headless-widget'
23
- | 'menu-item'
24
- | 'column-widget'
25
- | 'row-action'
26
- | 'bulk-action'
27
- | 'filter-widget'
28
- | 'toolbar-widget'
29
- | 'field-widget'
30
- | 'lifecycle-handler'
31
- | 'component-replacement'
32
- | 'response-enricher'
33
- | 'query-enricher'
34
- | 'api-interceptor'
35
- | 'command-interceptor'
36
- | 'mutation-guard'
37
- | 'entity-extension'
38
- | 'async-subscriber'
39
- | 'sync-subscriber'
40
- | 'browser-client'
41
- | 'browser-portal'
42
- | 'registry-contribution'
43
- | 'module-override'
44
-
45
- export type ExtensionHostActivation = 'always' | 'host-opt-in' | 'caller-opt-in' | 'feature-gated'
46
-
47
- export type ExtensionPointPatternParameter = {
48
- source: string
49
- pattern?: string
50
- }
51
-
52
- type ExtensionHostDeclarationBase = {
53
- source: string
54
- contextContract?: string
55
- dataContract?: string
56
- scopeContract?: string
57
- runtimeContract?: string
58
- activation?: ExtensionHostActivation
59
- aliases?: readonly string[]
60
- fallbacks?: readonly string[]
61
- }
62
-
63
- type InjectionExtensionHostDeclarationBase = ExtensionHostDeclarationBase & {
64
- family: Exclude<ExtensionHostFamily, 'data-table' | 'crud-form' | 'component-handle'>
65
- supported: readonly ExtensionHostCapability[]
66
- }
67
-
68
- export type InjectionExtensionHostDeclaration = InjectionExtensionHostDeclarationBase & (
69
- | {
70
- spotId: string
71
- pattern?: never
72
- parameters?: never
73
- }
74
- | {
75
- spotId?: never
76
- pattern: string
77
- parameters: Readonly<Record<string, ExtensionPointPatternParameter>>
78
- }
79
- )
80
-
81
- export type DataTableExtensionHostDeclaration = ExtensionHostDeclarationBase & {
82
- family: 'data-table'
83
- tableId: string
84
- baseSpotId?: string
85
- }
86
-
87
- export type CrudFormExtensionHostDeclaration = ExtensionHostDeclarationBase & {
88
- family: 'crud-form'
89
- entityId: string
90
- spotId?: string
91
- }
92
-
93
- export type ComponentExtensionHostDeclaration = ExtensionHostDeclarationBase & {
94
- family: 'component-handle'
95
- componentId: string
96
- propsContract?: string
97
- }
98
-
99
- export type ModuleExtensionHostDeclaration =
100
- | InjectionExtensionHostDeclaration
101
- | DataTableExtensionHostDeclaration
102
- | CrudFormExtensionHostDeclaration
103
- | ComponentExtensionHostDeclaration
104
-
105
- export type ModuleExtensionPoints<
106
- TModuleId extends string = string,
107
- THosts extends Readonly<Record<string, ModuleExtensionHostDeclaration>> = Readonly<Record<string, ModuleExtensionHostDeclaration>>,
108
- > = {
109
- moduleId: TModuleId
110
- hosts: THosts
111
- }
112
-
113
- export function defineModuleExtensionPoints<
114
- const TModuleId extends string,
115
- const THosts extends Readonly<Record<string, ModuleExtensionHostDeclaration>>,
116
- >(declaration: ModuleExtensionPoints<TModuleId, THosts>): ModuleExtensionPoints<TModuleId, THosts> {
117
- return Object.freeze({
118
- ...declaration,
119
- hosts: Object.freeze({ ...declaration.hosts }),
120
- })
121
- }
122
-
123
- export function injectionExtensionHost<const TDeclaration extends InjectionExtensionHostDeclaration>(
124
- declaration: TDeclaration,
125
- ): Readonly<TDeclaration> {
126
- const hasExactId = typeof declaration.spotId === 'string' && declaration.spotId.length > 0
127
- const hasPattern = typeof declaration.pattern === 'string' && declaration.pattern.length > 0
128
- if (hasExactId === hasPattern) {
129
- throw new Error('[internal] injection extension hosts require exactly one of spotId or pattern')
130
- }
131
- if (hasPattern && (!declaration.parameters || Object.keys(declaration.parameters).length === 0)) {
132
- throw new Error('[internal] patterned injection extension hosts require named parameters')
133
- }
134
- return Object.freeze({ ...declaration })
135
- }
136
-
137
- export function dataTableExtensionHost<
138
- const TDeclaration extends Omit<DataTableExtensionHostDeclaration, 'family'>,
139
- >(
140
- declaration: TDeclaration,
141
- ): Readonly<TDeclaration & { family: 'data-table' }> {
142
- return Object.freeze({ family: 'data-table', ...declaration })
143
- }
144
-
145
- export function crudFormExtensionHost<
146
- const TDeclaration extends Omit<CrudFormExtensionHostDeclaration, 'family'>,
147
- >(
148
- declaration: TDeclaration,
149
- ): Readonly<TDeclaration & { family: 'crud-form' }> {
150
- return Object.freeze({ family: 'crud-form', ...declaration })
151
- }
152
-
153
- export function componentExtensionHost<
154
- const TDeclaration extends Omit<ComponentExtensionHostDeclaration, 'family'>,
155
- >(
156
- declaration: TDeclaration,
157
- ): Readonly<TDeclaration & { family: 'component-handle' }> {
158
- return Object.freeze({ family: 'component-handle', ...declaration })
159
- }
160
-
161
- export type BoundExtensionSurface = {
162
- key: string
163
- suffix: string | null
164
- capabilities: readonly ExtensionHostCapability[]
165
- bound: boolean
166
- phases?: readonly string[]
167
- operations?: readonly string[]
168
- }
169
-
170
- export const DATA_TABLE_EXTENSION_SURFACES = [
171
- { key: 'header', suffix: 'header', capabilities: ['render-widget'], bound: true },
172
- { key: 'footer', suffix: 'footer', capabilities: ['render-widget'], bound: true },
173
- { key: 'toolbar', suffix: 'toolbar', capabilities: ['toolbar-widget'], bound: true },
174
- { key: 'searchTrailing', suffix: 'search-trailing', capabilities: ['render-widget'], bound: true },
175
- { key: 'columns', suffix: 'columns', capabilities: ['column-widget'], bound: true },
176
- { key: 'rowActions', suffix: 'row-actions', capabilities: ['row-action'], bound: true },
177
- { key: 'bulkActions', suffix: 'bulk-actions', capabilities: ['bulk-action'], bound: true },
178
- { key: 'filters', suffix: 'filters', capabilities: ['filter-widget'], bound: true },
179
- { key: 'replacement', suffix: null, capabilities: ['component-replacement'], bound: true },
180
- { key: 'emptyState', suffix: 'empty-state', capabilities: ['render-widget'], bound: false },
181
- ] as const satisfies readonly BoundExtensionSurface[]
182
-
183
- export const CRUD_FORM_EXTENSION_SURFACES = [
184
- { key: 'base', suffix: null, capabilities: ['render-widget', 'lifecycle-handler'], bound: true },
185
- { key: 'header', suffix: 'header', capabilities: ['render-widget'], bound: true },
186
- { key: 'fields', suffix: 'fields', capabilities: ['field-widget'], bound: true },
187
- { key: 'replacement', suffix: null, capabilities: ['component-replacement'], bound: true },
188
- { key: 'beforeFields', suffix: 'before-fields', capabilities: ['render-widget'], bound: false },
189
- { key: 'afterFields', suffix: 'after-fields', capabilities: ['render-widget'], bound: false },
190
- { key: 'footer', suffix: 'footer', capabilities: ['render-widget'], bound: false },
191
- { key: 'sidebar', suffix: 'sidebar', capabilities: ['render-widget'], bound: false },
192
- { key: 'group', suffix: 'group:{groupId}', capabilities: ['render-widget'], bound: false },
193
- { key: 'fieldBefore', suffix: 'field:{fieldId}:before', capabilities: ['render-widget'], bound: false },
194
- { key: 'fieldAfter', suffix: 'field:{fieldId}:after', capabilities: ['render-widget'], bound: false },
195
- ] as const satisfies readonly BoundExtensionSurface[]
196
-
197
- export const CRUD_FORM_LIFECYCLE_PHASES = [
198
- 'transformValidation',
199
- 'transformDisplayData',
200
- 'onBeforeNavigate',
201
- 'onAppEvent',
202
- 'onVisibilityChange',
203
- 'onBeforeDelete',
204
- 'onDelete',
205
- 'onAfterDelete',
206
- 'onDeleteError',
207
- 'onFieldChange',
208
- 'transformFormData',
209
- 'onBeforeSave',
210
- 'onSave',
211
- 'onAfterSave',
212
- ] as const
213
-
214
- export const CRUD_FORM_OPERATIONS = ['create', 'update', 'delete'] as const
215
-
216
- export function dataTableExtensionSpotId(tableId: string, suffix?: string): string {
217
- return suffix ? `data-table:${tableId}:${suffix}` : `data-table:${tableId}`
218
- }
219
-
220
- export function crudFormExtensionSpotId(entityId: string, suffix?: string): string {
221
- return suffix ? `crud-form:${entityId}:${suffix}` : `crud-form:${entityId}`
222
- }
223
-
224
- export function extensionSpotChildId(spotId: string, suffix: string): string {
225
- return `${spotId}:${suffix}`
226
- }
227
-
228
- export function resolveExtensionPointPattern(
229
- pattern: string,
230
- parameters: Readonly<Record<string, string>>,
231
- ): string {
232
- return pattern.replace(/\{([^}]+)\}/g, (token, parameterName: string) => parameters[parameterName] ?? token)
233
- }
234
-
235
- /**
236
- * Portable, structural copy of the CLI provenance source reference
237
- * (`ModuleFactSourceRef` in `packages/cli/.../module-facts.ts`). Redefined here
238
- * — rather than imported — so `@open-mercato/shared` keeps zero dependency on
239
- * `@open-mercato/cli`. Field names are kept byte-identical so the two are
240
- * assignable across the package boundary.
241
- */
242
- export type ModuleFactSourceRef = {
243
- sourcePath: string
244
- exportName?: string
245
- line?: number
246
- }
247
-
248
- /** Portable, structural copy of the CLI provenance fact reference. */
249
- export type ModuleFactRef = {
250
- factSection: string
251
- factKey: string
252
- }
253
-
254
- export type ModuleExtensionContributionKind = ModuleExtensionContributionFact['kind']
255
-
256
- export type ModuleExtensionActivationKind =
257
- | 'crud-response-enricher'
258
- | 'query-enricher'
259
- | 'mutation-guard'
260
- | 'api-interceptor-bridge'
261
- | 'command-interceptor-bridge'
262
- | 'widget-injection-consumer'
263
- | 'component-extension-consumer'
264
- | 'dashboard-host-consumer'
265
-
266
- export type ModuleExtensionTargetRef = {
267
- kind:
268
- | 'module'
269
- | 'entity'
270
- | 'api-route'
271
- | 'command'
272
- | 'widget-spot'
273
- | 'component'
274
- | 'event'
275
- | 'notification'
276
- | 'wildcard'
277
- id: string
278
- moduleId?: string
279
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
280
- }
281
-
282
- export type ModuleExtensionActivation = {
283
- id: string
284
- kind: ModuleExtensionActivationKind
285
- host: ModuleExtensionTargetRef
286
- contributionKinds: ModuleExtensionContributionKind[]
287
- phases?: string[]
288
- /**
289
- * Mutation operations the call site guards, when statically declared. A
290
- * contribution declaring only other operations is not bound by this activation.
291
- */
292
- operations?: string[]
293
- source: ModuleFactSourceRef
294
- bridge?: ModuleFactRef
295
- }
296
-
297
- export type ModuleExtensionResolution =
298
- | 'bound'
299
- | 'capability-only'
300
- | 'optional-target-missing'
301
- | 'wildcard'
302
- | 'unresolved'
303
-
304
- export type ModuleIncomingExtensionRef = {
305
- contributionId: string
306
- contributionKind: ModuleExtensionContributionKind
307
- contributorModuleId: string
308
- target: ModuleExtensionTargetRef
309
- activationId?: string
310
- resolution: ModuleExtensionResolution
311
- source: ModuleFactSourceRef
312
- }
313
-
314
- export type ModuleContributionResolution = {
315
- contributionId: string
316
- target: ModuleExtensionTargetRef
317
- resolution: ModuleExtensionResolution
318
- activationIds: string[]
319
- }
320
-
321
- /**
322
- * Additive, optional bidirectional-topology fields layered on top of the
323
- * existing extension-surface facts. Every field is optional so legacy consumers
324
- * and pre-existing generated output remain byte-compatible.
325
- */
326
- export type ModuleExtensionSurfaceFactsAdditions = {
327
- activations?: ModuleExtensionActivation[]
328
- incoming?: ModuleIncomingExtensionRef[]
329
- contributionResolutions?: ModuleContributionResolution[]
330
- }
331
-
332
- export type ModuleExtensionSurfaceFacts = {
333
- hosts: ModuleExtensionHostFact[]
334
- contributions: ModuleExtensionContributionFact[]
335
- unresolved: ModuleExtensionUnresolvedFact[]
336
- } & ModuleExtensionSurfaceFactsAdditions
337
-
338
- export type ModuleExtensionHostFact = {
339
- key: string
340
- id: string
341
- resolution: 'exact' | 'pattern' | 'framework' | 'fact-ref'
342
- family: ExtensionHostFamily
343
- ownerModule: string
344
- capabilities: ExtensionHostCapability[]
345
- phases?: string[]
346
- operations?: string[]
347
- contextContract?: string
348
- dataContract?: string
349
- scopeContract?: string
350
- runtimeContract?: string
351
- activation?: ExtensionHostActivation
352
- bound: boolean
353
- stability: 'frozen' | 'stable'
354
- source:
355
- | { kind: 'declaration'; path: string; symbol: string }
356
- | { kind: 'fact-ref'; factSection: string; factKey: string }
357
- | { kind: 'framework'; path: string; symbol: string }
358
- aliases?: string[]
359
- patternParameters?: Record<string, ExtensionPointPatternParameter>
360
- fallbacks?: string[]
361
- }
362
-
363
- export type ModuleExtensionTargetFact = {
364
- id: string
365
- resolution: 'exact' | 'pattern' | 'framework' | 'fact-ref' | 'optional-external' | 'unresolved'
366
- factRef?: { factSection: string; factKey: string }
367
- optionalOwnerPackage?: string
368
- }
369
-
370
- export type ModuleExtensionContributionBase = {
371
- id: string
372
- targets: ModuleExtensionTargetFact[]
373
- phases?: string[]
374
- operations?: string[]
375
- features?: string[]
376
- scopeContract: string
377
- activation?: ExtensionHostActivation
378
- placement?: { relativeTo?: string; position?: 'first' | 'last' | 'before' | 'after'; priority?: number }
379
- roundTripId?: string
380
- override?: { domain: string; key: string; mode: 'disable-replace' | 'replace' | 'additive' }
381
- source: { path: string; symbol?: string }
382
- }
383
-
384
- export type ModuleExtensionContributionFact = ModuleExtensionContributionBase & (
385
- | {
386
- kind: 'widget'
387
- details: {
388
- payload: 'render' | 'headless' | 'menu' | 'dashboard' | 'notification' | 'integration'
389
- registryKey: string
390
- itemIds?: string[]
391
- labelKeys?: string[]
392
- contextContract?: string
393
- dataContract?: string
394
- executionGuard: 'host' | 'contribution' | 'both'
395
- }
396
- }
397
- | {
398
- kind: 'data-table'
399
- details: {
400
- payload: 'column' | 'row-action' | 'bulk-action' | 'filter' | 'toolbar' | 'render'
401
- tableId: string
402
- executionGuard: 'host' | 'contribution' | 'both'
403
- }
404
- }
405
- | {
406
- kind: 'crud-form'
407
- details: {
408
- payload: 'render' | 'field' | 'lifecycle-handler'
409
- entityId: string
410
- fieldIds?: string[]
411
- groupIds?: string[]
412
- requestHeaderCapability: boolean
413
- }
414
- }
415
- | {
416
- kind: 'component-override'
417
- details: { handle: string; mode: 'replace' | 'wrapper' | 'props'; propsContract: string }
418
- }
419
- | {
420
- kind: 'response-enricher'
421
- details: {
422
- targetEntity: string
423
- surfaces: Array<'list' | 'detail'>
424
- timeoutMs: number
425
- fallback: 'none' | 'configured'
426
- critical: boolean
427
- cachePosture: 'record-pure' | 'rerun-on-list-cache-hit'
428
- queryEngine?: {
429
- engines: string[]
430
- applyOn: Array<'list' | 'detail'>
431
- activation: 'caller-opt-in'
432
- }
433
- }
434
- }
435
- | {
436
- kind: 'api-interceptor'
437
- details: {
438
- route: string
439
- methods: string[]
440
- phases: Array<'before' | 'after'>
441
- activation: 'crud-pipeline' | 'custom-route-bridge'
442
- timeoutMs: number
443
- failurePosture: 'fail-closed' | 'fallback'
444
- }
445
- }
446
- | {
447
- kind: 'command-interceptor'
448
- details: {
449
- targetCommand: string
450
- phases: Array<'before-execute' | 'after-execute' | 'before-undo' | 'after-undo'>
451
- }
452
- }
453
- | {
454
- kind: 'mutation-guard'
455
- details: {
456
- entityId: string
457
- operations: Array<'create' | 'update' | 'delete'>
458
- capabilities: Array<'block' | 'rewrite' | 'after-success'>
459
- optimisticLock: 'preserved'
460
- }
461
- }
462
- | {
463
- kind: 'entity-extension'
464
- details: {
465
- hostEntityId: string
466
- extensionEntityId: string
467
- linkId: string
468
- scopeContract: string
469
- orphanContract: string
470
- }
471
- }
472
- | {
473
- kind: 'subscriber'
474
- details: {
475
- event: string
476
- subscriberId: string
477
- persistent: boolean
478
- sync: boolean
479
- priority?: number
480
- }
481
- }
482
- | {
483
- kind: 'browser-reaction'
484
- details: {
485
- transports: Array<'client' | 'portal' | 'notification-effect'>
486
- hooks: string[]
487
- audienceScopeContract: string
488
- maxPayloadBytes?: number
489
- dedupWindowMs?: number
490
- /**
491
- * Statically declared handler id. Runtime keys `notifications.handlers`
492
- * overrides by it, so it is absent when the module declares no id (such a
493
- * handler cannot be overridden).
494
- */
495
- overrideKey?: string
496
- }
497
- }
498
- | {
499
- kind: 'specialized-registry'
500
- details: {
501
- registry: 'notification' | 'integration' | 'search' | 'vector' | 'ai' | 'payment' | 'shipping' | 'currency' | 'workflow'
502
- registryId: string
503
- specialistRoute: string
504
- }
505
- }
506
- | {
507
- kind: 'module-override'
508
- details: {
509
- domain: string
510
- key: string
511
- mode: 'disable-replace' | 'replace' | 'additive'
512
- }
513
- }
514
- )
515
-
516
- export type ModuleExtensionUnresolvedFact = {
517
- key: string
518
- source: { path: string; symbol?: string }
519
- reason:
520
- | 'unclassified-binding'
521
- | 'unbound-declaration'
522
- | 'dynamic-without-pattern'
523
- | 'unresolved-first-party-target'
524
- }
@@ -1,166 +0,0 @@
1
- import type { Module } from '../../modules/registry'
2
- import { getModules } from '../../lib/modules/registry'
3
- import {
4
- applyAclFeatureOverrides,
5
- resetModuleContractOverridesForTests,
6
- } from '../../modules/overrides'
7
- import {
8
- authorizeFeatures,
9
- getRemovedAclFeatureIds,
10
- isAclFeatureRemoved,
11
- resolveEffectiveFeatures,
12
- } from '../featurePolicy'
13
-
14
- jest.mock('../../lib/modules/registry', () => ({
15
- getModules: jest.fn(),
16
- }))
17
-
18
- const mockGetModules = jest.mocked(getModules)
19
-
20
- const modules: Module[] = [
21
- {
22
- id: 'auth',
23
- features: [
24
- { id: 'auth.users.view', title: 'View users', module: 'auth' },
25
- { id: 'auth.users.manage', title: 'Manage users', module: 'auth' },
26
- ],
27
- },
28
- {
29
- id: 'dashboards',
30
- features: [
31
- { id: 'analytics.view', title: 'View analytics', module: 'dashboards' },
32
- ],
33
- },
34
- {
35
- id: 'customer_accounts',
36
- setup: {
37
- defaultCustomerRoleFeatures: {
38
- portal_admin: ['portal.*'],
39
- buyer: ['portal.orders.view', 'portal.account.manage'],
40
- },
41
- },
42
- frontendRoutes: [
43
- {
44
- Component: () => null,
45
- requireCustomerFeatures: ['portal.quotes.view'],
46
- },
47
- ],
48
- },
49
- ]
50
-
51
- describe('featurePolicy', () => {
52
- beforeEach(() => {
53
- mockGetModules.mockReturnValue(modules)
54
- resetModuleContractOverridesForTests()
55
- })
56
-
57
- afterEach(() => {
58
- resetModuleContractOverridesForTests()
59
- jest.resetAllMocks()
60
- })
61
-
62
- it('reports exact removals and lets a later replacement restore the feature', () => {
63
- applyAclFeatureOverrides({
64
- 'auth.users.manage': null,
65
- 'legacy.feature': null,
66
- })
67
-
68
- expect(getRemovedAclFeatureIds()).toEqual(['auth.users.manage', 'legacy.feature'])
69
- expect(isAclFeatureRemoved('auth.users.manage')).toBe(true)
70
-
71
- applyAclFeatureOverrides({
72
- 'auth.users.manage': {
73
- id: 'auth.users.manage',
74
- title: 'Manage users',
75
- module: 'auth',
76
- },
77
- })
78
-
79
- expect(isAclFeatureRemoved('auth.users.manage')).toBe(false)
80
- expect(getRemovedAclFeatureIds()).toEqual(['legacy.feature'])
81
- })
82
-
83
- it.each([
84
- { grantedFeatures: ['auth.users.manage'], unrestricted: false, siblingAllowed: false },
85
- { grantedFeatures: ['auth.*'], unrestricted: false, siblingAllowed: true },
86
- { grantedFeatures: ['*'], unrestricted: false, siblingAllowed: true },
87
- { grantedFeatures: [], unrestricted: true, siblingAllowed: true },
88
- ])('denies a removed requirement before grants or unrestricted access', ({
89
- siblingAllowed,
90
- ...subject
91
- }) => {
92
- applyAclFeatureOverrides({ 'auth.users.manage': null })
93
-
94
- expect(authorizeFeatures(['auth.users.manage'], subject)).toBe(false)
95
- expect(authorizeFeatures(['auth.users.view'], subject)).toBe(siblingAllowed)
96
- })
97
-
98
- it('enforces invalid scope before unrestricted access', () => {
99
- expect(authorizeFeatures(['auth.users.view'], {
100
- grantedFeatures: ['*'],
101
- unrestricted: true,
102
- scopeAllowed: false,
103
- })).toBe(false)
104
- })
105
-
106
- it('denies requirements owned by disabled modules', () => {
107
- expect(authorizeFeatures(['search.global'], {
108
- grantedFeatures: ['*'],
109
- unrestricted: true,
110
- })).toBe(false)
111
- })
112
-
113
- it('expands wildcards into a deterministic concrete set including portal sources', () => {
114
- expect(resolveEffectiveFeatures(['*'])).toEqual([
115
- 'auth.users.view',
116
- 'auth.users.manage',
117
- 'analytics.view',
118
- 'portal.orders.view',
119
- 'portal.account.manage',
120
- 'portal.quotes.view',
121
- ])
122
- expect(resolveEffectiveFeatures(['portal.*'])).toEqual([
123
- 'portal.orders.view',
124
- 'portal.account.manage',
125
- 'portal.quotes.view',
126
- ])
127
- })
128
-
129
- it('removes nulled features while preserving siblings and explicit custom grants', () => {
130
- applyAclFeatureOverrides({
131
- 'auth.users.manage': null,
132
- 'auth.custom.audit': null,
133
- })
134
-
135
- expect(resolveEffectiveFeatures([
136
- 'auth.*',
137
- 'auth.custom.export',
138
- 'auth.custom.audit',
139
- ])).toEqual([
140
- 'auth.users.view',
141
- 'auth.custom.export',
142
- ])
143
- })
144
-
145
- it('uses declared ownership for off-convention features', () => {
146
- expect(resolveEffectiveFeatures(['analytics.*'])).toEqual(['analytics.view'])
147
- expect(authorizeFeatures(['analytics.view'], {
148
- grantedFeatures: ['analytics.*'],
149
- })).toBe(true)
150
- })
151
-
152
- it('fails closed for wildcards when the module registry is unavailable', () => {
153
- mockGetModules.mockImplementation(() => {
154
- throw new Error('registry unavailable')
155
- })
156
- applyAclFeatureOverrides({ 'legacy.removed': null })
157
-
158
- expect(resolveEffectiveFeatures([
159
- '*',
160
- 'legacy.*',
161
- 'legacy.explicit',
162
- 'legacy.removed',
163
- 'legacy.explicit',
164
- ])).toEqual(['legacy.explicit'])
165
- })
166
- })