@open-mercato/shared 0.6.7 → 0.6.8-develop.6874.1.982d6097d8
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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +57 -6
- package/build.mjs +2 -4
- package/dist/lib/ai/safety-identifier.js +32 -0
- package/dist/lib/ai/safety-identifier.js.map +7 -0
- package/dist/lib/auth/organizationScope.js +34 -0
- package/dist/lib/auth/organizationScope.js.map +7 -0
- package/dist/lib/auth/server.js +15 -2
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/clientOnlyModules.js +55 -0
- package/dist/lib/bootstrap/clientOnlyModules.js.map +7 -0
- package/dist/lib/bootstrap/dynamicLoader.js +117 -43
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/commands/command-bus.js +5 -1
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/command-interceptor-runner.js +2 -2
- package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
- package/dist/lib/crud/enricher-runner.js +2 -11
- package/dist/lib/crud/enricher-runner.js.map +2 -2
- package/dist/lib/crud/factory.js +72 -9
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/interceptor-runner.js +2 -2
- package/dist/lib/crud/interceptor-runner.js.map +2 -2
- package/dist/lib/crud/mutation-guard-registry.js +2 -2
- package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
- package/dist/lib/crud/types.js +4 -0
- package/dist/lib/crud/types.js.map +3 -3
- package/dist/lib/data/consistency.js +19 -0
- package/dist/lib/data/consistency.js.map +7 -0
- package/dist/lib/data/engine.js +37 -11
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/entityMetadata.js +26 -0
- package/dist/lib/db/entityMetadata.js.map +7 -0
- package/dist/lib/db/pg-errors.js +42 -0
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/di/container.js +48 -6
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/email/send.js +56 -0
- package/dist/lib/email/send.js.map +2 -2
- package/dist/lib/encryption/subscriber.js +2 -10
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +52 -17
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
- package/dist/lib/i18n/config.js +1 -1
- package/dist/lib/i18n/config.js.map +2 -2
- package/dist/lib/i18n/server.js +4 -4
- package/dist/lib/i18n/server.js.map +3 -3
- package/dist/lib/logger/extension.js +29 -0
- package/dist/lib/logger/extension.js.map +7 -0
- package/dist/lib/logger/index.js +47 -3
- package/dist/lib/logger/index.js.map +3 -3
- package/dist/lib/modules/registry.js +5 -1
- package/dist/lib/modules/registry.js.map +2 -2
- package/dist/lib/modules/surfaceFingerprint.js +47 -0
- package/dist/lib/modules/surfaceFingerprint.js.map +7 -0
- package/dist/lib/query/ciphertext-search-warning.js +45 -0
- package/dist/lib/query/ciphertext-search-warning.js.map +7 -0
- package/dist/lib/query/engine.js +54 -40
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/redis/connection.js +7 -2
- package/dist/lib/redis/connection.js.map +2 -2
- package/dist/lib/search/auto-indexing.js +14 -0
- package/dist/lib/search/auto-indexing.js.map +7 -0
- package/dist/lib/search/availability.js +107 -0
- package/dist/lib/search/availability.js.map +7 -0
- package/dist/lib/search/config.js +61 -2
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/search/tokenLookup.js +46 -0
- package/dist/lib/search/tokenLookup.js.map +7 -0
- package/dist/lib/search/tokenize.js +23 -11
- package/dist/lib/search/tokenize.js.map +2 -2
- package/dist/lib/telemetry/runtime.js +35 -0
- package/dist/lib/telemetry/runtime.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/body.js +57 -0
- package/dist/lib/webhooks/body.js.map +7 -0
- package/dist/lib/webhooks/inbound-types.js +1 -0
- package/dist/lib/webhooks/inbound-types.js.map +7 -0
- package/dist/lib/webhooks/index.js +13 -1
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/lib/webhooks/verify.js +12 -7
- package/dist/lib/webhooks/verify.js.map +2 -2
- package/dist/modules/analytics.js.map +2 -2
- package/dist/modules/overrides.js +50 -1
- package/dist/modules/overrides.js.map +2 -2
- package/dist/modules/payment_gateways/types.js +8 -1
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/registry.js +1 -0
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/widgets/extension-points.js +96 -0
- package/dist/modules/widgets/extension-points.js.map +7 -0
- package/dist/security/enabledModulesRegistry.js +56 -11
- package/dist/security/enabledModulesRegistry.js.map +2 -2
- package/dist/security/featurePolicy.js +62 -0
- package/dist/security/featurePolicy.js.map +7 -0
- package/jest.config.cjs +2 -1
- package/package.json +19 -9
- package/scripts/versionSource.cjs +65 -0
- package/src/lib/__tests__/versionSource.test.ts +46 -0
- package/src/lib/ai/__tests__/llm-provider-contract.test.ts +59 -0
- package/src/lib/ai/__tests__/safety-identifier.test.ts +71 -0
- package/src/lib/ai/llm-provider.ts +33 -0
- package/src/lib/ai/safety-identifier.ts +73 -0
- package/src/lib/auth/__tests__/organizationScope.test.ts +107 -0
- package/src/lib/auth/__tests__/server.test.ts +23 -0
- package/src/lib/auth/organizationScope.ts +65 -0
- package/src/lib/auth/server.ts +23 -2
- package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +189 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +98 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +211 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +63 -8
- package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +256 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +21 -1
- package/src/lib/bootstrap/clientOnlyModules.ts +85 -0
- package/src/lib/bootstrap/dynamicLoader.ts +213 -65
- package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +28 -0
- package/src/lib/commands/command-bus.ts +5 -1
- package/src/lib/commands/command-interceptor-runner.ts +2 -2
- package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +171 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +351 -0
- package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +26 -0
- package/src/lib/crud/enricher-runner.ts +2 -11
- package/src/lib/crud/factory.ts +82 -6
- package/src/lib/crud/interceptor-runner.ts +2 -2
- package/src/lib/crud/mutation-guard-registry.ts +2 -2
- package/src/lib/crud/types.ts +3 -0
- package/src/lib/data/__tests__/consistency.test.ts +38 -0
- package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +18 -3
- package/src/lib/data/consistency.ts +17 -0
- package/src/lib/data/engine.ts +50 -16
- package/src/lib/db/__tests__/entityMetadata.test.ts +38 -0
- package/src/lib/db/__tests__/pg-errors.test.ts +48 -0
- package/src/lib/db/entityMetadata.ts +43 -0
- package/src/lib/db/pg-errors.ts +58 -0
- package/src/lib/di/__tests__/container-app-di-absent.test.ts +155 -0
- package/src/lib/di/__tests__/container-app-di.test.ts +173 -0
- package/src/lib/di/__tests__/registrar-error-log.test.ts +154 -0
- package/src/lib/di/container.ts +72 -8
- package/src/lib/email/__tests__/send.test.ts +37 -1
- package/src/lib/email/send.ts +79 -0
- package/src/lib/encryption/__tests__/subscriber.test.ts +54 -2
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +58 -0
- package/src/lib/encryption/subscriber.ts +2 -14
- package/src/lib/encryption/tenantDataEncryptionService.ts +64 -18
- package/src/lib/i18n/__tests__/config.test.ts +8 -0
- package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +30 -0
- package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +29 -0
- package/src/lib/i18n/config.ts +2 -2
- package/src/lib/i18n/server.ts +6 -2
- package/src/lib/logger/__tests__/logger.test.ts +77 -0
- package/src/lib/logger/extension.ts +59 -0
- package/src/lib/logger/index.ts +59 -1
- package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +122 -0
- package/src/lib/modules/registry.ts +10 -0
- package/src/lib/modules/surfaceFingerprint.ts +87 -0
- package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +178 -0
- package/src/lib/query/__tests__/engine.test.ts +65 -13
- package/src/lib/query/ciphertext-search-warning.ts +95 -0
- package/src/lib/query/engine.ts +75 -58
- package/src/lib/query/types.ts +4 -5
- package/src/lib/redis/__tests__/connection.test.ts +16 -0
- package/src/lib/redis/connection.ts +10 -7
- package/src/lib/search/__tests__/availability.test.ts +211 -0
- package/src/lib/search/__tests__/config.test.ts +177 -0
- package/src/lib/search/__tests__/tokenLookup.test.ts +206 -0
- package/src/lib/search/__tests__/tokenize.test.ts +49 -0
- package/src/lib/search/auto-indexing.ts +22 -0
- package/src/lib/search/availability.ts +232 -0
- package/src/lib/search/config.ts +106 -8
- package/src/lib/search/tokenLookup.ts +133 -0
- package/src/lib/search/tokenize.ts +33 -11
- package/src/lib/telemetry/runtime.ts +72 -0
- package/src/lib/webhooks/__tests__/body.test.ts +95 -0
- package/src/lib/webhooks/__tests__/verify.test.ts +20 -1
- package/src/lib/webhooks/body.ts +70 -0
- package/src/lib/webhooks/inbound-types.ts +125 -0
- package/src/lib/webhooks/index.ts +19 -1
- package/src/lib/webhooks/verify.ts +17 -9
- package/src/modules/__tests__/nav-group-order-override.test.ts +183 -0
- package/src/modules/__tests__/registry.test.ts +10 -2
- package/src/modules/analytics.ts +7 -0
- package/src/modules/customer-auth.ts +1 -0
- package/src/modules/encryption.ts +3 -0
- package/src/modules/navigation/backendChrome.ts +15 -0
- package/src/modules/overrides.ts +103 -0
- package/src/modules/payment_gateways/__tests__/types.test.ts +37 -0
- package/src/modules/payment_gateways/types.ts +41 -0
- package/src/modules/registry.ts +3 -0
- package/src/modules/widgets/__tests__/extension-points.test.ts +101 -0
- package/src/modules/widgets/extension-points.ts +524 -0
- package/src/security/__tests__/featurePolicy.test.ts +166 -0
- package/src/security/enabledModulesRegistry.ts +64 -12
- package/src/security/featurePolicy.ts +89 -0
|
@@ -0,0 +1,524 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
})
|