@open-mercato/shared 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2
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/dist/lib/auth/server.js +23 -0
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/crud/factory.js +6 -5
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/overrides.js +64 -0
- package/dist/modules/overrides.js.map +7 -0
- package/dist/modules/registry.js +9 -0
- package/dist/modules/registry.js.map +2 -2
- package/package.json +2 -2
- package/src/lib/auth/server.ts +36 -0
- package/src/lib/crud/factory.ts +7 -4
- package/src/modules/__tests__/overrides.test.ts +141 -0
- package/src/modules/overrides.ts +266 -0
- package/src/modules/registry.ts +10 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified `modules.ts` override surface — one place for downstream apps to
|
|
3
|
+
* replace or disable any contract a module presents.
|
|
4
|
+
*
|
|
5
|
+
* Spec: `.ai/specs/2026-05-04-modules-ts-unified-overrides.md`
|
|
6
|
+
*
|
|
7
|
+
* Each `ModuleEntry` in `apps/<app>/src/modules.ts` may carry an
|
|
8
|
+
* `overrides` field whose sub-keys address one domain at a time:
|
|
9
|
+
*
|
|
10
|
+
* {
|
|
11
|
+
* id: 'example',
|
|
12
|
+
* from: '@app',
|
|
13
|
+
* overrides: {
|
|
14
|
+
* ai: { agents: {...}, tools: {...} }, // Phase 1 — wired
|
|
15
|
+
* routes: { api: {...}, pages: {...} }, // Phase 2/3 — stub
|
|
16
|
+
* events: { subscribers: {...} }, // Phase 4 — stub
|
|
17
|
+
* workers: {...}, // Phase 5 — stub
|
|
18
|
+
* ...
|
|
19
|
+
* },
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* The umbrella shape is the union of every per-domain sub-shape. Per-
|
|
23
|
+
* domain runtime hooks ("wired" domains) own their composers and apply
|
|
24
|
+
* the override map against their registry. Until a phase ships, the
|
|
25
|
+
* dispatcher emits a one-shot structured warning when it sees an
|
|
26
|
+
* override targeting that unwired domain — the runtime never throws on
|
|
27
|
+
* unwired domains so app boot stays unaffected during the rollout.
|
|
28
|
+
*
|
|
29
|
+
* Resolution order across all domains (highest precedence first):
|
|
30
|
+
*
|
|
31
|
+
* 1. Programmatic — direct calls into the per-domain `apply*Overrides()` API.
|
|
32
|
+
* 2. `modules.ts` inline — `entry.overrides.<domain>` here.
|
|
33
|
+
* 3. File-based — overrides exported from a contributing module's own files.
|
|
34
|
+
* 4. Base — the module's own registrations.
|
|
35
|
+
*
|
|
36
|
+
* `null` always means "disable"; a definition replaces.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Domain sub-shapes
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* AI domain — agents and tools. Re-exports the canonical maps from
|
|
45
|
+
* `@open-mercato/ai-assistant` so consumers do not need to import that
|
|
46
|
+
* package directly when they only want to declare overrides.
|
|
47
|
+
*
|
|
48
|
+
* Imported lazily as `unknown` here because `@open-mercato/shared` must
|
|
49
|
+
* NOT take a runtime dependency on `@open-mercato/ai-assistant` (the
|
|
50
|
+
* dependency direction is the other way around). Apps that author
|
|
51
|
+
* `entry.overrides.ai` should import the strongly-typed
|
|
52
|
+
* `AiAgentOverridesMap` / `AiToolOverridesMap` from `@open-mercato/ai-assistant`
|
|
53
|
+
* directly — TypeScript structurally compatible types make the loose
|
|
54
|
+
* shape here a no-op annotation cost.
|
|
55
|
+
*/
|
|
56
|
+
export interface AiOverridesShape {
|
|
57
|
+
agents?: Record<string, unknown>
|
|
58
|
+
tools?: Record<string, unknown>
|
|
59
|
+
extensions?: unknown[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Phase 2/3 — routes (api + pages). Stubbed until wired. */
|
|
63
|
+
export interface RoutesOverridesShape {
|
|
64
|
+
api?: Record<string, unknown>
|
|
65
|
+
pages?: Record<string, unknown>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Phase 4 — event subscribers. Stubbed until wired. */
|
|
69
|
+
export interface EventsOverridesShape {
|
|
70
|
+
subscribers?: Record<string, unknown>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Phase 6/7/8 — widget injection, component handles, dashboard widgets. */
|
|
74
|
+
export interface WidgetsOverridesShape {
|
|
75
|
+
injection?: Record<string, unknown>
|
|
76
|
+
components?: Record<string, unknown>
|
|
77
|
+
dashboard?: Record<string, unknown>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Phase 9 — notification types + handlers. */
|
|
81
|
+
export interface NotificationsOverridesShape {
|
|
82
|
+
types?: Record<string, unknown>
|
|
83
|
+
handlers?: Record<string, unknown>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Phase 15 — setup lifecycle hooks. */
|
|
87
|
+
export interface SetupOverridesShape {
|
|
88
|
+
defaultRoleFeatures?: Record<string, readonly string[]>
|
|
89
|
+
seedDefaults?: false
|
|
90
|
+
seedExamples?: false
|
|
91
|
+
onTenantCreated?: false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Phase 16 — ACL features (per-feature override). */
|
|
95
|
+
export interface AclOverridesShape {
|
|
96
|
+
features?: Record<string, unknown>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Phase 18 — encryption maps per entity id. */
|
|
100
|
+
export interface EncryptionOverridesShape {
|
|
101
|
+
maps?: Record<string, unknown>
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Umbrella shape for `entry.overrides`. Every key is optional; a
|
|
106
|
+
* downstream app sets only the domains it cares about.
|
|
107
|
+
*/
|
|
108
|
+
export interface ModuleOverrides {
|
|
109
|
+
ai?: AiOverridesShape
|
|
110
|
+
routes?: RoutesOverridesShape
|
|
111
|
+
events?: EventsOverridesShape
|
|
112
|
+
workers?: Record<string, unknown>
|
|
113
|
+
widgets?: WidgetsOverridesShape
|
|
114
|
+
notifications?: NotificationsOverridesShape
|
|
115
|
+
interceptors?: Record<string, unknown>
|
|
116
|
+
commandInterceptors?: Record<string, unknown>
|
|
117
|
+
enrichers?: Record<string, unknown>
|
|
118
|
+
guards?: Record<string, unknown>
|
|
119
|
+
cli?: Record<string, unknown>
|
|
120
|
+
setup?: SetupOverridesShape
|
|
121
|
+
acl?: AclOverridesShape
|
|
122
|
+
di?: Record<string, unknown>
|
|
123
|
+
encryption?: EncryptionOverridesShape
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Public shape consumed by the dispatcher. Mirrors the `ModuleEntry`
|
|
128
|
+
* defined in each app's `modules.ts` — the dispatcher only needs `id`
|
|
129
|
+
* and `overrides`.
|
|
130
|
+
*/
|
|
131
|
+
export interface ModuleEntryWithOverrides {
|
|
132
|
+
id: string
|
|
133
|
+
from?: string
|
|
134
|
+
overrides?: ModuleOverrides
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Per-domain runtime hook registry
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Each wired domain registers an applier that receives the list of
|
|
143
|
+
* `(moduleId, overrides)` pairs in module-load order and forwards them
|
|
144
|
+
* to its own runtime hook. Unwired domains do not register an applier
|
|
145
|
+
* and instead trigger the dispatcher's one-shot warning.
|
|
146
|
+
*/
|
|
147
|
+
export type ModuleOverrideDomain =
|
|
148
|
+
| 'ai'
|
|
149
|
+
| 'routes'
|
|
150
|
+
| 'events'
|
|
151
|
+
| 'workers'
|
|
152
|
+
| 'widgets'
|
|
153
|
+
| 'notifications'
|
|
154
|
+
| 'interceptors'
|
|
155
|
+
| 'commandInterceptors'
|
|
156
|
+
| 'enrichers'
|
|
157
|
+
| 'guards'
|
|
158
|
+
| 'cli'
|
|
159
|
+
| 'setup'
|
|
160
|
+
| 'acl'
|
|
161
|
+
| 'di'
|
|
162
|
+
| 'encryption'
|
|
163
|
+
|
|
164
|
+
export interface ModuleOverrideEntry<TShape> {
|
|
165
|
+
moduleId: string
|
|
166
|
+
overrides: TShape
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type ModuleOverrideApplier<TShape> = (
|
|
170
|
+
entries: ReadonlyArray<ModuleOverrideEntry<TShape>>,
|
|
171
|
+
) => void
|
|
172
|
+
|
|
173
|
+
const appliers = new Map<ModuleOverrideDomain, ModuleOverrideApplier<unknown>>()
|
|
174
|
+
const warnedUnwiredDomains = new Set<ModuleOverrideDomain>()
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Register a per-domain runtime hook. Called once at module-load time
|
|
178
|
+
* by each wired domain (e.g. the AI subsystem registers `'ai'` from
|
|
179
|
+
* `@open-mercato/ai-assistant`).
|
|
180
|
+
*/
|
|
181
|
+
export function registerModuleOverrideApplier<TShape>(
|
|
182
|
+
domain: ModuleOverrideDomain,
|
|
183
|
+
applier: ModuleOverrideApplier<TShape>,
|
|
184
|
+
): void {
|
|
185
|
+
appliers.set(domain, applier as ModuleOverrideApplier<unknown>)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** @__internal Test-only hook — clear all registered appliers + warnings. */
|
|
189
|
+
export function resetModuleOverrideAppliersForTests(): void {
|
|
190
|
+
appliers.clear()
|
|
191
|
+
warnedUnwiredDomains.clear()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Dispatcher
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
const DOMAIN_KEYS: ModuleOverrideDomain[] = [
|
|
199
|
+
'ai',
|
|
200
|
+
'routes',
|
|
201
|
+
'events',
|
|
202
|
+
'workers',
|
|
203
|
+
'widgets',
|
|
204
|
+
'notifications',
|
|
205
|
+
'interceptors',
|
|
206
|
+
'commandInterceptors',
|
|
207
|
+
'enrichers',
|
|
208
|
+
'guards',
|
|
209
|
+
'cli',
|
|
210
|
+
'setup',
|
|
211
|
+
'acl',
|
|
212
|
+
'di',
|
|
213
|
+
'encryption',
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
const TRACKING_ISSUE_HINT =
|
|
217
|
+
'See `.ai/specs/2026-05-04-modules-ts-unified-overrides.md` and tracking issue https://github.com/open-mercato/open-mercato/issues/1787.'
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Walk every `ModuleEntry` and dispatch its `overrides.<domain>` shape
|
|
221
|
+
* to the matching wired applier. Unwired domains emit a one-shot
|
|
222
|
+
* structured warning.
|
|
223
|
+
*
|
|
224
|
+
* Call this exactly once from `apps/<app>/src/bootstrap.ts` BEFORE any
|
|
225
|
+
* registry first-loads. Calling it more than once is safe but
|
|
226
|
+
* accumulates per-domain entries each time.
|
|
227
|
+
*/
|
|
228
|
+
export function applyModuleOverridesFromEnabledModules(
|
|
229
|
+
modules: ReadonlyArray<ModuleEntryWithOverrides>,
|
|
230
|
+
): void {
|
|
231
|
+
if (!Array.isArray(modules) || modules.length === 0) return
|
|
232
|
+
|
|
233
|
+
// Bucket entries by domain in module-load order.
|
|
234
|
+
const buckets = new Map<ModuleOverrideDomain, Array<ModuleOverrideEntry<unknown>>>()
|
|
235
|
+
|
|
236
|
+
for (const entry of modules) {
|
|
237
|
+
if (!entry || typeof entry.id !== 'string' || !entry.id) continue
|
|
238
|
+
const overrides = entry.overrides
|
|
239
|
+
if (!overrides || typeof overrides !== 'object') continue
|
|
240
|
+
|
|
241
|
+
for (const domain of DOMAIN_KEYS) {
|
|
242
|
+
const value = (overrides as Record<string, unknown>)[domain]
|
|
243
|
+
if (value === undefined || value === null) continue
|
|
244
|
+
if (typeof value !== 'object') continue
|
|
245
|
+
const list = buckets.get(domain) ?? []
|
|
246
|
+
list.push({ moduleId: entry.id, overrides: value })
|
|
247
|
+
buckets.set(domain, list)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Dispatch each domain to its wired applier; warn on unwired domains.
|
|
252
|
+
for (const [domain, entries] of buckets) {
|
|
253
|
+
const applier = appliers.get(domain)
|
|
254
|
+
if (!applier) {
|
|
255
|
+
if (!warnedUnwiredDomains.has(domain)) {
|
|
256
|
+
warnedUnwiredDomains.add(domain)
|
|
257
|
+
const moduleIds = Array.from(new Set(entries.map((e) => e.moduleId))).join(', ')
|
|
258
|
+
console.warn(
|
|
259
|
+
`[Module Overrides] Domain "${domain}" not yet wired — entry.overrides.${domain} for module(s) [${moduleIds}] was ignored. ${TRACKING_ISSUE_HINT}`,
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
applier(entries)
|
|
265
|
+
}
|
|
266
|
+
}
|
package/src/modules/registry.ts
CHANGED
|
@@ -373,6 +373,16 @@ export function getFrontendRouteManifests(): FrontendRouteManifestEntry[] {
|
|
|
373
373
|
return _frontendRouteManifests ?? []
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
let _apiRouteManifests: ApiRouteManifestEntry[] | null = null
|
|
377
|
+
|
|
378
|
+
export function registerApiRouteManifests(routes: ApiRouteManifestEntry[]) {
|
|
379
|
+
_apiRouteManifests = routes
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function getApiRouteManifests(): ApiRouteManifestEntry[] {
|
|
383
|
+
return _apiRouteManifests ?? []
|
|
384
|
+
}
|
|
385
|
+
|
|
376
386
|
// CLI modules registry - shared between CLI and module workers
|
|
377
387
|
let _cliModules: Module[] | null = null
|
|
378
388
|
|