@open-mercato/shared 0.8.1-develop.7206.1.45a3cee87f → 0.8.1-develop.7215.1.60786cf836

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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 /**\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 /**\n * Ceiling on token rows across all fields of one record; `0` disables it. The budget is spent in\n * the order the document's own keys iterate in, so on an over-budget record *which* fields stay\n * searchable depends on that key order \u2014 see `buildSearchTokenRows` in\n * `@open-mercato/core/modules/query_index/lib/search-tokens` before recomputing expected tokens\n * from a document that did not come straight from the indexer.\n */\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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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.8.1-develop.7206.1.45a3cee87f";
1
+ const APP_VERSION = "0.8.1-develop.7215.1.60786cf836";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.8.1-develop.7206.1.45a3cee87f';\nexport const appVersion = APP_VERSION;\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.8.1-develop.7215.1.60786cf836';\nexport const appVersion = APP_VERSION;\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/widgets/extension-points.ts"],
4
- "sourcesContent": ["export const EXTENSION_HOST_FAMILIES = [\n 'generic',\n 'menu',\n 'data-table',\n 'crud-form',\n 'detail',\n 'portal-page',\n 'component-handle',\n 'entity',\n 'api-route',\n 'command',\n 'event',\n 'query-lifecycle',\n 'dashboard',\n 'notification',\n 'integration',\n 'specialized-registry',\n 'module-override',\n] as const\n\nexport type ExtensionHostFamily = (typeof EXTENSION_HOST_FAMILIES)[number]\n\nexport const EXTENSION_HOST_CAPABILITIES = [\n 'render-widget',\n 'headless-widget',\n 'menu-item',\n 'column-widget',\n 'row-action',\n 'bulk-action',\n 'filter-widget',\n 'toolbar-widget',\n 'field-widget',\n 'lifecycle-handler',\n 'component-replacement',\n 'response-enricher',\n 'query-enricher',\n 'api-interceptor',\n 'command-interceptor',\n 'mutation-guard',\n 'entity-extension',\n 'async-subscriber',\n 'sync-subscriber',\n 'browser-client',\n 'browser-portal',\n 'registry-contribution',\n 'module-override',\n] as const\n\nexport type ExtensionHostCapability = (typeof EXTENSION_HOST_CAPABILITIES)[number]\n\nexport const EXTENSION_HOST_ACTIVATIONS = ['always', 'host-opt-in', 'caller-opt-in', 'feature-gated'] as const\n\nexport type ExtensionHostActivation = (typeof EXTENSION_HOST_ACTIVATIONS)[number]\n\nexport type ExtensionPointPatternParameter = {\n source: string\n pattern?: string\n}\n\ntype ExtensionHostDeclarationBase = {\n source: string\n contextContract?: string\n dataContract?: string\n scopeContract?: string\n runtimeContract?: string\n activation?: ExtensionHostActivation\n aliases?: readonly string[]\n fallbacks?: readonly string[]\n}\n\ntype InjectionExtensionHostDeclarationBase = ExtensionHostDeclarationBase & {\n family: Exclude<ExtensionHostFamily, 'data-table' | 'crud-form' | 'component-handle'>\n supported: readonly ExtensionHostCapability[]\n}\n\nexport type InjectionExtensionHostDeclaration = InjectionExtensionHostDeclarationBase & (\n | {\n spotId: string\n pattern?: never\n parameters?: never\n }\n | {\n spotId?: never\n pattern: string\n parameters: Readonly<Record<string, ExtensionPointPatternParameter>>\n }\n)\n\nexport type DataTableExtensionHostDeclaration = ExtensionHostDeclarationBase & {\n family: 'data-table'\n tableId: string\n baseSpotId?: string\n}\n\nexport type CrudFormExtensionHostDeclaration = ExtensionHostDeclarationBase & {\n family: 'crud-form'\n entityId: string\n spotId?: string\n}\n\nexport type ComponentExtensionHostDeclaration = ExtensionHostDeclarationBase & {\n family: 'component-handle'\n componentId: string\n propsContract?: string\n}\n\nexport type ModuleExtensionHostDeclaration =\n | InjectionExtensionHostDeclaration\n | DataTableExtensionHostDeclaration\n | CrudFormExtensionHostDeclaration\n | ComponentExtensionHostDeclaration\n\nexport type ModuleExtensionPoints<\n TModuleId extends string = string,\n THosts extends Readonly<Record<string, ModuleExtensionHostDeclaration>> = Readonly<Record<string, ModuleExtensionHostDeclaration>>,\n> = {\n moduleId: TModuleId\n hosts: THosts\n}\n\nexport function defineModuleExtensionPoints<\n const TModuleId extends string,\n const THosts extends Readonly<Record<string, ModuleExtensionHostDeclaration>>,\n>(declaration: ModuleExtensionPoints<TModuleId, THosts>): ModuleExtensionPoints<TModuleId, THosts> {\n return Object.freeze({\n ...declaration,\n hosts: Object.freeze({ ...declaration.hosts }),\n })\n}\n\nexport function injectionExtensionHost<const TDeclaration extends InjectionExtensionHostDeclaration>(\n declaration: TDeclaration,\n): Readonly<TDeclaration> {\n const hasExactId = typeof declaration.spotId === 'string' && declaration.spotId.length > 0\n const hasPattern = typeof declaration.pattern === 'string' && declaration.pattern.length > 0\n if (hasExactId === hasPattern) {\n throw new Error('[internal] injection extension hosts require exactly one of spotId or pattern')\n }\n if (hasPattern && (!declaration.parameters || Object.keys(declaration.parameters).length === 0)) {\n throw new Error('[internal] patterned injection extension hosts require named parameters')\n }\n return Object.freeze({ ...declaration })\n}\n\nexport function dataTableExtensionHost<\n const TDeclaration extends Omit<DataTableExtensionHostDeclaration, 'family'>,\n>(\n declaration: TDeclaration,\n): Readonly<TDeclaration & { family: 'data-table' }> {\n return Object.freeze({ family: 'data-table', ...declaration })\n}\n\nexport function crudFormExtensionHost<\n const TDeclaration extends Omit<CrudFormExtensionHostDeclaration, 'family'>,\n>(\n declaration: TDeclaration,\n): Readonly<TDeclaration & { family: 'crud-form' }> {\n return Object.freeze({ family: 'crud-form', ...declaration })\n}\n\nexport function componentExtensionHost<\n const TDeclaration extends Omit<ComponentExtensionHostDeclaration, 'family'>,\n>(\n declaration: TDeclaration,\n): Readonly<TDeclaration & { family: 'component-handle' }> {\n return Object.freeze({ family: 'component-handle', ...declaration })\n}\n\nexport type BoundExtensionSurface = {\n key: string\n suffix: string | null\n capabilities: readonly ExtensionHostCapability[]\n bound: boolean\n phases?: readonly string[]\n operations?: readonly string[]\n}\n\nexport const DATA_TABLE_EXTENSION_SURFACES = [\n { key: 'header', suffix: 'header', capabilities: ['render-widget'], bound: true },\n { key: 'footer', suffix: 'footer', capabilities: ['render-widget'], bound: true },\n { key: 'toolbar', suffix: 'toolbar', capabilities: ['toolbar-widget'], bound: true },\n { key: 'searchTrailing', suffix: 'search-trailing', capabilities: ['render-widget'], bound: true },\n { key: 'columns', suffix: 'columns', capabilities: ['column-widget'], bound: true },\n { key: 'rowActions', suffix: 'row-actions', capabilities: ['row-action'], bound: true },\n { key: 'bulkActions', suffix: 'bulk-actions', capabilities: ['bulk-action'], bound: true },\n { key: 'filters', suffix: 'filters', capabilities: ['filter-widget'], bound: true },\n { key: 'replacement', suffix: null, capabilities: ['component-replacement'], bound: true },\n { key: 'emptyState', suffix: 'empty-state', capabilities: ['render-widget'], bound: false },\n] as const satisfies readonly BoundExtensionSurface[]\n\nexport const DATA_TABLE_EXTENSION_SURFACE_KEYS = DATA_TABLE_EXTENSION_SURFACES.map((surface) => surface.key)\n\nexport const CRUD_FORM_EXTENSION_SURFACES = [\n { key: 'base', suffix: null, capabilities: ['render-widget', 'lifecycle-handler'], bound: true },\n { key: 'header', suffix: 'header', capabilities: ['render-widget'], bound: true },\n { key: 'fields', suffix: 'fields', capabilities: ['field-widget'], bound: true },\n { key: 'replacement', suffix: null, capabilities: ['component-replacement'], bound: true },\n { key: 'beforeFields', suffix: 'before-fields', capabilities: ['render-widget'], bound: false },\n { key: 'afterFields', suffix: 'after-fields', capabilities: ['render-widget'], bound: false },\n { key: 'footer', suffix: 'footer', capabilities: ['render-widget'], bound: false },\n { key: 'sidebar', suffix: 'sidebar', capabilities: ['render-widget'], bound: false },\n { key: 'group', suffix: 'group:{groupId}', capabilities: ['render-widget'], bound: false },\n { key: 'fieldBefore', suffix: 'field:{fieldId}:before', capabilities: ['render-widget'], bound: false },\n { key: 'fieldAfter', suffix: 'field:{fieldId}:after', capabilities: ['render-widget'], bound: false },\n] as const satisfies readonly BoundExtensionSurface[]\n\nexport const CRUD_FORM_EXTENSION_SURFACE_KEYS = CRUD_FORM_EXTENSION_SURFACES.map((surface) => surface.key)\n\nexport const CRUD_FORM_LIFECYCLE_PHASES = [\n 'transformValidation',\n 'transformDisplayData',\n 'onBeforeNavigate',\n 'onAppEvent',\n 'onVisibilityChange',\n 'onBeforeDelete',\n 'onDelete',\n 'onAfterDelete',\n 'onDeleteError',\n 'onFieldChange',\n 'transformFormData',\n 'onBeforeSave',\n 'onSave',\n 'onAfterSave',\n] as const\n\nexport const CRUD_FORM_OPERATIONS = ['create', 'update', 'delete'] as const\n\nexport function dataTableExtensionSpotId(tableId: string, suffix?: string): string {\n return suffix ? `data-table:${tableId}:${suffix}` : `data-table:${tableId}`\n}\n\nexport function crudFormExtensionSpotId(entityId: string, suffix?: string): string {\n return suffix ? `crud-form:${entityId}:${suffix}` : `crud-form:${entityId}`\n}\n\nexport function extensionSpotChildId(spotId: string, suffix: string): string {\n return `${spotId}:${suffix}`\n}\n\nexport function resolveExtensionPointPattern(\n pattern: string,\n parameters: Readonly<Record<string, string>>,\n): string {\n return pattern.replace(/\\{([^}]+)\\}/g, (token, parameterName: string) => parameters[parameterName] ?? token)\n}\n\n/**\n * Portable, structural copy of the CLI provenance source reference\n * (`ModuleFactSourceRef` in `packages/cli/.../module-facts.ts`). Redefined here\n * \u2014 rather than imported \u2014 so `@open-mercato/shared` keeps zero dependency on\n * `@open-mercato/cli`. Field names are kept byte-identical so the two are\n * assignable across the package boundary.\n */\nexport type ModuleFactSourceRef = {\n sourcePath: string\n exportName?: string\n line?: number\n}\n\n/** Portable, structural copy of the CLI provenance fact reference. */\nexport type ModuleFactRef = {\n factSection: string\n factKey: string\n}\n\nexport const MODULE_EXTENSION_CONTRIBUTION_KINDS = [\n 'widget',\n 'data-table',\n 'crud-form',\n 'component-override',\n 'response-enricher',\n 'api-interceptor',\n 'command-interceptor',\n 'mutation-guard',\n 'entity-extension',\n 'subscriber',\n 'browser-reaction',\n 'specialized-registry',\n 'module-override',\n] as const\n\nexport type ModuleExtensionContributionKind = (typeof MODULE_EXTENSION_CONTRIBUTION_KINDS)[number]\n\nexport const MODULE_EXTENSION_ACTIVATION_KINDS = [\n 'crud-response-enricher',\n 'query-enricher',\n 'mutation-guard',\n 'api-interceptor-bridge',\n 'command-interceptor-bridge',\n 'widget-injection-consumer',\n 'component-extension-consumer',\n 'dashboard-host-consumer',\n] as const\n\nexport type ModuleExtensionActivationKind = (typeof MODULE_EXTENSION_ACTIVATION_KINDS)[number]\n\nexport const MODULE_EXTENSION_TARGET_KINDS = [\n 'module',\n 'entity',\n 'api-route',\n 'command',\n 'widget-spot',\n 'component',\n 'event',\n 'notification',\n 'wildcard',\n] as const\n\nexport type ModuleExtensionTargetKind = (typeof MODULE_EXTENSION_TARGET_KINDS)[number]\n\nexport type ModuleExtensionTargetRef = {\n kind: ModuleExtensionTargetKind\n id: string\n moduleId?: string\n method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n}\n\nexport type ModuleExtensionActivation = {\n id: string\n kind: ModuleExtensionActivationKind\n host: ModuleExtensionTargetRef\n contributionKinds: ModuleExtensionContributionKind[]\n phases?: string[]\n /**\n * Mutation operations the call site guards, when statically declared. A\n * contribution declaring only other operations is not bound by this activation.\n */\n operations?: string[]\n source: ModuleFactSourceRef\n bridge?: ModuleFactRef\n}\n\nexport const MODULE_EXTENSION_RESOLUTIONS = [\n 'bound',\n 'capability-only',\n 'optional-target-missing',\n 'wildcard',\n 'unresolved',\n] as const\n\nexport type ModuleExtensionResolution = (typeof MODULE_EXTENSION_RESOLUTIONS)[number]\n\nexport type ModuleIncomingExtensionRef = {\n contributionId: string\n contributionKind: ModuleExtensionContributionKind\n contributorModuleId: string\n target: ModuleExtensionTargetRef\n activationId?: string\n resolution: ModuleExtensionResolution\n source: ModuleFactSourceRef\n}\n\nexport type ModuleContributionResolution = {\n contributionId: string\n target: ModuleExtensionTargetRef\n resolution: ModuleExtensionResolution\n activationIds: string[]\n}\n\n/**\n * Additive, optional bidirectional-topology fields layered on top of the\n * existing extension-surface facts. Every field is optional so legacy consumers\n * and pre-existing generated output remain byte-compatible.\n */\nexport type ModuleExtensionSurfaceFactsAdditions = {\n activations?: ModuleExtensionActivation[]\n incoming?: ModuleIncomingExtensionRef[]\n contributionResolutions?: ModuleContributionResolution[]\n}\n\nexport type ModuleExtensionSurfaceFacts = {\n hosts: ModuleExtensionHostFact[]\n contributions: ModuleExtensionContributionFact[]\n unresolved: ModuleExtensionUnresolvedFact[]\n} & ModuleExtensionSurfaceFactsAdditions\n\nexport const MODULE_EXTENSION_HOST_RESOLUTIONS = ['exact', 'pattern', 'framework', 'fact-ref'] as const\n\nexport type ModuleExtensionHostResolution = (typeof MODULE_EXTENSION_HOST_RESOLUTIONS)[number]\n\nexport type ModuleExtensionHostFact = {\n key: string\n id: string\n resolution: ModuleExtensionHostResolution\n family: ExtensionHostFamily\n ownerModule: string\n capabilities: ExtensionHostCapability[]\n phases?: string[]\n operations?: string[]\n contextContract?: string\n dataContract?: string\n scopeContract?: string\n runtimeContract?: string\n activation?: ExtensionHostActivation\n bound: boolean\n stability: 'frozen' | 'stable'\n source:\n | { kind: 'declaration'; path: string; symbol: string }\n | { kind: 'fact-ref'; factSection: string; factKey: string }\n | { kind: 'framework'; path: string; symbol: string }\n aliases?: string[]\n patternParameters?: Record<string, ExtensionPointPatternParameter>\n fallbacks?: string[]\n}\n\nexport const MODULE_EXTENSION_TARGET_RESOLUTIONS = [\n 'exact',\n 'pattern',\n 'framework',\n 'fact-ref',\n 'optional-external',\n 'unresolved',\n] as const\n\nexport type ModuleExtensionTargetResolution = (typeof MODULE_EXTENSION_TARGET_RESOLUTIONS)[number]\n\nexport type ModuleExtensionTargetFact = {\n id: string\n resolution: ModuleExtensionTargetResolution\n factRef?: { factSection: string; factKey: string }\n optionalOwnerPackage?: string\n}\n\nexport type ModuleExtensionContributionBase = {\n id: string\n targets: ModuleExtensionTargetFact[]\n phases?: string[]\n operations?: string[]\n features?: string[]\n scopeContract: string\n activation?: ExtensionHostActivation\n placement?: { relativeTo?: string; position?: 'first' | 'last' | 'before' | 'after'; priority?: number }\n roundTripId?: string\n override?: { domain: string; key: string; mode: 'disable-replace' | 'replace' | 'additive' }\n source: { path: string; symbol?: string }\n}\n\nexport const MODULE_SPECIALIZED_REGISTRIES = [\n 'notification',\n 'integration',\n 'search',\n 'vector',\n 'ai',\n 'payment',\n 'shipping',\n 'currency',\n 'workflow',\n] as const\n\nexport type ModuleSpecializedRegistry = (typeof MODULE_SPECIALIZED_REGISTRIES)[number]\n\nexport const COMPONENT_OVERRIDE_MODES = ['replace', 'wrapper', 'props'] as const\n\nexport type ModuleComponentOverrideMode = (typeof COMPONENT_OVERRIDE_MODES)[number]\n\nexport type ModuleExtensionContributionFact = ModuleExtensionContributionBase & (\n | {\n kind: 'widget'\n details: {\n payload: 'render' | 'headless' | 'menu' | 'dashboard' | 'notification' | 'integration'\n registryKey: string\n itemIds?: string[]\n labelKeys?: string[]\n contextContract?: string\n dataContract?: string\n executionGuard: 'host' | 'contribution' | 'both'\n }\n }\n | {\n kind: 'data-table'\n details: {\n payload: 'column' | 'row-action' | 'bulk-action' | 'filter' | 'toolbar' | 'render'\n tableId: string\n executionGuard: 'host' | 'contribution' | 'both'\n }\n }\n | {\n kind: 'crud-form'\n details: {\n payload: 'render' | 'field' | 'lifecycle-handler'\n entityId: string\n fieldIds?: string[]\n groupIds?: string[]\n requestHeaderCapability: boolean\n }\n }\n | {\n kind: 'component-override'\n details: { handle: string; mode: ModuleComponentOverrideMode; propsContract: string }\n }\n | {\n kind: 'response-enricher'\n details: {\n targetEntity: string\n surfaces: Array<'list' | 'detail'>\n timeoutMs: number\n fallback: 'none' | 'configured'\n critical: boolean\n cachePosture: 'record-pure' | 'rerun-on-list-cache-hit'\n queryEngine?: {\n engines: string[]\n applyOn: Array<'list' | 'detail'>\n activation: 'caller-opt-in'\n }\n }\n }\n | {\n kind: 'api-interceptor'\n details: {\n route: string\n methods: string[]\n phases: Array<'before' | 'after'>\n activation: 'crud-pipeline' | 'custom-route-bridge'\n timeoutMs: number\n failurePosture: 'fail-closed' | 'fallback'\n }\n }\n | {\n kind: 'command-interceptor'\n details: {\n targetCommand: string\n phases: Array<'before-execute' | 'after-execute' | 'before-undo' | 'after-undo'>\n }\n }\n | {\n kind: 'mutation-guard'\n details: {\n entityId: string\n operations: Array<'create' | 'update' | 'delete'>\n capabilities: Array<'block' | 'rewrite' | 'after-success'>\n optimisticLock: 'preserved'\n }\n }\n | {\n kind: 'entity-extension'\n details: {\n hostEntityId: string\n extensionEntityId: string\n linkId: string\n scopeContract: string\n orphanContract: string\n }\n }\n | {\n kind: 'subscriber'\n details: {\n event: string\n subscriberId: string\n persistent: boolean\n sync: boolean\n priority?: number\n }\n }\n | {\n kind: 'browser-reaction'\n details: {\n transports: Array<'client' | 'portal' | 'notification-effect'>\n hooks: string[]\n audienceScopeContract: string\n maxPayloadBytes?: number\n dedupWindowMs?: number\n /**\n * Statically declared handler id. Runtime keys `notifications.handlers`\n * overrides by it, so it is absent when the module declares no id (such a\n * handler cannot be overridden).\n */\n overrideKey?: string\n }\n }\n | {\n kind: 'specialized-registry'\n details: {\n registry: ModuleSpecializedRegistry\n registryId: string\n specialistRoute: string\n }\n }\n | {\n kind: 'module-override'\n details: {\n domain: string\n key: string\n mode: 'disable-replace' | 'replace' | 'additive'\n }\n }\n)\n\nexport const MODULE_EXTENSION_UNRESOLVED_REASONS = [\n 'unclassified-binding',\n 'unbound-declaration',\n 'dynamic-without-pattern',\n 'unresolved-first-party-target',\n] as const\n\nexport type ModuleExtensionUnresolvedReason = (typeof MODULE_EXTENSION_UNRESOLVED_REASONS)[number]\n\nexport type ModuleExtensionUnresolvedFact = {\n key: string\n source: { path: string; symbol?: string }\n reason: ModuleExtensionUnresolvedReason\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["export const EXTENSION_HOST_FAMILIES = [\n 'generic',\n 'menu',\n 'data-table',\n 'crud-form',\n 'detail',\n 'portal-page',\n 'component-handle',\n 'entity',\n 'api-route',\n 'command',\n 'event',\n 'query-lifecycle',\n 'dashboard',\n 'notification',\n 'integration',\n 'specialized-registry',\n 'module-override',\n] as const\n\nexport type ExtensionHostFamily = (typeof EXTENSION_HOST_FAMILIES)[number]\n\nexport const EXTENSION_HOST_CAPABILITIES = [\n 'render-widget',\n 'headless-widget',\n 'menu-item',\n 'column-widget',\n 'row-action',\n 'bulk-action',\n 'filter-widget',\n 'toolbar-widget',\n 'field-widget',\n 'lifecycle-handler',\n 'component-replacement',\n 'response-enricher',\n 'query-enricher',\n 'api-interceptor',\n 'command-interceptor',\n 'mutation-guard',\n 'entity-extension',\n 'async-subscriber',\n 'sync-subscriber',\n 'browser-client',\n 'browser-portal',\n 'registry-contribution',\n 'module-override',\n] as const\n\nexport type ExtensionHostCapability = (typeof EXTENSION_HOST_CAPABILITIES)[number]\n\nexport const EXTENSION_HOST_ACTIVATIONS = ['always', 'host-opt-in', 'caller-opt-in', 'feature-gated'] as const\n\nexport type ExtensionHostActivation = (typeof EXTENSION_HOST_ACTIVATIONS)[number]\n\nexport type ExtensionPointPatternParameter = {\n source: string\n pattern?: string\n}\n\ntype ExtensionHostDeclarationBase = {\n /**\n * File(s) this host is mounted from, relative to the module's source root. A\n * host mounted from more than one file may declare them all; the first entry\n * remains the authoritative provenance path, the rest surface on the\n * generated fact's `additionalSources`.\n */\n source: string | readonly string[]\n contextContract?: string\n dataContract?: string\n scopeContract?: string\n runtimeContract?: string\n activation?: ExtensionHostActivation\n aliases?: readonly string[]\n fallbacks?: readonly string[]\n}\n\ntype InjectionExtensionHostDeclarationBase = ExtensionHostDeclarationBase & {\n family: Exclude<ExtensionHostFamily, 'data-table' | 'crud-form' | 'component-handle'>\n supported: readonly ExtensionHostCapability[]\n}\n\nexport type InjectionExtensionHostDeclaration = InjectionExtensionHostDeclarationBase & (\n | {\n spotId: string\n pattern?: never\n parameters?: never\n }\n | {\n spotId?: never\n pattern: string\n parameters: Readonly<Record<string, ExtensionPointPatternParameter>>\n }\n)\n\nexport type DataTableExtensionHostDeclaration = ExtensionHostDeclarationBase & {\n family: 'data-table'\n tableId: string\n baseSpotId?: string\n}\n\nexport type CrudFormExtensionHostDeclaration = ExtensionHostDeclarationBase & {\n family: 'crud-form'\n entityId: string\n spotId?: string\n}\n\nexport type ComponentExtensionHostDeclaration = ExtensionHostDeclarationBase & {\n family: 'component-handle'\n componentId: string\n propsContract?: string\n}\n\nexport type ModuleExtensionHostDeclaration =\n | InjectionExtensionHostDeclaration\n | DataTableExtensionHostDeclaration\n | CrudFormExtensionHostDeclaration\n | ComponentExtensionHostDeclaration\n\nexport type ModuleExtensionPoints<\n TModuleId extends string = string,\n THosts extends Readonly<Record<string, ModuleExtensionHostDeclaration>> = Readonly<Record<string, ModuleExtensionHostDeclaration>>,\n> = {\n moduleId: TModuleId\n hosts: THosts\n}\n\nexport function defineModuleExtensionPoints<\n const TModuleId extends string,\n const THosts extends Readonly<Record<string, ModuleExtensionHostDeclaration>>,\n>(declaration: ModuleExtensionPoints<TModuleId, THosts>): ModuleExtensionPoints<TModuleId, THosts> {\n return Object.freeze({\n ...declaration,\n hosts: Object.freeze({ ...declaration.hosts }),\n })\n}\n\nexport function injectionExtensionHost<const TDeclaration extends InjectionExtensionHostDeclaration>(\n declaration: TDeclaration,\n): Readonly<TDeclaration> {\n const hasExactId = typeof declaration.spotId === 'string' && declaration.spotId.length > 0\n const hasPattern = typeof declaration.pattern === 'string' && declaration.pattern.length > 0\n if (hasExactId === hasPattern) {\n throw new Error('[internal] injection extension hosts require exactly one of spotId or pattern')\n }\n if (hasPattern && (!declaration.parameters || Object.keys(declaration.parameters).length === 0)) {\n throw new Error('[internal] patterned injection extension hosts require named parameters')\n }\n return Object.freeze({ ...declaration })\n}\n\nexport function dataTableExtensionHost<\n const TDeclaration extends Omit<DataTableExtensionHostDeclaration, 'family'>,\n>(\n declaration: TDeclaration,\n): Readonly<TDeclaration & { family: 'data-table' }> {\n return Object.freeze({ family: 'data-table', ...declaration })\n}\n\nexport function crudFormExtensionHost<\n const TDeclaration extends Omit<CrudFormExtensionHostDeclaration, 'family'>,\n>(\n declaration: TDeclaration,\n): Readonly<TDeclaration & { family: 'crud-form' }> {\n return Object.freeze({ family: 'crud-form', ...declaration })\n}\n\nexport function componentExtensionHost<\n const TDeclaration extends Omit<ComponentExtensionHostDeclaration, 'family'>,\n>(\n declaration: TDeclaration,\n): Readonly<TDeclaration & { family: 'component-handle' }> {\n return Object.freeze({ family: 'component-handle', ...declaration })\n}\n\nexport type BoundExtensionSurface = {\n key: string\n suffix: string | null\n capabilities: readonly ExtensionHostCapability[]\n bound: boolean\n phases?: readonly string[]\n operations?: readonly string[]\n}\n\nexport const DATA_TABLE_EXTENSION_SURFACES = [\n { key: 'header', suffix: 'header', capabilities: ['render-widget'], bound: true },\n { key: 'footer', suffix: 'footer', capabilities: ['render-widget'], bound: true },\n { key: 'toolbar', suffix: 'toolbar', capabilities: ['toolbar-widget'], bound: true },\n { key: 'searchTrailing', suffix: 'search-trailing', capabilities: ['render-widget'], bound: true },\n { key: 'columns', suffix: 'columns', capabilities: ['column-widget'], bound: true },\n { key: 'rowActions', suffix: 'row-actions', capabilities: ['row-action'], bound: true },\n { key: 'bulkActions', suffix: 'bulk-actions', capabilities: ['bulk-action'], bound: true },\n { key: 'filters', suffix: 'filters', capabilities: ['filter-widget'], bound: true },\n { key: 'replacement', suffix: null, capabilities: ['component-replacement'], bound: true },\n { key: 'emptyState', suffix: 'empty-state', capabilities: ['render-widget'], bound: false },\n] as const satisfies readonly BoundExtensionSurface[]\n\nexport const DATA_TABLE_EXTENSION_SURFACE_KEYS = DATA_TABLE_EXTENSION_SURFACES.map((surface) => surface.key)\n\nexport const CRUD_FORM_EXTENSION_SURFACES = [\n { key: 'base', suffix: null, capabilities: ['render-widget', 'lifecycle-handler'], bound: true },\n { key: 'header', suffix: 'header', capabilities: ['render-widget'], bound: true },\n { key: 'fields', suffix: 'fields', capabilities: ['field-widget'], bound: true },\n { key: 'replacement', suffix: null, capabilities: ['component-replacement'], bound: true },\n { key: 'beforeFields', suffix: 'before-fields', capabilities: ['render-widget'], bound: false },\n { key: 'afterFields', suffix: 'after-fields', capabilities: ['render-widget'], bound: false },\n { key: 'footer', suffix: 'footer', capabilities: ['render-widget'], bound: false },\n { key: 'sidebar', suffix: 'sidebar', capabilities: ['render-widget'], bound: false },\n { key: 'group', suffix: 'group:{groupId}', capabilities: ['render-widget'], bound: false },\n { key: 'fieldBefore', suffix: 'field:{fieldId}:before', capabilities: ['render-widget'], bound: false },\n { key: 'fieldAfter', suffix: 'field:{fieldId}:after', capabilities: ['render-widget'], bound: false },\n] as const satisfies readonly BoundExtensionSurface[]\n\nexport const CRUD_FORM_EXTENSION_SURFACE_KEYS = CRUD_FORM_EXTENSION_SURFACES.map((surface) => surface.key)\n\nexport const CRUD_FORM_LIFECYCLE_PHASES = [\n 'transformValidation',\n 'transformDisplayData',\n 'onBeforeNavigate',\n 'onAppEvent',\n 'onVisibilityChange',\n 'onBeforeDelete',\n 'onDelete',\n 'onAfterDelete',\n 'onDeleteError',\n 'onFieldChange',\n 'transformFormData',\n 'onBeforeSave',\n 'onSave',\n 'onAfterSave',\n] as const\n\nexport const CRUD_FORM_OPERATIONS = ['create', 'update', 'delete'] as const\n\nexport function dataTableExtensionSpotId(tableId: string, suffix?: string): string {\n return suffix ? `data-table:${tableId}:${suffix}` : `data-table:${tableId}`\n}\n\nexport function crudFormExtensionSpotId(entityId: string, suffix?: string): string {\n return suffix ? `crud-form:${entityId}:${suffix}` : `crud-form:${entityId}`\n}\n\nexport function extensionSpotChildId(spotId: string, suffix: string): string {\n return `${spotId}:${suffix}`\n}\n\nexport function resolveExtensionPointPattern(\n pattern: string,\n parameters: Readonly<Record<string, string>>,\n): string {\n return pattern.replace(/\\{([^}]+)\\}/g, (token, parameterName: string) => parameters[parameterName] ?? token)\n}\n\n/**\n * Portable, structural copy of the CLI provenance source reference\n * (`ModuleFactSourceRef` in `packages/cli/.../module-facts.ts`). Redefined here\n * \u2014 rather than imported \u2014 so `@open-mercato/shared` keeps zero dependency on\n * `@open-mercato/cli`. Field names are kept byte-identical so the two are\n * assignable across the package boundary.\n */\nexport type ModuleFactSourceRef = {\n sourcePath: string\n exportName?: string\n line?: number\n}\n\n/** Portable, structural copy of the CLI provenance fact reference. */\nexport type ModuleFactRef = {\n factSection: string\n factKey: string\n}\n\nexport const MODULE_EXTENSION_CONTRIBUTION_KINDS = [\n 'widget',\n 'data-table',\n 'crud-form',\n 'component-override',\n 'response-enricher',\n 'api-interceptor',\n 'command-interceptor',\n 'mutation-guard',\n 'entity-extension',\n 'subscriber',\n 'browser-reaction',\n 'specialized-registry',\n 'module-override',\n] as const\n\nexport type ModuleExtensionContributionKind = (typeof MODULE_EXTENSION_CONTRIBUTION_KINDS)[number]\n\nexport const MODULE_EXTENSION_ACTIVATION_KINDS = [\n 'crud-response-enricher',\n 'query-enricher',\n 'mutation-guard',\n 'api-interceptor-bridge',\n 'command-interceptor-bridge',\n 'widget-injection-consumer',\n 'component-extension-consumer',\n 'dashboard-host-consumer',\n] as const\n\nexport type ModuleExtensionActivationKind = (typeof MODULE_EXTENSION_ACTIVATION_KINDS)[number]\n\nexport const MODULE_EXTENSION_TARGET_KINDS = [\n 'module',\n 'entity',\n 'api-route',\n 'command',\n 'widget-spot',\n 'component',\n 'event',\n 'notification',\n 'wildcard',\n] as const\n\nexport type ModuleExtensionTargetKind = (typeof MODULE_EXTENSION_TARGET_KINDS)[number]\n\nexport type ModuleExtensionTargetRef = {\n kind: ModuleExtensionTargetKind\n id: string\n moduleId?: string\n method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n}\n\nexport type ModuleExtensionActivation = {\n id: string\n kind: ModuleExtensionActivationKind\n host: ModuleExtensionTargetRef\n contributionKinds: ModuleExtensionContributionKind[]\n phases?: string[]\n /**\n * Mutation operations the call site guards, when statically declared. A\n * contribution declaring only other operations is not bound by this activation.\n */\n operations?: string[]\n source: ModuleFactSourceRef\n bridge?: ModuleFactRef\n}\n\nexport const MODULE_EXTENSION_RESOLUTIONS = [\n 'bound',\n 'capability-only',\n 'optional-target-missing',\n 'wildcard',\n 'unresolved',\n] as const\n\nexport type ModuleExtensionResolution = (typeof MODULE_EXTENSION_RESOLUTIONS)[number]\n\nexport type ModuleIncomingExtensionRef = {\n contributionId: string\n contributionKind: ModuleExtensionContributionKind\n contributorModuleId: string\n target: ModuleExtensionTargetRef\n activationId?: string\n resolution: ModuleExtensionResolution\n source: ModuleFactSourceRef\n}\n\nexport type ModuleContributionResolution = {\n contributionId: string\n target: ModuleExtensionTargetRef\n resolution: ModuleExtensionResolution\n activationIds: string[]\n}\n\n/**\n * Additive, optional bidirectional-topology fields layered on top of the\n * existing extension-surface facts. Every field is optional so legacy consumers\n * and pre-existing generated output remain byte-compatible.\n */\nexport type ModuleExtensionSurfaceFactsAdditions = {\n activations?: ModuleExtensionActivation[]\n incoming?: ModuleIncomingExtensionRef[]\n contributionResolutions?: ModuleContributionResolution[]\n}\n\nexport type ModuleExtensionSurfaceFacts = {\n hosts: ModuleExtensionHostFact[]\n contributions: ModuleExtensionContributionFact[]\n unresolved: ModuleExtensionUnresolvedFact[]\n} & ModuleExtensionSurfaceFactsAdditions\n\nexport const MODULE_EXTENSION_HOST_RESOLUTIONS = ['exact', 'pattern', 'framework', 'fact-ref'] as const\n\nexport type ModuleExtensionHostResolution = (typeof MODULE_EXTENSION_HOST_RESOLUTIONS)[number]\n\nexport type ModuleExtensionHostFact = {\n key: string\n id: string\n resolution: ModuleExtensionHostResolution\n family: ExtensionHostFamily\n ownerModule: string\n capabilities: ExtensionHostCapability[]\n phases?: string[]\n operations?: string[]\n contextContract?: string\n dataContract?: string\n scopeContract?: string\n runtimeContract?: string\n activation?: ExtensionHostActivation\n bound: boolean\n stability: 'frozen' | 'stable'\n source:\n | { kind: 'declaration'; path: string; symbol: string }\n | { kind: 'fact-ref'; factSection: string; factKey: string }\n | { kind: 'framework'; path: string; symbol: string }\n /** Extra mount-point paths beyond the declaration's primary `source` entry. */\n additionalSources?: string[]\n aliases?: string[]\n patternParameters?: Record<string, ExtensionPointPatternParameter>\n fallbacks?: string[]\n}\n\nexport const MODULE_EXTENSION_TARGET_RESOLUTIONS = [\n 'exact',\n 'pattern',\n 'framework',\n 'fact-ref',\n 'optional-external',\n 'unresolved',\n] as const\n\nexport type ModuleExtensionTargetResolution = (typeof MODULE_EXTENSION_TARGET_RESOLUTIONS)[number]\n\nexport type ModuleExtensionTargetFact = {\n id: string\n resolution: ModuleExtensionTargetResolution\n factRef?: { factSection: string; factKey: string }\n optionalOwnerPackage?: string\n}\n\nexport type ModuleExtensionContributionBase = {\n id: string\n targets: ModuleExtensionTargetFact[]\n phases?: string[]\n operations?: string[]\n features?: string[]\n scopeContract: string\n activation?: ExtensionHostActivation\n placement?: { relativeTo?: string; position?: 'first' | 'last' | 'before' | 'after'; priority?: number }\n roundTripId?: string\n override?: { domain: string; key: string; mode: 'disable-replace' | 'replace' | 'additive' }\n source: { path: string; symbol?: string }\n}\n\nexport const MODULE_SPECIALIZED_REGISTRIES = [\n 'notification',\n 'integration',\n 'search',\n 'vector',\n 'ai',\n 'payment',\n 'shipping',\n 'currency',\n 'workflow',\n] as const\n\nexport type ModuleSpecializedRegistry = (typeof MODULE_SPECIALIZED_REGISTRIES)[number]\n\nexport const COMPONENT_OVERRIDE_MODES = ['replace', 'wrapper', 'props'] as const\n\nexport type ModuleComponentOverrideMode = (typeof COMPONENT_OVERRIDE_MODES)[number]\n\nexport type ModuleExtensionContributionFact = ModuleExtensionContributionBase & (\n | {\n kind: 'widget'\n details: {\n payload: 'render' | 'headless' | 'menu' | 'dashboard' | 'notification' | 'integration'\n registryKey: string\n itemIds?: string[]\n labelKeys?: string[]\n contextContract?: string\n dataContract?: string\n executionGuard: 'host' | 'contribution' | 'both'\n }\n }\n | {\n kind: 'data-table'\n details: {\n payload: 'column' | 'row-action' | 'bulk-action' | 'filter' | 'toolbar' | 'render'\n tableId: string\n executionGuard: 'host' | 'contribution' | 'both'\n }\n }\n | {\n kind: 'crud-form'\n details: {\n payload: 'render' | 'field' | 'lifecycle-handler'\n entityId: string\n fieldIds?: string[]\n groupIds?: string[]\n requestHeaderCapability: boolean\n }\n }\n | {\n kind: 'component-override'\n details: { handle: string; mode: ModuleComponentOverrideMode; propsContract: string }\n }\n | {\n kind: 'response-enricher'\n details: {\n targetEntity: string\n surfaces: Array<'list' | 'detail'>\n timeoutMs: number\n fallback: 'none' | 'configured'\n critical: boolean\n cachePosture: 'record-pure' | 'rerun-on-list-cache-hit'\n queryEngine?: {\n engines: string[]\n applyOn: Array<'list' | 'detail'>\n activation: 'caller-opt-in'\n }\n }\n }\n | {\n kind: 'api-interceptor'\n details: {\n route: string\n methods: string[]\n phases: Array<'before' | 'after'>\n activation: 'crud-pipeline' | 'custom-route-bridge'\n timeoutMs: number\n failurePosture: 'fail-closed' | 'fallback'\n }\n }\n | {\n kind: 'command-interceptor'\n details: {\n targetCommand: string\n phases: Array<'before-execute' | 'after-execute' | 'before-undo' | 'after-undo'>\n }\n }\n | {\n kind: 'mutation-guard'\n details: {\n entityId: string\n operations: Array<'create' | 'update' | 'delete'>\n capabilities: Array<'block' | 'rewrite' | 'after-success'>\n optimisticLock: 'preserved'\n }\n }\n | {\n kind: 'entity-extension'\n details: {\n hostEntityId: string\n extensionEntityId: string\n linkId: string\n scopeContract: string\n orphanContract: string\n }\n }\n | {\n kind: 'subscriber'\n details: {\n event: string\n subscriberId: string\n persistent: boolean\n sync: boolean\n priority?: number\n }\n }\n | {\n kind: 'browser-reaction'\n details: {\n transports: Array<'client' | 'portal' | 'notification-effect'>\n hooks: string[]\n audienceScopeContract: string\n maxPayloadBytes?: number\n dedupWindowMs?: number\n /**\n * Statically declared handler id. Runtime keys `notifications.handlers`\n * overrides by it, so it is absent when the module declares no id (such a\n * handler cannot be overridden).\n */\n overrideKey?: string\n }\n }\n | {\n kind: 'specialized-registry'\n details: {\n registry: ModuleSpecializedRegistry\n registryId: string\n specialistRoute: string\n }\n }\n | {\n kind: 'module-override'\n details: {\n domain: string\n key: string\n mode: 'disable-replace' | 'replace' | 'additive'\n }\n }\n)\n\nexport const MODULE_EXTENSION_UNRESOLVED_REASONS = [\n 'unclassified-binding',\n 'unbound-declaration',\n 'dynamic-without-pattern',\n 'unresolved-first-party-target',\n] as const\n\nexport type ModuleExtensionUnresolvedReason = (typeof MODULE_EXTENSION_UNRESOLVED_REASONS)[number]\n\nexport type ModuleExtensionUnresolvedFact = {\n key: string\n source: { path: string; symbol?: string }\n reason: ModuleExtensionUnresolvedReason\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.8.1-develop.7206.1.45a3cee87f",
3
+ "version": "0.8.1-develop.7215.1.60786cf836",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -113,7 +113,7 @@
113
113
  "@mikro-orm/core": "^7.1.14",
114
114
  "@mikro-orm/decorators": "^7.1.14",
115
115
  "@mikro-orm/postgresql": "^7.1.14",
116
- "@open-mercato/cache": "0.8.1-develop.7206.1.45a3cee87f",
116
+ "@open-mercato/cache": "0.8.1-develop.7215.1.60786cf836",
117
117
  "@types/html-to-text": "^9.0.4",
118
118
  "@types/sanitize-html": "^2.16.1",
119
119
  "dotenv": "^17.4.2",
@@ -297,3 +297,140 @@ describe('TenantDataEncryptionService.getEncryptedFieldNames', () => {
297
297
  )
298
298
  })
