@open-mercato/shared 0.8.1-develop.7296.1.2111d779db → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +2 -0
  3. package/dist/lib/auth/jwt.js +4 -0
  4. package/dist/lib/auth/jwt.js.map +2 -2
  5. package/dist/lib/auth/organizationScope.js +15 -1
  6. package/dist/lib/auth/organizationScope.js.map +2 -2
  7. package/dist/lib/availability/catalogOnlyProvider.js +77 -0
  8. package/dist/lib/availability/catalogOnlyProvider.js.map +7 -0
  9. package/dist/lib/availability/index.js +8 -0
  10. package/dist/lib/availability/index.js.map +7 -0
  11. package/dist/lib/availability/registry.js +47 -0
  12. package/dist/lib/availability/registry.js.map +7 -0
  13. package/dist/lib/availability/types.js +7 -0
  14. package/dist/lib/availability/types.js.map +7 -0
  15. package/dist/lib/catalog-visibility/index.js +11 -0
  16. package/dist/lib/catalog-visibility/index.js.map +7 -0
  17. package/dist/lib/catalog-visibility/intersectScopes.js +49 -0
  18. package/dist/lib/catalog-visibility/intersectScopes.js.map +7 -0
  19. package/dist/lib/catalog-visibility/matchesOne.js +27 -0
  20. package/dist/lib/catalog-visibility/matchesOne.js.map +7 -0
  21. package/dist/lib/catalog-visibility/matchesScope.js +9 -0
  22. package/dist/lib/catalog-visibility/matchesScope.js.map +7 -0
  23. package/dist/lib/catalog-visibility/types.js +1 -0
  24. package/dist/lib/catalog-visibility/types.js.map +7 -0
  25. package/dist/lib/catalog-visibility/unionScopes.js +9 -0
  26. package/dist/lib/catalog-visibility/unionScopes.js.map +7 -0
  27. package/dist/lib/commands/command-bus.js +383 -125
  28. package/dist/lib/commands/command-bus.js.map +3 -3
  29. package/dist/lib/commands/flush.js +16 -0
  30. package/dist/lib/commands/flush.js.map +2 -2
  31. package/dist/lib/commands/index.js +8 -0
  32. package/dist/lib/commands/index.js.map +2 -2
  33. package/dist/lib/commands/redo.js +2 -7
  34. package/dist/lib/commands/redo.js.map +2 -2
  35. package/dist/lib/commands/transaction-lifetime.js +67 -0
  36. package/dist/lib/commands/transaction-lifetime.js.map +7 -0
  37. package/dist/lib/commands/types.js.map +2 -2
  38. package/dist/lib/commands/undo.js +54 -2
  39. package/dist/lib/commands/undo.js.map +2 -2
  40. package/dist/lib/crud/enricher-runner.js +64 -9
  41. package/dist/lib/crud/enricher-runner.js.map +2 -2
  42. package/dist/lib/crud/factory.js +42 -18
  43. package/dist/lib/crud/factory.js.map +2 -2
  44. package/dist/lib/data/engine.js +12 -3
  45. package/dist/lib/data/engine.js.map +2 -2
  46. package/dist/lib/db/accentInsensitiveSearch.js +21 -0
  47. package/dist/lib/db/accentInsensitiveSearch.js.map +7 -0
  48. package/dist/lib/encryption/kms.js +1 -5
  49. package/dist/lib/encryption/kms.js.map +2 -2
  50. package/dist/lib/encryption/likeFilterWarning.js +3 -1
  51. package/dist/lib/encryption/likeFilterWarning.js.map +2 -2
  52. package/dist/lib/encryption/migration-backfill.js +69 -0
  53. package/dist/lib/encryption/migration-backfill.js.map +7 -0
  54. package/dist/lib/encryption/policyMemo.js +28 -0
  55. package/dist/lib/encryption/policyMemo.js.map +7 -0
  56. package/dist/lib/encryption/subscriber.js +30 -3
  57. package/dist/lib/encryption/subscriber.js.map +2 -2
  58. package/dist/lib/encryption/tenantDataEncryptionService.js +113 -200
  59. package/dist/lib/encryption/tenantDataEncryptionService.js.map +3 -3
  60. package/dist/lib/modules/api-route-identity.js +102 -0
  61. package/dist/lib/modules/api-route-identity.js.map +7 -0
  62. package/dist/lib/telemetry/runtime.js.map +2 -2
  63. package/dist/lib/version.js +1 -1
  64. package/dist/lib/version.js.map +1 -1
  65. package/dist/modules/events/factory.js +20 -0
  66. package/dist/modules/events/factory.js.map +2 -2
  67. package/dist/modules/registry.js +3 -1
  68. package/dist/modules/registry.js.map +2 -2
  69. package/package.json +12 -5
  70. package/src/lib/auth/__tests__/jwt.test.ts +12 -0
  71. package/src/lib/auth/__tests__/organizationScope.test.ts +27 -0
  72. package/src/lib/auth/jwt.ts +9 -0
  73. package/src/lib/auth/organizationScope.ts +39 -0
  74. package/src/lib/availability/__tests__/catalogOnlyProvider.test.ts +166 -0
  75. package/src/lib/availability/__tests__/registry.test.ts +130 -0
  76. package/src/lib/availability/catalogOnlyProvider.ts +152 -0
  77. package/src/lib/availability/index.ts +18 -0
  78. package/src/lib/availability/registry.ts +131 -0
  79. package/src/lib/availability/types.ts +86 -0
  80. package/src/lib/catalog-visibility/__tests__/intersectScopes.test.ts +168 -0
  81. package/src/lib/catalog-visibility/__tests__/matchesOne.test.ts +116 -0
  82. package/src/lib/catalog-visibility/__tests__/matchesScope.test.ts +35 -0
  83. package/src/lib/catalog-visibility/__tests__/testFixtures.ts +66 -0
  84. package/src/lib/catalog-visibility/__tests__/unionScopes.test.ts +62 -0
  85. package/src/lib/catalog-visibility/index.ts +5 -0
  86. package/src/lib/catalog-visibility/intersectScopes.ts +93 -0
  87. package/src/lib/catalog-visibility/matchesOne.ts +28 -0
  88. package/src/lib/catalog-visibility/matchesScope.ts +8 -0
  89. package/src/lib/catalog-visibility/types.ts +31 -0
  90. package/src/lib/catalog-visibility/unionScopes.ts +16 -0
  91. package/src/lib/commands/__tests__/command-bus.atomic-replay.test.ts +650 -0
  92. package/src/lib/commands/__tests__/command-bus.test.ts +175 -0
  93. package/src/lib/commands/__tests__/command-bus.undo-toctou.test.ts +1 -1
  94. package/src/lib/commands/__tests__/redo.test.ts +4 -0
  95. package/src/lib/commands/__tests__/transaction-lifetime.test.ts +118 -0
  96. package/src/lib/commands/__tests__/undo.test.ts +90 -1
  97. package/src/lib/commands/command-bus.ts +463 -148
  98. package/src/lib/commands/flush.ts +16 -0
  99. package/src/lib/commands/index.ts +7 -0
  100. package/src/lib/commands/redo.ts +4 -8
  101. package/src/lib/commands/transaction-lifetime.ts +88 -0
  102. package/src/lib/commands/types.ts +91 -0
  103. package/src/lib/commands/undo.ts +79 -1
  104. package/src/lib/crud/__tests__/crud-factory.cache-tenant-level.test.ts +149 -0
  105. package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +48 -6
  106. package/src/lib/crud/__tests__/crud-factory.test.ts +224 -27
  107. package/src/lib/crud/__tests__/enricher-runner.test.ts +270 -5
  108. package/src/lib/crud/enricher-runner.ts +83 -9
  109. package/src/lib/crud/factory.ts +46 -19
  110. package/src/lib/data/__tests__/engine.pin-organization-id.test.ts +102 -0
  111. package/src/lib/data/engine.ts +12 -0
  112. package/src/lib/db/__tests__/accentInsensitiveSearch.test.ts +51 -0
  113. package/src/lib/db/accentInsensitiveSearch.ts +70 -0
  114. package/src/lib/encryption/__tests__/dek-lifecycle.test.ts +11 -9
  115. package/src/lib/encryption/__tests__/fixtures/policy-process-worker.ts +96 -0
  116. package/src/lib/encryption/__tests__/kms.test.ts +45 -3
  117. package/src/lib/encryption/__tests__/likeFilterWarning.test.ts +43 -0
  118. package/src/lib/encryption/__tests__/migration-backfill.test.ts +57 -0
  119. package/src/lib/encryption/__tests__/process-policy-coherence.test.ts +128 -0
  120. package/src/lib/encryption/__tests__/subscriber.deep-decrypt-collections.test.ts +66 -1
  121. package/src/lib/encryption/__tests__/subscriber.test.ts +3 -0
  122. package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +412 -51
  123. package/src/lib/encryption/kms.ts +1 -6
  124. package/src/lib/encryption/likeFilterWarning.ts +10 -2
  125. package/src/lib/encryption/migration-backfill.ts +90 -0
  126. package/src/lib/encryption/policyMemo.ts +51 -0
  127. package/src/lib/encryption/subscriber.ts +41 -4
  128. package/src/lib/encryption/tenantDataEncryptionService.ts +153 -219
  129. package/src/lib/i18n/__tests__/detect-locale-narrowed.test.ts +20 -10
  130. package/src/lib/modules/api-route-identity.ts +116 -0
  131. package/src/lib/telemetry/runtime.ts +8 -0
  132. package/src/modules/__tests__/registry.test.ts +92 -0
  133. package/src/modules/events/__tests__/broadcast-coalescing-declaration.test.ts +53 -0
  134. package/src/modules/events/factory.ts +31 -0
  135. package/src/modules/events/types.ts +13 -0
  136. package/src/modules/notifications/types.ts +1 -0
  137. package/src/modules/registry.ts +4 -1
