@open-mercato/shared 0.6.8-develop.7099.1.7ba2771d06 → 0.7.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 (81) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -4
  3. package/dist/lib/auth/jwt.js +0 -6
  4. package/dist/lib/auth/jwt.js.map +2 -2
  5. package/dist/lib/auth/organizationAccess.js +4 -7
  6. package/dist/lib/auth/organizationAccess.js.map +2 -2
  7. package/dist/lib/auth/server.js +7 -38
  8. package/dist/lib/auth/server.js.map +2 -2
  9. package/dist/lib/commands/command-bus.js +1 -6
  10. package/dist/lib/commands/command-bus.js.map +2 -2
  11. package/dist/lib/crud/factory.js +8 -24
  12. package/dist/lib/crud/factory.js.map +2 -2
  13. package/dist/lib/data/engine.js +2 -8
  14. package/dist/lib/data/engine.js.map +2 -2
  15. package/dist/lib/location/countries.js +0 -12
  16. package/dist/lib/location/countries.js.map +2 -2
  17. package/dist/lib/openapi/crud.js +1 -4
  18. package/dist/lib/openapi/crud.js.map +2 -2
  19. package/dist/lib/query/engine.js +34 -270
  20. package/dist/lib/query/engine.js.map +3 -3
  21. package/dist/lib/query/types.js.map +1 -1
  22. package/dist/lib/search/config.js +0 -1
  23. package/dist/lib/search/config.js.map +2 -2
  24. package/dist/lib/version.js +1 -1
  25. package/dist/lib/version.js.map +1 -1
  26. package/dist/modules/events/factory.js +15 -69
  27. package/dist/modules/events/factory.js.map +2 -2
  28. package/dist/modules/registry.js +0 -15
  29. package/dist/modules/registry.js.map +2 -2
  30. package/dist/modules/widgets/component-registry.js.map +2 -2
  31. package/package.json +3 -10
  32. package/src/lib/auth/__tests__/jwt.test.ts +0 -13
  33. package/src/lib/auth/__tests__/organizationAccess.test.ts +1 -36
  34. package/src/lib/auth/__tests__/server.apiKeyCache.test.ts +0 -324
  35. package/src/lib/auth/__tests__/server.test.ts +0 -104
  36. package/src/lib/auth/jwt.ts +0 -17
  37. package/src/lib/auth/organizationAccess.ts +3 -11
  38. package/src/lib/auth/server.ts +8 -78
  39. package/src/lib/commands/__tests__/command-bus.test.ts +0 -31
  40. package/src/lib/commands/command-bus.ts +1 -8
  41. package/src/lib/crud/__tests__/crud-factory.test.ts +0 -165
  42. package/src/lib/crud/factory.ts +7 -33
  43. package/src/lib/data/__tests__/engine.event-validation.test.ts +1 -9
  44. package/src/lib/data/engine.ts +1 -7
  45. package/src/lib/location/__tests__/countries.test.ts +0 -15
  46. package/src/lib/location/countries.ts +0 -17
  47. package/src/lib/openapi/crud.ts +0 -3
  48. package/src/lib/query/__tests__/engine.count-distinct.test.ts +15 -162
  49. package/src/lib/query/__tests__/engine.scope-and-or.test.ts +1 -11
  50. package/src/lib/query/__tests__/engine.test.ts +7 -445
  51. package/src/lib/query/engine.ts +54 -434
  52. package/src/lib/query/types.ts +0 -15
  53. package/src/lib/search/config.ts +0 -10
  54. package/src/modules/events/factory.ts +19 -111
  55. package/src/modules/events/types.ts +0 -17
  56. package/src/modules/registry.ts +0 -40
  57. package/src/modules/widgets/component-registry.ts +0 -14
  58. package/dist/lib/auth/mfaPendingAccess.js +0 -42
  59. package/dist/lib/auth/mfaPendingAccess.js.map +0 -7
  60. package/dist/lib/auth/principal-service.js +0 -1
  61. package/dist/lib/auth/principal-service.js.map +0 -7
  62. package/dist/lib/html/htmlToPlainText.js +0 -16
  63. package/dist/lib/html/htmlToPlainText.js.map +0 -7
  64. package/dist/lib/query/count-cap.js +0 -11
  65. package/dist/lib/query/count-cap.js.map +0 -7
  66. package/dist/lib/queue/dispatchOrigin.js +0 -20
  67. package/dist/lib/queue/dispatchOrigin.js.map +0 -7
  68. package/dist/lib/search/entityAccess.js +0 -44
  69. package/dist/lib/search/entityAccess.js.map +0 -7
  70. package/src/lib/auth/__tests__/mfaPendingAccess.test.ts +0 -69
  71. package/src/lib/auth/__tests__/principalServiceExport.test.ts +0 -67
  72. package/src/lib/auth/mfaPendingAccess.ts +0 -70
  73. package/src/lib/auth/principal-service.ts +0 -110
  74. package/src/lib/html/__tests__/htmlToPlainText.test.ts +0 -59
  75. package/src/lib/html/htmlToPlainText.ts +0 -17
  76. package/src/lib/query/__tests__/count-cap-plan.test.ts +0 -240
  77. package/src/lib/query/__tests__/count-cap.test.ts +0 -41
  78. package/src/lib/query/count-cap.ts +0 -19
  79. package/src/lib/queue/dispatchOrigin.ts +0 -35
  80. package/src/lib/search/entityAccess.ts +0 -132
  81. package/src/modules/events/__tests__/factory.test.ts +0 -88