299
299
  })
300
+
301
+ describe('TenantDataEncryptionService tenant-wide scope parity (issue #5949)', () => {
302
+ const originalToggle = process.env.TENANT_DATA_ENCRYPTION
303
+ const tenantId = 'tenant-5949'
304
+
305
+ beforeEach(() => {
306
+ process.env.TENANT_DATA_ENCRYPTION = 'yes'
307
+ })
308
+
309
+ afterEach(() => {
310
+ if (originalToggle === undefined) delete process.env.TENANT_DATA_ENCRYPTION
311
+ else process.env.TENANT_DATA_ENCRYPTION = originalToggle
312
+ })
313
+
314
+ // A base (organization-less) map declaring `display_name`, plus an organization-scoped map that
315
+ // declares the extra `description` field. `getMap` only ever resolves the base map for
316
+ // organizationId = null; the all-organizations aggregate is the 2-parameter query.
317
+ function makeService(entityId: string) {
318
+ const execute = jest.fn(async (_sql: string, params: unknown[]) => {
319
+ if (params.length === 3) {
320
+ return params[2] === null
321
+ ? [{ entity_id: entityId, fields_json: [{ field: 'display_name' }] }]
322
+ : []
323
+ }
324
+ return [{ fields_json: [{ field: 'description', hashField: 'description_hash' }] }]
325
+ })
326
+ const service = new TenantDataEncryptionService(
327
+ { getConnection: () => ({ execute }) } as never,
328
+ {
329
+ kms: {
330
+ getTenantDek: jest.fn(async (keyId: string) => (
331
+ keyId === tenantId ? { tenantId: keyId, key: fixedKey, fetchedAt: new Date() } : null
332
+ )),
333
+ createTenantDek: jest.fn(async () => null),
334
+ isHealthy: () => true,
335
+ },
336
+ } as never,
337
+ )
338
+ return { service, execute }
339
+ }
340
+
341
+ it('encrypts fields declared only on an organization-scoped map at the tenant-wide scope', async () => {
342
+ const entityId = 'test:parity_encrypt_entity'
343
+ const { service } = makeService(entityId)
344
+
345
+ const encrypted = await service.encryptEntityPayload(
346
+ entityId,
347
+ { display_name: 'Acme Corp', description: 'confidential note' },
348
+ tenantId,
349
+ null,
350
+ )
351
+
352
+ expect(decryptWithAesGcm(encrypted.display_name as string, fixedKey)).toBe('Acme Corp')
353
+ expect(encrypted.description).not.toBe('confidential note')
354
+ expect(decryptWithAesGcm(encrypted.description as string, fixedKey)).toBe('confidential note')
355
+ expect(encrypted.description_hash).toBe(hashForLookup('confidential note'))
356
+ })
357
+
358
+ it('decrypts fields declared only on an organization-scoped map at the tenant-wide scope', async () => {
359
+ const entityId = 'test:parity_decrypt_entity'
360
+ const { service } = makeService(entityId)
361
+
362
+ const decrypted = await service.decryptEntityPayload(
363
+ entityId,
364
+ {
365
+ display_name: encryptWithAesGcm('Acme Corp', fixedKey).value as string,
366
+ description: encryptWithAesGcm('confidential note', fixedKey).value as string,
367
+ },
368
+ tenantId,
369
+ null,
370
+ )
371
+
372
+ expect(decrypted.display_name).toBe('Acme Corp')
373
+ expect(decrypted.description).toBe('confidential note')
374
+ })
375
+
376
+ it('reports exactly the fields the payload functions act on at the tenant-wide scope', async () => {
377
+ const entityId = 'test:parity_reported_entity'
378
+ const { service } = makeService(entityId)
379
+
380
+ const reported = await service.getEncryptedFieldNames(entityId, tenantId, null)
381
+ const encrypted = await service.encryptEntityPayload(
382
+ entityId,
383
+ { display_name: 'Acme Corp', description: 'confidential note' },
384
+ tenantId,
385
+ null,
386
+ )
387
+
388
+ expect(reported).toEqual(['display_name', 'description'])
389
+ const actuallyEncrypted = reported.filter((field) => encrypted[field] !== undefined
390
+ && decryptWithAesGcm(encrypted[field] as string, fixedKey) !== null)
391
+ expect(actuallyEncrypted).toEqual(reported)
392
+ })
393
+
394
+ it('leaves an organization-scoped call on its own map without the all-organizations read', async () => {
395
+ const entityId = 'test:parity_org_scoped_entity'
396
+ const { service, execute } = makeService(entityId)
397
+
398
+ const encrypted = await service.encryptEntityPayload(
399
+ entityId,
400
+ { display_name: 'Acme Corp', description: 'confidential note' },
401
+ tenantId,
402
+ 'org-1',
403
+ )
404
+
405
+ expect(encrypted.description).toBe('confidential note')
406
+ expect(execute).not.toHaveBeenCalledWith(expect.anything(), [entityId, tenantId])
407
+ })
408
+
409
+ it('caches the all-organizations aggregate across payload calls', async () => {
410
+ const entityId = 'test:parity_cached_entity'
411
+ const { service, execute } = makeService(entityId)
412
+
413
+ await service.encryptEntityPayload(entityId, { description: 'first' }, tenantId, null)
414
+ await service.encryptEntityPayload(entityId, { description: 'second' }, tenantId, null)
415
+
416
+ const aggregateReads = execute.mock.calls.filter(([, params]) => (params as unknown[]).length === 2)
417
+ expect(aggregateReads).toHaveLength(1)
418
+ })
419
+
420
+ it('re-reads the all-organizations aggregate once its memory entry passes the 300s TTL', async () => {
421
+ const entityId = 'test:parity_expired_cache_entity'
422
+ const { service, execute } = makeService(entityId)
423
+ const nowSpy = jest.spyOn(Date, 'now')
424
+
425
+ nowSpy.mockReturnValue(1_000_000)
426
+ await service.encryptEntityPayload(entityId, { description: 'first' }, tenantId, null)
427
+
428
+ nowSpy.mockReturnValue(1_000_000 + 300_000 + 1)
429
+ await service.encryptEntityPayload(entityId, { description: 'second' }, tenantId, null)
430
+
431
+ nowSpy.mockRestore()
432
+
433
+ const aggregateReads = execute.mock.calls.filter(([, params]) => (params as unknown[]).length === 2)
434
+ expect(aggregateReads).toHaveLength(2)
435
+ })
436
+ })