@@ -0,0 +1,102 @@
1
+ const matchedApiRoutePaths = /* @__PURE__ */ new WeakMap();
2
+ const ROUTE_IDENTITY_HEADER = "x-open-mercato-route-identity";
3
+ const MAX_ROUTE_IDENTITY_TOKENS = 4096;
4
+ const matchedApiRouteTokens = /* @__PURE__ */ new Map();
5
+ function normalizePath(path) {
6
+ return (path.startsWith("/") ? path : `/${path}`).replace(/\/+$/, "") || "/";
7
+ }
8
+ function hasValidPathEncoding(request) {
9
+ try {
10
+ const pathname = new URL(request.url).pathname;
11
+ for (const segment of pathname.split("/")) decodeURIComponent(segment);
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+ function buildCanonicalMatchedPath(pattern, params) {
18
+ const segments = normalizePath(pattern).split("/").slice(1);
19
+ const canonicalSegments = [];
20
+ for (const segment of segments) {
21
+ const catchAll = segment.match(/^\[\.\.\.(.+)\]$/);
22
+ const optionalCatchAll = segment.match(/^\[\[\.\.\.(.+)\]\]$/);
23
+ const dynamic = segment.match(/^\[(.+)\]$/);
24
+ const paramName = catchAll?.[1] ?? optionalCatchAll?.[1] ?? dynamic?.[1];
25
+ if (!paramName) {
26
+ canonicalSegments.push(segment);
27
+ continue;
28
+ }
29
+ const value = params[paramName];
30
+ if (Array.isArray(value)) {
31
+ canonicalSegments.push(...value);
32
+ continue;
33
+ }
34
+ if (typeof value === "string") {
35
+ canonicalSegments.push(value);
36
+ continue;
37
+ }
38
+ if (optionalCatchAll) continue;
39
+ return null;
40
+ }
41
+ return `/${canonicalSegments.join("/")}`;
42
+ }
43
+ function createRouteIdentityToken() {
44
+ const cryptoApi = globalThis.crypto;
45
+ if (!cryptoApi) return null;
46
+ if (typeof cryptoApi.randomUUID === "function") return cryptoApi.randomUUID();
47
+ const bytes = cryptoApi.getRandomValues(new Uint8Array(24));
48
+ return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
49
+ }
50
+ function rememberRouteIdentity(token, canonicalPath) {
51
+ matchedApiRouteTokens.set(token, canonicalPath);
52
+ while (matchedApiRouteTokens.size > MAX_ROUTE_IDENTITY_TOKENS) {
53
+ const oldestToken = matchedApiRouteTokens.keys().next().value;
54
+ if (typeof oldestToken !== "string") return;
55
+ matchedApiRouteTokens.delete(oldestToken);
56
+ }
57
+ }
58
+ function bindMatchedApiRoutePath(request, pattern, params) {
59
+ if (!hasValidPathEncoding(request)) return false;
60
+ const canonicalPath = buildCanonicalMatchedPath(pattern, params);
61
+ if (canonicalPath === null) return false;
62
+ const token = createRouteIdentityToken();
63
+ if (token === null) return false;
64
+ try {
65
+ request.headers.set(ROUTE_IDENTITY_HEADER, token);
66
+ } catch {
67
+ return false;
68
+ }
69
+ rememberRouteIdentity(token, canonicalPath);
70
+ matchedApiRoutePaths.set(request, canonicalPath);
71
+ return true;
72
+ }
73
+ function getMatchedApiRoutePath(request) {
74
+ const directPath = matchedApiRoutePaths.get(request);
75
+ if (directPath !== void 0) return directPath;
76
+ const token = request.headers.get(ROUTE_IDENTITY_HEADER);
77
+ if (!token) return void 0;
78
+ return matchedApiRouteTokens.get(token);
79
+ }
80
+ function toInterceptorRoutePath(pathname) {
81
+ if (pathname.startsWith("/api/")) return pathname.slice(5);
82
+ if (pathname === "/api") return "";
83
+ return pathname.replace(/^\/+/, "");
84
+ }
85
+ function resolveApiInterceptorRoutePath(request) {
86
+ const matchedPath = getMatchedApiRoutePath(request);
87
+ if (matchedPath !== void 0) return toInterceptorRoutePath(matchedPath);
88
+ if (request.headers.has(ROUTE_IDENTITY_HEADER)) return null;
89
+ try {
90
+ const pathname = new URL(request.url).pathname;
91
+ for (const segment of pathname.split("/")) decodeURIComponent(segment);
92
+ return toInterceptorRoutePath(pathname);
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+ export {
98
+ bindMatchedApiRoutePath,
99
+ getMatchedApiRoutePath,
100
+ resolveApiInterceptorRoutePath
101
+ };
102
+ //# sourceMappingURL=api-route-identity.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/modules/api-route-identity.ts"],
4
+ "sourcesContent": ["import type { RouteMatchParams } from '../../modules/registry'\n\nconst matchedApiRoutePaths = new WeakMap<Request, string>()\nconst ROUTE_IDENTITY_HEADER = 'x-open-mercato-route-identity'\nconst MAX_ROUTE_IDENTITY_TOKENS = 4096\nconst matchedApiRouteTokens = new Map<string, string>()\n\nfunction normalizePath(path: string): string {\n return (path.startsWith('/') ? path : `/${path}`).replace(/\\/+$/, '') || '/'\n}\n\nfunction hasValidPathEncoding(request: Request): boolean {\n try {\n const pathname = new URL(request.url).pathname\n for (const segment of pathname.split('/')) decodeURIComponent(segment)\n return true\n } catch {\n return false\n }\n}\n\nfunction buildCanonicalMatchedPath(pattern: string, params: RouteMatchParams): string | null {\n const segments = normalizePath(pattern).split('/').slice(1)\n const canonicalSegments: string[] = []\n\n for (const segment of segments) {\n const catchAll = segment.match(/^\\[\\.\\.\\.(.+)\\]$/)\n const optionalCatchAll = segment.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/)\n const dynamic = segment.match(/^\\[(.+)\\]$/)\n const paramName = catchAll?.[1] ?? optionalCatchAll?.[1] ?? dynamic?.[1]\n\n if (!paramName) {\n canonicalSegments.push(segment)\n continue\n }\n\n const value = params[paramName]\n if (Array.isArray(value)) {\n canonicalSegments.push(...value)\n continue\n }\n if (typeof value === 'string') {\n canonicalSegments.push(value)\n continue\n }\n if (optionalCatchAll) continue\n return null\n }\n\n return `/${canonicalSegments.join('/')}`\n}\n\nfunction createRouteIdentityToken(): string | null {\n const cryptoApi = globalThis.crypto\n if (!cryptoApi) return null\n if (typeof cryptoApi.randomUUID === 'function') return cryptoApi.randomUUID()\n const bytes = cryptoApi.getRandomValues(new Uint8Array(24))\n return Array.from(bytes, (value) => value.toString(16).padStart(2, '0')).join('')\n}\n\nfunction rememberRouteIdentity(token: string, canonicalPath: string): void {\n matchedApiRouteTokens.set(token, canonicalPath)\n while (matchedApiRouteTokens.size > MAX_ROUTE_IDENTITY_TOKENS) {\n const oldestToken = matchedApiRouteTokens.keys().next().value\n if (typeof oldestToken !== 'string') return\n matchedApiRouteTokens.delete(oldestToken)\n }\n}\n\nexport function bindMatchedApiRoutePath(\n request: Request,\n pattern: string,\n params: RouteMatchParams,\n): boolean {\n if (!hasValidPathEncoding(request)) return false\n const canonicalPath = buildCanonicalMatchedPath(pattern, params)\n if (canonicalPath === null) return false\n const token = createRouteIdentityToken()\n if (token === null) return false\n try {\n request.headers.set(ROUTE_IDENTITY_HEADER, token)\n } catch {\n return false\n }\n rememberRouteIdentity(token, canonicalPath)\n matchedApiRoutePaths.set(request, canonicalPath)\n return true\n}\n\nexport function getMatchedApiRoutePath(request: Request): string | undefined {\n const directPath = matchedApiRoutePaths.get(request)\n if (directPath !== undefined) return directPath\n const token = request.headers.get(ROUTE_IDENTITY_HEADER)\n if (!token) return undefined\n return matchedApiRouteTokens.get(token)\n}\n\nfunction toInterceptorRoutePath(pathname: string): string {\n if (pathname.startsWith('/api/')) return pathname.slice(5)\n if (pathname === '/api') return ''\n return pathname.replace(/^\\/+/, '')\n}\n\nexport function resolveApiInterceptorRoutePath(request: Request): string | null {\n const matchedPath = getMatchedApiRoutePath(request)\n if (matchedPath !== undefined) return toInterceptorRoutePath(matchedPath)\n if (request.headers.has(ROUTE_IDENTITY_HEADER)) return null\n\n try {\n const pathname = new URL(request.url).pathname\n for (const segment of pathname.split('/')) decodeURIComponent(segment)\n return toInterceptorRoutePath(pathname)\n } catch {\n return null\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/telemetry/runtime.ts"],
4
- "sourcesContent": ["export type TelemetryTraceCarrier = Record<string, string>\n\nexport type TelemetrySpanAttributes = Record<string, string | number | boolean | undefined>\n\nexport type TelemetrySpanKind = 'internal' | 'server' | 'client' | 'producer' | 'consumer'\n\n/** The subset of the telemetry package's `Span` that bridge consumers need. */\nexport type TelemetrySpan = {\n setAttributes(attributes: TelemetrySpanAttributes): void\n /**\n * Rename an in-flight span whose identity is only known once it has run.\n * Optional so a bootstrap predating it still satisfies the contract \u2014 call it\n * as `span.updateName?.(\u2026)`.\n */\n updateName?(name: string): void\n}\n\nexport type TelemetrySpanOptions = {\n kind?: TelemetrySpanKind\n attributes?: TelemetrySpanAttributes\n /** Start a new trace so the sampler decides for this span alone. */\n root?: boolean\n /** Causal links to other traces, as W3C carriers. */\n links?: TelemetryTraceCarrier[]\n}\n\nexport type TelemetryRuntime = {\n /**\n * True only when the active SDK may safely use the process-global W3C\n * propagator for cross-boundary extraction.\n */\n canUseGlobalTracePropagation(): boolean\n captureTraceContext(): TelemetryTraceCarrier\n continueTrace<T>(\n carrier: TelemetryTraceCarrier | undefined,\n name: string,\n fn: () => T,\n options?: { kind?: 'internal' | 'server' | 'client' | 'producer' | 'consumer' },\n ): T\n /**\n * Optional so an older bootstrap that predates span support still satisfies\n * the contract; consumers go through `withTelemetrySpan` and degrade to\n * running `fn` untraced.\n */\n withSpan?<T>(name: string, fn: (span: TelemetrySpan) => T, options?: TelemetrySpanOptions): T\n recordHttpDuration(method: string, route: string, status: number, startedAt: number): void\n reportError(\n error: unknown,\n context?: {\n module?: string\n /**\n * Stable, enumerated fingerprint (`module.reason`) the backend groups on.\n * Optional so a bootstrap predating it still satisfies the contract \u2014 an\n * older bridge simply ignores the field.\n */\n code?: string\n attributes?: Record<string, string | number | boolean | undefined>\n },\n ): void\n shutdown(): Promise<void>\n}\n\nconst GLOBAL_KEY = Symbol.for('@open-mercato/shared.telemetryRuntime')\nconst ENABLED_BACKENDS = new Set(['console', 'signoz', 'newrelic', 'otlp'])\n\ntype TelemetryRuntimeStore = {\n active?: TelemetryRuntime\n}\n\nfunction store(): TelemetryRuntimeStore {\n const globalStore = globalThis as unknown as Record<symbol, TelemetryRuntimeStore | undefined>\n let current = globalStore[GLOBAL_KEY]\n if (!current) {\n current = {}\n globalStore[GLOBAL_KEY] = current\n }\n return current\n}\n\n/**\n * This check is intentionally owned by shared code so hosts can decide whether\n * to dynamically import the telemetry package without evaluating that package.\n */\nexport function isTelemetryBackendEnabled(raw?: string): boolean {\n const value = raw ?? (\n typeof process === 'undefined'\n ? undefined\n : process.env.TELEMETRY_BACKEND\n )\n return ENABLED_BACKENDS.has((value ?? '').trim().toLowerCase())\n}\n\nexport function registerTelemetryRuntime(runtime: TelemetryRuntime): () => void {\n store().active = runtime\n return () => {\n const current = store()\n if (current.active === runtime) current.active = undefined\n }\n}\n\nexport function getTelemetryRuntime(): TelemetryRuntime | undefined {\n return store().active\n}\n\n/** Test-only: clear the process-wide telemetry bridge. */\nexport function resetTelemetryRuntime(): void {\n store().active = undefined\n}\n\nconst NOOP_SPAN: TelemetrySpan = { setAttributes() {} }\n\n/**\n * Run `fn` inside a span, from a package that must not depend on\n * `@open-mercato/telemetry`. With telemetry off this is `fn` plus one global\n * lookup \u2014 no span object is allocated and the OTEL SDK is never reached.\n *\n * Pass `root: true` for the unit of work a long-lived job should be sampled and\n * rendered by (a batch, a page) so the job is not one trace under one sampling\n * decision, and `links` to keep the causal chain back to what triggered it.\n */\nexport function withTelemetrySpan<T>(\n name: string,\n fn: (span: TelemetrySpan) => T,\n options?: TelemetrySpanOptions,\n): T {\n const runtime = getTelemetryRuntime()\n if (!runtime?.withSpan) return fn(NOOP_SPAN)\n return runtime.withSpan(name, fn, options)\n}\n\n/**\n * The active trace as a carrier, for use as a `links` entry. `undefined` when\n * telemetry is off or nothing is active, which `withTelemetrySpan` treats as\n * \"no link\" rather than an invalid one.\n */\nexport function captureTelemetryTrace(): TelemetryTraceCarrier | undefined {\n const carrier = getTelemetryRuntime()?.captureTraceContext()\n return carrier && Object.keys(carrier).length > 0 ? carrier : undefined\n}\n"],
5
- "mappings": "AA8DA,MAAM,aAAa,uBAAO,IAAI,uCAAuC;AACrE,MAAM,mBAAmB,oBAAI,IAAI,CAAC,WAAW,UAAU,YAAY,MAAM,CAAC;AAM1E,SAAS,QAA+B;AACtC,QAAM,cAAc;AACpB,MAAI,UAAU,YAAY,UAAU;AACpC,MAAI,CAAC,SAAS;AACZ,cAAU,CAAC;AACX,gBAAY,UAAU,IAAI;AAAA,EAC5B;AACA,SAAO;AACT;AAMO,SAAS,0BAA0B,KAAuB;AAC/D,QAAM,QAAQ,QACZ,OAAO,YAAY,cACf,SACA,QAAQ,IAAI;AAElB,SAAO,iBAAiB,KAAK,SAAS,IAAI,KAAK,EAAE,YAAY,CAAC;AAChE;AAEO,SAAS,yBAAyB,SAAuC;AAC9E,QAAM,EAAE,SAAS;AACjB,SAAO,MAAM;AACX,UAAM,UAAU,MAAM;AACtB,QAAI,QAAQ,WAAW,QAAS,SAAQ,SAAS;AAAA,EACnD;AACF;AAEO,SAAS,sBAAoD;AAClE,SAAO,MAAM,EAAE;AACjB;AAGO,SAAS,wBAA8B;AAC5C,QAAM,EAAE,SAAS;AACnB;AAEA,MAAM,YAA2B,EAAE,gBAAgB;AAAC,EAAE;AAW/C,SAAS,kBACd,MACA,IACA,SACG;AACH,QAAM,UAAU,oBAAoB;AACpC,MAAI,CAAC,SAAS,SAAU,QAAO,GAAG,SAAS;AAC3C,SAAO,QAAQ,SAAS,MAAM,IAAI,OAAO;AAC3C;AAOO,SAAS,wBAA2D;AACzE,QAAM,UAAU,oBAAoB,GAAG,oBAAoB;AAC3D,SAAO,WAAW,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,UAAU;AAChE;",
4
+ "sourcesContent": ["export type TelemetryTraceCarrier = Record<string, string>\n\nexport type TelemetrySpanAttributes = Record<string, string | number | boolean | undefined>\n\nexport type TelemetryMetricLabels = Record<string, string | number | boolean | undefined>\n\nexport type TelemetrySpanKind = 'internal' | 'server' | 'client' | 'producer' | 'consumer'\n\n/** The subset of the telemetry package's `Span` that bridge consumers need. */\nexport type TelemetrySpan = {\n setAttributes(attributes: TelemetrySpanAttributes): void\n /**\n * Rename an in-flight span whose identity is only known once it has run.\n * Optional so a bootstrap predating it still satisfies the contract \u2014 call it\n * as `span.updateName?.(\u2026)`.\n */\n updateName?(name: string): void\n}\n\nexport type TelemetrySpanOptions = {\n kind?: TelemetrySpanKind\n attributes?: TelemetrySpanAttributes\n /** Start a new trace so the sampler decides for this span alone. */\n root?: boolean\n /** Causal links to other traces, as W3C carriers. */\n links?: TelemetryTraceCarrier[]\n}\n\nexport type TelemetryRuntime = {\n /**\n * True only when the active SDK may safely use the process-global W3C\n * propagator for cross-boundary extraction.\n */\n canUseGlobalTracePropagation(): boolean\n captureTraceContext(): TelemetryTraceCarrier\n continueTrace<T>(\n carrier: TelemetryTraceCarrier | undefined,\n name: string,\n fn: () => T,\n options?: { kind?: 'internal' | 'server' | 'client' | 'producer' | 'consumer' },\n ): T\n /**\n * Optional so an older bootstrap that predates span support still satisfies\n * the contract; consumers go through `withTelemetrySpan` and degrade to\n * running `fn` untraced.\n */\n withSpan?<T>(name: string, fn: (span: TelemetrySpan) => T, options?: TelemetrySpanOptions): T\n recordHistogram?(\n name: string,\n value: number,\n labels?: TelemetryMetricLabels,\n unit?: string,\n ): void\n recordHttpDuration(method: string, route: string, status: number, startedAt: number): void\n reportError(\n error: unknown,\n context?: {\n module?: string\n /**\n * Stable, enumerated fingerprint (`module.reason`) the backend groups on.\n * Optional so a bootstrap predating it still satisfies the contract \u2014 an\n * older bridge simply ignores the field.\n */\n code?: string\n attributes?: Record<string, string | number | boolean | undefined>\n },\n ): void\n shutdown(): Promise<void>\n}\n\nconst GLOBAL_KEY = Symbol.for('@open-mercato/shared.telemetryRuntime')\nconst ENABLED_BACKENDS = new Set(['console', 'signoz', 'newrelic', 'otlp'])\n\ntype TelemetryRuntimeStore = {\n active?: TelemetryRuntime\n}\n\nfunction store(): TelemetryRuntimeStore {\n const globalStore = globalThis as unknown as Record<symbol, TelemetryRuntimeStore | undefined>\n let current = globalStore[GLOBAL_KEY]\n if (!current) {\n current = {}\n globalStore[GLOBAL_KEY] = current\n }\n return current\n}\n\n/**\n * This check is intentionally owned by shared code so hosts can decide whether\n * to dynamically import the telemetry package without evaluating that package.\n */\nexport function isTelemetryBackendEnabled(raw?: string): boolean {\n const value = raw ?? (\n typeof process === 'undefined'\n ? undefined\n : process.env.TELEMETRY_BACKEND\n )\n return ENABLED_BACKENDS.has((value ?? '').trim().toLowerCase())\n}\n\nexport function registerTelemetryRuntime(runtime: TelemetryRuntime): () => void {\n store().active = runtime\n return () => {\n const current = store()\n if (current.active === runtime) current.active = undefined\n }\n}\n\nexport function getTelemetryRuntime(): TelemetryRuntime | undefined {\n return store().active\n}\n\n/** Test-only: clear the process-wide telemetry bridge. */\nexport function resetTelemetryRuntime(): void {\n store().active = undefined\n}\n\nconst NOOP_SPAN: TelemetrySpan = { setAttributes() {} }\n\n/**\n * Run `fn` inside a span, from a package that must not depend on\n * `@open-mercato/telemetry`. With telemetry off this is `fn` plus one global\n * lookup \u2014 no span object is allocated and the OTEL SDK is never reached.\n *\n * Pass `root: true` for the unit of work a long-lived job should be sampled and\n * rendered by (a batch, a page) so the job is not one trace under one sampling\n * decision, and `links` to keep the causal chain back to what triggered it.\n */\nexport function withTelemetrySpan<T>(\n name: string,\n fn: (span: TelemetrySpan) => T,\n options?: TelemetrySpanOptions,\n): T {\n const runtime = getTelemetryRuntime()\n if (!runtime?.withSpan) return fn(NOOP_SPAN)\n return runtime.withSpan(name, fn, options)\n}\n\n/**\n * The active trace as a carrier, for use as a `links` entry. `undefined` when\n * telemetry is off or nothing is active, which `withTelemetrySpan` treats as\n * \"no link\" rather than an invalid one.\n */\nexport function captureTelemetryTrace(): TelemetryTraceCarrier | undefined {\n const carrier = getTelemetryRuntime()?.captureTraceContext()\n return carrier && Object.keys(carrier).length > 0 ? carrier : undefined\n}\n"],
5
+ "mappings": "AAsEA,MAAM,aAAa,uBAAO,IAAI,uCAAuC;AACrE,MAAM,mBAAmB,oBAAI,IAAI,CAAC,WAAW,UAAU,YAAY,MAAM,CAAC;AAM1E,SAAS,QAA+B;AACtC,QAAM,cAAc;AACpB,MAAI,UAAU,YAAY,UAAU;AACpC,MAAI,CAAC,SAAS;AACZ,cAAU,CAAC;AACX,gBAAY,UAAU,IAAI;AAAA,EAC5B;AACA,SAAO;AACT;AAMO,SAAS,0BAA0B,KAAuB;AAC/D,QAAM,QAAQ,QACZ,OAAO,YAAY,cACf,SACA,QAAQ,IAAI;AAElB,SAAO,iBAAiB,KAAK,SAAS,IAAI,KAAK,EAAE,YAAY,CAAC;AAChE;AAEO,SAAS,yBAAyB,SAAuC;AAC9E,QAAM,EAAE,SAAS;AACjB,SAAO,MAAM;AACX,UAAM,UAAU,MAAM;AACtB,QAAI,QAAQ,WAAW,QAAS,SAAQ,SAAS;AAAA,EACnD;AACF;AAEO,SAAS,sBAAoD;AAClE,SAAO,MAAM,EAAE;AACjB;AAGO,SAAS,wBAA8B;AAC5C,QAAM,EAAE,SAAS;AACnB;AAEA,MAAM,YAA2B,EAAE,gBAAgB;AAAC,EAAE;AAW/C,SAAS,kBACd,MACA,IACA,SACG;AACH,QAAM,UAAU,oBAAoB;AACpC,MAAI,CAAC,SAAS,SAAU,QAAO,GAAG,SAAS;AAC3C,SAAO,QAAQ,SAAS,MAAM,IAAI,OAAO;AAC3C;AAOO,SAAS,wBAA2D;AACzE,QAAM,UAAU,oBAAoB,GAAG,oBAAoB;AAC3D,SAAO,WAAW,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,UAAU;AAChE;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.8.1-develop.7296.1.2111d779db";
1
+ const APP_VERSION = "0.9.0";
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.8.1-develop.7296.1.2111d779db';\nexport const appVersion = APP_VERSION;\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.9.0';\nexport const appVersion = APP_VERSION;\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
@@ -96,6 +96,12 @@ function isPrivateCrossProcessEventEmitter(eventId, emitterModuleId) {
96
96
  if (event?.crossProcessBroadcast !== true) return true;
97
97
  return typeof event.module === "string" && event.module.length > 0 && event.module === emitterModuleId;
98
98
  }
99
+ function isCoalescedBroadcastEvent(eventId) {
100
+ const event = getEventRegistryState().declaredEvents.find((e) => e.id === eventId);
101
+ if (event?.broadcastCoalescing !== true) return false;
102
+ if (event.crossProcessBroadcast === true) return false;
103
+ return event.clientBroadcast === true || event.portalBroadcast === true;
104
+ }
99
105
  function isPortalBroadcastEvent(eventId) {
100
106
  const event = getEventRegistryState().declaredEvents.find((e) => e.id === eventId);
101
107
  return event?.portalBroadcast === true;
@@ -124,6 +130,19 @@ function createModuleEvents(options) {
124
130
  module: moduleId
125
131
  })
126
132
  );
133
+ for (const event of fullEvents) {
134
+ if (event.broadcastCoalescing !== true) continue;
135
+ if (event.crossProcessBroadcast === true) {
136
+ throw new Error(
137
+ `[internal] Event "${event.id}" declared by module "${moduleId}" combines crossProcessBroadcast with broadcastCoalescing. Private cross-process coordination must be delivered immediately \u2014 delaying it would let another process serve stale data. Drop one of the two flags.`
138
+ );
139
+ }
140
+ if (event.clientBroadcast !== true && event.portalBroadcast !== true) {
141
+ throw new Error(
142
+ `[internal] Event "${event.id}" declared by module "${moduleId}" sets broadcastCoalescing without clientBroadcast or portalBroadcast. There is no browser delivery to coalesce.`
143
+ );
144
+ }
145
+ }
127
146
  for (const event of fullEvents) {
128
147
  addDeclaredEvent(event);
129
148
  }
@@ -169,6 +188,7 @@ export {
169
188
  getEventModuleConfigs,
170
189
  getGlobalEventBus,
171
190
  isBroadcastEvent,
191
+ isCoalescedBroadcastEvent,
172
192
  isCrossProcessBroadcastEvent,
173
193
  isEventDeclared,
174
194
  isPortalBroadcastEvent,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/events/factory.ts"],
4
- "sourcesContent": ["/**\n * Event Module Factory\n *\n * Provides factory functions for creating type-safe event configurations.\n */\n\nimport { createLogger } from '../../lib/logger'\nimport type {\n EventDefinition,\n EventModuleConfig,\n EventPayload,\n EventPayloadSchema,\n EmitOptions,\n CreateModuleEventsOptions,\n ModuleEventEmitter,\n} from './types'\n\nconst logger = createLogger('events').child({ component: 'factory' })\n\n// =============================================================================\n// Global Event Bus Reference\n// =============================================================================\n\n/**\n * Type for the global event bus interface\n */\ninterface GlobalEventBus {\n emit(event: string, payload: unknown, options?: EmitOptions): Promise<void>\n}\n\nconst GLOBAL_EVENT_BUS_KEY = '__openMercatoGlobalEventBus__'\n\n// Global event bus reference (set during bootstrap)\nlet globalEventBus: GlobalEventBus | null = null\n\n/**\n * Set the global event bus instance.\n * Called during app bootstrap to wire up event emission.\n */\nexport function setGlobalEventBus(bus: GlobalEventBus): void {\n globalEventBus = bus\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY] = bus\n } catch {\n // ignore global assignment failures\n }\n}\n\n/**\n * Get the global event bus instance.\n * Returns null if not yet bootstrapped.\n */\nexport function getGlobalEventBus(): GlobalEventBus | null {\n try {\n const sharedBus = (globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY]\n if (sharedBus && typeof sharedBus === 'object' && typeof (sharedBus as GlobalEventBus).emit === 'function') {\n return sharedBus as GlobalEventBus\n }\n } catch {\n // ignore global read failures\n }\n return globalEventBus\n}\n\n// =============================================================================\n// Event Registry for Validation\n// =============================================================================\n\ntype EventRegistryState = {\n declaredEventIds: Set<string>\n declaredEvents: EventDefinition[]\n registeredEventConfigs: EventModuleConfig[] | null\n}\n\nconst GLOBAL_EVENT_REGISTRY_KEY = '__openMercatoEventDefinitionRegistry__'\n\nconst fallbackEventRegistryState: EventRegistryState = {\n declaredEventIds: new Set<string>(),\n declaredEvents: [],\n registeredEventConfigs: null,\n}\n\nfunction isEventRegistryState(value: unknown): value is EventRegistryState {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Partial<EventRegistryState>\n return candidate.declaredEventIds instanceof Set\n && Array.isArray(candidate.declaredEvents)\n && (candidate.registeredEventConfigs === null || Array.isArray(candidate.registeredEventConfigs))\n}\n\nfunction getEventRegistryState(): EventRegistryState {\n try {\n const globalScope = globalThis as Record<string, unknown>\n const existing = globalScope[GLOBAL_EVENT_REGISTRY_KEY]\n if (isEventRegistryState(existing)) return existing\n globalScope[GLOBAL_EVENT_REGISTRY_KEY] = fallbackEventRegistryState\n return fallbackEventRegistryState\n } catch {\n // Restricted runtimes may deny global access. Keep the previous\n // module-local behavior as a safe fallback.\n return fallbackEventRegistryState\n }\n}\n\nconst CRUD_AFTER_EVENT_SUFFIXES = ['.created', '.updated', '.deleted'] as const\n\n/**\n * Generated payload schema for platform-emitted CRUD after-events. Mirrors the\n * default payload built by the data engine's `emitOrmEntityEvent` when no\n * `buildPayload` override is configured: `{ id, organizationId, tenantId }`\n * plus `syncOrigin` when the write originated from a sync. organizationId and\n * tenantId keys are always present but may be null, so they are `optional`.\n */\nexport const DEFAULT_CRUD_PAYLOAD_SCHEMA: EventPayloadSchema = {\n fields: [\n { path: 'id', type: 'text' },\n { path: 'organizationId', type: 'text', optional: true },\n { path: 'tenantId', type: 'text', optional: true },\n { path: 'syncOrigin', type: 'text', optional: true },\n ],\n}\n\nfunction applyDefaultCrudPayloadSchema(event: EventDefinition): EventDefinition {\n if (event.payloadSchema) return event\n if (event.category !== 'crud') return event\n if (!CRUD_AFTER_EVENT_SUFFIXES.some(suffix => event.id.endsWith(suffix))) return event\n return { ...event, payloadSchema: DEFAULT_CRUD_PAYLOAD_SCHEMA }\n}\n\nfunction addDeclaredEvent(event: EventDefinition): void {\n const declared = applyDefaultCrudPayloadSchema(event)\n const state = getEventRegistryState()\n state.declaredEventIds.add(declared.id)\n const existingIndex = state.declaredEvents.findIndex((candidate) => candidate.id === declared.id)\n if (existingIndex < 0) {\n state.declaredEvents.push(declared)\n return\n }\n // Refresh a module's own definition in place during HMR without allowing a\n // duplicate declaration from another module to take over the event id.\n if (state.declaredEvents[existingIndex]?.module === declared.module) {\n state.declaredEvents[existingIndex] = declared\n }\n}\n\n/**\n * Check if an event ID has been declared by any module.\n * Used for runtime validation to ensure only declared events are emitted.\n */\nexport function isEventDeclared(eventId: string): boolean {\n return getEventRegistryState().declaredEventIds.has(eventId)\n}\n\n/**\n * Get all declared event IDs.\n * Useful for debugging and introspection.\n */\nexport function getAllDeclaredEventIds(): string[] {\n return Array.from(getEventRegistryState().declaredEventIds)\n}\n\n/**\n * Get all declared events with their full definitions.\n * Used by the API to return available events for workflow triggers.\n */\nexport function getDeclaredEvents(): EventDefinition[] {\n return [...getEventRegistryState().declaredEvents]\n}\n\n/**\n * Check if an event has clientBroadcast enabled.\n * Used by the SSE endpoint to filter events for the DOM Event Bridge.\n */\nexport function isBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true\n}\n\n/**\n * Check if an event should be published over the server-to-server event bridge.\n * Browser-broadcast events remain eligible for backward compatibility, while\n * crossProcessBroadcast supports private process coordination without SSE.\n */\nexport function isCrossProcessBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true || event?.crossProcessBroadcast === true\n}\n\n/**\n * Check whether an event is reserved for private server-to-server\n * coordination. Workflow-authored EMIT_EVENT activities must not emit these\n * events because their payload and event id are tenant-managed input.\n */\nexport function isPrivateCrossProcessBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.crossProcessBroadcast === true && event?.clientBroadcast !== true\n}\n\n/**\n * Verify provenance for a private cross-process event. The module id is\n * stamped by a declared module emitter or another trusted server-side seam;\n * tenant-managed event payloads never participate in this decision.\n */\nexport function isPrivateCrossProcessEventEmitter(\n eventId: string,\n emitterModuleId: string | undefined,\n): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n if (event?.crossProcessBroadcast !== true) return true\n return typeof event.module === 'string'\n && event.module.length > 0\n && event.module === emitterModuleId\n}\n\n/**\n * Check if an event has portalBroadcast enabled.\n * Used by the portal SSE endpoint to filter events for the Portal Event Bridge.\n */\nexport function isPortalBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.portalBroadcast === true\n}\n\n// =============================================================================\n// Bootstrap Registration (similar to searchModuleConfigs pattern)\n// =============================================================================\n\n/**\n * Register event module configurations globally.\n * Called during app bootstrap with configs from events.generated.ts.\n */\nexport function registerEventModuleConfigs(configs: EventModuleConfig[]): void {\n const state = getEventRegistryState()\n if (state.registeredEventConfigs !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Event module configs re-registered (this may occur during HMR)')\n }\n state.registeredEventConfigs = configs\n for (const config of configs) {\n for (const event of config.events) {\n addDeclaredEvent(event)\n }\n }\n}\n\n/**\n * Get registered event module configurations.\n * Returns empty array if not registered.\n */\nexport function getEventModuleConfigs(): EventModuleConfig[] {\n return getEventRegistryState().registeredEventConfigs ?? []\n}\n\n// =============================================================================\n// Factory Function\n// =============================================================================\n\n/**\n * Creates a type-safe event configuration for a module.\n *\n * Usage in module events.ts:\n * ```typescript\n * import { createModuleEvents } from '@open-mercato/shared/modules/events'\n *\n * const events = [\n * { id: 'customers.people.created', label: 'Person Created', category: 'crud' },\n * { id: 'customers.people.updated', label: 'Person Updated', category: 'crud' },\n * ] as const\n *\n * export const eventsConfig = createModuleEvents({\n * moduleId: 'customers',\n * events,\n * })\n *\n * // Export the typed emit function for use in commands\n * export const emitCustomersEvent = eventsConfig.emit\n *\n * // Export event IDs as a type for external use\n * export type CustomersEventId = typeof events[number]['id']\n *\n * export default eventsConfig\n * ```\n *\n * TypeScript will enforce that only declared event IDs can be emitted:\n * ```typescript\n * // \u2705 This compiles - event is declared\n * emitCustomersEvent('customers.people.created', { id: '123', tenantId: 'abc' })\n *\n * // \u274C TypeScript error - event not declared\n * emitCustomersEvent('customers.people.exploded', { id: '123' })\n * ```\n */\nexport function createModuleEvents<\n const TEvents extends readonly { id: string }[],\n TEventIds extends TEvents[number]['id'] = TEvents[number]['id']\n>(options: CreateModuleEventsOptions<TEventIds>): EventModuleConfig<TEventIds> {\n const { moduleId, events, strict = false } = options\n\n // Build set of valid event IDs for runtime validation\n const validEventIds = new Set(events.map(e => e.id))\n\n // Build full event definitions with module added and the generated CRUD\n // payload-schema default applied, so config consumers and the global\n // registry see the same definitions.\n const fullEvents: EventDefinition[] = events.map(e =>\n applyDefaultCrudPayloadSchema({\n ...e,\n module: moduleId,\n }),\n )\n\n // Register all event IDs and definitions in the global registry.\n for (const event of fullEvents) {\n addDeclaredEvent(event)\n }\n\n /**\n * The emit function - validates events and delegates to the global event bus\n */\n const emit = async (\n eventId: TEventIds,\n payload: EventPayload,\n emitOptions?: EmitOptions\n ): Promise<void> => {\n // Runtime validation - event must be declared\n if (!validEventIds.has(eventId)) {\n const message =\n `[events] Module \"${moduleId}\" tried to emit undeclared event \"${eventId}\". ` +\n `Add it to the module's events.ts file first.`\n\n if (strict) {\n throw new Error(message)\n } else {\n logger.error('Module tried to emit undeclared event \u2014 add it to the module events.ts first', { moduleId, eventId })\n // In non-strict mode, still emit but with warning\n }\n }\n\n // Get event bus from global reference\n const eventBus = getGlobalEventBus()\n if (!eventBus) {\n logger.warn('Event bus not available, cannot emit event', { eventId })\n return\n }\n\n const eventDefinition = fullEvents.find((event) => event.id === eventId)\n const isClientBroadcast = eventDefinition?.clientBroadcast === true\n const trustedOptions = eventDefinition?.crossProcessBroadcast === true || isClientBroadcast\n ? {\n ...emitOptions,\n // Browser-broadcast module emitters historically accepted scope in\n // their typed payload. Preserve that contract at the trusted module\n // boundary while the event bus itself relies only on options.\n ...(isClientBroadcast && emitOptions?.tenantId === undefined\n ? { tenantId: payload.tenantId ?? null }\n : {}),\n ...(isClientBroadcast && emitOptions?.organizationId === undefined\n ? { organizationId: payload.organizationId ?? null }\n : {}),\n ...(isClientBroadcast && emitOptions?.organizationIds === undefined && Array.isArray(payload.organizationIds)\n ? { organizationIds: payload.organizationIds.filter((value): value is string => typeof value === 'string') }\n : {}),\n emitterModuleId: moduleId,\n }\n : emitOptions\n await eventBus.emit(eventId, payload, trustedOptions)\n }\n\n return {\n moduleId,\n events: fullEvents,\n emit: emit as unknown as ModuleEventEmitter<TEventIds>,\n }\n}\n"],
5
- "mappings": "AAMA,SAAS,oBAAoB;AAW7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,UAAU,CAAC;AAapE,MAAM,uBAAuB;AAG7B,IAAI,iBAAwC;AAMrC,SAAS,kBAAkB,KAA2B;AAC3D,mBAAiB;AACjB,MAAI;AACF;AAAC,IAAC,WAAuC,oBAAoB,IAAI;AAAA,EACnE,QAAQ;AAAA,EAER;AACF;AAMO,SAAS,oBAA2C;AACzD,MAAI;AACF,UAAM,YAAa,WAAuC,oBAAoB;AAC9E,QAAI,aAAa,OAAO,cAAc,YAAY,OAAQ,UAA6B,SAAS,YAAY;AAC1G,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAYA,MAAM,4BAA4B;AAElC,MAAM,6BAAiD;AAAA,EACrD,kBAAkB,oBAAI,IAAY;AAAA,EAClC,gBAAgB,CAAC;AAAA,EACjB,wBAAwB;AAC1B;AAEA,SAAS,qBAAqB,OAA6C;AACzE,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,YAAY;AAClB,SAAO,UAAU,4BAA4B,OACxC,MAAM,QAAQ,UAAU,cAAc,MACrC,UAAU,2BAA2B,QAAQ,MAAM,QAAQ,UAAU,sBAAsB;AACnG;AAEA,SAAS,wBAA4C;AACnD,MAAI;AACF,UAAM,cAAc;AACpB,UAAM,WAAW,YAAY,yBAAyB;AACtD,QAAI,qBAAqB,QAAQ,EAAG,QAAO;AAC3C,gBAAY,yBAAyB,IAAI;AACzC,WAAO;AAAA,EACT,QAAQ;AAGN,WAAO;AAAA,EACT;AACF;AAEA,MAAM,4BAA4B,CAAC,YAAY,YAAY,UAAU;AAS9D,MAAM,8BAAkD;AAAA,EAC7D,QAAQ;AAAA,IACN,EAAE,MAAM,MAAM,MAAM,OAAO;AAAA,IAC3B,EAAE,MAAM,kBAAkB,MAAM,QAAQ,UAAU,KAAK;AAAA,IACvD,EAAE,MAAM,YAAY,MAAM,QAAQ,UAAU,KAAK;AAAA,IACjD,EAAE,MAAM,cAAc,MAAM,QAAQ,UAAU,KAAK;AAAA,EACrD;AACF;AAEA,SAAS,8BAA8B,OAAyC;AAC9E,MAAI,MAAM,cAAe,QAAO;AAChC,MAAI,MAAM,aAAa,OAAQ,QAAO;AACtC,MAAI,CAAC,0BAA0B,KAAK,YAAU,MAAM,GAAG,SAAS,MAAM,CAAC,EAAG,QAAO;AACjF,SAAO,EAAE,GAAG,OAAO,eAAe,4BAA4B;AAChE;AAEA,SAAS,iBAAiB,OAA8B;AACtD,QAAM,WAAW,8BAA8B,KAAK;AACpD,QAAM,QAAQ,sBAAsB;AACpC,QAAM,iBAAiB,IAAI,SAAS,EAAE;AACtC,QAAM,gBAAgB,MAAM,eAAe,UAAU,CAAC,cAAc,UAAU,OAAO,SAAS,EAAE;AAChG,MAAI,gBAAgB,GAAG;AACrB,UAAM,eAAe,KAAK,QAAQ;AAClC;AAAA,EACF;AAGA,MAAI,MAAM,eAAe,aAAa,GAAG,WAAW,SAAS,QAAQ;AACnE,UAAM,eAAe,aAAa,IAAI;AAAA,EACxC;AACF;AAMO,SAAS,gBAAgB,SAA0B;AACxD,SAAO,sBAAsB,EAAE,iBAAiB,IAAI,OAAO;AAC7D;AAMO,SAAS,yBAAmC;AACjD,SAAO,MAAM,KAAK,sBAAsB,EAAE,gBAAgB;AAC5D;AAMO,SAAS,oBAAuC;AACrD,SAAO,CAAC,GAAG,sBAAsB,EAAE,cAAc;AACnD;AAMO,SAAS,iBAAiB,SAA0B;AACzD,QAAM,QAAQ,sBAAsB,EAAE,eAAe,KAAK,OAAK,EAAE,OAAO,OAAO;AAC/E,SAAO,OAAO,oBAAoB;AACpC;AAOO,SAAS,6BAA6B,SAA0B;AACrE,QAAM,QAAQ,sBAAsB,EAAE,eAAe,KAAK,OAAK,EAAE,OAAO,OAAO;AAC/E,SAAO,OAAO,oBAAoB,QAAQ,OAAO,0BAA0B;AAC7E;AAOO,SAAS,oCAAoC,SAA0B;AAC5E,QAAM,QAAQ,sBAAsB,EAAE,eAAe,KAAK,OAAK,EAAE,OAAO,OAAO;AAC/E,SAAO,OAAO,0BAA0B,QAAQ,OAAO,oBAAoB;AAC7E;AAOO,SAAS,kCACd,SACA,iBACS;AACT,QAAM,QAAQ,sBAAsB,EAAE,eAAe,KAAK,OAAK,EAAE,OAAO,OAAO;AAC/E,MAAI,OAAO,0BAA0B,KAAM,QAAO;AAClD,SAAO,OAAO,MAAM,WAAW,YAC1B,MAAM,OAAO,SAAS,KACtB,MAAM,WAAW;AACxB;AAMO,SAAS,uBAAuB,SAA0B;AAC/D,QAAM,QAAQ,sBAAsB,EAAE,eAAe,KAAK,OAAK,EAAE,OAAO,OAAO;AAC/E,SAAO,OAAO,oBAAoB;AACpC;AAUO,SAAS,2BAA2B,SAAoC;AAC7E,QAAM,QAAQ,sBAAsB;AACpC,MAAI,MAAM,2BAA2B,QAAQ,QAAQ,IAAI,aAAa,eAAe;AACnF,WAAO,MAAM,gEAAgE;AAAA,EAC/E;AACA,QAAM,yBAAyB;AAC/B,aAAW,UAAU,SAAS;AAC5B,eAAW,SAAS,OAAO,QAAQ;AACjC,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF;AACF;AAMO,SAAS,wBAA6C;AAC3D,SAAO,sBAAsB,EAAE,0BAA0B,CAAC;AAC5D;AAyCO,SAAS,mBAGd,SAA6E;AAC7E,QAAM,EAAE,UAAU,QAAQ,SAAS,MAAM,IAAI;AAG7C,QAAM,gBAAgB,IAAI,IAAI,OAAO,IAAI,OAAK,EAAE,EAAE,CAAC;AAKnD,QAAM,aAAgC,OAAO;AAAA,IAAI,OAC/C,8BAA8B;AAAA,MAC5B,GAAG;AAAA,MACH,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AAGA,aAAW,SAAS,YAAY;AAC9B,qBAAiB,KAAK;AAAA,EACxB;AAKA,QAAM,OAAO,OACX,SACA,SACA,gBACkB;AAElB,QAAI,CAAC,cAAc,IAAI,OAAO,GAAG;AAC/B,YAAM,UACJ,oBAAoB,QAAQ,qCAAqC,OAAO;AAG1E,UAAI,QAAQ;AACV,cAAM,IAAI,MAAM,OAAO;AAAA,MACzB,OAAO;AACL,eAAO,MAAM,qFAAgF,EAAE,UAAU,QAAQ,CAAC;AAAA,MAEpH;AAAA,IACF;AAGA,UAAM,WAAW,kBAAkB;AACnC,QAAI,CAAC,UAAU;AACb,aAAO,KAAK,8CAA8C,EAAE,QAAQ,CAAC;AACrE;AAAA,IACF;AAEA,UAAM,kBAAkB,WAAW,KAAK,CAAC,UAAU,MAAM,OAAO,OAAO;AACvE,UAAM,oBAAoB,iBAAiB,oBAAoB;AAC/D,UAAM,iBAAiB,iBAAiB,0BAA0B,QAAQ,oBACtE;AAAA,MACE,GAAG;AAAA;AAAA;AAAA;AAAA,MAIH,GAAI,qBAAqB,aAAa,aAAa,SAC/C,EAAE,UAAU,QAAQ,YAAY,KAAK,IACrC,CAAC;AAAA,MACL,GAAI,qBAAqB,aAAa,mBAAmB,SACrD,EAAE,gBAAgB,QAAQ,kBAAkB,KAAK,IACjD,CAAC;AAAA,MACL,GAAI,qBAAqB,aAAa,oBAAoB,UAAa,MAAM,QAAQ,QAAQ,eAAe,IACxG,EAAE,iBAAiB,QAAQ,gBAAgB,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ,EAAE,IACzG,CAAC;AAAA,MACL,iBAAiB;AAAA,IACnB,IACA;AACJ,UAAM,SAAS,KAAK,SAAS,SAAS,cAAc;AAAA,EACtD;AAEA,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["/**\n * Event Module Factory\n *\n * Provides factory functions for creating type-safe event configurations.\n */\n\nimport { createLogger } from '../../lib/logger'\nimport type {\n EventDefinition,\n EventModuleConfig,\n EventPayload,\n EventPayloadSchema,\n EmitOptions,\n CreateModuleEventsOptions,\n ModuleEventEmitter,\n} from './types'\n\nconst logger = createLogger('events').child({ component: 'factory' })\n\n// =============================================================================\n// Global Event Bus Reference\n// =============================================================================\n\n/**\n * Type for the global event bus interface\n */\ninterface GlobalEventBus {\n emit(event: string, payload: unknown, options?: EmitOptions): Promise<void>\n}\n\nconst GLOBAL_EVENT_BUS_KEY = '__openMercatoGlobalEventBus__'\n\n// Global event bus reference (set during bootstrap)\nlet globalEventBus: GlobalEventBus | null = null\n\n/**\n * Set the global event bus instance.\n * Called during app bootstrap to wire up event emission.\n */\nexport function setGlobalEventBus(bus: GlobalEventBus): void {\n globalEventBus = bus\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY] = bus\n } catch {\n // ignore global assignment failures\n }\n}\n\n/**\n * Get the global event bus instance.\n * Returns null if not yet bootstrapped.\n */\nexport function getGlobalEventBus(): GlobalEventBus | null {\n try {\n const sharedBus = (globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY]\n if (sharedBus && typeof sharedBus === 'object' && typeof (sharedBus as GlobalEventBus).emit === 'function') {\n return sharedBus as GlobalEventBus\n }\n } catch {\n // ignore global read failures\n }\n return globalEventBus\n}\n\n// =============================================================================\n// Event Registry for Validation\n// =============================================================================\n\ntype EventRegistryState = {\n declaredEventIds: Set<string>\n declaredEvents: EventDefinition[]\n registeredEventConfigs: EventModuleConfig[] | null\n}\n\nconst GLOBAL_EVENT_REGISTRY_KEY = '__openMercatoEventDefinitionRegistry__'\n\nconst fallbackEventRegistryState: EventRegistryState = {\n declaredEventIds: new Set<string>(),\n declaredEvents: [],\n registeredEventConfigs: null,\n}\n\nfunction isEventRegistryState(value: unknown): value is EventRegistryState {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Partial<EventRegistryState>\n return candidate.declaredEventIds instanceof Set\n && Array.isArray(candidate.declaredEvents)\n && (candidate.registeredEventConfigs === null || Array.isArray(candidate.registeredEventConfigs))\n}\n\nfunction getEventRegistryState(): EventRegistryState {\n try {\n const globalScope = globalThis as Record<string, unknown>\n const existing = globalScope[GLOBAL_EVENT_REGISTRY_KEY]\n if (isEventRegistryState(existing)) return existing\n globalScope[GLOBAL_EVENT_REGISTRY_KEY] = fallbackEventRegistryState\n return fallbackEventRegistryState\n } catch {\n // Restricted runtimes may deny global access. Keep the previous\n // module-local behavior as a safe fallback.\n return fallbackEventRegistryState\n }\n}\n\nconst CRUD_AFTER_EVENT_SUFFIXES = ['.created', '.updated', '.deleted'] as const\n\n/**\n * Generated payload schema for platform-emitted CRUD after-events. Mirrors the\n * default payload built by the data engine's `emitOrmEntityEvent` when no\n * `buildPayload` override is configured: `{ id, organizationId, tenantId }`\n * plus `syncOrigin` when the write originated from a sync. organizationId and\n * tenantId keys are always present but may be null, so they are `optional`.\n */\nexport const DEFAULT_CRUD_PAYLOAD_SCHEMA: EventPayloadSchema = {\n fields: [\n { path: 'id', type: 'text' },\n { path: 'organizationId', type: 'text', optional: true },\n { path: 'tenantId', type: 'text', optional: true },\n { path: 'syncOrigin', type: 'text', optional: true },\n ],\n}\n\nfunction applyDefaultCrudPayloadSchema(event: EventDefinition): EventDefinition {\n if (event.payloadSchema) return event\n if (event.category !== 'crud') return event\n if (!CRUD_AFTER_EVENT_SUFFIXES.some(suffix => event.id.endsWith(suffix))) return event\n return { ...event, payloadSchema: DEFAULT_CRUD_PAYLOAD_SCHEMA }\n}\n\nfunction addDeclaredEvent(event: EventDefinition): void {\n const declared = applyDefaultCrudPayloadSchema(event)\n const state = getEventRegistryState()\n state.declaredEventIds.add(declared.id)\n const existingIndex = state.declaredEvents.findIndex((candidate) => candidate.id === declared.id)\n if (existingIndex < 0) {\n state.declaredEvents.push(declared)\n return\n }\n // Refresh a module's own definition in place during HMR without allowing a\n // duplicate declaration from another module to take over the event id.\n if (state.declaredEvents[existingIndex]?.module === declared.module) {\n state.declaredEvents[existingIndex] = declared\n }\n}\n\n/**\n * Check if an event ID has been declared by any module.\n * Used for runtime validation to ensure only declared events are emitted.\n */\nexport function isEventDeclared(eventId: string): boolean {\n return getEventRegistryState().declaredEventIds.has(eventId)\n}\n\n/**\n * Get all declared event IDs.\n * Useful for debugging and introspection.\n */\nexport function getAllDeclaredEventIds(): string[] {\n return Array.from(getEventRegistryState().declaredEventIds)\n}\n\n/**\n * Get all declared events with their full definitions.\n * Used by the API to return available events for workflow triggers.\n */\nexport function getDeclaredEvents(): EventDefinition[] {\n return [...getEventRegistryState().declaredEvents]\n}\n\n/**\n * Check if an event has clientBroadcast enabled.\n * Used by the SSE endpoint to filter events for the DOM Event Bridge.\n */\nexport function isBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true\n}\n\n/**\n * Check if an event should be published over the server-to-server event bridge.\n * Browser-broadcast events remain eligible for backward compatibility, while\n * crossProcessBroadcast supports private process coordination without SSE.\n */\nexport function isCrossProcessBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true || event?.crossProcessBroadcast === true\n}\n\n/**\n * Check whether an event is reserved for private server-to-server\n * coordination. Workflow-authored EMIT_EVENT activities must not emit these\n * events because their payload and event id are tenant-managed input.\n */\nexport function isPrivateCrossProcessBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.crossProcessBroadcast === true && event?.clientBroadcast !== true\n}\n\n/**\n * Verify provenance for a private cross-process event. The module id is\n * stamped by a declared module emitter or another trusted server-side seam;\n * tenant-managed event payloads never participate in this decision.\n */\nexport function isPrivateCrossProcessEventEmitter(\n eventId: string,\n emitterModuleId: string | undefined,\n): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n if (event?.crossProcessBroadcast !== true) return true\n return typeof event.module === 'string'\n && event.module.length > 0\n && event.module === emitterModuleId\n}\n\n/**\n * Check whether an event opted into coalesced browser delivery.\n * Read by the event bus before it hands a browser dispatch to the coalescer.\n */\nexport function isCoalescedBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n if (event?.broadcastCoalescing !== true) return false\n // Private cross-process coordination must never be delayed, and an event with\n // no browser sink has nothing to coalesce. Both are rejected at declaration\n // time, so this is defence in depth for events registered by other paths.\n if (event.crossProcessBroadcast === true) return false\n return event.clientBroadcast === true || event.portalBroadcast === true\n}\n\n/**\n * Check if an event has portalBroadcast enabled.\n * Used by the portal SSE endpoint to filter events for the Portal Event Bridge.\n */\nexport function isPortalBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.portalBroadcast === true\n}\n\n// =============================================================================\n// Bootstrap Registration (similar to searchModuleConfigs pattern)\n// =============================================================================\n\n/**\n * Register event module configurations globally.\n * Called during app bootstrap with configs from events.generated.ts.\n */\nexport function registerEventModuleConfigs(configs: EventModuleConfig[]): void {\n const state = getEventRegistryState()\n if (state.registeredEventConfigs !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Event module configs re-registered (this may occur during HMR)')\n }\n state.registeredEventConfigs = configs\n for (const config of configs) {\n for (const event of config.events) {\n addDeclaredEvent(event)\n }\n }\n}\n\n/**\n * Get registered event module configurations.\n * Returns empty array if not registered.\n */\nexport function getEventModuleConfigs(): EventModuleConfig[] {\n return getEventRegistryState().registeredEventConfigs ?? []\n}\n\n// =============================================================================\n// Factory Function\n// =============================================================================\n\n/**\n * Creates a type-safe event configuration for a module.\n *\n * Usage in module events.ts:\n * ```typescript\n * import { createModuleEvents } from '@open-mercato/shared/modules/events'\n *\n * const events = [\n * { id: 'customers.people.created', label: 'Person Created', category: 'crud' },\n * { id: 'customers.people.updated', label: 'Person Updated', category: 'crud' },\n * ] as const\n *\n * export const eventsConfig = createModuleEvents({\n * moduleId: 'customers',\n * events,\n * })\n *\n * // Export the typed emit function for use in commands\n * export const emitCustomersEvent = eventsConfig.emit\n *\n * // Export event IDs as a type for external use\n * export type CustomersEventId = typeof events[number]['id']\n *\n * export default eventsConfig\n * ```\n *\n * TypeScript will enforce that only declared event IDs can be emitted:\n * ```typescript\n * // \u2705 This compiles - event is declared\n * emitCustomersEvent('customers.people.created', { id: '123', tenantId: 'abc' })\n *\n * // \u274C TypeScript error - event not declared\n * emitCustomersEvent('customers.people.exploded', { id: '123' })\n * ```\n */\nexport function createModuleEvents<\n const TEvents extends readonly { id: string }[],\n TEventIds extends TEvents[number]['id'] = TEvents[number]['id']\n>(options: CreateModuleEventsOptions<TEventIds>): EventModuleConfig<TEventIds> {\n const { moduleId, events, strict = false } = options\n\n // Build set of valid event IDs for runtime validation\n const validEventIds = new Set(events.map(e => e.id))\n\n // Build full event definitions with module added and the generated CRUD\n // payload-schema default applied, so config consumers and the global\n // registry see the same definitions.\n const fullEvents: EventDefinition[] = events.map(e =>\n applyDefaultCrudPayloadSchema({\n ...e,\n module: moduleId,\n }),\n )\n\n for (const event of fullEvents) {\n if (event.broadcastCoalescing !== true) continue\n if (event.crossProcessBroadcast === true) {\n throw new Error(\n `[internal] Event \"${event.id}\" declared by module \"${moduleId}\" combines crossProcessBroadcast with ` +\n 'broadcastCoalescing. Private cross-process coordination must be delivered immediately \u2014 delaying it ' +\n 'would let another process serve stale data. Drop one of the two flags.',\n )\n }\n if (event.clientBroadcast !== true && event.portalBroadcast !== true) {\n throw new Error(\n `[internal] Event \"${event.id}\" declared by module \"${moduleId}\" sets broadcastCoalescing without ` +\n 'clientBroadcast or portalBroadcast. There is no browser delivery to coalesce.',\n )\n }\n }\n\n // Register all event IDs and definitions in the global registry.\n for (const event of fullEvents) {\n addDeclaredEvent(event)\n }\n\n /**\n * The emit function - validates events and delegates to the global event bus\n */\n const emit = async (\n eventId: TEventIds,\n payload: EventPayload,\n emitOptions?: EmitOptions\n ): Promise<void> => {\n // Runtime validation - event must be declared\n if (!validEventIds.has(eventId)) {\n const message =\n `[events] Module \"${moduleId}\" tried to emit undeclared event \"${eventId}\". ` +\n `Add it to the module's events.ts file first.`\n\n if (strict) {\n throw new Error(message)\n } else {\n logger.error('Module tried to emit undeclared event \u2014 add it to the module events.ts first', { moduleId, eventId })\n // In non-strict mode, still emit but with warning\n }\n }\n\n // Get event bus from global reference\n const eventBus = getGlobalEventBus()\n if (!eventBus) {\n logger.warn('Event bus not available, cannot emit event', { eventId })\n return\n }\n\n const eventDefinition = fullEvents.find((event) => event.id === eventId)\n const isClientBroadcast = eventDefinition?.clientBroadcast === true\n const trustedOptions = eventDefinition?.crossProcessBroadcast === true || isClientBroadcast\n ? {\n ...emitOptions,\n // Browser-broadcast module emitters historically accepted scope in\n // their typed payload. Preserve that contract at the trusted module\n // boundary while the event bus itself relies only on options.\n ...(isClientBroadcast && emitOptions?.tenantId === undefined\n ? { tenantId: payload.tenantId ?? null }\n : {}),\n ...(isClientBroadcast && emitOptions?.organizationId === undefined\n ? { organizationId: payload.organizationId ?? null }\n : {}),\n ...(isClientBroadcast && emitOptions?.organizationIds === undefined && Array.isArray(payload.organizationIds)\n ? { organizationIds: payload.organizationIds.filter((value): value is string => typeof value === 'string') }\n : {}),\n emitterModuleId: moduleId,\n }\n : emitOptions\n await eventBus.emit(eventId, payload, trustedOptions)\n }\n\n return {\n moduleId,\n events: fullEvents,\n emit: emit as unknown as ModuleEventEmitter<TEventIds>,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,6 +6,7 @@ import {
6
6
  composeApiRouteOverrides,
7
7
  composePageRouteOverrides
8
8
  } from "./overrides.js";
9
+ import { bindMatchedApiRoutePath } from "../lib/modules/api-route-identity.js";
9
10
  const logger = createLogger("shared").child({ component: "cli-registry" });
10
11
  function normPath(s) {
11
12
  return (s.startsWith("/") ? s : "/" + s).replace(/\/+$/, "") || "/";
@@ -118,11 +119,12 @@ function findRouteManifestMatch(routes, pathname) {
118
119
  }
119
120
  }
120
121
  }
121
- function findApiRouteManifestMatch(routes, method, pathname) {
122
+ function findApiRouteManifestMatch(routes, method, pathname, request) {
122
123
  for (const route of ensureSortedRoutes(routes)) {
123
124
  if (!route.methods.includes(method)) continue;
124
125
  const params = matchRoutePattern(route.path, pathname);
125
126
  if (params) {
127
+ if (request && !bindMatchedApiRoutePath(request, route.path, params)) return void 0;
126
128
  return { route, params };
127
129
  }
128
130
  }
@@ -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'\nimport { createLogger } from '../lib/logger'\nimport {\n applyApiOverridesToManifests,\n applyModuleOverridesToModules,\n applyPageOverridesToManifests,\n composeApiRouteOverrides,\n composePageRouteOverrides,\n} from './overrides'\n\nconst logger = createLogger('shared').child({ component: 'cli-registry' })\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 priority?: number\n pagePriority?: 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 ModuleRouteMetadata = Omit<ModuleRoute, 'Component'>\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 moduleId?: 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 moduleId?: string\n queue: string\n concurrency: number\n lockDuration?: number\n maxStalledCount?: number\n /**\n * Reports a job the queue abandoned without running the handler.\n *\n * Present only for workers whose metadata declares it; the generator emits it as a lazy import\n * beside the handler, because the registry serializes metadata as literals and a function cannot\n * survive that.\n */\n onJobAbandoned?: (payload: unknown, info: { jobId: string | null; reason: string }) => void | Promise<void>\n handler: ModuleWorkerHandler\n /** Opt-in flag exposing this queue as a user-facing scheduler target (issue #5213). */\n schedulerSafe?: boolean\n /** Creator features required beyond scheduler.jobs.manage when schedulerSafe is set. */\n schedulerRequiredFeatures?: string[]\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 widgetId?: string\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: a long-lived process-wide runtime the module starts itself (from runtime.ts).\n // Invoked once per process by `mercato server start` and `mercato queue worker`.\n runtime?: import('./runtime').ModuleRuntime\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\n// 0 = literal (most specific), 1 = dynamic [param], 2 = catch-all [...param] or [[...param]]\nfunction segmentSpecificity(seg: string): 0 | 1 | 2 {\n if (seg.startsWith('[[...') || seg.startsWith('[...')) return 2\n if (seg.startsWith('[')) return 1\n return 0\n}\n\nfunction compareRouteSpecificity(aPattern: string, bPattern: string): number {\n const aSegs = aPattern.split('/')\n const bSegs = bPattern.split('/')\n const len = Math.max(aSegs.length, bSegs.length)\n for (let i = 0; i < len; i++) {\n const av = i < aSegs.length ? segmentSpecificity(aSegs[i]) : -1\n const bv = i < bSegs.length ? segmentSpecificity(bSegs[i]) : -1\n if (av !== bv) return av - bv\n }\n return 0\n}\n\nexport function sortRoutesBySpecificity<T extends { pattern?: string; path?: string }>(routes: T[]): T[] {\n return [...routes].sort((a, b) =>\n compareRouteSpecificity(a.pattern ?? a.path ?? '/', b.pattern ?? b.path ?? '/'),\n )\n}\n\n// Memoized per-array sorted view, so direct callers (e.g., the Next.js catch-all\n// routes that import generated `frontendRoutes`/`backendRoutes`/`apiRoutes`\n// arrays) match against a specificity-sorted view even if they never call\n// `register*RouteManifests`. Keyed by array reference; generated arrays are\n// module-level constants so this caches once per process.\nconst sortedRoutesCache = new WeakMap<object, readonly unknown[]>()\n\nfunction ensureSortedRoutes<T extends { pattern?: string; path?: string }>(routes: readonly T[]): readonly T[] {\n const cached = sortedRoutesCache.get(routes) as readonly T[] | undefined\n if (cached) return cached\n const sorted = sortRoutesBySpecificity([...routes])\n sortedRoutesCache.set(routes, sorted)\n return sorted\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 return params\n } else if (mOptCatch) {\n const key = mOptCatch[1]\n params[key] = i < uSegs.length ? uSegs.slice(i) : []\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 ensureSortedRoutes(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 ensureSortedRoutes(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\nexport { resolvePageRouteMetadata } from './pageRouteMetadata'\n\nlet _backendRouteManifests: BackendRouteManifestEntry[] | null = null\n\nexport function registerBackendRouteManifests(routes: BackendRouteManifestEntry[]) {\n const pageOverrides = composePageRouteOverrides()\n const finalRoutes = Object.keys(pageOverrides).length === 0\n ? routes\n : applyPageOverridesToManifests(routes, pageOverrides, 'backend')\n _backendRouteManifests = sortRoutesBySpecificity(finalRoutes)\n}\n\nexport function getBackendRouteManifests(): BackendRouteManifestEntry[] {\n return _backendRouteManifests ?? []\n}\n\nlet _frontendRouteManifests: FrontendRouteManifestEntry[] | null = null\n\nexport function registerFrontendRouteManifests(routes: FrontendRouteManifestEntry[]) {\n const pageOverrides = composePageRouteOverrides()\n const finalRoutes = Object.keys(pageOverrides).length === 0\n ? routes\n : applyPageOverridesToManifests(routes, pageOverrides, 'frontend')\n _frontendRouteManifests = sortRoutesBySpecificity(finalRoutes)\n}\n\nexport function getFrontendRouteManifests(): FrontendRouteManifestEntry[] {\n return _frontendRouteManifests ?? []\n}\n\nlet _apiRouteManifests: ApiRouteManifestEntry[] | null = null\n\nexport function registerApiRouteManifests(routes: ApiRouteManifestEntry[]) {\n // Apply any `entry.overrides.routes.api` overrides registered through the\n // unified `modules.ts` dispatcher or programmatic API before storing the\n // manifest. The composer is cheap and returns an empty object when no\n // overrides exist, so this is a no-op for apps that do not opt in.\n const routeOverrides = composeApiRouteOverrides()\n const finalRoutes = Object.keys(routeOverrides).length === 0\n ? routes\n : applyApiOverridesToManifests(routes, routeOverrides)\n _apiRouteManifests = sortRoutesBySpecificity(finalRoutes)\n}\n\nexport function getApiRouteManifests(): ApiRouteManifestEntry[] {\n return _apiRouteManifests ?? []\n}\n\n// CLI modules registry - populated ONLY by the `mercato` bin (packages/cli/src/bin.ts\n// plus the `init` and `seed:defaults` commands). Runtime code MUST NOT read it: it\n// fails open (see getCliModules below), so outside a CLI process a reader gets an\n// empty list and silently does nothing. The events worker made exactly that mistake\n// and dropped every persistent subscriber. Runtime code uses the app registry\n// (`getModules` from ../lib/modules/registry) or a DI-resolved service.\n// Enforced by src/modules/__tests__/cli-registry-boundary.test.ts.\nlet _cliModules: Module[] | null = null\n\nexport function registerCliModules(modules: Module[]) {\n if (_cliModules !== null && process.env.NODE_ENV === 'development') {\n logger.debug('CLI modules re-registered (this may occur during HMR)')\n }\n _cliModules = applyModuleOverridesToModules(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 ...(entry.keyScope ? { keyScope: entry.keyScope } : {}),\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\n/**\n * Resolves a worker's `metadata.onJobAbandoned` on first use.\n *\n * The generator serializes worker metadata as literals, so a function declared there cannot be\n * emitted inline the way `concurrency` or `lockDuration` are. It is emitted as this thunk instead \u2014\n * the same lazy-import treatment the handler already gets \u2014 and only for workers whose metadata\n * declares the callback, so no other queue acquires one (and with it, a sweep) by accident.\n */\nexport function createLazyModuleWorkerAbandonHook(\n loadModule: () => Promise<unknown>,\n id: string\n): (payload: unknown, info: { jobId: string | null; reason: string }) => Promise<void> {\n let hookPromise: Promise<((payload: unknown, info: { jobId: string | null; reason: string }) => unknown) | null> | null = null\n return async (payload, info) => {\n hookPromise ??= loadModule().then((loaded) => {\n const metadata = (loaded as { metadata?: { onJobAbandoned?: unknown } } | null)?.metadata\n return typeof metadata?.onJobAbandoned === 'function'\n ? (metadata.onJobAbandoned as (payload: unknown, info: { jobId: string | null; reason: string }) => unknown)\n : null\n })\n const hook = await hookPromise\n if (!hook) {\n throw new Error(`[registry] Worker \"${id}\" was registered with an abandoned-job hook but its metadata no longer declares one`)\n }\n await hook(payload, info)\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'\nimport { createLogger } from '../lib/logger'\nimport {\n applyApiOverridesToManifests,\n applyModuleOverridesToModules,\n applyPageOverridesToManifests,\n composeApiRouteOverrides,\n composePageRouteOverrides,\n} from './overrides'\nimport { bindMatchedApiRoutePath } from '../lib/modules/api-route-identity'\n\nconst logger = createLogger('shared').child({ component: 'cli-registry' })\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 priority?: number\n pagePriority?: 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 ModuleRouteMetadata = Omit<ModuleRoute, 'Component'>\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 moduleId?: 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 moduleId?: string\n queue: string\n concurrency: number\n lockDuration?: number\n maxStalledCount?: number\n /**\n * Reports a job the queue abandoned without running the handler.\n *\n * Present only for workers whose metadata declares it; the generator emits it as a lazy import\n * beside the handler, because the registry serializes metadata as literals and a function cannot\n * survive that.\n */\n onJobAbandoned?: (payload: unknown, info: { jobId: string | null; reason: string }) => void | Promise<void>\n handler: ModuleWorkerHandler\n /** Opt-in flag exposing this queue as a user-facing scheduler target (issue #5213). */\n schedulerSafe?: boolean\n /** Creator features required beyond scheduler.jobs.manage when schedulerSafe is set. */\n schedulerRequiredFeatures?: string[]\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 widgetId?: string\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: a long-lived process-wide runtime the module starts itself (from runtime.ts).\n // Invoked once per process by `mercato server start` and `mercato queue worker`.\n runtime?: import('./runtime').ModuleRuntime\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\n// 0 = literal (most specific), 1 = dynamic [param], 2 = catch-all [...param] or [[...param]]\nfunction segmentSpecificity(seg: string): 0 | 1 | 2 {\n if (seg.startsWith('[[...') || seg.startsWith('[...')) return 2\n if (seg.startsWith('[')) return 1\n return 0\n}\n\nfunction compareRouteSpecificity(aPattern: string, bPattern: string): number {\n const aSegs = aPattern.split('/')\n const bSegs = bPattern.split('/')\n const len = Math.max(aSegs.length, bSegs.length)\n for (let i = 0; i < len; i++) {\n const av = i < aSegs.length ? segmentSpecificity(aSegs[i]) : -1\n const bv = i < bSegs.length ? segmentSpecificity(bSegs[i]) : -1\n if (av !== bv) return av - bv\n }\n return 0\n}\n\nexport function sortRoutesBySpecificity<T extends { pattern?: string; path?: string }>(routes: T[]): T[] {\n return [...routes].sort((a, b) =>\n compareRouteSpecificity(a.pattern ?? a.path ?? '/', b.pattern ?? b.path ?? '/'),\n )\n}\n\n// Memoized per-array sorted view, so direct callers (e.g., the Next.js catch-all\n// routes that import generated `frontendRoutes`/`backendRoutes`/`apiRoutes`\n// arrays) match against a specificity-sorted view even if they never call\n// `register*RouteManifests`. Keyed by array reference; generated arrays are\n// module-level constants so this caches once per process.\nconst sortedRoutesCache = new WeakMap<object, readonly unknown[]>()\n\nfunction ensureSortedRoutes<T extends { pattern?: string; path?: string }>(routes: readonly T[]): readonly T[] {\n const cached = sortedRoutesCache.get(routes) as readonly T[] | undefined\n if (cached) return cached\n const sorted = sortRoutesBySpecificity([...routes])\n sortedRoutesCache.set(routes, sorted)\n return sorted\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 return params\n } else if (mOptCatch) {\n const key = mOptCatch[1]\n params[key] = i < uSegs.length ? uSegs.slice(i) : []\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 ensureSortedRoutes(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 request?: Request,\n): { route: T; params: RouteMatchParams } | undefined {\n for (const route of ensureSortedRoutes(routes)) {\n if (!route.methods.includes(method)) continue\n const params = matchRoutePattern(route.path, pathname)\n if (params) {\n if (request && !bindMatchedApiRoutePath(request, route.path, params)) return undefined\n return { route, params }\n }\n }\n}\n\nexport { resolvePageRouteMetadata } from './pageRouteMetadata'\n\nlet _backendRouteManifests: BackendRouteManifestEntry[] | null = null\n\nexport function registerBackendRouteManifests(routes: BackendRouteManifestEntry[]) {\n const pageOverrides = composePageRouteOverrides()\n const finalRoutes = Object.keys(pageOverrides).length === 0\n ? routes\n : applyPageOverridesToManifests(routes, pageOverrides, 'backend')\n _backendRouteManifests = sortRoutesBySpecificity(finalRoutes)\n}\n\nexport function getBackendRouteManifests(): BackendRouteManifestEntry[] {\n return _backendRouteManifests ?? []\n}\n\nlet _frontendRouteManifests: FrontendRouteManifestEntry[] | null = null\n\nexport function registerFrontendRouteManifests(routes: FrontendRouteManifestEntry[]) {\n const pageOverrides = composePageRouteOverrides()\n const finalRoutes = Object.keys(pageOverrides).length === 0\n ? routes\n : applyPageOverridesToManifests(routes, pageOverrides, 'frontend')\n _frontendRouteManifests = sortRoutesBySpecificity(finalRoutes)\n}\n\nexport function getFrontendRouteManifests(): FrontendRouteManifestEntry[] {\n return _frontendRouteManifests ?? []\n}\n\nlet _apiRouteManifests: ApiRouteManifestEntry[] | null = null\n\nexport function registerApiRouteManifests(routes: ApiRouteManifestEntry[]) {\n // Apply any `entry.overrides.routes.api` overrides registered through the\n // unified `modules.ts` dispatcher or programmatic API before storing the\n // manifest. The composer is cheap and returns an empty object when no\n // overrides exist, so this is a no-op for apps that do not opt in.\n const routeOverrides = composeApiRouteOverrides()\n const finalRoutes = Object.keys(routeOverrides).length === 0\n ? routes\n : applyApiOverridesToManifests(routes, routeOverrides)\n _apiRouteManifests = sortRoutesBySpecificity(finalRoutes)\n}\n\nexport function getApiRouteManifests(): ApiRouteManifestEntry[] {\n return _apiRouteManifests ?? []\n}\n\n// CLI modules registry - populated ONLY by the `mercato` bin (packages/cli/src/bin.ts\n// plus the `init` and `seed:defaults` commands). Runtime code MUST NOT read it: it\n// fails open (see getCliModules below), so outside a CLI process a reader gets an\n// empty list and silently does nothing. The events worker made exactly that mistake\n// and dropped every persistent subscriber. Runtime code uses the app registry\n// (`getModules` from ../lib/modules/registry) or a DI-resolved service.\n// Enforced by src/modules/__tests__/cli-registry-boundary.test.ts.\nlet _cliModules: Module[] | null = null\n\nexport function registerCliModules(modules: Module[]) {\n if (_cliModules !== null && process.env.NODE_ENV === 'development') {\n logger.debug('CLI modules re-registered (this may occur during HMR)')\n }\n _cliModules = applyModuleOverridesToModules(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 ...(entry.keyScope ? { keyScope: entry.keyScope } : {}),\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\n/**\n * Resolves a worker's `metadata.onJobAbandoned` on first use.\n *\n * The generator serializes worker metadata as literals, so a function declared there cannot be\n * emitted inline the way `concurrency` or `lockDuration` are. It is emitted as this thunk instead \u2014\n * the same lazy-import treatment the handler already gets \u2014 and only for workers whose metadata\n * declares the callback, so no other queue acquires one (and with it, a sweep) by accident.\n */\nexport function createLazyModuleWorkerAbandonHook(\n loadModule: () => Promise<unknown>,\n id: string\n): (payload: unknown, info: { jobId: string | null; reason: string }) => Promise<void> {\n let hookPromise: Promise<((payload: unknown, info: { jobId: string | null; reason: string }) => unknown) | null> | null = null\n return async (payload, info) => {\n hookPromise ??= loadModule().then((loaded) => {\n const metadata = (loaded as { metadata?: { onJobAbandoned?: unknown } } | null)?.metadata\n return typeof metadata?.onJobAbandoned === 'function'\n ? (metadata.onJobAbandoned as (payload: unknown, info: { jobId: string | null; reason: string }) => unknown)\n : null\n })\n const hook = await hookPromise\n if (!hook) {\n throw new Error(`[registry] Worker \"${id}\" was registered with an abandoned-job hook but its metadata no longer declares one`)\n }\n await hook(payload, info)\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.8.1-develop.7296.1.2111d779db",
3
+ "version": "0.9.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,6 +12,10 @@
12
12
  },
13
13
  "exports": {
14
14
  ".": "./dist/index.js",
15
+ "./lib/availability": {
16
+ "types": "./src/lib/availability/index.ts",
17
+ "default": "./dist/lib/availability/index.js"
18
+ },
15
19
  "./lib/openapi": {
16
20
  "types": "./src/lib/openapi/index.ts",
17
21
  "default": "./dist/lib/openapi/index.js"
@@ -20,6 +24,10 @@
20
24
  "types": "./src/lib/commands/index.ts",
21
25
  "default": "./dist/lib/commands/index.js"
22
26
  },
27
+ "./lib/catalog-visibility": {
28
+ "types": "./src/lib/catalog-visibility/index.ts",
29
+ "default": "./dist/lib/catalog-visibility/index.js"
30
+ },
23
31
  "./lib/hotkeys": {
24
32
  "types": "./src/lib/hotkeys/index.ts",
25
33
  "default": "./dist/lib/hotkeys/index.js"
@@ -113,7 +121,7 @@
113
121
  "@mikro-orm/core": "^7.1.14",
114
122
  "@mikro-orm/decorators": "^7.1.14",
115
123
  "@mikro-orm/postgresql": "^7.1.14",
116
- "@open-mercato/cache": "0.8.1-develop.7296.1.2111d779db",
124
+ "@open-mercato/cache": "0.9.0",
117
125
  "@types/html-to-text": "^9.0.4",
118
126
  "@types/sanitize-html": "^2.16.1",
119
127
  "dotenv": "^17.4.2",
@@ -124,7 +132,7 @@
124
132
  "re2js": "2.8.6",
125
133
  "reflect-metadata": "^0.2.2",
126
134
  "sanitize-html": "2.17.5",
127
- "undici": "^8.9.0"
135
+ "undici": "^8.11.2"
128
136
  },
129
137
  "devDependencies": {
130
138
  "@types/jest": "^30.0.0",
@@ -141,6 +149,5 @@
141
149
  "type": "git",
142
150
  "url": "https://github.com/open-mercato/open-mercato",
143
151
  "directory": "packages/shared"
144
- },
145
- "stableVersion": "0.8.0"
152
+ }
146
153
  }
@@ -3,6 +3,7 @@ import crypto from 'node:crypto'
3
3
  import {
4
4
  assertJwtSecretPolicy,
5
5
  deriveJwtAudienceSecret,
6
+ isUnsafeJwtSecret,
6
7
  signAudienceJwt,
7
8
  signJwt,
8
9
  verifyAudienceJwt,
@@ -468,6 +469,17 @@ describe('jwt helpers', () => {
468
469
  process.env.JWT_CUSTOMER_SECRET = 'b'.repeat(64)
469
470
  expect(() => assertJwtSecretPolicy()).not.toThrow()
470
471
  })
472
+
473
+ it('isUnsafeJwtSecret flags exactly the secrets the production policy refuses', () => {
474
+ expect(isUnsafeJwtSecret(undefined)).toBe(true)
475
+ expect(isUnsafeJwtSecret(null)).toBe(true)
476
+ expect(isUnsafeJwtSecret(' ')).toBe(true)
477
+ expect(isUnsafeJwtSecret('change-me-dev-secret')).toBe(true)
478
+ expect(isUnsafeJwtSecret('Your-Secure-JWT-Secret-Change-Me')).toBe(true)
479
+ expect(isUnsafeJwtSecret('a'.repeat(31))).toBe(true)
480
+ expect(isUnsafeJwtSecret('a'.repeat(32))).toBe(false)
481
+ expect(isUnsafeJwtSecret(strongSecret)).toBe(false)
482
+ })
471
483
  })
472
484
  })
473
485
 
@@ -4,6 +4,7 @@ import {
4
4
  ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE,
5
5
  organizationScopeRequiredResponse,
6
6
  resolveActiveOrganizationId,
7
+ resolveSingleOrganizationIdOrDeny,
7
8
  } from '../organizationScope'
8
9
 
9
10
  const accountOrgId = '22222222-2222-4222-8222-222222222222'
@@ -105,3 +106,29 @@ describe('organizationScopeRequiredResponse', () => {
105
106
  expect(typeof body.error).toBe('string')
106
107
  })
107
108
  })
109
+
110
+ describe('resolveSingleOrganizationIdOrDeny', () => {
111
+ const auth = { orgId: accountOrgId }
112
+
113
+ it('prefers a selected organization and preserves unrestricted or absent home fallback', () => {
114
+ expect(resolveSingleOrganizationIdOrDeny(
115
+ { selectedId: selectedOrgId, filterIds: [selectedOrgId], allowedIds: [selectedOrgId] },
116
+ auth,
117
+ )).toBe(selectedOrgId)
118
+ expect(resolveSingleOrganizationIdOrDeny(
119
+ { selectedId: null, filterIds: null, allowedIds: null },
120
+ auth,
121
+ )).toBe(accountOrgId)
122
+ expect(resolveSingleOrganizationIdOrDeny(undefined, auth)).toBe(accountOrgId)
123
+ })
124
+
125
+ it.each([
126
+ { selectedId: null, filterIds: [], allowedIds: [] },
127
+ { selectedId: null, filterIds: [], allowedIds: null },
128
+ { selectedId: null, filterIds: null, allowedIds: [] },
129
+ ])('denies an explicit finite empty scope', (scope) => {
130
+ expect(() => resolveSingleOrganizationIdOrDeny(scope, auth)).toThrow(
131
+ expect.objectContaining({ status: 403, body: { error: 'Forbidden' } }),
132
+ )
133
+ })
134
+ })