@@ -63,7 +63,6 @@ function resolveSearchConfig() {
63
63
  enablePartials: parseBoolean(process.env.OM_SEARCH_ENABLE_PARTIAL, true),
64
64
  hashAlgorithm: parseHashAlgorithm(process.env.OM_SEARCH_HASH_ALGO),
65
65
  storeRawTokens: parseBoolean(process.env.OM_SEARCH_STORE_RAW_TOKENS, false),
66
- useIlikeForNonEncryptedFields: parseBoolean(process.env.OM_SEARCH_USE_ILIKE_FOR_NON_ENCRYPTED_FIELDS, false),
67
66
  blocklistedFields: blocklist.global,
68
67
  entityBlocklistedFields: blocklist.byEntity,
69
68
  maxFieldChars: parseNumber(process.env.OM_SEARCH_MAX_FIELD_CHARS, DEFAULT_SEARCH_MAX_FIELD_CHARS, 0),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/search/config.ts"],
4
- "sourcesContent": ["import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { parseNumberWithDefault } from '@open-mercato/shared/lib/number'\nimport { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'\n\nexport type SearchConfig = {\n enabled: boolean\n minTokenLength: number\n enablePartials: boolean\n hashAlgorithm: 'sha256' | 'sha1' | 'md5'\n storeRawTokens: boolean\n /**\n * When true, a like/ilike on a PLAINTEXT base column runs as exact SQL ILIKE instead of being\n * rewritten into an approximate search-token match; encrypted columns always keep the token\n * path (ILIKE against ciphertext cannot match). Off by default: token matching can be faster\n * than an unanchored ILIKE, which may need a full scan without a trigram index \u2014 but it is\n * approximate (fragments under minTokenLength vanish, so `ZK 1/2026` degrades to its year and\n * an all-short term drops the predicate). Flip it on when list search must be exact.\n */\n useIlikeForNonEncryptedFields?: boolean\n blocklistedFields: string[]\n entityBlocklistedFields?: Record<string, string[]>\n maxFieldChars?: number\n maxTokensPerField?: number\n maxTokensPerRecord?: number\n}\n\nexport const DEFAULT_SEARCH_MIN_TOKEN_LENGTH = 3\nexport const DEFAULT_SEARCH_MAX_FIELD_CHARS = 20_000\nexport const DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD = 5_000\nexport const DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD = 20_000\n\nexport type SearchTokenLimits = {\n maxFieldChars: number\n maxTokensPerField: number\n maxTokensPerRecord: number\n}\n\nconst DEFAULT_BLOCKLIST = ['password', 'token', 'secret', 'hash']\n\nconst ENTITY_BLOCKLIST_SEPARATOR = '@'\n\nfunction parseBoolean(raw: string | undefined, fallback: boolean): boolean {\n return parseBooleanWithDefault(raw, fallback)\n}\n\nfunction parseNumber(raw: string | undefined, fallback: number, min = 1): number {\n return parseNumberWithDefault(raw, fallback, { integer: true, min })\n}\n\nexport function resolveSearchTokenLimits(config: SearchConfig): SearchTokenLimits {\n const resolveLimit = (value: number | undefined, fallback: number): number => {\n if (value === undefined) return fallback\n if (!Number.isFinite(value) || value < 0) return fallback\n return Math.trunc(value)\n }\n return {\n maxFieldChars: resolveLimit(config.maxFieldChars, DEFAULT_SEARCH_MAX_FIELD_CHARS),\n maxTokensPerField: resolveLimit(config.maxTokensPerField, DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD),\n maxTokensPerRecord: resolveLimit(config.maxTokensPerRecord, DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD),\n }\n}\n\nfunction parseHashAlgorithm(raw: string | undefined): 'sha256' | 'sha1' | 'md5' {\n const value = (raw ?? '').trim().toLowerCase()\n if (value === 'sha1') return 'sha1'\n if (value === 'md5') return 'md5'\n return 'sha256'\n}\n\n/**\n * Parses `OM_SEARCH_FIELD_BLOCKLIST` into a global list plus per-entity-type lists.\n *\n * Why: a deployment often needs to keep one large free-text column out of the token\n * index (e-mail bodies on `customers:customer_interaction`) while still indexing the\n * same-named column elsewhere. A flat global list cannot express that.\n *\n * How to apply: entries are comma-separated; an entry may carry an optional\n * `entityType@` prefix \u2014 `body` blocks the field everywhere, while\n * `customers:customer_interaction@body` blocks it only for that entity type. Entries\n * whose field part is empty are ignored so malformed env input cannot break indexing.\n */\nfunction parseFieldBlocklist(raw: string | undefined): {\n global: string[]\n byEntity: Record<string, string[]>\n} {\n const global: string[] = []\n const byEntity = new Map<string, string[]>()\n\n for (const rawEntry of parseCommaSeparatedList(raw)) {\n const entry = rawEntry.toLowerCase()\n const separatorIndex = entry.indexOf(ENTITY_BLOCKLIST_SEPARATOR)\n const entityType = separatorIndex >= 0 ? entry.slice(0, separatorIndex).trim() : ''\n const field = separatorIndex >= 0 ? entry.slice(separatorIndex + 1).trim() : entry\n if (!field.length) continue\n\n if (!entityType.length) {\n if (!global.includes(field)) global.push(field)\n continue\n }\n\n const scoped = byEntity.get(entityType) ?? []\n if (!scoped.includes(field)) scoped.push(field)\n byEntity.set(entityType, scoped)\n }\n\n for (const fallback of DEFAULT_BLOCKLIST) {\n if (!global.includes(fallback)) global.push(fallback)\n }\n\n const scopedBlocklist = Object.create(null) as Record<string, string[]>\n for (const [entityType, fields] of byEntity) scopedBlocklist[entityType] = fields\n\n return { global, byEntity: scopedBlocklist }\n}\n\nexport function resolveSearchConfig(): SearchConfig {\n const blocklist = parseFieldBlocklist(process.env.OM_SEARCH_FIELD_BLOCKLIST)\n return {\n enabled: parseBoolean(process.env.OM_SEARCH_ENABLED, true),\n minTokenLength: resolveSearchMinTokenLength(),\n enablePartials: parseBoolean(process.env.OM_SEARCH_ENABLE_PARTIAL, true),\n hashAlgorithm: parseHashAlgorithm(process.env.OM_SEARCH_HASH_ALGO),\n storeRawTokens: parseBoolean(process.env.OM_SEARCH_STORE_RAW_TOKENS, false),\n useIlikeForNonEncryptedFields: parseBoolean(process.env.OM_SEARCH_USE_ILIKE_FOR_NON_ENCRYPTED_FIELDS, false),\n blocklistedFields: blocklist.global,\n entityBlocklistedFields: blocklist.byEntity,\n maxFieldChars: parseNumber(process.env.OM_SEARCH_MAX_FIELD_CHARS, DEFAULT_SEARCH_MAX_FIELD_CHARS, 0),\n maxTokensPerField: parseNumber(process.env.OM_SEARCH_MAX_TOKENS_PER_FIELD, DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD, 0),\n maxTokensPerRecord: parseNumber(process.env.OM_SEARCH_MAX_TOKENS_PER_RECORD, DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD, 0),\n }\n}\n\n/**\n * Single matcher for \"should this field be kept out of the search index?\".\n *\n * Why: the per-field token path and the `search_text` aggregate previously each\n * decided this on their own, and the aggregate simply never consulted the config \u2014\n * so a blocklisted column's text came back into the index under the aggregate's\n * field name (#4624). Both paths now share this function so they cannot drift.\n *\n * How to apply: pass the document's field name and the entity type being indexed;\n * `entityType` may be omitted when unknown, in which case only global entries apply.\n * Matching keeps the historical substring semantics (`fieldName.includes(pattern)`).\n */\nexport function isSearchFieldBlocklisted(\n field: string,\n entityType: string | null | undefined,\n config: SearchConfig,\n): boolean {\n const lower = field.toLowerCase()\n if (config.blocklistedFields.some((blocked) => lower.includes(blocked))) return true\n if (!entityType) return false\n const scoped = config.entityBlocklistedFields?.[entityType.trim().toLowerCase()]\n if (!Array.isArray(scoped) || !scoped.length) return false\n return scoped.some((blocked) => lower.includes(blocked))\n}\n\n/**\n * Browser-safe accessor for the minimum search token length.\n *\n * Why: client components (e.g. global search dialog) must mirror the server-side\n * tokenizer's `minTokenLength` so the UI gates the request before hitting an\n * empty result set. Pulling the value through this single helper keeps the env\n * contract (`OM_SEARCH_MIN_LEN`) authoritative on both sides.\n *\n * How to apply: call from anywhere \u2014 server, client (when the host app exposes\n * `OM_SEARCH_MIN_LEN` through `next.config.ts`'s `env` block), or tests.\n */\nexport function resolveSearchMinTokenLength(): number {\n return parseNumber(process.env.OM_SEARCH_MIN_LEN, DEFAULT_SEARCH_MIN_TOKEN_LENGTH, 1)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { parseNumberWithDefault } from '@open-mercato/shared/lib/number'\nimport { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'\n\nexport type SearchConfig = {\n enabled: boolean\n minTokenLength: number\n enablePartials: boolean\n hashAlgorithm: 'sha256' | 'sha1' | 'md5'\n storeRawTokens: boolean\n blocklistedFields: string[]\n entityBlocklistedFields?: Record<string, string[]>\n maxFieldChars?: number\n maxTokensPerField?: number\n maxTokensPerRecord?: number\n}\n\nexport const DEFAULT_SEARCH_MIN_TOKEN_LENGTH = 3\nexport const DEFAULT_SEARCH_MAX_FIELD_CHARS = 20_000\nexport const DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD = 5_000\nexport const DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD = 20_000\n\nexport type SearchTokenLimits = {\n maxFieldChars: number\n maxTokensPerField: number\n maxTokensPerRecord: number\n}\n\nconst DEFAULT_BLOCKLIST = ['password', 'token', 'secret', 'hash']\n\nconst ENTITY_BLOCKLIST_SEPARATOR = '@'\n\nfunction parseBoolean(raw: string | undefined, fallback: boolean): boolean {\n return parseBooleanWithDefault(raw, fallback)\n}\n\nfunction parseNumber(raw: string | undefined, fallback: number, min = 1): number {\n return parseNumberWithDefault(raw, fallback, { integer: true, min })\n}\n\nexport function resolveSearchTokenLimits(config: SearchConfig): SearchTokenLimits {\n const resolveLimit = (value: number | undefined, fallback: number): number => {\n if (value === undefined) return fallback\n if (!Number.isFinite(value) || value < 0) return fallback\n return Math.trunc(value)\n }\n return {\n maxFieldChars: resolveLimit(config.maxFieldChars, DEFAULT_SEARCH_MAX_FIELD_CHARS),\n maxTokensPerField: resolveLimit(config.maxTokensPerField, DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD),\n maxTokensPerRecord: resolveLimit(config.maxTokensPerRecord, DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD),\n }\n}\n\nfunction parseHashAlgorithm(raw: string | undefined): 'sha256' | 'sha1' | 'md5' {\n const value = (raw ?? '').trim().toLowerCase()\n if (value === 'sha1') return 'sha1'\n if (value === 'md5') return 'md5'\n return 'sha256'\n}\n\n/**\n * Parses `OM_SEARCH_FIELD_BLOCKLIST` into a global list plus per-entity-type lists.\n *\n * Why: a deployment often needs to keep one large free-text column out of the token\n * index (e-mail bodies on `customers:customer_interaction`) while still indexing the\n * same-named column elsewhere. A flat global list cannot express that.\n *\n * How to apply: entries are comma-separated; an entry may carry an optional\n * `entityType@` prefix \u2014 `body` blocks the field everywhere, while\n * `customers:customer_interaction@body` blocks it only for that entity type. Entries\n * whose field part is empty are ignored so malformed env input cannot break indexing.\n */\nfunction parseFieldBlocklist(raw: string | undefined): {\n global: string[]\n byEntity: Record<string, string[]>\n} {\n const global: string[] = []\n const byEntity = new Map<string, string[]>()\n\n for (const rawEntry of parseCommaSeparatedList(raw)) {\n const entry = rawEntry.toLowerCase()\n const separatorIndex = entry.indexOf(ENTITY_BLOCKLIST_SEPARATOR)\n const entityType = separatorIndex >= 0 ? entry.slice(0, separatorIndex).trim() : ''\n const field = separatorIndex >= 0 ? entry.slice(separatorIndex + 1).trim() : entry\n if (!field.length) continue\n\n if (!entityType.length) {\n if (!global.includes(field)) global.push(field)\n continue\n }\n\n const scoped = byEntity.get(entityType) ?? []\n if (!scoped.includes(field)) scoped.push(field)\n byEntity.set(entityType, scoped)\n }\n\n for (const fallback of DEFAULT_BLOCKLIST) {\n if (!global.includes(fallback)) global.push(fallback)\n }\n\n const scopedBlocklist = Object.create(null) as Record<string, string[]>\n for (const [entityType, fields] of byEntity) scopedBlocklist[entityType] = fields\n\n return { global, byEntity: scopedBlocklist }\n}\n\nexport function resolveSearchConfig(): SearchConfig {\n const blocklist = parseFieldBlocklist(process.env.OM_SEARCH_FIELD_BLOCKLIST)\n return {\n enabled: parseBoolean(process.env.OM_SEARCH_ENABLED, true),\n minTokenLength: resolveSearchMinTokenLength(),\n enablePartials: parseBoolean(process.env.OM_SEARCH_ENABLE_PARTIAL, true),\n hashAlgorithm: parseHashAlgorithm(process.env.OM_SEARCH_HASH_ALGO),\n storeRawTokens: parseBoolean(process.env.OM_SEARCH_STORE_RAW_TOKENS, false),\n blocklistedFields: blocklist.global,\n entityBlocklistedFields: blocklist.byEntity,\n maxFieldChars: parseNumber(process.env.OM_SEARCH_MAX_FIELD_CHARS, DEFAULT_SEARCH_MAX_FIELD_CHARS, 0),\n maxTokensPerField: parseNumber(process.env.OM_SEARCH_MAX_TOKENS_PER_FIELD, DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD, 0),\n maxTokensPerRecord: parseNumber(process.env.OM_SEARCH_MAX_TOKENS_PER_RECORD, DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD, 0),\n }\n}\n\n/**\n * Single matcher for \"should this field be kept out of the search index?\".\n *\n * Why: the per-field token path and the `search_text` aggregate previously each\n * decided this on their own, and the aggregate simply never consulted the config \u2014\n * so a blocklisted column's text came back into the index under the aggregate's\n * field name (#4624). Both paths now share this function so they cannot drift.\n *\n * How to apply: pass the document's field name and the entity type being indexed;\n * `entityType` may be omitted when unknown, in which case only global entries apply.\n * Matching keeps the historical substring semantics (`fieldName.includes(pattern)`).\n */\nexport function isSearchFieldBlocklisted(\n field: string,\n entityType: string | null | undefined,\n config: SearchConfig,\n): boolean {\n const lower = field.toLowerCase()\n if (config.blocklistedFields.some((blocked) => lower.includes(blocked))) return true\n if (!entityType) return false\n const scoped = config.entityBlocklistedFields?.[entityType.trim().toLowerCase()]\n if (!Array.isArray(scoped) || !scoped.length) return false\n return scoped.some((blocked) => lower.includes(blocked))\n}\n\n/**\n * Browser-safe accessor for the minimum search token length.\n *\n * Why: client components (e.g. global search dialog) must mirror the server-side\n * tokenizer's `minTokenLength` so the UI gates the request before hitting an\n * empty result set. Pulling the value through this single helper keeps the env\n * contract (`OM_SEARCH_MIN_LEN`) authoritative on both sides.\n *\n * How to apply: call from anywhere \u2014 server, client (when the host app exposes\n * `OM_SEARCH_MIN_LEN` through `next.config.ts`'s `env` block), or tests.\n */\nexport function resolveSearchMinTokenLength(): number {\n return parseNumber(process.env.OM_SEARCH_MIN_LEN, DEFAULT_SEARCH_MIN_TOKEN_LENGTH, 1)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.8-develop.7099.1.7ba2771d06";
1
+ const APP_VERSION = "0.7.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.6.8-develop.7099.1.7ba2771d06';\nexport const appVersion = APP_VERSION;\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.7.0';\nexport const appVersion = APP_VERSION;\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
@@ -20,76 +20,37 @@ function getGlobalEventBus() {
20
20
  }
21
21
  return globalEventBus;
22
22
  }
23
- const GLOBAL_EVENT_REGISTRY_KEY = "__openMercatoEventDefinitionRegistry__";
24
- const fallbackEventRegistryState = {
25
- declaredEventIds: /* @__PURE__ */ new Set(),
26
- declaredEvents: [],
27
- registeredEventConfigs: null
28
- };
29
- function isEventRegistryState(value) {
30
- if (!value || typeof value !== "object") return false;
31
- const candidate = value;
32
- return candidate.declaredEventIds instanceof Set && Array.isArray(candidate.declaredEvents) && (candidate.registeredEventConfigs === null || Array.isArray(candidate.registeredEventConfigs));
33
- }
34
- function getEventRegistryState() {
35
- try {
36
- const globalScope = globalThis;
37
- const existing = globalScope[GLOBAL_EVENT_REGISTRY_KEY];
38
- if (isEventRegistryState(existing)) return existing;
39
- globalScope[GLOBAL_EVENT_REGISTRY_KEY] = fallbackEventRegistryState;
40
- return fallbackEventRegistryState;
41
- } catch {
42
- return fallbackEventRegistryState;
43
- }
44
- }
23
+ const allDeclaredEventIds = /* @__PURE__ */ new Set();
24
+ const allDeclaredEvents = [];
45
25
  function addDeclaredEvent(event) {
46
- const state = getEventRegistryState();
47
- state.declaredEventIds.add(event.id);
48
- const existingIndex = state.declaredEvents.findIndex((candidate) => candidate.id === event.id);
49
- if (existingIndex < 0) {
50
- state.declaredEvents.push(event);
51
- return;
52
- }
53
- if (state.declaredEvents[existingIndex]?.module === event.module) {
54
- state.declaredEvents[existingIndex] = event;
26
+ allDeclaredEventIds.add(event.id);
27
+ if (!allDeclaredEvents.find((e) => e.id === event.id)) {
28
+ allDeclaredEvents.push(event);
55
29
  }
56
30
  }
57
31
  function isEventDeclared(eventId) {
58
- return getEventRegistryState().declaredEventIds.has(eventId);
32
+ return allDeclaredEventIds.has(eventId);
59
33
  }
60
34
  function getAllDeclaredEventIds() {
61
- return Array.from(getEventRegistryState().declaredEventIds);
35
+ return Array.from(allDeclaredEventIds);
62
36
  }
63
37
  function getDeclaredEvents() {
64
- return [...getEventRegistryState().declaredEvents];
38
+ return [...allDeclaredEvents];
65
39
  }
66
40
  function isBroadcastEvent(eventId) {
67
- const event = getEventRegistryState().declaredEvents.find((e) => e.id === eventId);
41
+ const event = allDeclaredEvents.find((e) => e.id === eventId);
68
42
  return event?.clientBroadcast === true;
69
43
  }
70
- function isCrossProcessBroadcastEvent(eventId) {
71
- const event = getEventRegistryState().declaredEvents.find((e) => e.id === eventId);
72
- return event?.clientBroadcast === true || event?.crossProcessBroadcast === true;
73
- }
74
- function isPrivateCrossProcessBroadcastEvent(eventId) {
75
- const event = getEventRegistryState().declaredEvents.find((e) => e.id === eventId);
76
- return event?.crossProcessBroadcast === true && event?.clientBroadcast !== true;
77
- }
78
- function isPrivateCrossProcessEventEmitter(eventId, emitterModuleId) {
79
- const event = getEventRegistryState().declaredEvents.find((e) => e.id === eventId);
80
- if (event?.crossProcessBroadcast !== true) return true;
81
- return typeof event.module === "string" && event.module.length > 0 && event.module === emitterModuleId;
82
- }
83
44
  function isPortalBroadcastEvent(eventId) {
84
- const event = getEventRegistryState().declaredEvents.find((e) => e.id === eventId);
45
+ const event = allDeclaredEvents.find((e) => e.id === eventId);
85
46
  return event?.portalBroadcast === true;
86
47
  }
48
+ let _registeredEventConfigs = null;
87
49
  function registerEventModuleConfigs(configs) {
88
- const state = getEventRegistryState();
89
- if (state.registeredEventConfigs !== null && process.env.NODE_ENV === "development") {
50
+ if (_registeredEventConfigs !== null && process.env.NODE_ENV === "development") {
90
51
  logger.debug("Event module configs re-registered (this may occur during HMR)");
91
52
  }
92
- state.registeredEventConfigs = configs;
53
+ _registeredEventConfigs = configs;
93
54
  for (const config of configs) {
94
55
  for (const event of config.events) {
95
56
  addDeclaredEvent(event);
@@ -97,7 +58,7 @@ function registerEventModuleConfigs(configs) {
97
58
  }
98
59
  }
99
60
  function getEventModuleConfigs() {
100
- return getEventRegistryState().registeredEventConfigs ?? [];
61
+ return _registeredEventConfigs ?? [];
101
62
  }
102
63
  function createModuleEvents(options) {
103
64
  const { moduleId, events, strict = false } = options;
@@ -123,19 +84,7 @@ function createModuleEvents(options) {
123
84
  logger.warn("Event bus not available, cannot emit event", { eventId });
124
85
  return;
125
86
  }
126
- const eventDefinition = fullEvents.find((event) => event.id === eventId);
127
- const isClientBroadcast = eventDefinition?.clientBroadcast === true;
128
- const trustedOptions = eventDefinition?.crossProcessBroadcast === true || isClientBroadcast ? {
129
- ...emitOptions,
130
- // Browser-broadcast module emitters historically accepted scope in
131
- // their typed payload. Preserve that contract at the trusted module
132
- // boundary while the event bus itself relies only on options.
133
- ...isClientBroadcast && emitOptions?.tenantId === void 0 ? { tenantId: payload.tenantId ?? null } : {},
134
- ...isClientBroadcast && emitOptions?.organizationId === void 0 ? { organizationId: payload.organizationId ?? null } : {},
135
- ...isClientBroadcast && emitOptions?.organizationIds === void 0 && Array.isArray(payload.organizationIds) ? { organizationIds: payload.organizationIds.filter((value) => typeof value === "string") } : {},
136
- emitterModuleId: moduleId
137
- } : emitOptions;
138
- await eventBus.emit(eventId, payload, trustedOptions);
87
+ await eventBus.emit(eventId, payload, emitOptions);
139
88
  };
140
89
  return {
141
90
  moduleId,
@@ -150,11 +99,8 @@ export {
150
99
  getEventModuleConfigs,
151
100
  getGlobalEventBus,
152
101
  isBroadcastEvent,
153
- isCrossProcessBroadcastEvent,
154
102
  isEventDeclared,
155
103
  isPortalBroadcastEvent,
156
- isPrivateCrossProcessBroadcastEvent,
157
- isPrivateCrossProcessEventEmitter,
158
104
  registerEventModuleConfigs,
159
105
  setGlobalEventBus
160
106
  };
@@ -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 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\nfunction addDeclaredEvent(event: EventDefinition): void {\n const state = getEventRegistryState()\n state.declaredEventIds.add(event.id)\n const existingIndex = state.declaredEvents.findIndex((candidate) => candidate.id === event.id)\n if (existingIndex < 0) {\n state.declaredEvents.push(event)\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 === event.module) {\n state.declaredEvents[existingIndex] = event\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\n const fullEvents: EventDefinition[] = events.map(e => ({\n ...e,\n module: moduleId,\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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",
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 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\n// Global set of all declared event IDs for runtime validation\nconst allDeclaredEventIds = new Set<string>()\n\n// Global registry of all declared events with their full definitions\nconst allDeclaredEvents: EventDefinition[] = []\n\nfunction addDeclaredEvent(event: EventDefinition): void {\n allDeclaredEventIds.add(event.id)\n // Avoid duplicates if createModuleEvents/registerEventModuleConfigs is called multiple times (e.g., HMR)\n if (!allDeclaredEvents.find(e => e.id === event.id)) {\n allDeclaredEvents.push(event)\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 allDeclaredEventIds.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(allDeclaredEventIds)\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 [...allDeclaredEvents]\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 = allDeclaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === 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 = allDeclaredEvents.find(e => e.id === eventId)\n return event?.portalBroadcast === true\n}\n\n// =============================================================================\n// Bootstrap Registration (similar to searchModuleConfigs pattern)\n// =============================================================================\n\nlet _registeredEventConfigs: EventModuleConfig[] | null = null\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 if (_registeredEventConfigs !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Event module configs re-registered (this may occur during HMR)')\n }\n _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 _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\n const fullEvents: EventDefinition[] = events.map(e => ({\n ...e,\n module: moduleId,\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 await eventBus.emit(eventId, payload, emitOptions)\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;AAU7B,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;AAOA,MAAM,sBAAsB,oBAAI,IAAY;AAG5C,MAAM,oBAAuC,CAAC;AAE9C,SAAS,iBAAiB,OAA8B;AACtD,sBAAoB,IAAI,MAAM,EAAE;AAEhC,MAAI,CAAC,kBAAkB,KAAK,OAAK,EAAE,OAAO,MAAM,EAAE,GAAG;AACnD,sBAAkB,KAAK,KAAK;AAAA,EAC9B;AACF;AAMO,SAAS,gBAAgB,SAA0B;AACxD,SAAO,oBAAoB,IAAI,OAAO;AACxC;AAMO,SAAS,yBAAmC;AACjD,SAAO,MAAM,KAAK,mBAAmB;AACvC;AAMO,SAAS,oBAAuC;AACrD,SAAO,CAAC,GAAG,iBAAiB;AAC9B;AAMO,SAAS,iBAAiB,SAA0B;AACzD,QAAM,QAAQ,kBAAkB,KAAK,OAAK,EAAE,OAAO,OAAO;AAC1D,SAAO,OAAO,oBAAoB;AACpC;AAMO,SAAS,uBAAuB,SAA0B;AAC/D,QAAM,QAAQ,kBAAkB,KAAK,OAAK,EAAE,OAAO,OAAO;AAC1D,SAAO,OAAO,oBAAoB;AACpC;AAMA,IAAI,0BAAsD;AAMnD,SAAS,2BAA2B,SAAoC;AAC7E,MAAI,4BAA4B,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC9E,WAAO,MAAM,gEAAgE;AAAA,EAC/E;AACA,4BAA0B;AAC1B,aAAW,UAAU,SAAS;AAC5B,eAAW,SAAS,OAAO,QAAQ;AACjC,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF;AACF;AAMO,SAAS,wBAA6C;AAC3D,SAAO,2BAA2B,CAAC;AACrC;AAyCO,SAAS,mBAGd,SAA6E;AAC7E,QAAM,EAAE,UAAU,QAAQ,SAAS,MAAM,IAAI;AAG7C,QAAM,gBAAgB,IAAI,IAAI,OAAO,IAAI,OAAK,EAAE,EAAE,CAAC;AAGnD,QAAM,aAAgC,OAAO,IAAI,QAAM;AAAA,IACrD,GAAG;AAAA,IACH,QAAQ;AAAA,EACV,EAAE;AAGF,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,SAAS,KAAK,SAAS,SAAS,WAAW;AAAA,EACnD;AAEA,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -220,24 +220,9 @@ function createLazyModuleWorker(loadModule, id) {
220
220
  return handler(job, ctx);
221
221
  };
222
222
  }
223
- function createLazyModuleWorkerAbandonHook(loadModule, id) {
224
- let hookPromise = null;
225
- return async (payload, info) => {
226
- hookPromise ??= loadModule().then((loaded) => {
227
- const metadata = loaded?.metadata;
228
- return typeof metadata?.onJobAbandoned === "function" ? metadata.onJobAbandoned : null;
229
- });
230
- const hook = await hookPromise;
231
- if (!hook) {
232
- throw new Error(`[registry] Worker "${id}" was registered with an abandoned-job hook but its metadata no longer declares one`);
233
- }
234
- await hook(payload, info);
235
- };
236
- }
237
223
  export {
238
224
  createLazyModuleSubscriber,
239
225
  createLazyModuleWorker,
240
- createLazyModuleWorkerAbandonHook,
241
226
  findApi,
242
227
  findApiRouteManifestMatch,
243
228
  findBackendMatch,
@@ -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: 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'\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 handler: ModuleWorkerHandler\n}\n\nexport type ModuleInfo = {\n name?: string\n title?: string\n version?: string\n description?: string\n author?: string\n license?: string\n homepage?: string\n copyright?: string\n // Optional hard dependencies: module ids that must be enabled\n requires?: string[]\n // Whether this module can be ejected into the app's src/modules/ for customization\n ejectable?: boolean\n}\n\nexport type ModuleDashboardWidgetEntry = {\n moduleId: string\n key: string\n source: 'app' | 'package'\n loader: () => Promise<DashboardWidgetModule<any>>\n}\n\nexport type ModuleInjectionWidgetEntry = {\n moduleId: string\n key: string\n source: 'app' | 'package'\n 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: 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"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/widgets/component-registry.ts"],
4
- "sourcesContent": ["import * as React from 'react'\nimport type { ComponentType, LazyExoticComponent } from 'react'\nimport type { ZodType } from 'zod'\nimport { hasAllFeatures } from '../../security/features'\n\nexport type ComponentRegistryEntry<TProps = unknown> = {\n id: string\n component: ComponentType<TProps>\n metadata: {\n module: string\n description?: string\n propsSchema?: ZodType<TProps>\n }\n}\n\nexport type ComponentOverride<TProps = unknown> = {\n target: { componentId: string }\n priority: number\n features?: string[]\n metadata?: {\n module?: string\n }\n} & (\n | {\n replacement: LazyExoticComponent<ComponentType<TProps>> | ComponentType<TProps>\n propsSchema: ZodType<TProps>\n }\n | {\n /**\n * Higher-order component composed around the resolved component.\n *\n * The platform invokes it **at most once per `(wrapper, wrapped component)` pair**\n * and caches the composed component for the lifetime of the registry, so that a\n * wrapped subtree keeps a stable React identity instead of remounting on every\n * override resolution.\n *\n * A wrapper MUST therefore be a pure function of `Original`: it may only read\n * dynamic values \u2014 feature flags, locale, tenant configuration, the clock, request\n * state \u2014 inside the render body of the component it returns, never at composition\n * time. On the server the registry outlives a single request, so a value captured\n * at composition time would be frozen across requests and tenants.\n */\n wrapper: (Original: ComponentType<TProps>) => ComponentType<TProps>\n }\n | {\n propsTransform: (props: TProps) => TProps\n }\n)\n\ntype RuntimeState = {\n components: Map<string, ComponentRegistryEntry>\n overrides: ComponentOverride[]\n}\n\nconst GLOBAL_COMPONENT_REGISTRY_KEY = '__openMercatoComponentRegistry__'\n\nfunction isComponentOverride(value: unknown): value is ComponentOverride {\n if (!value || typeof value !== 'object') {\n return false\n }\n\n const target = (value as { target?: { componentId?: unknown } }).target\n if (!target || typeof target !== 'object') {\n return false\n }\n\n return typeof target.componentId === 'string' && target.componentId.length > 0\n}\n\nfunction getState(): RuntimeState {\n const globalValue = (globalThis as Record<string, unknown>)[GLOBAL_COMPONENT_REGISTRY_KEY]\n if (globalValue && typeof globalValue === 'object') {\n const typed = globalValue as RuntimeState\n if (typed.components instanceof Map && Array.isArray(typed.overrides)) {\n return typed\n }\n }\n const initial: RuntimeState = {\n components: new Map<string, ComponentRegistryEntry>(),\n overrides: [],\n }\n ;(globalThis as Record<string, unknown>)[GLOBAL_COMPONENT_REGISTRY_KEY] = initial\n return initial\n}\n\nexport function registerComponent<TProps = unknown>(entry: ComponentRegistryEntry<TProps>) {\n const state = getState()\n state.components.set(entry.id, entry as ComponentRegistryEntry)\n}\n\nexport function registerComponentOverrides(overrides: ComponentOverride[]) {\n const state = getState()\n state.overrides = overrides.filter(isComponentOverride)\n}\n\nexport function getComponentEntry(componentId: string): ComponentRegistryEntry | null {\n const state = getState()\n return state.components.get(componentId) ?? null\n}\n\nexport function getComponentOverrides(componentId: string, userFeatures?: readonly string[]): ComponentOverride[] {\n const state = getState()\n const relevant = state.overrides.filter((override) => {\n if (!isComponentOverride(override)) return false\n if (override.target.componentId !== componentId) return false\n if (override.features && override.features.length > 0) {\n if (!hasAllFeatures(userFeatures, override.features)) return false\n }\n return true\n })\n return relevant.sort((a, b) => a.priority - b.priority)\n}\n\nexport function resolveRegisteredComponent<TProps>(\n componentId: string,\n fallback: ComponentType<TProps>,\n userFeatures?: readonly string[],\n): ComponentType<TProps> {\n const overrides = getComponentOverrides(componentId, userFeatures)\n let resolved: ComponentType<TProps> = fallback\n for (const override of overrides) {\n if ('replacement' in override) {\n resolved = override.replacement as ComponentType<TProps>\n continue\n }\n if ('wrapper' in override) {\n resolved = override.wrapper(resolved as ComponentType<unknown>) as ComponentType<TProps>\n continue\n }\n if ('propsTransform' in override) {\n const transform = override.propsTransform as (props: TProps) => TProps\n const Current = resolved\n resolved = ((props: TProps) => {\n const transformed = transform(props)\n return React.createElement(Current as ComponentType<Record<string, unknown>>, transformed as Record<string, unknown>)\n }) as ComponentType<TProps>\n }\n }\n return resolved\n}\n\nexport const ComponentReplacementHandles = {\n page: (path: string) => `page:${path}`,\n dataTable: (tableId: string) => `data-table:${tableId}`,\n crudForm: (entityId: string) => `crud-form:${entityId}`,\n section: (scope: string, sectionId: string) => `section:${scope}.${sectionId}`,\n} as const\n"],
5
- "mappings": "AAAA,YAAY,WAAW;AAGvB,SAAS,sBAAsB;AAmD/B,MAAM,gCAAgC;AAEtC,SAAS,oBAAoB,OAA4C;AACvE,MAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,WAAO;AAAA,EACT;AAEA,QAAM,SAAU,MAAiD;AACjE,MAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACzC,WAAO;AAAA,EACT;AAEA,SAAO,OAAO,OAAO,gBAAgB,YAAY,OAAO,YAAY,SAAS;AAC/E;AAEA,SAAS,WAAyB;AAChC,QAAM,cAAe,WAAuC,6BAA6B;AACzF,MAAI,eAAe,OAAO,gBAAgB,UAAU;AAClD,UAAM,QAAQ;AACd,QAAI,MAAM,sBAAsB,OAAO,MAAM,QAAQ,MAAM,SAAS,GAAG;AACrE,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,UAAwB;AAAA,IAC5B,YAAY,oBAAI,IAAoC;AAAA,IACpD,WAAW,CAAC;AAAA,EACd;AACC,EAAC,WAAuC,6BAA6B,IAAI;AAC1E,SAAO;AACT;AAEO,SAAS,kBAAoC,OAAuC;AACzF,QAAM,QAAQ,SAAS;AACvB,QAAM,WAAW,IAAI,MAAM,IAAI,KAA+B;AAChE;AAEO,SAAS,2BAA2B,WAAgC;AACzE,QAAM,QAAQ,SAAS;AACvB,QAAM,YAAY,UAAU,OAAO,mBAAmB;AACxD;AAEO,SAAS,kBAAkB,aAAoD;AACpF,QAAM,QAAQ,SAAS;AACvB,SAAO,MAAM,WAAW,IAAI,WAAW,KAAK;AAC9C;AAEO,SAAS,sBAAsB,aAAqB,cAAuD;AAChH,QAAM,QAAQ,SAAS;AACvB,QAAM,WAAW,MAAM,UAAU,OAAO,CAAC,aAAa;AACpD,QAAI,CAAC,oBAAoB,QAAQ,EAAG,QAAO;AAC3C,QAAI,SAAS,OAAO,gBAAgB,YAAa,QAAO;AACxD,QAAI,SAAS,YAAY,SAAS,SAAS,SAAS,GAAG;AACrD,UAAI,CAAC,eAAe,cAAc,SAAS,QAAQ,EAAG,QAAO;AAAA,IAC/D;AACA,WAAO;AAAA,EACT,CAAC;AACD,SAAO,SAAS,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AACxD;AAEO,SAAS,2BACd,aACA,UACA,cACuB;AACvB,QAAM,YAAY,sBAAsB,aAAa,YAAY;AACjE,MAAI,WAAkC;AACtC,aAAW,YAAY,WAAW;AAChC,QAAI,iBAAiB,UAAU;AAC7B,iBAAW,SAAS;AACpB;AAAA,IACF;AACA,QAAI,aAAa,UAAU;AACzB,iBAAW,SAAS,QAAQ,QAAkC;AAC9D;AAAA,IACF;AACA,QAAI,oBAAoB,UAAU;AAChC,YAAM,YAAY,SAAS;AAC3B,YAAM,UAAU;AAChB,kBAAY,CAAC,UAAkB;AAC7B,cAAM,cAAc,UAAU,KAAK;AACnC,eAAO,MAAM,cAAc,SAAmD,WAAsC;AAAA,MACtH;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEO,MAAM,8BAA8B;AAAA,EACzC,MAAM,CAAC,SAAiB,QAAQ,IAAI;AAAA,EACpC,WAAW,CAAC,YAAoB,cAAc,OAAO;AAAA,EACrD,UAAU,CAAC,aAAqB,aAAa,QAAQ;AAAA,EACrD,SAAS,CAAC,OAAe,cAAsB,WAAW,KAAK,IAAI,SAAS;AAC9E;",
4
+ "sourcesContent": ["import * as React from 'react'\nimport type { ComponentType, LazyExoticComponent } from 'react'\nimport type { ZodType } from 'zod'\nimport { hasAllFeatures } from '../../security/features'\n\nexport type ComponentRegistryEntry<TProps = unknown> = {\n id: string\n component: ComponentType<TProps>\n metadata: {\n module: string\n description?: string\n propsSchema?: ZodType<TProps>\n }\n}\n\nexport type ComponentOverride<TProps = unknown> = {\n target: { componentId: string }\n priority: number\n features?: string[]\n metadata?: {\n module?: string\n }\n} & (\n | {\n replacement: LazyExoticComponent<ComponentType<TProps>> | ComponentType<TProps>\n propsSchema: ZodType<TProps>\n }\n | {\n wrapper: (Original: ComponentType<TProps>) => ComponentType<TProps>\n }\n | {\n propsTransform: (props: TProps) => TProps\n }\n)\n\ntype RuntimeState = {\n components: Map<string, ComponentRegistryEntry>\n overrides: ComponentOverride[]\n}\n\nconst GLOBAL_COMPONENT_REGISTRY_KEY = '__openMercatoComponentRegistry__'\n\nfunction isComponentOverride(value: unknown): value is ComponentOverride {\n if (!value || typeof value !== 'object') {\n return false\n }\n\n const target = (value as { target?: { componentId?: unknown } }).target\n if (!target || typeof target !== 'object') {\n return false\n }\n\n return typeof target.componentId === 'string' && target.componentId.length > 0\n}\n\nfunction getState(): RuntimeState {\n const globalValue = (globalThis as Record<string, unknown>)[GLOBAL_COMPONENT_REGISTRY_KEY]\n if (globalValue && typeof globalValue === 'object') {\n const typed = globalValue as RuntimeState\n if (typed.components instanceof Map && Array.isArray(typed.overrides)) {\n return typed\n }\n }\n const initial: RuntimeState = {\n components: new Map<string, ComponentRegistryEntry>(),\n overrides: [],\n }\n ;(globalThis as Record<string, unknown>)[GLOBAL_COMPONENT_REGISTRY_KEY] = initial\n return initial\n}\n\nexport function registerComponent<TProps = unknown>(entry: ComponentRegistryEntry<TProps>) {\n const state = getState()\n state.components.set(entry.id, entry as ComponentRegistryEntry)\n}\n\nexport function registerComponentOverrides(overrides: ComponentOverride[]) {\n const state = getState()\n state.overrides = overrides.filter(isComponentOverride)\n}\n\nexport function getComponentEntry(componentId: string): ComponentRegistryEntry | null {\n const state = getState()\n return state.components.get(componentId) ?? null\n}\n\nexport function getComponentOverrides(componentId: string, userFeatures?: readonly string[]): ComponentOverride[] {\n const state = getState()\n const relevant = state.overrides.filter((override) => {\n if (!isComponentOverride(override)) return false\n if (override.target.componentId !== componentId) return false\n if (override.features && override.features.length > 0) {\n if (!hasAllFeatures(userFeatures, override.features)) return false\n }\n return true\n })\n return relevant.sort((a, b) => a.priority - b.priority)\n}\n\nexport function resolveRegisteredComponent<TProps>(\n componentId: string,\n fallback: ComponentType<TProps>,\n userFeatures?: readonly string[],\n): ComponentType<TProps> {\n const overrides = getComponentOverrides(componentId, userFeatures)\n let resolved: ComponentType<TProps> = fallback\n for (const override of overrides) {\n if ('replacement' in override) {\n resolved = override.replacement as ComponentType<TProps>\n continue\n }\n if ('wrapper' in override) {\n resolved = override.wrapper(resolved as ComponentType<unknown>) as ComponentType<TProps>\n continue\n }\n if ('propsTransform' in override) {\n const transform = override.propsTransform as (props: TProps) => TProps\n const Current = resolved\n resolved = ((props: TProps) => {\n const transformed = transform(props)\n return React.createElement(Current as ComponentType<Record<string, unknown>>, transformed as Record<string, unknown>)\n }) as ComponentType<TProps>\n }\n }\n return resolved\n}\n\nexport const ComponentReplacementHandles = {\n page: (path: string) => `page:${path}`,\n dataTable: (tableId: string) => `data-table:${tableId}`,\n crudForm: (entityId: string) => `crud-form:${entityId}`,\n section: (scope: string, sectionId: string) => `section:${scope}.${sectionId}`,\n} as const\n"],
5
+ "mappings": "AAAA,YAAY,WAAW;AAGvB,SAAS,sBAAsB;AAqC/B,MAAM,gCAAgC;AAEtC,SAAS,oBAAoB,OAA4C;AACvE,MAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,WAAO;AAAA,EACT;AAEA,QAAM,SAAU,MAAiD;AACjE,MAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACzC,WAAO;AAAA,EACT;AAEA,SAAO,OAAO,OAAO,gBAAgB,YAAY,OAAO,YAAY,SAAS;AAC/E;AAEA,SAAS,WAAyB;AAChC,QAAM,cAAe,WAAuC,6BAA6B;AACzF,MAAI,eAAe,OAAO,gBAAgB,UAAU;AAClD,UAAM,QAAQ;AACd,QAAI,MAAM,sBAAsB,OAAO,MAAM,QAAQ,MAAM,SAAS,GAAG;AACrE,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,UAAwB;AAAA,IAC5B,YAAY,oBAAI,IAAoC;AAAA,IACpD,WAAW,CAAC;AAAA,EACd;AACC,EAAC,WAAuC,6BAA6B,IAAI;AAC1E,SAAO;AACT;AAEO,SAAS,kBAAoC,OAAuC;AACzF,QAAM,QAAQ,SAAS;AACvB,QAAM,WAAW,IAAI,MAAM,IAAI,KAA+B;AAChE;AAEO,SAAS,2BAA2B,WAAgC;AACzE,QAAM,QAAQ,SAAS;AACvB,QAAM,YAAY,UAAU,OAAO,mBAAmB;AACxD;AAEO,SAAS,kBAAkB,aAAoD;AACpF,QAAM,QAAQ,SAAS;AACvB,SAAO,MAAM,WAAW,IAAI,WAAW,KAAK;AAC9C;AAEO,SAAS,sBAAsB,aAAqB,cAAuD;AAChH,QAAM,QAAQ,SAAS;AACvB,QAAM,WAAW,MAAM,UAAU,OAAO,CAAC,aAAa;AACpD,QAAI,CAAC,oBAAoB,QAAQ,EAAG,QAAO;AAC3C,QAAI,SAAS,OAAO,gBAAgB,YAAa,QAAO;AACxD,QAAI,SAAS,YAAY,SAAS,SAAS,SAAS,GAAG;AACrD,UAAI,CAAC,eAAe,cAAc,SAAS,QAAQ,EAAG,QAAO;AAAA,IAC/D;AACA,WAAO;AAAA,EACT,CAAC;AACD,SAAO,SAAS,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AACxD;AAEO,SAAS,2BACd,aACA,UACA,cACuB;AACvB,QAAM,YAAY,sBAAsB,aAAa,YAAY;AACjE,MAAI,WAAkC;AACtC,aAAW,YAAY,WAAW;AAChC,QAAI,iBAAiB,UAAU;AAC7B,iBAAW,SAAS;AACpB;AAAA,IACF;AACA,QAAI,aAAa,UAAU;AACzB,iBAAW,SAAS,QAAQ,QAAkC;AAC9D;AAAA,IACF;AACA,QAAI,oBAAoB,UAAU;AAChC,YAAM,YAAY,SAAS;AAC3B,YAAM,UAAU;AAChB,kBAAY,CAAC,UAAkB;AAC7B,cAAM,cAAc,UAAU,KAAK;AACnC,eAAO,MAAM,cAAc,SAAmD,WAAsC;AAAA,MACtH;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEO,MAAM,8BAA8B;AAAA,EACzC,MAAM,CAAC,SAAiB,QAAQ,IAAI;AAAA,EACpC,WAAW,CAAC,YAAoB,cAAc,OAAO;AAAA,EACrD,UAAU,CAAC,aAAqB,aAAa,QAAQ;AAAA,EACrD,SAAS,CAAC,OAAe,cAAsB,WAAW,KAAK,IAAI,SAAS;AAC9E;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.6.8-develop.7099.1.7ba2771d06",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -40,10 +40,6 @@
40
40
  "types": "./src/lib/bootstrap/dynamicLoader.ts",
41
41
  "default": "./dist/lib/bootstrap/dynamicLoader.js"
42
42
  },
43
- "./lib/auth/principal-service": {
44
- "types": "./src/lib/auth/principal-service.ts",
45
- "default": "./dist/lib/auth/principal-service.js"
46
- },
47
43
  "./lib/webhooks": {
48
44
  "types": "./src/lib/webhooks/index.ts",
49
45
  "default": "./dist/lib/webhooks/index.js"
@@ -109,11 +105,9 @@
109
105
  "@mikro-orm/core": "^7.1.8",
110
106
  "@mikro-orm/decorators": "^7.1.8",
111
107
  "@mikro-orm/postgresql": "^7.1.8",
112
- "@open-mercato/cache": "0.6.8-develop.7099.1.7ba2771d06",
113
- "@types/html-to-text": "^9.0.4",
108
+ "@open-mercato/cache": "0.7.0",
114
109
  "@types/sanitize-html": "^2.16.1",
115
110
  "dotenv": "^17.4.2",
116
- "html-to-text": "^10.0.0",
117
111
  "pino": "^10.3.1",
118
112
  "rate-limiter-flexible": "^11.2.0",
119
113
  "re2js": "2.8.6",
@@ -135,6 +129,5 @@
135
129
  "type": "git",
136
130
  "url": "https://github.com/open-mercato/open-mercato",
137
131
  "directory": "packages/shared"
138
- },
139
- "stableVersion": "0.6.7"
132
+ }
140
133
  }
@@ -470,16 +470,3 @@ describe('jwt helpers', () => {
470
470
  })
471
471
  })
472
472
  })
473
-
474
- describe('isMfaPendingJwtPayload', () => {
475
- it('flags pending challenge payloads only', async () => {
476
- const { isMfaPendingJwtPayload } = await import('../jwt')
477
- expect(isMfaPendingJwtPayload({ mfa_pending: true, mfa_verified: false })).toBe(true)
478
- expect(isMfaPendingJwtPayload({ mfa_pending: true })).toBe(true)
479
- expect(isMfaPendingJwtPayload({ mfa_pending: true, mfa_verified: true })).toBe(false)
480
- expect(isMfaPendingJwtPayload({ mfa_pending: false, mfa_verified: true })).toBe(false)
481
- expect(isMfaPendingJwtPayload({})).toBe(false)
482
- expect(isMfaPendingJwtPayload(null)).toBe(false)
483
- expect(isMfaPendingJwtPayload('token')).toBe(false)
484
- })
485
- })
@@ -1,39 +1,4 @@
1
- import {
2
- isOrganizationAccessAllowed,
3
- isUnrestrictedOrganizationScope,
4
- } from '@open-mercato/shared/lib/auth/organizationAccess'
5
-
6
- describe('isUnrestrictedOrganizationScope', () => {
7
- it('allows super admins regardless of allowed organizations', () => {
8
- expect(
9
- isUnrestrictedOrganizationScope({
10
- isSuperAdmin: true,
11
- allowedOrganizationIds: [],
12
- }),
13
- ).toBe(true)
14
- })
15
-
16
- it('allows truly unrestricted non-superadmin scopes', () => {
17
- expect(
18
- isUnrestrictedOrganizationScope({
19
- isSuperAdmin: false,
20
- allowedOrganizationIds: null,
21
- }),
22
- ).toBe(true)
23
- })
24
-
25
- it.each([undefined, [] as string[], ['org-a']])(
26
- 'denies restricted non-superadmin scopes with allowedOrganizationIds=%p',
27
- (allowedOrganizationIds) => {
28
- expect(
29
- isUnrestrictedOrganizationScope({
30
- isSuperAdmin: false,
31
- allowedOrganizationIds,
32
- }),
33
- ).toBe(false)
34
- },
35
- )
36
- })
1
+ import { isOrganizationAccessAllowed } from '@open-mercato/shared/lib/auth/organizationAccess'
37
2
 
38
3
  describe('isOrganizationAccessAllowed', () => {
39
4
  it('allows super admins regardless of scope', () => {