@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.
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.5.1-develop.3036.f02c281f23";
1
+ const APP_VERSION = "0.5.1-develop.3045.b4b3320cc2";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.5.1-develop.3036.f02c281f23'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.5.1-develop.3045.b4b3320cc2'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,64 @@
1
+ const appliers = /* @__PURE__ */ new Map();
2
+ const warnedUnwiredDomains = /* @__PURE__ */ new Set();
3
+ function registerModuleOverrideApplier(domain, applier) {
4
+ appliers.set(domain, applier);
5
+ }
6
+ function resetModuleOverrideAppliersForTests() {
7
+ appliers.clear();
8
+ warnedUnwiredDomains.clear();
9
+ }
10
+ const DOMAIN_KEYS = [
11
+ "ai",
12
+ "routes",
13
+ "events",
14
+ "workers",
15
+ "widgets",
16
+ "notifications",
17
+ "interceptors",
18
+ "commandInterceptors",
19
+ "enrichers",
20
+ "guards",
21
+ "cli",
22
+ "setup",
23
+ "acl",
24
+ "di",
25
+ "encryption"
26
+ ];
27
+ const TRACKING_ISSUE_HINT = "See `.ai/specs/2026-05-04-modules-ts-unified-overrides.md` and tracking issue https://github.com/open-mercato/open-mercato/issues/1787.";
28
+ function applyModuleOverridesFromEnabledModules(modules) {
29
+ if (!Array.isArray(modules) || modules.length === 0) return;
30
+ const buckets = /* @__PURE__ */ new Map();
31
+ for (const entry of modules) {
32
+ if (!entry || typeof entry.id !== "string" || !entry.id) continue;
33
+ const overrides = entry.overrides;
34
+ if (!overrides || typeof overrides !== "object") continue;
35
+ for (const domain of DOMAIN_KEYS) {
36
+ const value = overrides[domain];
37
+ if (value === void 0 || value === null) continue;
38
+ if (typeof value !== "object") continue;
39
+ const list = buckets.get(domain) ?? [];
40
+ list.push({ moduleId: entry.id, overrides: value });
41
+ buckets.set(domain, list);
42
+ }
43
+ }
44
+ for (const [domain, entries] of buckets) {
45
+ const applier = appliers.get(domain);
46
+ if (!applier) {
47
+ if (!warnedUnwiredDomains.has(domain)) {
48
+ warnedUnwiredDomains.add(domain);
49
+ const moduleIds = Array.from(new Set(entries.map((e) => e.moduleId))).join(", ");
50
+ console.warn(
51
+ `[Module Overrides] Domain "${domain}" not yet wired \u2014 entry.overrides.${domain} for module(s) [${moduleIds}] was ignored. ${TRACKING_ISSUE_HINT}`
52
+ );
53
+ }
54
+ continue;
55
+ }
56
+ applier(entries);
57
+ }
58
+ }
59
+ export {
60
+ applyModuleOverridesFromEnabledModules,
61
+ registerModuleOverrideApplier,
62
+ resetModuleOverrideAppliersForTests
63
+ };
64
+ //# sourceMappingURL=overrides.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/modules/overrides.ts"],
4
+ "sourcesContent": ["/**\n * Unified `modules.ts` override surface \u2014 one place for downstream apps to\n * replace or disable any contract a module presents.\n *\n * Spec: `.ai/specs/2026-05-04-modules-ts-unified-overrides.md`\n *\n * Each `ModuleEntry` in `apps/<app>/src/modules.ts` may carry an\n * `overrides` field whose sub-keys address one domain at a time:\n *\n * {\n * id: 'example',\n * from: '@app',\n * overrides: {\n * ai: { agents: {...}, tools: {...} }, // Phase 1 \u2014 wired\n * routes: { api: {...}, pages: {...} }, // Phase 2/3 \u2014 stub\n * events: { subscribers: {...} }, // Phase 4 \u2014 stub\n * workers: {...}, // Phase 5 \u2014 stub\n * ...\n * },\n * }\n *\n * The umbrella shape is the union of every per-domain sub-shape. Per-\n * domain runtime hooks (\"wired\" domains) own their composers and apply\n * the override map against their registry. Until a phase ships, the\n * dispatcher emits a one-shot structured warning when it sees an\n * override targeting that unwired domain \u2014 the runtime never throws on\n * unwired domains so app boot stays unaffected during the rollout.\n *\n * Resolution order across all domains (highest precedence first):\n *\n * 1. Programmatic \u2014 direct calls into the per-domain `apply*Overrides()` API.\n * 2. `modules.ts` inline \u2014 `entry.overrides.<domain>` here.\n * 3. File-based \u2014 overrides exported from a contributing module's own files.\n * 4. Base \u2014 the module's own registrations.\n *\n * `null` always means \"disable\"; a definition replaces.\n */\n\n// ---------------------------------------------------------------------------\n// Domain sub-shapes\n// ---------------------------------------------------------------------------\n\n/**\n * AI domain \u2014 agents and tools. Re-exports the canonical maps from\n * `@open-mercato/ai-assistant` so consumers do not need to import that\n * package directly when they only want to declare overrides.\n *\n * Imported lazily as `unknown` here because `@open-mercato/shared` must\n * NOT take a runtime dependency on `@open-mercato/ai-assistant` (the\n * dependency direction is the other way around). Apps that author\n * `entry.overrides.ai` should import the strongly-typed\n * `AiAgentOverridesMap` / `AiToolOverridesMap` from `@open-mercato/ai-assistant`\n * directly \u2014 TypeScript structurally compatible types make the loose\n * shape here a no-op annotation cost.\n */\nexport interface AiOverridesShape {\n agents?: Record<string, unknown>\n tools?: Record<string, unknown>\n extensions?: unknown[]\n}\n\n/** Phase 2/3 \u2014 routes (api + pages). Stubbed until wired. */\nexport interface RoutesOverridesShape {\n api?: Record<string, unknown>\n pages?: Record<string, unknown>\n}\n\n/** Phase 4 \u2014 event subscribers. Stubbed until wired. */\nexport interface EventsOverridesShape {\n subscribers?: Record<string, unknown>\n}\n\n/** Phase 6/7/8 \u2014 widget injection, component handles, dashboard widgets. */\nexport interface WidgetsOverridesShape {\n injection?: Record<string, unknown>\n components?: Record<string, unknown>\n dashboard?: Record<string, unknown>\n}\n\n/** Phase 9 \u2014 notification types + handlers. */\nexport interface NotificationsOverridesShape {\n types?: Record<string, unknown>\n handlers?: Record<string, unknown>\n}\n\n/** Phase 15 \u2014 setup lifecycle hooks. */\nexport interface SetupOverridesShape {\n defaultRoleFeatures?: Record<string, readonly string[]>\n seedDefaults?: false\n seedExamples?: false\n onTenantCreated?: false\n}\n\n/** Phase 16 \u2014 ACL features (per-feature override). */\nexport interface AclOverridesShape {\n features?: Record<string, unknown>\n}\n\n/** Phase 18 \u2014 encryption maps per entity id. */\nexport interface EncryptionOverridesShape {\n maps?: Record<string, unknown>\n}\n\n/**\n * Umbrella shape for `entry.overrides`. Every key is optional; a\n * downstream app sets only the domains it cares about.\n */\nexport interface ModuleOverrides {\n ai?: AiOverridesShape\n routes?: RoutesOverridesShape\n events?: EventsOverridesShape\n workers?: Record<string, unknown>\n widgets?: WidgetsOverridesShape\n notifications?: NotificationsOverridesShape\n interceptors?: Record<string, unknown>\n commandInterceptors?: Record<string, unknown>\n enrichers?: Record<string, unknown>\n guards?: Record<string, unknown>\n cli?: Record<string, unknown>\n setup?: SetupOverridesShape\n acl?: AclOverridesShape\n di?: Record<string, unknown>\n encryption?: EncryptionOverridesShape\n}\n\n/**\n * Public shape consumed by the dispatcher. Mirrors the `ModuleEntry`\n * defined in each app's `modules.ts` \u2014 the dispatcher only needs `id`\n * and `overrides`.\n */\nexport interface ModuleEntryWithOverrides {\n id: string\n from?: string\n overrides?: ModuleOverrides\n}\n\n// ---------------------------------------------------------------------------\n// Per-domain runtime hook registry\n// ---------------------------------------------------------------------------\n\n/**\n * Each wired domain registers an applier that receives the list of\n * `(moduleId, overrides)` pairs in module-load order and forwards them\n * to its own runtime hook. Unwired domains do not register an applier\n * and instead trigger the dispatcher's one-shot warning.\n */\nexport type ModuleOverrideDomain =\n | 'ai'\n | 'routes'\n | 'events'\n | 'workers'\n | 'widgets'\n | 'notifications'\n | 'interceptors'\n | 'commandInterceptors'\n | 'enrichers'\n | 'guards'\n | 'cli'\n | 'setup'\n | 'acl'\n | 'di'\n | 'encryption'\n\nexport interface ModuleOverrideEntry<TShape> {\n moduleId: string\n overrides: TShape\n}\n\nexport type ModuleOverrideApplier<TShape> = (\n entries: ReadonlyArray<ModuleOverrideEntry<TShape>>,\n) => void\n\nconst appliers = new Map<ModuleOverrideDomain, ModuleOverrideApplier<unknown>>()\nconst warnedUnwiredDomains = new Set<ModuleOverrideDomain>()\n\n/**\n * Register a per-domain runtime hook. Called once at module-load time\n * by each wired domain (e.g. the AI subsystem registers `'ai'` from\n * `@open-mercato/ai-assistant`).\n */\nexport function registerModuleOverrideApplier<TShape>(\n domain: ModuleOverrideDomain,\n applier: ModuleOverrideApplier<TShape>,\n): void {\n appliers.set(domain, applier as ModuleOverrideApplier<unknown>)\n}\n\n/** @__internal Test-only hook \u2014 clear all registered appliers + warnings. */\nexport function resetModuleOverrideAppliersForTests(): void {\n appliers.clear()\n warnedUnwiredDomains.clear()\n}\n\n// ---------------------------------------------------------------------------\n// Dispatcher\n// ---------------------------------------------------------------------------\n\nconst DOMAIN_KEYS: ModuleOverrideDomain[] = [\n 'ai',\n 'routes',\n 'events',\n 'workers',\n 'widgets',\n 'notifications',\n 'interceptors',\n 'commandInterceptors',\n 'enrichers',\n 'guards',\n 'cli',\n 'setup',\n 'acl',\n 'di',\n 'encryption',\n]\n\nconst TRACKING_ISSUE_HINT =\n 'See `.ai/specs/2026-05-04-modules-ts-unified-overrides.md` and tracking issue https://github.com/open-mercato/open-mercato/issues/1787.'\n\n/**\n * Walk every `ModuleEntry` and dispatch its `overrides.<domain>` shape\n * to the matching wired applier. Unwired domains emit a one-shot\n * structured warning.\n *\n * Call this exactly once from `apps/<app>/src/bootstrap.ts` BEFORE any\n * registry first-loads. Calling it more than once is safe but\n * accumulates per-domain entries each time.\n */\nexport function applyModuleOverridesFromEnabledModules(\n modules: ReadonlyArray<ModuleEntryWithOverrides>,\n): void {\n if (!Array.isArray(modules) || modules.length === 0) return\n\n // Bucket entries by domain in module-load order.\n const buckets = new Map<ModuleOverrideDomain, Array<ModuleOverrideEntry<unknown>>>()\n\n for (const entry of modules) {\n if (!entry || typeof entry.id !== 'string' || !entry.id) continue\n const overrides = entry.overrides\n if (!overrides || typeof overrides !== 'object') continue\n\n for (const domain of DOMAIN_KEYS) {\n const value = (overrides as Record<string, unknown>)[domain]\n if (value === undefined || value === null) continue\n if (typeof value !== 'object') continue\n const list = buckets.get(domain) ?? []\n list.push({ moduleId: entry.id, overrides: value })\n buckets.set(domain, list)\n }\n }\n\n // Dispatch each domain to its wired applier; warn on unwired domains.\n for (const [domain, entries] of buckets) {\n const applier = appliers.get(domain)\n if (!applier) {\n if (!warnedUnwiredDomains.has(domain)) {\n warnedUnwiredDomains.add(domain)\n const moduleIds = Array.from(new Set(entries.map((e) => e.moduleId))).join(', ')\n console.warn(\n `[Module Overrides] Domain \"${domain}\" not yet wired \u2014 entry.overrides.${domain} for module(s) [${moduleIds}] was ignored. ${TRACKING_ISSUE_HINT}`,\n )\n }\n continue\n }\n applier(entries)\n }\n}\n"],
5
+ "mappings": "AA4KA,MAAM,WAAW,oBAAI,IAA0D;AAC/E,MAAM,uBAAuB,oBAAI,IAA0B;AAOpD,SAAS,8BACd,QACA,SACM;AACN,WAAS,IAAI,QAAQ,OAAyC;AAChE;AAGO,SAAS,sCAA4C;AAC1D,WAAS,MAAM;AACf,uBAAqB,MAAM;AAC7B;AAMA,MAAM,cAAsC;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,sBACJ;AAWK,SAAS,uCACd,SACM;AACN,MAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,QAAQ,WAAW,EAAG;AAGrD,QAAM,UAAU,oBAAI,IAA+D;AAEnF,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,SAAS,OAAO,MAAM,OAAO,YAAY,CAAC,MAAM,GAAI;AACzD,UAAM,YAAY,MAAM;AACxB,QAAI,CAAC,aAAa,OAAO,cAAc,SAAU;AAEjD,eAAW,UAAU,aAAa;AAChC,YAAM,QAAS,UAAsC,MAAM;AAC3D,UAAI,UAAU,UAAa,UAAU,KAAM;AAC3C,UAAI,OAAO,UAAU,SAAU;AAC/B,YAAM,OAAO,QAAQ,IAAI,MAAM,KAAK,CAAC;AACrC,WAAK,KAAK,EAAE,UAAU,MAAM,IAAI,WAAW,MAAM,CAAC;AAClD,cAAQ,IAAI,QAAQ,IAAI;AAAA,IAC1B;AAAA,EACF;AAGA,aAAW,CAAC,QAAQ,OAAO,KAAK,SAAS;AACvC,UAAM,UAAU,SAAS,IAAI,MAAM;AACnC,QAAI,CAAC,SAAS;AACZ,UAAI,CAAC,qBAAqB,IAAI,MAAM,GAAG;AACrC,6BAAqB,IAAI,MAAM;AAC/B,cAAM,YAAY,MAAM,KAAK,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,EAAE,KAAK,IAAI;AAC/E,gBAAQ;AAAA,UACN,8BAA8B,MAAM,0CAAqC,MAAM,mBAAmB,SAAS,kBAAkB,mBAAmB;AAAA,QAClJ;AAAA,MACF;AACA;AAAA,IACF;AACA,YAAQ,OAAO;AAAA,EACjB;AACF;",
6
+ "names": []
7
+ }
@@ -105,6 +105,13 @@ function registerFrontendRouteManifests(routes) {
105
105
  function getFrontendRouteManifests() {
106
106
  return _frontendRouteManifests ?? [];
107
107
  }
108
+ let _apiRouteManifests = null;
109
+ function registerApiRouteManifests(routes) {
110
+ _apiRouteManifests = routes;
111
+ }
112
+ function getApiRouteManifests() {
113
+ return _apiRouteManifests ?? [];
114
+ }
108
115
  let _cliModules = null;
109
116
  function registerCliModules(modules) {
110
117
  if (_cliModules !== null && process.env.NODE_ENV === "development") {
@@ -177,12 +184,14 @@ export {
177
184
  findBackendMatch,
178
185
  findFrontendMatch,
179
186
  findRouteManifestMatch,
187
+ getApiRouteManifests,
180
188
  getBackendRouteManifests,
181
189
  getCliModules,
182
190
  getDefaultEncryptionMaps,
183
191
  getFrontendRouteManifests,
184
192
  hasCliModules,
185
193
  matchRoutePattern,
194
+ registerApiRouteManifests,
186
195
  registerBackendRouteManifests,
187
196
  registerCliModules,
188
197
  registerFrontendRouteManifests
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/modules/registry.ts"],
4
- "sourcesContent": ["import type { ReactNode } from 'react'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi/types'\nimport type { SyncCrudEventResult } from '../lib/crud/sync-event-types'\nimport type { DashboardWidgetModule } from './dashboard/widgets'\nimport type { InjectionAnyWidgetModule, ModuleInjectionTable } from './widgets/injection'\nimport type { IntegrationBundle, IntegrationDefinition } from './integrations/types'\n\n// Context passed to dynamic metadata guards\nexport type RouteVisibilityContext = { path?: string; auth?: any }\n\n/**\n * Portal sidebar navigation hint. When declared on a portal page's metadata,\n * the page is auto-listed in the portal sidebar (subject to RBAC) by the\n * `/api/customer_accounts/portal/nav` endpoint.\n *\n * Absence of `nav` means the page is routable but not auto-listed (useful for\n * detail pages, create forms, etc.).\n */\nexport type PortalNavMetadata = {\n label: string\n labelKey?: string\n group?: 'main' | 'account'\n order?: number\n icon?: string\n}\n\n// Metadata you can export from page.meta.ts or directly from a server page\nexport type PageMetadata = {\n requireAuth?: boolean\n /** @deprecated Use `requireFeatures` instead \u2014 role names are mutable and can be spoofed */\n requireRoles?: readonly string[]\n // Optional fine-grained feature requirements\n requireFeatures?: readonly string[]\n // Portal: require customer (portal user) authentication instead of staff auth\n requireCustomerAuth?: boolean\n // Portal: require customer-specific features (checked against CustomerRbacService)\n requireCustomerFeatures?: readonly string[]\n // Portal: optional sidebar presentation hint (auto-listed by portal nav endpoint)\n nav?: PortalNavMetadata\n // Titles and grouping (aliases supported)\n title?: string\n titleKey?: string\n pageTitle?: string\n pageTitleKey?: string\n group?: string\n groupKey?: string\n pageGroup?: string\n pageGroupKey?: string\n // Ordering and visuals\n order?: number\n pageOrder?: number\n icon?: ReactNode\n navHidden?: boolean\n // Dynamic flags\n visible?: (ctx: RouteVisibilityContext) => boolean | Promise<boolean>\n enabled?: (ctx: RouteVisibilityContext) => boolean | Promise<boolean>\n // Optional static breadcrumb trail for header\n breadcrumb?: Array<{ label: string; labelKey?: string; href?: string }>\n // Navigation context for tiered navigation:\n // - 'main' (default): Main sidebar business operations\n // - 'admin': Collapsible \"Settings & Admin\" section at bottom of sidebar\n // - 'settings': Hidden from sidebar, only accessible via Settings hub page\n // - 'profile': Profile dropdown items\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n placement?: {\n section: string\n sectionLabel?: string\n sectionLabelKey?: string\n order?: number\n }\n}\n\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n\nexport type ApiHandler = (req: Request, ctx?: any) => Promise<Response> | Response\n\nexport type ModuleSubscriberHandler = (\n payload: any,\n ctx: any\n) => Promise<void | SyncCrudEventResult> | void | SyncCrudEventResult\n\nexport type ModuleWorkerHandler = (job: unknown, ctx: unknown) => Promise<void> | void\n\nexport type ModuleRoute = {\n pattern?: string\n path?: string\n requireAuth?: boolean\n /** @deprecated Use `requireFeatures` instead \u2014 role names are mutable and can be spoofed */\n requireRoles?: string[]\n // Optional fine-grained feature requirements\n requireFeatures?: string[]\n // Portal: require customer (portal user) authentication instead of staff auth\n requireCustomerAuth?: boolean\n // Portal: require customer-specific features (checked against CustomerRbacService)\n requireCustomerFeatures?: string[]\n // Portal: optional sidebar presentation hint (auto-listed by portal nav endpoint)\n nav?: PortalNavMetadata\n title?: string\n titleKey?: string\n group?: string\n groupKey?: string\n icon?: ReactNode\n order?: number\n priority?: number\n navHidden?: boolean\n visible?: (ctx: RouteVisibilityContext) => boolean | Promise<boolean>\n enabled?: (ctx: RouteVisibilityContext) => boolean | Promise<boolean>\n breadcrumb?: Array<{ label: string; labelKey?: string; href?: string }>\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n placement?: {\n section: string\n sectionLabel?: string\n sectionLabelKey?: string\n order?: number\n }\n Component: (props: any) => ReactNode | Promise<ReactNode>\n}\n\nexport type ModuleApiLegacy = {\n method: HttpMethod\n path: string\n handler: ApiHandler\n metadata?: Record<string, unknown>\n docs?: OpenApiMethodDoc\n}\n\nexport type ModuleApiRouteFile = {\n path: string\n handlers: Partial<Record<HttpMethod, ApiHandler>>\n requireAuth?: boolean\n /** @deprecated Use `requireFeatures` instead \u2014 role names are mutable and can be spoofed */\n requireRoles?: string[]\n // Optional fine-grained feature requirements for the entire route file\n // Note: per-method feature requirements should be expressed inside metadata\n requireFeatures?: string[]\n docs?: OpenApiRouteDoc\n metadata?: Partial<Record<HttpMethod, unknown>>\n}\n\nexport type ModuleApi = ModuleApiLegacy | ModuleApiRouteFile\n\nexport type RouteMatchParams = Record<string, string | string[]>\n\nexport type FrontendRouteManifestEntry = Omit<ModuleRoute, 'Component'> & {\n moduleId: string\n load: () => Promise<ModuleRoute['Component']>\n}\n\nexport type BackendRouteManifestEntry = Omit<ModuleRoute, 'Component'> & {\n moduleId: string\n load: () => Promise<ModuleRoute['Component']>\n}\n\nexport type ApiRouteManifestEntry = {\n moduleId: string\n kind: 'route-file' | 'legacy'\n path: string\n methods: HttpMethod[]\n method?: HttpMethod\n load: () => Promise<Record<string, unknown>>\n}\n\nexport type ModuleCli = {\n command: string\n run: (argv: string[]) => Promise<void> | void\n}\n\nexport type ModuleSubscriber = {\n id: string\n event: string\n persistent?: boolean\n sync?: boolean\n priority?: number\n handler: ModuleSubscriberHandler\n}\n\nexport type ModuleWorker = {\n id: string\n queue: string\n concurrency: number\n handler: ModuleWorkerHandler\n}\n\nexport type ModuleInfo = {\n name?: string\n title?: string\n version?: string\n description?: string\n author?: string\n license?: string\n homepage?: string\n copyright?: string\n // Optional hard dependencies: module ids that must be enabled\n requires?: string[]\n // Whether this module can be ejected into the app's src/modules/ for customization\n ejectable?: boolean\n}\n\nexport type ModuleDashboardWidgetEntry = {\n moduleId: string\n key: string\n source: 'app' | 'package'\n loader: () => Promise<DashboardWidgetModule<any>>\n}\n\nexport type ModuleInjectionWidgetEntry = {\n moduleId: string\n key: string\n source: 'app' | 'package'\n loader: () => Promise<InjectionAnyWidgetModule<any, any>>\n}\n\nexport type Module = {\n id: string\n info?: ModuleInfo\n backendRoutes?: ModuleRoute[]\n frontendRoutes?: ModuleRoute[]\n apis?: ModuleApi[]\n cli?: ModuleCli[]\n translations?: Record<string, Record<string, string>>\n // Optional: per-module feature declarations discovered from acl.ts (module root)\n features?: Array<{ id: string; title: string; module: string }>\n // Auto-discovered event subscribers\n subscribers?: ModuleSubscriber[]\n // Auto-discovered queue workers\n workers?: ModuleWorker[]\n // Optional: per-module declared entity extensions and custom fields (static)\n // Extensions discovered from data/extensions.ts; Custom fields discovered from ce.ts (entities[].fields)\n entityExtensions?: import('./entities').EntityExtension[]\n customFieldSets?: import('./entities').CustomFieldSet[]\n // Optional: per-module declared custom entities (virtual/logical entities)\n // Discovered from ce.ts (module root). Each entry represents an entityId with optional label/description.\n customEntities?: Array<{ id: string; label?: string; description?: string }>\n dashboardWidgets?: ModuleDashboardWidgetEntry[]\n injectionWidgets?: ModuleInjectionWidgetEntry[]\n injectionTable?: ModuleInjectionTable\n // Optional: per-module vector search configuration (discovered from vector.ts)\n vector?: import('./vector').VectorModuleConfig\n // Optional: module-specific tenant setup configuration (from setup.ts)\n setup?: import('./setup').ModuleSetupConfig\n // Optional: default encryption maps owned by the module (from encryption.ts)\n defaultEncryptionMaps?: import('./encryption').ModuleEncryptionMap[]\n // Optional: integration marketplace declarations discovered from integration.ts\n integrations?: IntegrationDefinition[]\n bundles?: IntegrationBundle[]\n}\n\nfunction normPath(s: string) {\n return (s.startsWith('/') ? s : '/' + s).replace(/\\/+$/, '') || '/'\n}\n\nexport function matchRoutePattern(pattern: string, pathname: string): RouteMatchParams | undefined {\n const p = normPath(pattern)\n const u = normPath(pathname)\n const pSegs = p.split('/').slice(1)\n const uSegs = u.split('/').slice(1)\n const params: Record<string, string | string[]> = {}\n let i = 0\n for (let j = 0; j < pSegs.length; j++, i++) {\n const seg = pSegs[j]\n const mCatchAll = seg.match(/^\\[\\.\\.\\.(.+)\\]$/)\n const mOptCatch = seg.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/)\n const mDyn = seg.match(/^\\[(.+)\\]$/)\n if (mCatchAll) {\n const key = mCatchAll[1]\n if (i >= uSegs.length) return undefined\n params[key] = uSegs.slice(i)\n i = uSegs.length\n return i === uSegs.length ? params : undefined\n } else if (mOptCatch) {\n const key = mOptCatch[1]\n params[key] = i < uSegs.length ? uSegs.slice(i) : []\n i = uSegs.length\n return params\n } else if (mDyn) {\n if (i >= uSegs.length) return undefined\n params[mDyn[1]] = uSegs[i]\n } else {\n if (i >= uSegs.length || uSegs[i].toLowerCase() !== seg.toLowerCase()) return undefined\n }\n }\n if (i !== uSegs.length) return undefined\n return params\n}\n\nfunction getPattern(r: ModuleRoute) {\n return r.pattern ?? r.path ?? '/'\n}\n\nexport function findFrontendMatch(modules: Module[], pathname: string): { route: ModuleRoute; params: Record<string, string | string[]> } | undefined {\n for (const m of modules) {\n const routes = m.frontendRoutes ?? []\n for (const r of routes) {\n const params = matchRoutePattern(getPattern(r), pathname)\n if (params) return { route: r, params }\n }\n }\n}\n\nexport function findBackendMatch(modules: Module[], pathname: string): { route: ModuleRoute; params: Record<string, string | string[]> } | undefined {\n for (const m of modules) {\n const routes = m.backendRoutes ?? []\n for (const r of routes) {\n const params = matchRoutePattern(getPattern(r), pathname)\n if (params) return { route: r, params }\n }\n }\n}\n\nexport function findApi(modules: Module[], method: HttpMethod, pathname: string): { handler: ApiHandler; params: Record<string, string | string[]>; requireAuth?: boolean; requireRoles?: string[]; metadata?: any } | undefined {\n for (const m of modules) {\n const apis = m.apis ?? []\n for (const a of apis) {\n if ('handlers' in a) {\n const params = matchRoutePattern(a.path, pathname)\n const handler = (a.handlers as any)[method]\n if (params && handler) return { handler, params, requireAuth: a.requireAuth, requireRoles: (a as any).requireRoles, metadata: (a as any).metadata }\n } else {\n const al = a as ModuleApiLegacy\n if (al.method !== method) continue\n const params = matchRoutePattern(al.path, pathname)\n if (params) {\n return { handler: al.handler, params, metadata: al.metadata }\n }\n }\n }\n }\n}\n\nexport function findRouteManifestMatch<T extends { pattern?: string; path?: string }>(\n routes: T[],\n pathname: string\n): { route: T; params: RouteMatchParams } | undefined {\n for (const route of routes) {\n const params = matchRoutePattern(route.pattern ?? route.path ?? '/', pathname)\n if (params) {\n return { route, params }\n }\n }\n}\n\nexport function findApiRouteManifestMatch<T extends { path: string; methods: HttpMethod[] }>(\n routes: T[],\n method: HttpMethod,\n pathname: string\n): { route: T; params: RouteMatchParams } | undefined {\n for (const route of routes) {\n if (!route.methods.includes(method)) continue\n const params = matchRoutePattern(route.path, pathname)\n if (params) {\n return { route, params }\n }\n }\n}\n\nlet _backendRouteManifests: BackendRouteManifestEntry[] | null = null\n\nexport function registerBackendRouteManifests(routes: BackendRouteManifestEntry[]) {\n _backendRouteManifests = routes\n}\n\nexport function getBackendRouteManifests(): BackendRouteManifestEntry[] {\n return _backendRouteManifests ?? []\n}\n\nlet _frontendRouteManifests: FrontendRouteManifestEntry[] | null = null\n\nexport function registerFrontendRouteManifests(routes: FrontendRouteManifestEntry[]) {\n _frontendRouteManifests = routes\n}\n\nexport function getFrontendRouteManifests(): FrontendRouteManifestEntry[] {\n return _frontendRouteManifests ?? []\n}\n\n// CLI modules registry - shared between CLI and module workers\nlet _cliModules: Module[] | null = null\n\nexport function registerCliModules(modules: Module[]) {\n if (_cliModules !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] CLI modules re-registered (this may occur during HMR)')\n }\n _cliModules = modules\n}\n\nexport function getCliModules(): Module[] {\n // Return empty array if not registered - allows generate command to work without bootstrap\n return _cliModules ?? []\n}\n\nexport function hasCliModules(): boolean {\n return _cliModules !== null && _cliModules.length > 0\n}\n\nexport function getDefaultEncryptionMaps(modules: Module[]): import('./encryption').ModuleEncryptionMap[] {\n const byEntityId = new Map<string, { moduleId: string; map: import('./encryption').ModuleEncryptionMap }>()\n\n for (const mod of modules) {\n for (const entry of mod.defaultEncryptionMaps ?? []) {\n const previous = byEntityId.get(entry.entityId)\n if (previous) {\n throw new Error(\n `[registry] Duplicate default encryption map for \"${entry.entityId}\" declared by \"${previous.moduleId}\" and \"${mod.id}\"`\n )\n }\n byEntityId.set(entry.entityId, {\n moduleId: mod.id,\n map: {\n entityId: entry.entityId,\n fields: entry.fields.map((field) => ({\n field: field.field,\n hashField: field.hashField ?? null,\n })),\n },\n })\n }\n }\n\n return Array.from(byEntityId.values(), ({ map }) => map)\n}\n\nfunction ensureLazyHandler<T extends (...args: any[]) => any>(\n loaded: unknown,\n kind: 'subscriber' | 'worker',\n id: string\n): T {\n const handler = typeof loaded === 'function'\n ? loaded\n : loaded && typeof loaded === 'object' && 'default' in loaded\n ? (loaded as Record<string, unknown>).default\n : null\n if (typeof handler !== 'function') {\n throw new Error(`[registry] Invalid ${kind} module \"${id}\" (missing default export handler)`)\n }\n return handler as T\n}\n\nexport function createLazyModuleSubscriber(\n loadModule: () => Promise<unknown>,\n id: string\n): ModuleSubscriberHandler {\n let handlerPromise: Promise<ModuleSubscriberHandler> | null = null\n return async (payload, ctx) => {\n handlerPromise ??= loadModule().then((loaded) =>\n ensureLazyHandler<ModuleSubscriberHandler>(loaded, 'subscriber', id)\n )\n const handler = await handlerPromise\n return handler(payload, ctx)\n }\n}\n\nexport function createLazyModuleWorker(\n loadModule: () => Promise<unknown>,\n id: string\n): ModuleWorkerHandler {\n let handlerPromise: Promise<ModuleWorkerHandler> | null = null\n return async (job, ctx) => {\n handlerPromise ??= loadModule().then((loaded) =>\n ensureLazyHandler<ModuleWorkerHandler>(loaded, 'worker', id)\n )\n const handler = await handlerPromise\n return handler(job, ctx)\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { ReactNode } from 'react'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi/types'\nimport type { SyncCrudEventResult } from '../lib/crud/sync-event-types'\nimport type { DashboardWidgetModule } from './dashboard/widgets'\nimport type { InjectionAnyWidgetModule, ModuleInjectionTable } from './widgets/injection'\nimport type { IntegrationBundle, IntegrationDefinition } from './integrations/types'\n\n// Context passed to dynamic metadata guards\nexport type RouteVisibilityContext = { path?: string; auth?: any }\n\n/**\n * Portal sidebar navigation hint. When declared on a portal page's metadata,\n * the page is auto-listed in the portal sidebar (subject to RBAC) by the\n * `/api/customer_accounts/portal/nav` endpoint.\n *\n * Absence of `nav` means the page is routable but not auto-listed (useful for\n * detail pages, create forms, etc.).\n */\nexport type PortalNavMetadata = {\n label: string\n labelKey?: string\n group?: 'main' | 'account'\n order?: number\n icon?: string\n}\n\n// Metadata you can export from page.meta.ts or directly from a server page\nexport type PageMetadata = {\n requireAuth?: boolean\n /** @deprecated Use `requireFeatures` instead \u2014 role names are mutable and can be spoofed */\n requireRoles?: readonly string[]\n // Optional fine-grained feature requirements\n requireFeatures?: readonly string[]\n // Portal: require customer (portal user) authentication instead of staff auth\n requireCustomerAuth?: boolean\n // Portal: require customer-specific features (checked against CustomerRbacService)\n requireCustomerFeatures?: readonly string[]\n // Portal: optional sidebar presentation hint (auto-listed by portal nav endpoint)\n nav?: PortalNavMetadata\n // Titles and grouping (aliases supported)\n title?: string\n titleKey?: string\n pageTitle?: string\n pageTitleKey?: string\n group?: string\n groupKey?: string\n pageGroup?: string\n pageGroupKey?: string\n // Ordering and visuals\n order?: number\n pageOrder?: number\n icon?: ReactNode\n navHidden?: boolean\n // Dynamic flags\n visible?: (ctx: RouteVisibilityContext) => boolean | Promise<boolean>\n enabled?: (ctx: RouteVisibilityContext) => boolean | Promise<boolean>\n // Optional static breadcrumb trail for header\n breadcrumb?: Array<{ label: string; labelKey?: string; href?: string }>\n // Navigation context for tiered navigation:\n // - 'main' (default): Main sidebar business operations\n // - 'admin': Collapsible \"Settings & Admin\" section at bottom of sidebar\n // - 'settings': Hidden from sidebar, only accessible via Settings hub page\n // - 'profile': Profile dropdown items\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n placement?: {\n section: string\n sectionLabel?: string\n sectionLabelKey?: string\n order?: number\n }\n}\n\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n\nexport type ApiHandler = (req: Request, ctx?: any) => Promise<Response> | Response\n\nexport type ModuleSubscriberHandler = (\n payload: any,\n ctx: any\n) => Promise<void | SyncCrudEventResult> | void | SyncCrudEventResult\n\nexport type ModuleWorkerHandler = (job: unknown, ctx: unknown) => Promise<void> | void\n\nexport type ModuleRoute = {\n pattern?: string\n path?: string\n requireAuth?: boolean\n /** @deprecated Use `requireFeatures` instead \u2014 role names are mutable and can be spoofed */\n requireRoles?: string[]\n // Optional fine-grained feature requirements\n requireFeatures?: string[]\n // Portal: require customer (portal user) authentication instead of staff auth\n requireCustomerAuth?: boolean\n // Portal: require customer-specific features (checked against CustomerRbacService)\n requireCustomerFeatures?: string[]\n // Portal: optional sidebar presentation hint (auto-listed by portal nav endpoint)\n nav?: PortalNavMetadata\n title?: string\n titleKey?: string\n group?: string\n groupKey?: string\n icon?: ReactNode\n order?: number\n priority?: number\n navHidden?: boolean\n visible?: (ctx: RouteVisibilityContext) => boolean | Promise<boolean>\n enabled?: (ctx: RouteVisibilityContext) => boolean | Promise<boolean>\n breadcrumb?: Array<{ label: string; labelKey?: string; href?: string }>\n pageContext?: 'main' | 'admin' | 'settings' | 'profile'\n placement?: {\n section: string\n sectionLabel?: string\n sectionLabelKey?: string\n order?: number\n }\n Component: (props: any) => ReactNode | Promise<ReactNode>\n}\n\nexport type ModuleApiLegacy = {\n method: HttpMethod\n path: string\n handler: ApiHandler\n metadata?: Record<string, unknown>\n docs?: OpenApiMethodDoc\n}\n\nexport type ModuleApiRouteFile = {\n path: string\n handlers: Partial<Record<HttpMethod, ApiHandler>>\n requireAuth?: boolean\n /** @deprecated Use `requireFeatures` instead \u2014 role names are mutable and can be spoofed */\n requireRoles?: string[]\n // Optional fine-grained feature requirements for the entire route file\n // Note: per-method feature requirements should be expressed inside metadata\n requireFeatures?: string[]\n docs?: OpenApiRouteDoc\n metadata?: Partial<Record<HttpMethod, unknown>>\n}\n\nexport type ModuleApi = ModuleApiLegacy | ModuleApiRouteFile\n\nexport type RouteMatchParams = Record<string, string | string[]>\n\nexport type FrontendRouteManifestEntry = Omit<ModuleRoute, 'Component'> & {\n moduleId: string\n load: () => Promise<ModuleRoute['Component']>\n}\n\nexport type BackendRouteManifestEntry = Omit<ModuleRoute, 'Component'> & {\n moduleId: string\n load: () => Promise<ModuleRoute['Component']>\n}\n\nexport type ApiRouteManifestEntry = {\n moduleId: string\n kind: 'route-file' | 'legacy'\n path: string\n methods: HttpMethod[]\n method?: HttpMethod\n load: () => Promise<Record<string, unknown>>\n}\n\nexport type ModuleCli = {\n command: string\n run: (argv: string[]) => Promise<void> | void\n}\n\nexport type ModuleSubscriber = {\n id: string\n event: string\n persistent?: boolean\n sync?: boolean\n priority?: number\n handler: ModuleSubscriberHandler\n}\n\nexport type ModuleWorker = {\n id: string\n queue: string\n concurrency: number\n handler: ModuleWorkerHandler\n}\n\nexport type ModuleInfo = {\n name?: string\n title?: string\n version?: string\n description?: string\n author?: string\n license?: string\n homepage?: string\n copyright?: string\n // Optional hard dependencies: module ids that must be enabled\n requires?: string[]\n // Whether this module can be ejected into the app's src/modules/ for customization\n ejectable?: boolean\n}\n\nexport type ModuleDashboardWidgetEntry = {\n moduleId: string\n key: string\n source: 'app' | 'package'\n loader: () => Promise<DashboardWidgetModule<any>>\n}\n\nexport type ModuleInjectionWidgetEntry = {\n moduleId: string\n key: string\n source: 'app' | 'package'\n loader: () => Promise<InjectionAnyWidgetModule<any, any>>\n}\n\nexport type Module = {\n id: string\n info?: ModuleInfo\n backendRoutes?: ModuleRoute[]\n frontendRoutes?: ModuleRoute[]\n apis?: ModuleApi[]\n cli?: ModuleCli[]\n translations?: Record<string, Record<string, string>>\n // Optional: per-module feature declarations discovered from acl.ts (module root)\n features?: Array<{ id: string; title: string; module: string }>\n // Auto-discovered event subscribers\n subscribers?: ModuleSubscriber[]\n // Auto-discovered queue workers\n workers?: ModuleWorker[]\n // Optional: per-module declared entity extensions and custom fields (static)\n // Extensions discovered from data/extensions.ts; Custom fields discovered from ce.ts (entities[].fields)\n entityExtensions?: import('./entities').EntityExtension[]\n customFieldSets?: import('./entities').CustomFieldSet[]\n // Optional: per-module declared custom entities (virtual/logical entities)\n // Discovered from ce.ts (module root). Each entry represents an entityId with optional label/description.\n customEntities?: Array<{ id: string; label?: string; description?: string }>\n dashboardWidgets?: ModuleDashboardWidgetEntry[]\n injectionWidgets?: ModuleInjectionWidgetEntry[]\n injectionTable?: ModuleInjectionTable\n // Optional: per-module vector search configuration (discovered from vector.ts)\n vector?: import('./vector').VectorModuleConfig\n // Optional: module-specific tenant setup configuration (from setup.ts)\n setup?: import('./setup').ModuleSetupConfig\n // Optional: default encryption maps owned by the module (from encryption.ts)\n defaultEncryptionMaps?: import('./encryption').ModuleEncryptionMap[]\n // Optional: integration marketplace declarations discovered from integration.ts\n integrations?: IntegrationDefinition[]\n bundles?: IntegrationBundle[]\n}\n\nfunction normPath(s: string) {\n return (s.startsWith('/') ? s : '/' + s).replace(/\\/+$/, '') || '/'\n}\n\nexport function matchRoutePattern(pattern: string, pathname: string): RouteMatchParams | undefined {\n const p = normPath(pattern)\n const u = normPath(pathname)\n const pSegs = p.split('/').slice(1)\n const uSegs = u.split('/').slice(1)\n const params: Record<string, string | string[]> = {}\n let i = 0\n for (let j = 0; j < pSegs.length; j++, i++) {\n const seg = pSegs[j]\n const mCatchAll = seg.match(/^\\[\\.\\.\\.(.+)\\]$/)\n const mOptCatch = seg.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/)\n const mDyn = seg.match(/^\\[(.+)\\]$/)\n if (mCatchAll) {\n const key = mCatchAll[1]\n if (i >= uSegs.length) return undefined\n params[key] = uSegs.slice(i)\n i = uSegs.length\n return i === uSegs.length ? params : undefined\n } else if (mOptCatch) {\n const key = mOptCatch[1]\n params[key] = i < uSegs.length ? uSegs.slice(i) : []\n i = uSegs.length\n return params\n } else if (mDyn) {\n if (i >= uSegs.length) return undefined\n params[mDyn[1]] = uSegs[i]\n } else {\n if (i >= uSegs.length || uSegs[i].toLowerCase() !== seg.toLowerCase()) return undefined\n }\n }\n if (i !== uSegs.length) return undefined\n return params\n}\n\nfunction getPattern(r: ModuleRoute) {\n return r.pattern ?? r.path ?? '/'\n}\n\nexport function findFrontendMatch(modules: Module[], pathname: string): { route: ModuleRoute; params: Record<string, string | string[]> } | undefined {\n for (const m of modules) {\n const routes = m.frontendRoutes ?? []\n for (const r of routes) {\n const params = matchRoutePattern(getPattern(r), pathname)\n if (params) return { route: r, params }\n }\n }\n}\n\nexport function findBackendMatch(modules: Module[], pathname: string): { route: ModuleRoute; params: Record<string, string | string[]> } | undefined {\n for (const m of modules) {\n const routes = m.backendRoutes ?? []\n for (const r of routes) {\n const params = matchRoutePattern(getPattern(r), pathname)\n if (params) return { route: r, params }\n }\n }\n}\n\nexport function findApi(modules: Module[], method: HttpMethod, pathname: string): { handler: ApiHandler; params: Record<string, string | string[]>; requireAuth?: boolean; requireRoles?: string[]; metadata?: any } | undefined {\n for (const m of modules) {\n const apis = m.apis ?? []\n for (const a of apis) {\n if ('handlers' in a) {\n const params = matchRoutePattern(a.path, pathname)\n const handler = (a.handlers as any)[method]\n if (params && handler) return { handler, params, requireAuth: a.requireAuth, requireRoles: (a as any).requireRoles, metadata: (a as any).metadata }\n } else {\n const al = a as ModuleApiLegacy\n if (al.method !== method) continue\n const params = matchRoutePattern(al.path, pathname)\n if (params) {\n return { handler: al.handler, params, metadata: al.metadata }\n }\n }\n }\n }\n}\n\nexport function findRouteManifestMatch<T extends { pattern?: string; path?: string }>(\n routes: T[],\n pathname: string\n): { route: T; params: RouteMatchParams } | undefined {\n for (const route of routes) {\n const params = matchRoutePattern(route.pattern ?? route.path ?? '/', pathname)\n if (params) {\n return { route, params }\n }\n }\n}\n\nexport function findApiRouteManifestMatch<T extends { path: string; methods: HttpMethod[] }>(\n routes: T[],\n method: HttpMethod,\n pathname: string\n): { route: T; params: RouteMatchParams } | undefined {\n for (const route of routes) {\n if (!route.methods.includes(method)) continue\n const params = matchRoutePattern(route.path, pathname)\n if (params) {\n return { route, params }\n }\n }\n}\n\nlet _backendRouteManifests: BackendRouteManifestEntry[] | null = null\n\nexport function registerBackendRouteManifests(routes: BackendRouteManifestEntry[]) {\n _backendRouteManifests = routes\n}\n\nexport function getBackendRouteManifests(): BackendRouteManifestEntry[] {\n return _backendRouteManifests ?? []\n}\n\nlet _frontendRouteManifests: FrontendRouteManifestEntry[] | null = null\n\nexport function registerFrontendRouteManifests(routes: FrontendRouteManifestEntry[]) {\n _frontendRouteManifests = routes\n}\n\nexport function getFrontendRouteManifests(): FrontendRouteManifestEntry[] {\n return _frontendRouteManifests ?? []\n}\n\nlet _apiRouteManifests: ApiRouteManifestEntry[] | null = null\n\nexport function registerApiRouteManifests(routes: ApiRouteManifestEntry[]) {\n _apiRouteManifests = routes\n}\n\nexport function getApiRouteManifests(): ApiRouteManifestEntry[] {\n return _apiRouteManifests ?? []\n}\n\n// CLI modules registry - shared between CLI and module workers\nlet _cliModules: Module[] | null = null\n\nexport function registerCliModules(modules: Module[]) {\n if (_cliModules !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] CLI modules re-registered (this may occur during HMR)')\n }\n _cliModules = modules\n}\n\nexport function getCliModules(): Module[] {\n // Return empty array if not registered - allows generate command to work without bootstrap\n return _cliModules ?? []\n}\n\nexport function hasCliModules(): boolean {\n return _cliModules !== null && _cliModules.length > 0\n}\n\nexport function getDefaultEncryptionMaps(modules: Module[]): import('./encryption').ModuleEncryptionMap[] {\n const byEntityId = new Map<string, { moduleId: string; map: import('./encryption').ModuleEncryptionMap }>()\n\n for (const mod of modules) {\n for (const entry of mod.defaultEncryptionMaps ?? []) {\n const previous = byEntityId.get(entry.entityId)\n if (previous) {\n throw new Error(\n `[registry] Duplicate default encryption map for \"${entry.entityId}\" declared by \"${previous.moduleId}\" and \"${mod.id}\"`\n )\n }\n byEntityId.set(entry.entityId, {\n moduleId: mod.id,\n map: {\n entityId: entry.entityId,\n fields: entry.fields.map((field) => ({\n field: field.field,\n hashField: field.hashField ?? null,\n })),\n },\n })\n }\n }\n\n return Array.from(byEntityId.values(), ({ map }) => map)\n}\n\nfunction ensureLazyHandler<T extends (...args: any[]) => any>(\n loaded: unknown,\n kind: 'subscriber' | 'worker',\n id: string\n): T {\n const handler = typeof loaded === 'function'\n ? loaded\n : loaded && typeof loaded === 'object' && 'default' in loaded\n ? (loaded as Record<string, unknown>).default\n : null\n if (typeof handler !== 'function') {\n throw new Error(`[registry] Invalid ${kind} module \"${id}\" (missing default export handler)`)\n }\n return handler as T\n}\n\nexport function createLazyModuleSubscriber(\n loadModule: () => Promise<unknown>,\n id: string\n): ModuleSubscriberHandler {\n let handlerPromise: Promise<ModuleSubscriberHandler> | null = null\n return async (payload, ctx) => {\n handlerPromise ??= loadModule().then((loaded) =>\n ensureLazyHandler<ModuleSubscriberHandler>(loaded, 'subscriber', id)\n )\n const handler = await handlerPromise\n return handler(payload, ctx)\n }\n}\n\nexport function createLazyModuleWorker(\n loadModule: () => Promise<unknown>,\n id: string\n): ModuleWorkerHandler {\n let handlerPromise: Promise<ModuleWorkerHandler> | null = null\n return async (job, ctx) => {\n handlerPromise ??= loadModule().then((loaded) =>\n ensureLazyHandler<ModuleWorkerHandler>(loaded, 'worker', id)\n )\n const handler = await handlerPromise\n return handler(job, ctx)\n }\n}\n"],
5
+ "mappings": "AAuPA,SAAS,SAAS,GAAW;AAC3B,UAAQ,EAAE,WAAW,GAAG,IAAI,IAAI,MAAM,GAAG,QAAQ,QAAQ,EAAE,KAAK;AAClE;AAEO,SAAS,kBAAkB,SAAiB,UAAgD;AACjG,QAAM,IAAI,SAAS,OAAO;AAC1B,QAAM,IAAI,SAAS,QAAQ;AAC3B,QAAM,QAAQ,EAAE,MAAM,GAAG,EAAE,MAAM,CAAC;AAClC,QAAM,QAAQ,EAAE,MAAM,GAAG,EAAE,MAAM,CAAC;AAClC,QAAM,SAA4C,CAAC;AACnD,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,KAAK;AAC1C,UAAM,MAAM,MAAM,CAAC;AACnB,UAAM,YAAY,IAAI,MAAM,kBAAkB;AAC9C,UAAM,YAAY,IAAI,MAAM,sBAAsB;AAClD,UAAM,OAAO,IAAI,MAAM,YAAY;AACnC,QAAI,WAAW;AACb,YAAM,MAAM,UAAU,CAAC;AACvB,UAAI,KAAK,MAAM,OAAQ,QAAO;AAC9B,aAAO,GAAG,IAAI,MAAM,MAAM,CAAC;AAC3B,UAAI,MAAM;AACV,aAAO,MAAM,MAAM,SAAS,SAAS;AAAA,IACvC,WAAW,WAAW;AACpB,YAAM,MAAM,UAAU,CAAC;AACvB,aAAO,GAAG,IAAI,IAAI,MAAM,SAAS,MAAM,MAAM,CAAC,IAAI,CAAC;AACnD,UAAI,MAAM;AACV,aAAO;AAAA,IACT,WAAW,MAAM;AACf,UAAI,KAAK,MAAM,OAAQ,QAAO;AAC9B,aAAO,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC;AAAA,IAC3B,OAAO;AACL,UAAI,KAAK,MAAM,UAAU,MAAM,CAAC,EAAE,YAAY,MAAM,IAAI,YAAY,EAAG,QAAO;AAAA,IAChF;AAAA,EACF;AACA,MAAI,MAAM,MAAM,OAAQ,QAAO;AAC/B,SAAO;AACT;AAEA,SAAS,WAAW,GAAgB;AAClC,SAAO,EAAE,WAAW,EAAE,QAAQ;AAChC;AAEO,SAAS,kBAAkB,SAAmB,UAAiG;AACpJ,aAAW,KAAK,SAAS;AACvB,UAAM,SAAS,EAAE,kBAAkB,CAAC;AACpC,eAAW,KAAK,QAAQ;AACtB,YAAM,SAAS,kBAAkB,WAAW,CAAC,GAAG,QAAQ;AACxD,UAAI,OAAQ,QAAO,EAAE,OAAO,GAAG,OAAO;AAAA,IACxC;AAAA,EACF;AACF;AAEO,SAAS,iBAAiB,SAAmB,UAAiG;AACnJ,aAAW,KAAK,SAAS;AACvB,UAAM,SAAS,EAAE,iBAAiB,CAAC;AACnC,eAAW,KAAK,QAAQ;AACtB,YAAM,SAAS,kBAAkB,WAAW,CAAC,GAAG,QAAQ;AACxD,UAAI,OAAQ,QAAO,EAAE,OAAO,GAAG,OAAO;AAAA,IACxC;AAAA,EACF;AACF;AAEO,SAAS,QAAQ,SAAmB,QAAoB,UAAkK;AAC/N,aAAW,KAAK,SAAS;AACvB,UAAM,OAAO,EAAE,QAAQ,CAAC;AACxB,eAAW,KAAK,MAAM;AACpB,UAAI,cAAc,GAAG;AACnB,cAAM,SAAS,kBAAkB,EAAE,MAAM,QAAQ;AACjD,cAAM,UAAW,EAAE,SAAiB,MAAM;AAC1C,YAAI,UAAU,QAAS,QAAO,EAAE,SAAS,QAAQ,aAAa,EAAE,aAAa,cAAe,EAAU,cAAc,UAAW,EAAU,SAAS;AAAA,MACpJ,OAAO;AACL,cAAM,KAAK;AACX,YAAI,GAAG,WAAW,OAAQ;AAC1B,cAAM,SAAS,kBAAkB,GAAG,MAAM,QAAQ;AAClD,YAAI,QAAQ;AACV,iBAAO,EAAE,SAAS,GAAG,SAAS,QAAQ,UAAU,GAAG,SAAS;AAAA,QAC9D;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,uBACd,QACA,UACoD;AACpD,aAAW,SAAS,QAAQ;AAC1B,UAAM,SAAS,kBAAkB,MAAM,WAAW,MAAM,QAAQ,KAAK,QAAQ;AAC7E,QAAI,QAAQ;AACV,aAAO,EAAE,OAAO,OAAO;AAAA,IACzB;AAAA,EACF;AACF;AAEO,SAAS,0BACd,QACA,QACA,UACoD;AACpD,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,MAAM,QAAQ,SAAS,MAAM,EAAG;AACrC,UAAM,SAAS,kBAAkB,MAAM,MAAM,QAAQ;AACrD,QAAI,QAAQ;AACV,aAAO,EAAE,OAAO,OAAO;AAAA,IACzB;AAAA,EACF;AACF;AAEA,IAAI,yBAA6D;AAE1D,SAAS,8BAA8B,QAAqC;AACjF,2BAAyB;AAC3B;AAEO,SAAS,2BAAwD;AACtE,SAAO,0BAA0B,CAAC;AACpC;AAEA,IAAI,0BAA+D;AAE5D,SAAS,+BAA+B,QAAsC;AACnF,4BAA0B;AAC5B;AAEO,SAAS,4BAA0D;AACxE,SAAO,2BAA2B,CAAC;AACrC;AAEA,IAAI,qBAAqD;AAElD,SAAS,0BAA0B,QAAiC;AACzE,uBAAqB;AACvB;AAEO,SAAS,uBAAgD;AAC9D,SAAO,sBAAsB,CAAC;AAChC;AAGA,IAAI,cAA+B;AAE5B,SAAS,mBAAmB,SAAmB;AACpD,MAAI,gBAAgB,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAClE,YAAQ,MAAM,mEAAmE;AAAA,EACnF;AACA,gBAAc;AAChB;AAEO,SAAS,gBAA0B;AAExC,SAAO,eAAe,CAAC;AACzB;AAEO,SAAS,gBAAyB;AACvC,SAAO,gBAAgB,QAAQ,YAAY,SAAS;AACtD;AAEO,SAAS,yBAAyB,SAAiE;AACxG,QAAM,aAAa,oBAAI,IAAmF;AAE1G,aAAW,OAAO,SAAS;AACzB,eAAW,SAAS,IAAI,yBAAyB,CAAC,GAAG;AACnD,YAAM,WAAW,WAAW,IAAI,MAAM,QAAQ;AAC9C,UAAI,UAAU;AACZ,cAAM,IAAI;AAAA,UACR,oDAAoD,MAAM,QAAQ,kBAAkB,SAAS,QAAQ,UAAU,IAAI,EAAE;AAAA,QACvH;AAAA,MACF;AACA,iBAAW,IAAI,MAAM,UAAU;AAAA,QAC7B,UAAU,IAAI;AAAA,QACd,KAAK;AAAA,UACH,UAAU,MAAM;AAAA,UAChB,QAAQ,MAAM,OAAO,IAAI,CAAC,WAAW;AAAA,YACnC,OAAO,MAAM;AAAA,YACb,WAAW,MAAM,aAAa;AAAA,UAChC,EAAE;AAAA,QACJ;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,WAAW,OAAO,GAAG,CAAC,EAAE,IAAI,MAAM,GAAG;AACzD;AAEA,SAAS,kBACP,QACA,MACA,IACG;AACH,QAAM,UAAU,OAAO,WAAW,aAC9B,SACA,UAAU,OAAO,WAAW,YAAY,aAAa,SAClD,OAAmC,UACpC;AACN,MAAI,OAAO,YAAY,YAAY;AACjC,UAAM,IAAI,MAAM,sBAAsB,IAAI,YAAY,EAAE,oCAAoC;AAAA,EAC9F;AACA,SAAO;AACT;AAEO,SAAS,2BACd,YACA,IACyB;AACzB,MAAI,iBAA0D;AAC9D,SAAO,OAAO,SAAS,QAAQ;AAC7B,uBAAmB,WAAW,EAAE;AAAA,MAAK,CAAC,WACpC,kBAA2C,QAAQ,cAAc,EAAE;AAAA,IACrE;AACA,UAAM,UAAU,MAAM;AACtB,WAAO,QAAQ,SAAS,GAAG;AAAA,EAC7B;AACF;AAEO,SAAS,uBACd,YACA,IACqB;AACrB,MAAI,iBAAsD;AAC1D,SAAO,OAAO,KAAK,QAAQ;AACzB,uBAAmB,WAAW,EAAE;AAAA,MAAK,CAAC,WACpC,kBAAuC,QAAQ,UAAU,EAAE;AAAA,IAC7D;AACA,UAAM,UAAU,MAAM;AACtB,WAAO,QAAQ,KAAK,GAAG;AAAA,EACzB;AACF;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.5.1-develop.3036.f02c281f23",
3
+ "version": "0.5.1-develop.3045.b4b3320cc2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -92,7 +92,7 @@
92
92
  "@mikro-orm/core": "^7.0.13",
93
93
  "@mikro-orm/decorators": "^7.0.13",
94
94
  "@mikro-orm/postgresql": "^7.0.13",
95
- "@open-mercato/cache": "0.5.1-develop.3036.f02c281f23",
95
+ "@open-mercato/cache": "0.5.1-develop.3045.b4b3320cc2",
96
96
  "dotenv": "^17.4.2",
97
97
  "rate-limiter-flexible": "^11.0.1",
98
98
  "reflect-metadata": "^0.2.2",
@@ -29,6 +29,35 @@ type AuthResolution = {
29
29
  status: AuthResolutionStatus
30
30
  }
31
31
 
32
+ // Symbol-keyed trusted auth context. Set on synthetic Request objects by
33
+ // callers that have already authenticated (e.g. the AI in-process operation
34
+ // runner) so downstream auth resolution short-circuits without re-running
35
+ // cookie/JWT/API-key parsing. The hook is fail-open: if absent the normal
36
+ // resolution path runs unchanged.
37
+ export const TRUSTED_AUTH_CONTEXT_SYMBOL = Symbol.for('open-mercato.auth.trustedContext')
38
+
39
+ export type TrustedAuthContextEnvelope = {
40
+ auth: AuthContext
41
+ status?: AuthResolutionStatus
42
+ }
43
+
44
+ export function attachTrustedAuthContext(
45
+ request: Request,
46
+ envelope: TrustedAuthContextEnvelope
47
+ ): Request {
48
+ ;(request as unknown as Record<symbol, TrustedAuthContextEnvelope>)[TRUSTED_AUTH_CONTEXT_SYMBOL] = envelope
49
+ return request
50
+ }
51
+
52
+ function readTrustedAuthContext(request: Request): TrustedAuthContextEnvelope | null {
53
+ const carrier = request as unknown as Record<symbol, unknown>
54
+ const envelope = carrier[TRUSTED_AUTH_CONTEXT_SYMBOL]
55
+ if (!envelope || typeof envelope !== 'object') return null
56
+ const candidate = envelope as TrustedAuthContextEnvelope
57
+ if (!('auth' in candidate)) return null
58
+ return candidate
59
+ }
60
+
32
61
  function decodeCookieValue(raw: string | undefined): string | null {
33
62
  if (raw === undefined) return null
34
63
  try {
@@ -265,6 +294,13 @@ export async function getAuthFromCookies(): Promise<AuthContext> {
265
294
  }
266
295
 
267
296
  export async function resolveAuthFromRequestDetailed(req: Request): Promise<AuthResolution> {
297
+ const trusted = readTrustedAuthContext(req)
298
+ if (trusted) {
299
+ return {
300
+ auth: trusted.auth,
301
+ status: trusted.status ?? (trusted.auth ? 'authenticated' : 'missing'),
302
+ }
303
+ }
268
304
  const cookieHeader = req.headers.get('cookie') || ''
269
305
  const tenantCookie = readCookieFromHeader(cookieHeader, TENANT_COOKIE_NAME)
270
306
  const orgCookie = readCookieFromHeader(cookieHeader, ORGANIZATION_COOKIE_NAME)
@@ -631,13 +631,16 @@ function isUuid(v: any): v is string {
631
631
  type AccessLogServiceLike = { log: (input: any) => Promise<unknown> | unknown }
632
632
 
633
633
  function resolveAccessLogService(container: AwilixContainer): AccessLogServiceLike | null {
634
+ const registrations = (container as { registrations?: Record<string, unknown> }).registrations
635
+ if (registrations && !Object.prototype.hasOwnProperty.call(registrations, 'accessLogService')) {
636
+ return null
637
+ }
638
+
634
639
  try {
635
640
  const service = container.resolve?.('accessLogService') as AccessLogServiceLike | undefined
636
641
  if (service && typeof service.log === 'function') return service
637
- } catch (err) {
638
- try {
639
- console.warn('[crud] accessLogService not available in container', err)
640
- } catch {}
642
+ } catch {
643
+ return null
641
644
  }
642
645
  return null
643
646
  }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Coverage for the unified `modules.ts` override dispatcher. The
3
+ * dispatcher walks `enabledModules`, buckets every entry's
4
+ * `overrides.<domain>` shape by domain, and forwards each domain's
5
+ * bucket to the registered per-domain applier. Unwired domains emit a
6
+ * one-shot structured warning so an early adopter notices instead of
7
+ * silently no-opping.
8
+ *
9
+ * Spec: `.ai/specs/2026-05-04-modules-ts-unified-overrides.md`.
10
+ */
11
+ import {
12
+ applyModuleOverridesFromEnabledModules,
13
+ registerModuleOverrideApplier,
14
+ resetModuleOverrideAppliersForTests,
15
+ type ModuleEntryWithOverrides,
16
+ type ModuleOverrideEntry,
17
+ } from '../overrides'
18
+
19
+ beforeEach(() => {
20
+ resetModuleOverrideAppliersForTests()
21
+ })
22
+
23
+ describe('applyModuleOverridesFromEnabledModules', () => {
24
+ it('forwards an entry.overrides.<domain> sub-tree to the registered applier', () => {
25
+ const received: Array<ModuleOverrideEntry<{ agents?: Record<string, unknown> }>> = []
26
+ registerModuleOverrideApplier<{ agents?: Record<string, unknown> }>('ai', (entries) => {
27
+ received.push(...entries)
28
+ })
29
+
30
+ const modules: ModuleEntryWithOverrides[] = [
31
+ {
32
+ id: 'example',
33
+ from: '@app',
34
+ overrides: {
35
+ ai: { agents: { 'catalog.catalog_assistant': null } },
36
+ },
37
+ },
38
+ ]
39
+
40
+ applyModuleOverridesFromEnabledModules(modules)
41
+
42
+ expect(received).toHaveLength(1)
43
+ expect(received[0].moduleId).toBe('example')
44
+ expect(received[0].overrides).toEqual({
45
+ agents: { 'catalog.catalog_assistant': null },
46
+ })
47
+ })
48
+
49
+ it('preserves module load order across multiple entries', () => {
50
+ const received: string[] = []
51
+ registerModuleOverrideApplier<unknown>('ai', (entries) => {
52
+ for (const entry of entries) received.push(entry.moduleId)
53
+ })
54
+
55
+ applyModuleOverridesFromEnabledModules([
56
+ { id: 'first', overrides: { ai: { agents: { 'm.x': null } } } },
57
+ { id: 'second', overrides: { ai: { agents: { 'm.x': null } } } },
58
+ ])
59
+
60
+ expect(received).toEqual(['first', 'second'])
61
+ })
62
+
63
+ it('skips entries without `overrides`', () => {
64
+ const applier = jest.fn()
65
+ registerModuleOverrideApplier('ai', applier)
66
+
67
+ applyModuleOverridesFromEnabledModules([
68
+ { id: 'plain', from: '@app' },
69
+ { id: 'noai', from: '@app', overrides: {} },
70
+ ])
71
+
72
+ expect(applier).not.toHaveBeenCalled()
73
+ })
74
+
75
+ it('emits a one-shot structured warning per unwired domain', () => {
76
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
77
+
78
+ applyModuleOverridesFromEnabledModules([
79
+ {
80
+ id: 'a',
81
+ overrides: { routes: { api: { 'GET /api/x': null } } },
82
+ },
83
+ {
84
+ id: 'b',
85
+ overrides: { routes: { api: { 'POST /api/y': null } } },
86
+ },
87
+ ])
88
+
89
+ // Same domain hit twice — only one warning per process.
90
+ const routesCalls = warnSpy.mock.calls.filter((args) =>
91
+ typeof args[0] === 'string' && args[0].includes('Domain "routes"'),
92
+ )
93
+ expect(routesCalls).toHaveLength(1)
94
+ expect(routesCalls[0][0]).toContain('not yet wired')
95
+ expect(routesCalls[0][0]).toContain('module(s) [a, b]')
96
+ expect(routesCalls[0][0]).toContain('issues/1787')
97
+
98
+ // Different unwired domain — separate one-shot warning.
99
+ applyModuleOverridesFromEnabledModules([
100
+ { id: 'c', overrides: { events: { subscribers: { 'x': null } } } },
101
+ ])
102
+ const eventsCalls = warnSpy.mock.calls.filter((args) =>
103
+ typeof args[0] === 'string' && args[0].includes('Domain "events"'),
104
+ )
105
+ expect(eventsCalls).toHaveLength(1)
106
+
107
+ warnSpy.mockRestore()
108
+ })
109
+
110
+ it('does NOT consume the legacy `aiAgentOverrides` / `aiToolOverrides` keys', () => {
111
+ const applier = jest.fn()
112
+ registerModuleOverrideApplier('ai', applier)
113
+
114
+ applyModuleOverridesFromEnabledModules([
115
+ // The umbrella dispatcher only reads `overrides.ai`.
116
+ // Legacy top-level keys are intentionally ignored — the key rename
117
+ // is hard because the AI shape never shipped on `develop`.
118
+ {
119
+ id: 'legacy',
120
+ // @ts-expect-error legacy shape kept here just to assert it is not picked up
121
+ aiAgentOverrides: { 'catalog.catalog_assistant': null },
122
+ },
123
+ ])
124
+
125
+ expect(applier).not.toHaveBeenCalled()
126
+ })
127
+
128
+ it('routes only the domains that are present on the entry', () => {
129
+ const aiApplier = jest.fn()
130
+ const widgetsApplier = jest.fn()
131
+ registerModuleOverrideApplier('ai', aiApplier)
132
+ registerModuleOverrideApplier('widgets', widgetsApplier)
133
+
134
+ applyModuleOverridesFromEnabledModules([
135
+ { id: 'a', overrides: { ai: { agents: { 'm.x': null } } } },
136
+ ])
137
+
138
+ expect(aiApplier).toHaveBeenCalledTimes(1)
139
+ expect(widgetsApplier).not.toHaveBeenCalled()
140
+ })
141
+ })