@open-mercato/shared 0.5.1-develop.2975.ccbadc8198 → 0.5.1-develop.2996.ce62fd491c

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.
@@ -290,20 +290,21 @@ function decorateRecordWithCustomFields(record, definitions, context = {}) {
290
290
  const bareKey = prefixedKey.replace(/^cf_/, "");
291
291
  const normalizedKey = normalizeDefinitionKey(bareKey);
292
292
  if (!normalizedKey) return;
293
- values[bareKey] = value;
294
293
  const defsForKey = definitions.get(normalizedKey) ?? [];
295
294
  const resolvedDef = selectDefinitionForRecord(defsForKey, organizationId, tenantId);
295
+ if (!resolvedDef) return;
296
+ values[bareKey] = value;
296
297
  const entry = {
297
298
  key: bareKey,
298
- label: resolvedDef?.label ?? bareKey,
299
+ label: resolvedDef.label ?? bareKey,
299
300
  value,
300
- kind: resolvedDef?.kind ?? null,
301
- multi: resolvedDef?.multi ?? Array.isArray(value)
301
+ kind: resolvedDef.kind ?? null,
302
+ multi: resolvedDef.multi ?? Array.isArray(value)
302
303
  };
303
304
  entries.push({
304
305
  entry,
305
- priority: resolvedDef?.priority ?? Number.MAX_SAFE_INTEGER,
306
- updatedAt: resolvedDef?.updatedAt ?? 0
306
+ priority: resolvedDef.priority ?? Number.MAX_SAFE_INTEGER,
307
+ updatedAt: resolvedDef.updatedAt ?? 0
307
308
  });
308
309
  });
309
310
  const ordered = entries.sort((a, b) => {
@@ -416,7 +417,13 @@ async function loadCustomFieldValues(opts) {
416
417
  const def = pickDefinition(key, resolvedOrgId, resolvedTenantId);
417
418
  const encrypted = Boolean(def?.configJson && def.configJson?.encrypted);
418
419
  const value = valueFromRow(row);
419
- const decrypted = encrypted ? await decryptCustomFieldValue(value, resolvedTenantId ?? tenantId ?? null, getEncryptionService(), encryptionCache) : value;
420
+ const decrypted = encrypted ? await decryptCustomFieldValue(
421
+ value,
422
+ resolvedTenantId ?? tenantId ?? null,
423
+ getEncryptionService(),
424
+ encryptionCache,
425
+ { kind: def?.kind ?? null }
426
+ ) : value;
420
427
  const existing = buckets.get(bucketKey);
421
428
  if (existing) {
422
429
  if (existing.orgId == null && resolvedOrgId) existing.orgId = resolvedOrgId;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/custom-fields.ts"],
4
- "sourcesContent": ["import type { CustomFieldSet, EntityId } from '@open-mercato/shared/modules/entities'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { CustomFieldDef, CustomFieldValue } from '@open-mercato/core/modules/entities/data/entities'\nimport type { WhereValue } from '@open-mercato/shared/lib/query/types'\nimport type { TenantDataEncryptionService } from '../encryption/tenantDataEncryptionService'\nimport { decryptCustomFieldValue, resolveTenantEncryptionService } from '../encryption/customFieldValues'\nimport { parseBooleanToken } from '../boolean'\nimport { extractCustomFieldEntries } from './custom-fields-client'\n\nexport type CustomFieldSelectors = {\n keys: string[]\n selectors: string[] // e.g. ['cf:priority', 'cf:severity']\n outputKeys: string[] // e.g. ['cf_priority', 'cf_severity']\n}\n\nexport type SplitCustomFieldPayload = {\n base: Record<string, unknown>\n custom: Record<string, unknown>\n}\n\nexport type CustomFieldDefinitionSummary = {\n key: string\n label: string | null\n kind: string | null\n multi: boolean\n dictionaryId?: string | null\n organizationId?: string | null\n tenantId?: string | null\n priority: number\n updatedAt: number\n}\n\nexport type CustomFieldDefinitionIndex = Map<string, CustomFieldDefinitionSummary[]>\n\nexport type CustomFieldDisplayEntry = {\n key: string\n label: string | null\n value: unknown\n kind: string | null\n multi: boolean\n}\n\nexport type CustomFieldDisplayPayload = {\n customValues: Record<string, unknown> | null\n customFields: CustomFieldDisplayEntry[]\n}\n\nexport type CustomFieldSnapshot = {\n entries: Record<string, unknown>\n customValues: Record<string, unknown> | null\n customFields: CustomFieldDisplayEntry[]\n}\n\nexport function buildCustomFieldSelectorsForEntity(entityId: EntityId, sets: CustomFieldSet[]): CustomFieldSelectors {\n const keys = Array.from(new Set(\n (sets || [])\n .filter((s) => s.entity === entityId)\n .flatMap((s) => (s.fields || []).map((f) => f.key))\n ))\n const selectors = keys.map((k) => `cf:${k}`)\n const outputKeys = keys.map((k) => `cf_${k}`)\n return { keys, selectors, outputKeys }\n}\n\nexport function normalizeCustomFieldValue(val: unknown): unknown {\n if (Array.isArray(val)) return val\n if (typeof val === 'string') {\n const s = val.trim()\n // Parse Postgres array-like '{a,b,c}' to string[] when present\n if (s.startsWith('{') && s.endsWith('}')) {\n const inner = s.slice(1, -1).trim()\n if (!inner) return []\n return inner.split(/[\\s,]+/).map((x) => x.trim()).filter(Boolean)\n }\n return s\n }\n return val as any\n}\n\n// Extracts cf_* fields from a record that may contain both 'cf:<key>' and/or 'cf_<key>'\nexport function extractCustomFieldsFromItem(item: Record<string, unknown>, keys: string[]): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const key of keys) {\n const colon = item[`cf:${key}` as keyof typeof item]\n const snake = item[`cf_${key}` as keyof typeof item]\n const value = colon !== undefined ? colon : snake\n if (value !== undefined) out[`cf_${key}`] = normalizeCustomFieldValue(value)\n }\n return out\n}\n\nexport function extractAllCustomFieldEntries(item: Record<string, unknown>): Record<string, unknown> {\n return extractCustomFieldEntries(item)\n}\n\nfunction normalizeFieldsetFilter(input?: string | string[] | null): Set<string | null> | null {\n if (input == null) return null\n const values = Array.isArray(input) ? input : [input]\n const normalized = new Set<string | null>()\n for (const raw of values) {\n if (raw == null) continue\n const trimmed = String(raw).trim()\n if (!trimmed) {\n normalized.add(null)\n } else {\n normalized.add(trimmed)\n }\n }\n return normalized.size ? normalized : null\n}\n\nexport async function buildCustomFieldFiltersFromQuery(opts: {\n entityId?: EntityId\n entityIds?: EntityId[]\n query: Record<string, unknown>\n em: EntityManager\n tenantId: string | null | undefined\n fieldset?: string | string[] | null\n}): Promise<Record<string, WhereValue>> {\n const out: Record<string, WhereValue> = {}\n const entries = Object.entries(opts.query).filter(([k]) => k.startsWith('cf_'))\n if (!entries.length) return out\n\n const entityIdList = Array.isArray(opts.entityIds) && opts.entityIds.length\n ? opts.entityIds\n : opts.entityId\n ? [opts.entityId]\n : []\n if (!entityIdList.length) return out\n\n // Tenant-only scope: allow global (null) or tenant match; ignore organization here\n const defs = await opts.em.find(CustomFieldDef, {\n entityId: { $in: entityIdList as any },\n isActive: true,\n $and: [\n { $or: [ { tenantId: opts.tenantId as any }, { tenantId: null } ] },\n ],\n })\n const fieldsetFilter = normalizeFieldsetFilter(opts.fieldset)\n const order = new Map<string, number>()\n entityIdList.map(String).forEach((id, index) => order.set(id, index))\n const byKey: Record<string, { kind: string; multi?: boolean; entityId: string }> = {}\n for (const d of defs) {\n if (fieldsetFilter) {\n const fieldsets = Array.isArray(d.configJson?.fieldsets)\n ? d.configJson.fieldsets\n .filter((entry: unknown): entry is string => typeof entry === 'string')\n .map((entry: string) => entry.trim())\n .filter((entry: string) => entry.length > 0)\n : []\n const rawFieldset = typeof d.configJson?.fieldset === 'string' ? d.configJson.fieldset.trim() : ''\n const normalizedFieldset = rawFieldset.length ? rawFieldset : null\n const matches = fieldsets.length > 0\n ? fieldsets.some((entry: string) => fieldsetFilter.has(entry))\n : fieldsetFilter.has(normalizedFieldset)\n if (!matches) continue\n }\n const key = d.key\n const entityId = String(d.entityId)\n const current = byKey[key]\n const rankNew = order.get(entityId) ?? Number.MAX_SAFE_INTEGER\n if (!current) {\n byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }\n continue\n }\n const rankOld = order.get(current.entityId) ?? Number.MAX_SAFE_INTEGER\n if (rankNew < rankOld) {\n byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }\n }\n }\n\n const coerce = (kind: string, v: unknown) => {\n if (v == null) return v as undefined\n switch (kind) {\n case 'integer': return Number.parseInt(String(v), 10)\n case 'float': return Number.parseFloat(String(v))\n case 'boolean': return parseBooleanToken(String(v)) === true\n case 'date':\n case 'datetime': return String(v)\n default: return String(v)\n }\n }\n\n for (const [rawKey, rawVal] of entries) {\n const isIn = rawKey.endsWith('In')\n const key = isIn ? rawKey.replace(/^cf_/, '').replace(/In$/, '') : rawKey.replace(/^cf_/, '')\n const def = byKey[key]\n const fieldId = `cf:${key}`\n if (!def) continue\n if (isIn) {\n const list = Array.isArray(rawVal)\n ? (rawVal as unknown[])\n : String(rawVal)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (list.length) out[fieldId] = { $in: list.map((x) => coerce(def.kind, x)) as (string[] | number[] | boolean[]) }\n } else {\n out[fieldId] = coerce(def.kind, rawVal)\n }\n }\n\n return out\n}\n\nexport function splitCustomFieldPayload(raw: unknown): SplitCustomFieldPayload {\n const base: Record<string, unknown> = {}\n const custom: Record<string, unknown> = {}\n if (!raw || typeof raw !== 'object') return { base, custom }\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (key === 'customFields') {\n if (Array.isArray(value)) {\n value.forEach((entry) => {\n if (!entry || typeof entry !== 'object') return\n const entryKey = typeof (entry as any).key === 'string' ? (entry as any).key.trim() : ''\n if (!entryKey) return\n custom[entryKey] = (entry as any).value\n })\n continue\n }\n if (value && typeof value === 'object') {\n for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {\n const normalizedKey = typeof ck === 'string' ? ck.trim() : ''\n if (!normalizedKey) continue\n custom[normalizedKey] = cv\n }\n continue\n }\n }\n if (key === 'customValues' && value && typeof value === 'object' && !Array.isArray(value)) {\n for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {\n custom[String(ck)] = cv\n }\n continue\n }\n if (key.startsWith('cf_')) {\n custom[key.slice(3)] = value\n continue\n }\n if (key.startsWith('cf:')) {\n custom[key.slice(3)] = value\n continue\n }\n base[key] = value\n }\n return { base, custom }\n}\n\nexport function extractCustomFieldValuesFromPayload(raw: Record<string, unknown>): Record<string, unknown> {\n return splitCustomFieldPayload(raw).custom\n}\n\nfunction normalizeDefinitionKey(key: unknown): string {\n if (typeof key !== 'string') return ''\n const trimmed = key.trim()\n return trimmed.length ? trimmed.toLowerCase() : ''\n}\n\nfunction normalizeDefinitionConfig(raw: unknown): Record<string, any> {\n if (!raw) return {}\n if (typeof raw === 'string') {\n try {\n const parsed = JSON.parse(raw)\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return { ...(parsed as Record<string, any>) }\n }\n return {}\n } catch {\n return {}\n }\n }\n if (typeof raw === 'object' && !Array.isArray(raw)) {\n return { ...(raw as Record<string, any>) }\n }\n return {}\n}\n\nfunction summarizeDefinition(def: CustomFieldDef): CustomFieldDefinitionSummary | null {\n const normalizedKey = normalizeDefinitionKey(def.key)\n if (!normalizedKey) return null\n const cfg = normalizeDefinitionConfig((def as any).configJson)\n const label =\n typeof cfg.label === 'string' && cfg.label.trim().length\n ? cfg.label.trim()\n : def.key\n const dictionaryId =\n typeof cfg.dictionaryId === 'string' && cfg.dictionaryId.trim().length\n ? cfg.dictionaryId.trim()\n : null\n const multi =\n cfg.multi !== undefined ? Boolean(cfg.multi) : false\n const priority =\n typeof cfg.priority === 'number' ? cfg.priority : 0\n const updatedAt =\n def.updatedAt instanceof Date\n ? def.updatedAt.getTime()\n : new Date(def.updatedAt as any).getTime()\n return {\n key: def.key,\n label,\n kind: typeof def.kind === 'string' ? def.kind : null,\n multi,\n dictionaryId,\n organizationId: def.organizationId ?? null,\n tenantId: def.tenantId ?? null,\n priority,\n updatedAt: Number.isNaN(updatedAt) ? 0 : updatedAt,\n }\n}\n\nfunction sortDefinitionSummaries(defs: CustomFieldDefinitionSummary[]): CustomFieldDefinitionSummary[] {\n return [...defs].sort((a, b) => {\n const priorityDiff = (a.priority ?? 0) - (b.priority ?? 0)\n if (priorityDiff !== 0) return priorityDiff\n const updatedDiff = (b.updatedAt ?? 0) - (a.updatedAt ?? 0)\n if (updatedDiff !== 0) return updatedDiff\n return a.key.localeCompare(b.key)\n })\n}\n\nfunction selectDefinitionForRecord(\n defs: CustomFieldDefinitionSummary[],\n organizationId: string | null,\n tenantId: string | null,\n): CustomFieldDefinitionSummary | null {\n if (!defs.length) return null\n const prioritizedForOrg = defs.filter(\n (def) => def.organizationId && organizationId && def.organizationId === organizationId,\n )\n if (prioritizedForOrg.length) return sortDefinitionSummaries(prioritizedForOrg)[0]\n const prioritizedForTenant = defs.filter(\n (def) => def.tenantId && tenantId && def.tenantId === tenantId && !def.organizationId,\n )\n if (prioritizedForTenant.length) return sortDefinitionSummaries(prioritizedForTenant)[0]\n const global = defs.filter((def) => !def.organizationId)\n if (global.length) return sortDefinitionSummaries(global)[0]\n return sortDefinitionSummaries(defs)[0] ?? null\n}\n\nexport async function loadCustomFieldDefinitionIndex(opts: {\n em: EntityManager\n entityIds: string | string[]\n tenantId?: string | null | undefined\n organizationIds?: Array<string | null | undefined> | null\n fieldset?: string | string[] | null\n}): Promise<CustomFieldDefinitionIndex> {\n const list = Array.isArray(opts.entityIds) ? opts.entityIds : [opts.entityIds]\n const entityIds = list\n .map((id) => (typeof id === 'string' ? id.trim() : String(id ?? '')))\n .filter((id) => id.length > 0)\n if (!entityIds.length) return new Map()\n const tenantId = opts.tenantId ?? null\n const orgCandidates = Array.isArray(opts.organizationIds)\n ? opts.organizationIds\n .map((id) => (typeof id === 'string' ? id.trim() : id))\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n : []\n const scopeClauses: Record<string, unknown>[] = [\n tenantId\n ? { $or: [{ tenantId: tenantId as any }, { tenantId: null }] }\n : { tenantId: null },\n ]\n if (orgCandidates.length) {\n scopeClauses.push({\n $or: [{ organizationId: { $in: orgCandidates as any } }, { organizationId: null }],\n })\n } else {\n scopeClauses.push({ organizationId: null })\n }\n const where: Record<string, unknown> = {\n entityId: { $in: entityIds as any },\n deletedAt: null,\n isActive: true,\n $and: scopeClauses,\n }\n const defs = await opts.em.find(CustomFieldDef, where as any)\n const fieldsetFilter = normalizeFieldsetFilter(opts.fieldset)\n const index: CustomFieldDefinitionIndex = new Map()\n defs.forEach((def) => {\n if (fieldsetFilter) {\n const config = normalizeDefinitionConfig((def as any).configJson)\n const fieldsets = Array.isArray(config.fieldsets)\n ? config.fieldsets\n .filter((entry: unknown): entry is string => typeof entry === 'string')\n .map((entry: string) => entry.trim())\n .filter((entry: string) => entry.length > 0)\n : []\n const fieldset = typeof config.fieldset === 'string' && config.fieldset.trim().length > 0\n ? config.fieldset.trim()\n : null\n const matches = fieldsets.length > 0\n ? fieldsets.some((entry: string) => fieldsetFilter.has(entry))\n : fieldsetFilter.has(fieldset)\n if (!matches) return\n }\n const summary = summarizeDefinition(def)\n if (!summary) return\n const normalizedKey = normalizeDefinitionKey(summary.key)\n if (!normalizedKey) return\n if (!index.has(normalizedKey)) index.set(normalizedKey, [])\n index.get(normalizedKey)!.push(summary)\n })\n index.forEach((entries, key) => {\n index.set(key, sortDefinitionSummaries(entries))\n })\n return index\n}\n\nexport function decorateRecordWithCustomFields(\n record: Record<string, unknown>,\n definitions: CustomFieldDefinitionIndex,\n context: {\n organizationId?: string | null\n tenantId?: string | null\n } = {},\n): CustomFieldDisplayPayload {\n const rawEntries = extractAllCustomFieldEntries(record)\n if (!Object.keys(rawEntries).length) {\n return { customValues: null, customFields: [] }\n }\n const values: Record<string, unknown> = {}\n const entries: Array<{ entry: CustomFieldDisplayEntry; priority: number; updatedAt: number }> = []\n const organizationId = context.organizationId ?? null\n const tenantId = context.tenantId ?? null\n\n Object.entries(rawEntries).forEach(([prefixedKey, value]) => {\n const bareKey = prefixedKey.replace(/^cf_/, '')\n const normalizedKey = normalizeDefinitionKey(bareKey)\n if (!normalizedKey) return\n values[bareKey] = value\n const defsForKey = definitions.get(normalizedKey) ?? []\n const resolvedDef = selectDefinitionForRecord(defsForKey, organizationId, tenantId)\n const entry: CustomFieldDisplayEntry = {\n key: bareKey,\n label: resolvedDef?.label ?? bareKey,\n value,\n kind: resolvedDef?.kind ?? null,\n multi: resolvedDef?.multi ?? Array.isArray(value),\n }\n entries.push({\n entry,\n priority: resolvedDef?.priority ?? Number.MAX_SAFE_INTEGER,\n updatedAt: resolvedDef?.updatedAt ?? 0,\n })\n })\n\n const ordered = entries\n .sort((a, b) => {\n const priorityDiff = a.priority - b.priority\n if (priorityDiff !== 0) return priorityDiff\n const updatedDiff = b.updatedAt - a.updatedAt\n if (updatedDiff !== 0) return updatedDiff\n return a.entry.key.localeCompare(b.entry.key)\n })\n .map((item) => item.entry)\n\n return {\n customValues: Object.keys(values).length ? values : null,\n customFields: ordered,\n }\n}\n\nexport async function loadCustomFieldValues(opts: {\n em: EntityManager\n entityId: EntityId\n recordIds: string[]\n tenantIdByRecord?: Record<string, string | null | undefined>\n organizationIdByRecord?: Record<string, string | null | undefined>\n tenantFallbacks?: (string | null | undefined)[]\n encryptionService?: TenantDataEncryptionService | null\n}): Promise<Record<string, Record<string, unknown>>> {\n const { em, entityId, recordIds } = opts\n if (!Array.isArray(recordIds) || recordIds.length === 0) return {}\n\n const normalizedRecordIds = recordIds.map((id) => String(id))\n let encryptionService: TenantDataEncryptionService | null | undefined\n const encryptionCache = new Map<string | null, string | null>()\n const getEncryptionService = () => {\n if (encryptionService !== undefined) return encryptionService\n encryptionService = resolveTenantEncryptionService(em, opts.encryptionService)\n return encryptionService\n }\n const tenantCandidates = new Set<string | null>()\n tenantCandidates.add(null)\n if (opts.tenantIdByRecord) {\n for (const val of Object.values(opts.tenantIdByRecord)) {\n tenantCandidates.add(val ? String(val) : null)\n }\n }\n if (opts.tenantFallbacks) {\n for (const val of opts.tenantFallbacks) tenantCandidates.add(val ? String(val) : null)\n }\n const fallbackTenant = (opts.tenantFallbacks || []).find((t) => t != null) ?? null\n\n const tenantList = Array.from(tenantCandidates)\n const tenantNonNull = tenantList.filter((t): t is string => t !== null)\n const tenantFilter = tenantNonNull.length\n ? { tenantId: { $in: [...tenantNonNull, null] as any } }\n : { tenantId: null }\n const cfRows = await em.find(CustomFieldValue, {\n entityId: entityId as any,\n recordId: { $in: normalizedRecordIds as any },\n deletedAt: null,\n ...(tenantList.length ? tenantFilter : {}),\n })\n\n if (!cfRows.length) return {}\n\n const allKeys = Array.from(new Set(cfRows.map((row) => String(row.fieldKey))))\n const organizationCandidates = new Set<string | null>()\n organizationCandidates.add(null)\n if (opts.organizationIdByRecord) {\n for (const val of Object.values(opts.organizationIdByRecord)) {\n organizationCandidates.add(val ? String(val) : null)\n }\n }\n for (const row of cfRows) {\n organizationCandidates.add(row.organizationId ? String(row.organizationId) : null)\n }\n const orgList = Array.from(organizationCandidates)\n\n const defs = allKeys.length\n ? await em.find(CustomFieldDef, {\n entityId: entityId as any,\n key: { $in: allKeys as any },\n deletedAt: null,\n isActive: true,\n ...(tenantList.length ? { tenantId: tenantFilter.tenantId } : {}),\n organizationId: { $in: orgList as any },\n })\n : []\n\n const defsByKey = new Map<string, CustomFieldDef[]>()\n for (const def of defs) {\n const list = defsByKey.get(def.key) || []\n list.push(def)\n defsByKey.set(def.key, list)\n }\n\n const pickDefinition = (fieldKey: string, organizationId: string | null, tenantId: string | null) => {\n const candidates = defsByKey.get(fieldKey)\n if (!candidates || candidates.length === 0) return null\n const active = candidates.filter((opt) => opt.isActive !== false && !opt.deletedAt)\n const list = active.length ? active : candidates\n if (organizationId && tenantId) {\n const exact = list.find((opt) => opt.organizationId === organizationId && opt.tenantId === tenantId)\n if (exact) return exact\n }\n if (organizationId) {\n const orgMatch = list.find((opt) => opt.organizationId === organizationId && (!tenantId || opt.tenantId == null || opt.tenantId === tenantId))\n if (orgMatch) return orgMatch\n }\n if (tenantId) {\n const tenantMatch = list.find((opt) => opt.organizationId == null && opt.tenantId === tenantId)\n if (tenantMatch) return tenantMatch\n }\n const global = list.find((opt) => opt.organizationId == null && opt.tenantId == null)\n return global ?? list[0]\n }\n\n const valueFromRow = (row: CustomFieldValue): unknown => {\n if (row.valueMultiline !== null && row.valueMultiline !== undefined) return row.valueMultiline\n if (row.valueText !== null && row.valueText !== undefined) return row.valueText\n if (row.valueInt !== null && row.valueInt !== undefined) return row.valueInt\n if (row.valueFloat !== null && row.valueFloat !== undefined) return row.valueFloat\n if (row.valueBool !== null && row.valueBool !== undefined) return row.valueBool\n return null\n }\n\n type Bucket = { orgId: string | null; tenantId: string | null; values: unknown[]; def?: CustomFieldDef | null; encrypted?: boolean }\n const buckets = new Map<string, Bucket>()\n\n for (const row of cfRows) {\n const recordId = String(row.recordId)\n const key = String(row.fieldKey)\n const bucketKey = `${recordId}::${key}`\n const orgId = row.organizationId ? String(row.organizationId) : null\n const tenantId = row.tenantId ? String(row.tenantId) : null\n const resolvedOrgId = orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null)\n const resolvedTenantId = tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? fallbackTenant)\n const def = pickDefinition(key, resolvedOrgId, resolvedTenantId)\n const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)\n const value = valueFromRow(row)\n const decrypted = encrypted\n ? await decryptCustomFieldValue(value, resolvedTenantId ?? tenantId ?? null, getEncryptionService(), encryptionCache)\n : value\n const existing = buckets.get(bucketKey)\n if (existing) {\n if (existing.orgId == null && resolvedOrgId) existing.orgId = resolvedOrgId\n if (existing.tenantId == null && resolvedTenantId) existing.tenantId = resolvedTenantId\n if (existing.def == null && def) existing.def = def\n existing.encrypted = existing.encrypted || encrypted\n existing.values.push(decrypted)\n } else {\n buckets.set(bucketKey, { orgId: resolvedOrgId, tenantId: resolvedTenantId, values: [decrypted], def: def ?? null, encrypted })\n }\n }\n\n const result: Record<string, Record<string, unknown>> = {}\n for (const [compoundKey, bucket] of buckets.entries()) {\n const [recordId, fieldKey] = compoundKey.split('::')\n if (!result[recordId]) result[recordId] = {}\n const prefixed = `cf_${fieldKey}`\n const def = bucket.def ?? pickDefinition(fieldKey, bucket.orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null), bucket.tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? null))\n if (def && def.configJson && typeof def.configJson === 'object' && (def.configJson as any).multi) {\n const cleaned = bucket.values.filter((v) => v !== undefined && v !== null)\n result[recordId][prefixed] = cleaned\n } else if (bucket.values.length > 1) {\n const cleaned = bucket.values.filter((v) => v !== undefined)\n result[recordId][prefixed] = cleaned\n } else {\n result[recordId][prefixed] = bucket.values[0] ?? null\n }\n }\n\n return result\n}\n\nexport function summarizeCustomFields(record: Record<string, unknown>): CustomFieldSnapshot {\n const entries = extractAllCustomFieldEntries(record)\n const values = Object.fromEntries(\n Object.entries(entries).map(([prefixedKey, value]) => [\n prefixedKey.replace(/^cf_/, ''),\n value,\n ]),\n )\n const customValues = Object.keys(values).length ? values : null\n const customFields = Object.entries(values).map(([key, value]) => ({\n key,\n label: key,\n value,\n kind: null,\n multi: Array.isArray(value),\n }))\n return { entries, customValues, customFields }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { CustomFieldSet, EntityId } from '@open-mercato/shared/modules/entities'\nimport type { EntityManager } from '@mikro-orm/core'\nimport { CustomFieldDef, CustomFieldValue } from '@open-mercato/core/modules/entities/data/entities'\nimport type { WhereValue } from '@open-mercato/shared/lib/query/types'\nimport type { TenantDataEncryptionService } from '../encryption/tenantDataEncryptionService'\nimport { decryptCustomFieldValue, resolveTenantEncryptionService } from '../encryption/customFieldValues'\nimport { parseBooleanToken } from '../boolean'\nimport { extractCustomFieldEntries } from './custom-fields-client'\n\nexport type CustomFieldSelectors = {\n keys: string[]\n selectors: string[] // e.g. ['cf:priority', 'cf:severity']\n outputKeys: string[] // e.g. ['cf_priority', 'cf_severity']\n}\n\nexport type SplitCustomFieldPayload = {\n base: Record<string, unknown>\n custom: Record<string, unknown>\n}\n\nexport type CustomFieldDefinitionSummary = {\n key: string\n label: string | null\n kind: string | null\n multi: boolean\n dictionaryId?: string | null\n organizationId?: string | null\n tenantId?: string | null\n priority: number\n updatedAt: number\n}\n\nexport type CustomFieldDefinitionIndex = Map<string, CustomFieldDefinitionSummary[]>\n\nexport type CustomFieldDisplayEntry = {\n key: string\n label: string | null\n value: unknown\n kind: string | null\n multi: boolean\n}\n\nexport type CustomFieldDisplayPayload = {\n customValues: Record<string, unknown> | null\n customFields: CustomFieldDisplayEntry[]\n}\n\nexport type CustomFieldSnapshot = {\n entries: Record<string, unknown>\n customValues: Record<string, unknown> | null\n customFields: CustomFieldDisplayEntry[]\n}\n\nexport function buildCustomFieldSelectorsForEntity(entityId: EntityId, sets: CustomFieldSet[]): CustomFieldSelectors {\n const keys = Array.from(new Set(\n (sets || [])\n .filter((s) => s.entity === entityId)\n .flatMap((s) => (s.fields || []).map((f) => f.key))\n ))\n const selectors = keys.map((k) => `cf:${k}`)\n const outputKeys = keys.map((k) => `cf_${k}`)\n return { keys, selectors, outputKeys }\n}\n\nexport function normalizeCustomFieldValue(val: unknown): unknown {\n if (Array.isArray(val)) return val\n if (typeof val === 'string') {\n const s = val.trim()\n // Parse Postgres array-like '{a,b,c}' to string[] when present\n if (s.startsWith('{') && s.endsWith('}')) {\n const inner = s.slice(1, -1).trim()\n if (!inner) return []\n return inner.split(/[\\s,]+/).map((x) => x.trim()).filter(Boolean)\n }\n return s\n }\n return val as any\n}\n\n// Extracts cf_* fields from a record that may contain both 'cf:<key>' and/or 'cf_<key>'\nexport function extractCustomFieldsFromItem(item: Record<string, unknown>, keys: string[]): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const key of keys) {\n const colon = item[`cf:${key}` as keyof typeof item]\n const snake = item[`cf_${key}` as keyof typeof item]\n const value = colon !== undefined ? colon : snake\n if (value !== undefined) out[`cf_${key}`] = normalizeCustomFieldValue(value)\n }\n return out\n}\n\nexport function extractAllCustomFieldEntries(item: Record<string, unknown>): Record<string, unknown> {\n return extractCustomFieldEntries(item)\n}\n\nfunction normalizeFieldsetFilter(input?: string | string[] | null): Set<string | null> | null {\n if (input == null) return null\n const values = Array.isArray(input) ? input : [input]\n const normalized = new Set<string | null>()\n for (const raw of values) {\n if (raw == null) continue\n const trimmed = String(raw).trim()\n if (!trimmed) {\n normalized.add(null)\n } else {\n normalized.add(trimmed)\n }\n }\n return normalized.size ? normalized : null\n}\n\nexport async function buildCustomFieldFiltersFromQuery(opts: {\n entityId?: EntityId\n entityIds?: EntityId[]\n query: Record<string, unknown>\n em: EntityManager\n tenantId: string | null | undefined\n fieldset?: string | string[] | null\n}): Promise<Record<string, WhereValue>> {\n const out: Record<string, WhereValue> = {}\n const entries = Object.entries(opts.query).filter(([k]) => k.startsWith('cf_'))\n if (!entries.length) return out\n\n const entityIdList = Array.isArray(opts.entityIds) && opts.entityIds.length\n ? opts.entityIds\n : opts.entityId\n ? [opts.entityId]\n : []\n if (!entityIdList.length) return out\n\n // Tenant-only scope: allow global (null) or tenant match; ignore organization here\n const defs = await opts.em.find(CustomFieldDef, {\n entityId: { $in: entityIdList as any },\n isActive: true,\n $and: [\n { $or: [ { tenantId: opts.tenantId as any }, { tenantId: null } ] },\n ],\n })\n const fieldsetFilter = normalizeFieldsetFilter(opts.fieldset)\n const order = new Map<string, number>()\n entityIdList.map(String).forEach((id, index) => order.set(id, index))\n const byKey: Record<string, { kind: string; multi?: boolean; entityId: string }> = {}\n for (const d of defs) {\n if (fieldsetFilter) {\n const fieldsets = Array.isArray(d.configJson?.fieldsets)\n ? d.configJson.fieldsets\n .filter((entry: unknown): entry is string => typeof entry === 'string')\n .map((entry: string) => entry.trim())\n .filter((entry: string) => entry.length > 0)\n : []\n const rawFieldset = typeof d.configJson?.fieldset === 'string' ? d.configJson.fieldset.trim() : ''\n const normalizedFieldset = rawFieldset.length ? rawFieldset : null\n const matches = fieldsets.length > 0\n ? fieldsets.some((entry: string) => fieldsetFilter.has(entry))\n : fieldsetFilter.has(normalizedFieldset)\n if (!matches) continue\n }\n const key = d.key\n const entityId = String(d.entityId)\n const current = byKey[key]\n const rankNew = order.get(entityId) ?? Number.MAX_SAFE_INTEGER\n if (!current) {\n byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }\n continue\n }\n const rankOld = order.get(current.entityId) ?? Number.MAX_SAFE_INTEGER\n if (rankNew < rankOld) {\n byKey[key] = { kind: d.kind, multi: Boolean((d as any).configJson?.multi), entityId }\n }\n }\n\n const coerce = (kind: string, v: unknown) => {\n if (v == null) return v as undefined\n switch (kind) {\n case 'integer': return Number.parseInt(String(v), 10)\n case 'float': return Number.parseFloat(String(v))\n case 'boolean': return parseBooleanToken(String(v)) === true\n case 'date':\n case 'datetime': return String(v)\n default: return String(v)\n }\n }\n\n for (const [rawKey, rawVal] of entries) {\n const isIn = rawKey.endsWith('In')\n const key = isIn ? rawKey.replace(/^cf_/, '').replace(/In$/, '') : rawKey.replace(/^cf_/, '')\n const def = byKey[key]\n const fieldId = `cf:${key}`\n if (!def) continue\n if (isIn) {\n const list = Array.isArray(rawVal)\n ? (rawVal as unknown[])\n : String(rawVal)\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (list.length) out[fieldId] = { $in: list.map((x) => coerce(def.kind, x)) as (string[] | number[] | boolean[]) }\n } else {\n out[fieldId] = coerce(def.kind, rawVal)\n }\n }\n\n return out\n}\n\nexport function splitCustomFieldPayload(raw: unknown): SplitCustomFieldPayload {\n const base: Record<string, unknown> = {}\n const custom: Record<string, unknown> = {}\n if (!raw || typeof raw !== 'object') return { base, custom }\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (key === 'customFields') {\n if (Array.isArray(value)) {\n value.forEach((entry) => {\n if (!entry || typeof entry !== 'object') return\n const entryKey = typeof (entry as any).key === 'string' ? (entry as any).key.trim() : ''\n if (!entryKey) return\n custom[entryKey] = (entry as any).value\n })\n continue\n }\n if (value && typeof value === 'object') {\n for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {\n const normalizedKey = typeof ck === 'string' ? ck.trim() : ''\n if (!normalizedKey) continue\n custom[normalizedKey] = cv\n }\n continue\n }\n }\n if (key === 'customValues' && value && typeof value === 'object' && !Array.isArray(value)) {\n for (const [ck, cv] of Object.entries(value as Record<string, unknown>)) {\n custom[String(ck)] = cv\n }\n continue\n }\n if (key.startsWith('cf_')) {\n custom[key.slice(3)] = value\n continue\n }\n if (key.startsWith('cf:')) {\n custom[key.slice(3)] = value\n continue\n }\n base[key] = value\n }\n return { base, custom }\n}\n\nexport function extractCustomFieldValuesFromPayload(raw: Record<string, unknown>): Record<string, unknown> {\n return splitCustomFieldPayload(raw).custom\n}\n\nfunction normalizeDefinitionKey(key: unknown): string {\n if (typeof key !== 'string') return ''\n const trimmed = key.trim()\n return trimmed.length ? trimmed.toLowerCase() : ''\n}\n\nfunction normalizeDefinitionConfig(raw: unknown): Record<string, any> {\n if (!raw) return {}\n if (typeof raw === 'string') {\n try {\n const parsed = JSON.parse(raw)\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return { ...(parsed as Record<string, any>) }\n }\n return {}\n } catch {\n return {}\n }\n }\n if (typeof raw === 'object' && !Array.isArray(raw)) {\n return { ...(raw as Record<string, any>) }\n }\n return {}\n}\n\nfunction summarizeDefinition(def: CustomFieldDef): CustomFieldDefinitionSummary | null {\n const normalizedKey = normalizeDefinitionKey(def.key)\n if (!normalizedKey) return null\n const cfg = normalizeDefinitionConfig((def as any).configJson)\n const label =\n typeof cfg.label === 'string' && cfg.label.trim().length\n ? cfg.label.trim()\n : def.key\n const dictionaryId =\n typeof cfg.dictionaryId === 'string' && cfg.dictionaryId.trim().length\n ? cfg.dictionaryId.trim()\n : null\n const multi =\n cfg.multi !== undefined ? Boolean(cfg.multi) : false\n const priority =\n typeof cfg.priority === 'number' ? cfg.priority : 0\n const updatedAt =\n def.updatedAt instanceof Date\n ? def.updatedAt.getTime()\n : new Date(def.updatedAt as any).getTime()\n return {\n key: def.key,\n label,\n kind: typeof def.kind === 'string' ? def.kind : null,\n multi,\n dictionaryId,\n organizationId: def.organizationId ?? null,\n tenantId: def.tenantId ?? null,\n priority,\n updatedAt: Number.isNaN(updatedAt) ? 0 : updatedAt,\n }\n}\n\nfunction sortDefinitionSummaries(defs: CustomFieldDefinitionSummary[]): CustomFieldDefinitionSummary[] {\n return [...defs].sort((a, b) => {\n const priorityDiff = (a.priority ?? 0) - (b.priority ?? 0)\n if (priorityDiff !== 0) return priorityDiff\n const updatedDiff = (b.updatedAt ?? 0) - (a.updatedAt ?? 0)\n if (updatedDiff !== 0) return updatedDiff\n return a.key.localeCompare(b.key)\n })\n}\n\nfunction selectDefinitionForRecord(\n defs: CustomFieldDefinitionSummary[],\n organizationId: string | null,\n tenantId: string | null,\n): CustomFieldDefinitionSummary | null {\n if (!defs.length) return null\n const prioritizedForOrg = defs.filter(\n (def) => def.organizationId && organizationId && def.organizationId === organizationId,\n )\n if (prioritizedForOrg.length) return sortDefinitionSummaries(prioritizedForOrg)[0]\n const prioritizedForTenant = defs.filter(\n (def) => def.tenantId && tenantId && def.tenantId === tenantId && !def.organizationId,\n )\n if (prioritizedForTenant.length) return sortDefinitionSummaries(prioritizedForTenant)[0]\n const global = defs.filter((def) => !def.organizationId)\n if (global.length) return sortDefinitionSummaries(global)[0]\n return sortDefinitionSummaries(defs)[0] ?? null\n}\n\nexport async function loadCustomFieldDefinitionIndex(opts: {\n em: EntityManager\n entityIds: string | string[]\n tenantId?: string | null | undefined\n organizationIds?: Array<string | null | undefined> | null\n fieldset?: string | string[] | null\n}): Promise<CustomFieldDefinitionIndex> {\n const list = Array.isArray(opts.entityIds) ? opts.entityIds : [opts.entityIds]\n const entityIds = list\n .map((id) => (typeof id === 'string' ? id.trim() : String(id ?? '')))\n .filter((id) => id.length > 0)\n if (!entityIds.length) return new Map()\n const tenantId = opts.tenantId ?? null\n const orgCandidates = Array.isArray(opts.organizationIds)\n ? opts.organizationIds\n .map((id) => (typeof id === 'string' ? id.trim() : id))\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n : []\n const scopeClauses: Record<string, unknown>[] = [\n tenantId\n ? { $or: [{ tenantId: tenantId as any }, { tenantId: null }] }\n : { tenantId: null },\n ]\n if (orgCandidates.length) {\n scopeClauses.push({\n $or: [{ organizationId: { $in: orgCandidates as any } }, { organizationId: null }],\n })\n } else {\n scopeClauses.push({ organizationId: null })\n }\n const where: Record<string, unknown> = {\n entityId: { $in: entityIds as any },\n deletedAt: null,\n isActive: true,\n $and: scopeClauses,\n }\n const defs = await opts.em.find(CustomFieldDef, where as any)\n const fieldsetFilter = normalizeFieldsetFilter(opts.fieldset)\n const index: CustomFieldDefinitionIndex = new Map()\n defs.forEach((def) => {\n if (fieldsetFilter) {\n const config = normalizeDefinitionConfig((def as any).configJson)\n const fieldsets = Array.isArray(config.fieldsets)\n ? config.fieldsets\n .filter((entry: unknown): entry is string => typeof entry === 'string')\n .map((entry: string) => entry.trim())\n .filter((entry: string) => entry.length > 0)\n : []\n const fieldset = typeof config.fieldset === 'string' && config.fieldset.trim().length > 0\n ? config.fieldset.trim()\n : null\n const matches = fieldsets.length > 0\n ? fieldsets.some((entry: string) => fieldsetFilter.has(entry))\n : fieldsetFilter.has(fieldset)\n if (!matches) return\n }\n const summary = summarizeDefinition(def)\n if (!summary) return\n const normalizedKey = normalizeDefinitionKey(summary.key)\n if (!normalizedKey) return\n if (!index.has(normalizedKey)) index.set(normalizedKey, [])\n index.get(normalizedKey)!.push(summary)\n })\n index.forEach((entries, key) => {\n index.set(key, sortDefinitionSummaries(entries))\n })\n return index\n}\n\nexport function decorateRecordWithCustomFields(\n record: Record<string, unknown>,\n definitions: CustomFieldDefinitionIndex,\n context: {\n organizationId?: string | null\n tenantId?: string | null\n } = {},\n): CustomFieldDisplayPayload {\n const rawEntries = extractAllCustomFieldEntries(record)\n if (!Object.keys(rawEntries).length) {\n return { customValues: null, customFields: [] }\n }\n const values: Record<string, unknown> = {}\n const entries: Array<{ entry: CustomFieldDisplayEntry; priority: number; updatedAt: number }> = []\n const organizationId = context.organizationId ?? null\n const tenantId = context.tenantId ?? null\n\n Object.entries(rawEntries).forEach(([prefixedKey, value]) => {\n const bareKey = prefixedKey.replace(/^cf_/, '')\n const normalizedKey = normalizeDefinitionKey(bareKey)\n if (!normalizedKey) return\n const defsForKey = definitions.get(normalizedKey) ?? []\n const resolvedDef = selectDefinitionForRecord(defsForKey, organizationId, tenantId)\n // Skip custom field values without active definitions to prevent orphaned fields\n if (!resolvedDef) return\n values[bareKey] = value\n const entry: CustomFieldDisplayEntry = {\n key: bareKey,\n label: resolvedDef.label ?? bareKey,\n value,\n kind: resolvedDef.kind ?? null,\n multi: resolvedDef.multi ?? Array.isArray(value),\n }\n entries.push({\n entry,\n priority: resolvedDef.priority ?? Number.MAX_SAFE_INTEGER,\n updatedAt: resolvedDef.updatedAt ?? 0,\n })\n })\n\n const ordered = entries\n .sort((a, b) => {\n const priorityDiff = a.priority - b.priority\n if (priorityDiff !== 0) return priorityDiff\n const updatedDiff = b.updatedAt - a.updatedAt\n if (updatedDiff !== 0) return updatedDiff\n return a.entry.key.localeCompare(b.entry.key)\n })\n .map((item) => item.entry)\n\n return {\n customValues: Object.keys(values).length ? values : null,\n customFields: ordered,\n }\n}\n\nexport async function loadCustomFieldValues(opts: {\n em: EntityManager\n entityId: EntityId\n recordIds: string[]\n tenantIdByRecord?: Record<string, string | null | undefined>\n organizationIdByRecord?: Record<string, string | null | undefined>\n tenantFallbacks?: (string | null | undefined)[]\n encryptionService?: TenantDataEncryptionService | null\n}): Promise<Record<string, Record<string, unknown>>> {\n const { em, entityId, recordIds } = opts\n if (!Array.isArray(recordIds) || recordIds.length === 0) return {}\n\n const normalizedRecordIds = recordIds.map((id) => String(id))\n let encryptionService: TenantDataEncryptionService | null | undefined\n const encryptionCache = new Map<string | null, string | null>()\n const getEncryptionService = () => {\n if (encryptionService !== undefined) return encryptionService\n encryptionService = resolveTenantEncryptionService(em, opts.encryptionService)\n return encryptionService\n }\n const tenantCandidates = new Set<string | null>()\n tenantCandidates.add(null)\n if (opts.tenantIdByRecord) {\n for (const val of Object.values(opts.tenantIdByRecord)) {\n tenantCandidates.add(val ? String(val) : null)\n }\n }\n if (opts.tenantFallbacks) {\n for (const val of opts.tenantFallbacks) tenantCandidates.add(val ? String(val) : null)\n }\n const fallbackTenant = (opts.tenantFallbacks || []).find((t) => t != null) ?? null\n\n const tenantList = Array.from(tenantCandidates)\n const tenantNonNull = tenantList.filter((t): t is string => t !== null)\n const tenantFilter = tenantNonNull.length\n ? { tenantId: { $in: [...tenantNonNull, null] as any } }\n : { tenantId: null }\n const cfRows = await em.find(CustomFieldValue, {\n entityId: entityId as any,\n recordId: { $in: normalizedRecordIds as any },\n deletedAt: null,\n ...(tenantList.length ? tenantFilter : {}),\n })\n\n if (!cfRows.length) return {}\n\n const allKeys = Array.from(new Set(cfRows.map((row) => String(row.fieldKey))))\n const organizationCandidates = new Set<string | null>()\n organizationCandidates.add(null)\n if (opts.organizationIdByRecord) {\n for (const val of Object.values(opts.organizationIdByRecord)) {\n organizationCandidates.add(val ? String(val) : null)\n }\n }\n for (const row of cfRows) {\n organizationCandidates.add(row.organizationId ? String(row.organizationId) : null)\n }\n const orgList = Array.from(organizationCandidates)\n\n const defs = allKeys.length\n ? await em.find(CustomFieldDef, {\n entityId: entityId as any,\n key: { $in: allKeys as any },\n deletedAt: null,\n isActive: true,\n ...(tenantList.length ? { tenantId: tenantFilter.tenantId } : {}),\n organizationId: { $in: orgList as any },\n })\n : []\n\n const defsByKey = new Map<string, CustomFieldDef[]>()\n for (const def of defs) {\n const list = defsByKey.get(def.key) || []\n list.push(def)\n defsByKey.set(def.key, list)\n }\n\n const pickDefinition = (fieldKey: string, organizationId: string | null, tenantId: string | null) => {\n const candidates = defsByKey.get(fieldKey)\n if (!candidates || candidates.length === 0) return null\n const active = candidates.filter((opt) => opt.isActive !== false && !opt.deletedAt)\n const list = active.length ? active : candidates\n if (organizationId && tenantId) {\n const exact = list.find((opt) => opt.organizationId === organizationId && opt.tenantId === tenantId)\n if (exact) return exact\n }\n if (organizationId) {\n const orgMatch = list.find((opt) => opt.organizationId === organizationId && (!tenantId || opt.tenantId == null || opt.tenantId === tenantId))\n if (orgMatch) return orgMatch\n }\n if (tenantId) {\n const tenantMatch = list.find((opt) => opt.organizationId == null && opt.tenantId === tenantId)\n if (tenantMatch) return tenantMatch\n }\n const global = list.find((opt) => opt.organizationId == null && opt.tenantId == null)\n return global ?? list[0]\n }\n\n const valueFromRow = (row: CustomFieldValue): unknown => {\n if (row.valueMultiline !== null && row.valueMultiline !== undefined) return row.valueMultiline\n if (row.valueText !== null && row.valueText !== undefined) return row.valueText\n if (row.valueInt !== null && row.valueInt !== undefined) return row.valueInt\n if (row.valueFloat !== null && row.valueFloat !== undefined) return row.valueFloat\n if (row.valueBool !== null && row.valueBool !== undefined) return row.valueBool\n return null\n }\n\n type Bucket = { orgId: string | null; tenantId: string | null; values: unknown[]; def?: CustomFieldDef | null; encrypted?: boolean }\n const buckets = new Map<string, Bucket>()\n\n for (const row of cfRows) {\n const recordId = String(row.recordId)\n const key = String(row.fieldKey)\n const bucketKey = `${recordId}::${key}`\n const orgId = row.organizationId ? String(row.organizationId) : null\n const tenantId = row.tenantId ? String(row.tenantId) : null\n const resolvedOrgId = orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null)\n const resolvedTenantId = tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? fallbackTenant)\n const def = pickDefinition(key, resolvedOrgId, resolvedTenantId)\n const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)\n const value = valueFromRow(row)\n const decrypted = encrypted\n ? await decryptCustomFieldValue(\n value,\n resolvedTenantId ?? tenantId ?? null,\n getEncryptionService(),\n encryptionCache,\n { kind: def?.kind ?? null },\n )\n : value\n const existing = buckets.get(bucketKey)\n if (existing) {\n if (existing.orgId == null && resolvedOrgId) existing.orgId = resolvedOrgId\n if (existing.tenantId == null && resolvedTenantId) existing.tenantId = resolvedTenantId\n if (existing.def == null && def) existing.def = def\n existing.encrypted = existing.encrypted || encrypted\n existing.values.push(decrypted)\n } else {\n buckets.set(bucketKey, { orgId: resolvedOrgId, tenantId: resolvedTenantId, values: [decrypted], def: def ?? null, encrypted })\n }\n }\n\n const result: Record<string, Record<string, unknown>> = {}\n for (const [compoundKey, bucket] of buckets.entries()) {\n const [recordId, fieldKey] = compoundKey.split('::')\n if (!result[recordId]) result[recordId] = {}\n const prefixed = `cf_${fieldKey}`\n const def = bucket.def ?? pickDefinition(fieldKey, bucket.orgId ?? (opts.organizationIdByRecord?.[recordId] ?? null), bucket.tenantId ?? (opts.tenantIdByRecord?.[recordId] ?? null))\n if (def && def.configJson && typeof def.configJson === 'object' && (def.configJson as any).multi) {\n const cleaned = bucket.values.filter((v) => v !== undefined && v !== null)\n result[recordId][prefixed] = cleaned\n } else if (bucket.values.length > 1) {\n const cleaned = bucket.values.filter((v) => v !== undefined)\n result[recordId][prefixed] = cleaned\n } else {\n result[recordId][prefixed] = bucket.values[0] ?? null\n }\n }\n\n return result\n}\n\nexport function summarizeCustomFields(record: Record<string, unknown>): CustomFieldSnapshot {\n const entries = extractAllCustomFieldEntries(record)\n const values = Object.fromEntries(\n Object.entries(entries).map(([prefixedKey, value]) => [\n prefixedKey.replace(/^cf_/, ''),\n value,\n ]),\n )\n const customValues = Object.keys(values).length ? values : null\n const customFields = Object.entries(values).map(([key, value]) => ({\n key,\n label: key,\n value,\n kind: null,\n multi: Array.isArray(value),\n }))\n return { entries, customValues, customFields }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,19 @@
1
1
  import { encryptWithAesGcm, decryptWithAesGcm } from "./aes.js";
2
2
  import { TenantDataEncryptionService } from "./tenantDataEncryptionService.js";
3
+ const STRING_TYPED_CUSTOM_FIELD_KINDS = /* @__PURE__ */ new Set([
4
+ "text",
5
+ "multiline",
6
+ "select",
7
+ "currency",
8
+ "dictionary",
9
+ "email",
10
+ "url",
11
+ "string"
12
+ ]);
13
+ function shouldPreserveAsString(kind) {
14
+ if (!kind) return false;
15
+ return STRING_TYPED_CUSTOM_FIELD_KINDS.has(kind);
16
+ }
3
17
  const serviceCache = /* @__PURE__ */ new WeakMap();
4
18
  function resolveTenantEncryptionService(em, provided) {
5
19
  if (provided) return provided;
@@ -29,12 +43,13 @@ async function encryptCustomFieldValue(value, tenantId, service, cache) {
29
43
  const serialized = typeof value === "string" ? value : JSON.stringify(value);
30
44
  return encryptWithAesGcm(serialized, key).value;
31
45
  }
32
- async function decryptCustomFieldValue(value, tenantId, service, cache) {
46
+ async function decryptCustomFieldValue(value, tenantId, service, cache, options) {
33
47
  if (value === void 0 || value === null || typeof value !== "string") return value;
34
48
  const key = await resolveDekKey(service, tenantId, cache);
35
49
  if (!key) return value;
36
50
  const decrypted = decryptWithAesGcm(value, key);
37
51
  if (decrypted === null) return value;
52
+ if (shouldPreserveAsString(options?.kind ?? null)) return decrypted;
38
53
  try {
39
54
  return JSON.parse(decrypted);
40
55
  } catch {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/encryption/customFieldValues.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { encryptWithAesGcm, decryptWithAesGcm } from './aes'\nimport { TenantDataEncryptionService } from './tenantDataEncryptionService'\n\nconst serviceCache = new WeakMap<EntityManager, TenantDataEncryptionService>()\n\nexport function resolveTenantEncryptionService(\n em: EntityManager,\n provided?: TenantDataEncryptionService | null,\n): TenantDataEncryptionService | null {\n if (provided) return provided\n const cached = serviceCache.get(em)\n if (cached) return cached\n const service = new TenantDataEncryptionService(em as any)\n serviceCache.set(em, service)\n return service\n}\n\nasync function resolveDekKey(\n service: TenantDataEncryptionService | null,\n tenantId: string | null | undefined,\n cache?: Map<string | null, string | null>,\n opts?: { createIfMissing?: boolean },\n): Promise<string | null> {\n const scopedTenantId = tenantId ?? null\n if (!service || !service.isEnabled() || !scopedTenantId) return null\n if (cache?.has(scopedTenantId)) return cache.get(scopedTenantId) ?? null\n const dek = await service.getDek(scopedTenantId)\n let key = dek?.key ?? null\n if (!key && opts?.createIfMissing && typeof service.createDek === 'function') {\n const created = await service.createDek(scopedTenantId)\n key = created?.key ?? null\n }\n cache?.set(scopedTenantId, key)\n return key\n}\n\nexport async function encryptCustomFieldValue(\n value: unknown,\n tenantId: string | null | undefined,\n service: TenantDataEncryptionService | null,\n cache?: Map<string | null, string | null>,\n): Promise<unknown> {\n if (value === undefined || value === null) return value\n const key = await resolveDekKey(service, tenantId, cache, { createIfMissing: true })\n if (!key) return value\n const serialized = typeof value === 'string' ? value : JSON.stringify(value)\n return encryptWithAesGcm(serialized, key).value\n}\n\nexport async function decryptCustomFieldValue(\n value: unknown,\n tenantId: string | null | undefined,\n service: TenantDataEncryptionService | null,\n cache?: Map<string | null, string | null>,\n): Promise<unknown> {\n if (value === undefined || value === null || typeof value !== 'string') return value\n const key = await resolveDekKey(service, tenantId, cache)\n if (!key) return value\n const decrypted = decryptWithAesGcm(value, key)\n if (decrypted === null) return value\n try {\n return JSON.parse(decrypted)\n } catch {\n return decrypted\n }\n}\n"],
5
- "mappings": "AACA,SAAS,mBAAmB,yBAAyB;AACrD,SAAS,mCAAmC;AAE5C,MAAM,eAAe,oBAAI,QAAoD;AAEtE,SAAS,+BACd,IACA,UACoC;AACpC,MAAI,SAAU,QAAO;AACrB,QAAM,SAAS,aAAa,IAAI,EAAE;AAClC,MAAI,OAAQ,QAAO;AACnB,QAAM,UAAU,IAAI,4BAA4B,EAAS;AACzD,eAAa,IAAI,IAAI,OAAO;AAC5B,SAAO;AACT;AAEA,eAAe,cACb,SACA,UACA,OACA,MACwB;AACxB,QAAM,iBAAiB,YAAY;AACnC,MAAI,CAAC,WAAW,CAAC,QAAQ,UAAU,KAAK,CAAC,eAAgB,QAAO;AAChE,MAAI,OAAO,IAAI,cAAc,EAAG,QAAO,MAAM,IAAI,cAAc,KAAK;AACpE,QAAM,MAAM,MAAM,QAAQ,OAAO,cAAc;AAC/C,MAAI,MAAM,KAAK,OAAO;AACtB,MAAI,CAAC,OAAO,MAAM,mBAAmB,OAAO,QAAQ,cAAc,YAAY;AAC5E,UAAM,UAAU,MAAM,QAAQ,UAAU,cAAc;AACtD,UAAM,SAAS,OAAO;AAAA,EACxB;AACA,SAAO,IAAI,gBAAgB,GAAG;AAC9B,SAAO;AACT;AAEA,eAAsB,wBACpB,OACA,UACA,SACA,OACkB;AAClB,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,QAAM,MAAM,MAAM,cAAc,SAAS,UAAU,OAAO,EAAE,iBAAiB,KAAK,CAAC;AACnF,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,aAAa,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;AAC3E,SAAO,kBAAkB,YAAY,GAAG,EAAE;AAC5C;AAEA,eAAsB,wBACpB,OACA,UACA,SACA,OACkB;AAClB,MAAI,UAAU,UAAa,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AAC/E,QAAM,MAAM,MAAM,cAAc,SAAS,UAAU,KAAK;AACxD,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,YAAY,kBAAkB,OAAO,GAAG;AAC9C,MAAI,cAAc,KAAM,QAAO;AAC/B,MAAI;AACF,WAAO,KAAK,MAAM,SAAS;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { encryptWithAesGcm, decryptWithAesGcm } from './aes'\nimport { TenantDataEncryptionService } from './tenantDataEncryptionService'\n\n/**\n * Custom field kinds that ALWAYS round-trip as a string. The encrypt path\n * stores raw strings unwrapped, so blindly running `JSON.parse` on the\n * decrypted payload coerces text values like `\"123\"` or `\"true\"` back into\n * numbers/booleans (issue #1734). For these kinds, callers MUST pass the\n * `kind` option so we keep the decrypted value as a string.\n *\n * Numeric (`integer`/`float`) and `boolean` kinds rely on JSON round-trip\n * because the encrypt path JSON-stringifies the typed value before storage.\n * Omitting the kind preserves legacy round-trip behavior for backward\n * compatibility.\n */\nconst STRING_TYPED_CUSTOM_FIELD_KINDS = new Set([\n 'text',\n 'multiline',\n 'select',\n 'currency',\n 'dictionary',\n 'email',\n 'url',\n 'string',\n])\n\nexport type DecryptCustomFieldOptions = {\n /** Field kind, e.g. from `CustomFieldDef.kind`. When string-typed, the helper preserves the decrypted string verbatim. */\n kind?: string | null\n}\n\nfunction shouldPreserveAsString(kind: string | null | undefined): boolean {\n if (!kind) return false\n return STRING_TYPED_CUSTOM_FIELD_KINDS.has(kind)\n}\n\nconst serviceCache = new WeakMap<EntityManager, TenantDataEncryptionService>()\n\nexport function resolveTenantEncryptionService(\n em: EntityManager,\n provided?: TenantDataEncryptionService | null,\n): TenantDataEncryptionService | null {\n if (provided) return provided\n const cached = serviceCache.get(em)\n if (cached) return cached\n const service = new TenantDataEncryptionService(em as any)\n serviceCache.set(em, service)\n return service\n}\n\nasync function resolveDekKey(\n service: TenantDataEncryptionService | null,\n tenantId: string | null | undefined,\n cache?: Map<string | null, string | null>,\n opts?: { createIfMissing?: boolean },\n): Promise<string | null> {\n const scopedTenantId = tenantId ?? null\n if (!service || !service.isEnabled() || !scopedTenantId) return null\n if (cache?.has(scopedTenantId)) return cache.get(scopedTenantId) ?? null\n const dek = await service.getDek(scopedTenantId)\n let key = dek?.key ?? null\n if (!key && opts?.createIfMissing && typeof service.createDek === 'function') {\n const created = await service.createDek(scopedTenantId)\n key = created?.key ?? null\n }\n cache?.set(scopedTenantId, key)\n return key\n}\n\nexport async function encryptCustomFieldValue(\n value: unknown,\n tenantId: string | null | undefined,\n service: TenantDataEncryptionService | null,\n cache?: Map<string | null, string | null>,\n): Promise<unknown> {\n if (value === undefined || value === null) return value\n const key = await resolveDekKey(service, tenantId, cache, { createIfMissing: true })\n if (!key) return value\n const serialized = typeof value === 'string' ? value : JSON.stringify(value)\n return encryptWithAesGcm(serialized, key).value\n}\n\nexport async function decryptCustomFieldValue(\n value: unknown,\n tenantId: string | null | undefined,\n service: TenantDataEncryptionService | null,\n cache?: Map<string | null, string | null>,\n options?: DecryptCustomFieldOptions,\n): Promise<unknown> {\n if (value === undefined || value === null || typeof value !== 'string') return value\n const key = await resolveDekKey(service, tenantId, cache)\n if (!key) return value\n const decrypted = decryptWithAesGcm(value, key)\n if (decrypted === null) return value\n if (shouldPreserveAsString(options?.kind ?? null)) return decrypted\n try {\n return JSON.parse(decrypted)\n } catch {\n return decrypted\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,mBAAmB,yBAAyB;AACrD,SAAS,mCAAmC;AAc5C,MAAM,kCAAkC,oBAAI,IAAI;AAAA,EAC9C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAOD,SAAS,uBAAuB,MAA0C;AACxE,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,gCAAgC,IAAI,IAAI;AACjD;AAEA,MAAM,eAAe,oBAAI,QAAoD;AAEtE,SAAS,+BACd,IACA,UACoC;AACpC,MAAI,SAAU,QAAO;AACrB,QAAM,SAAS,aAAa,IAAI,EAAE;AAClC,MAAI,OAAQ,QAAO;AACnB,QAAM,UAAU,IAAI,4BAA4B,EAAS;AACzD,eAAa,IAAI,IAAI,OAAO;AAC5B,SAAO;AACT;AAEA,eAAe,cACb,SACA,UACA,OACA,MACwB;AACxB,QAAM,iBAAiB,YAAY;AACnC,MAAI,CAAC,WAAW,CAAC,QAAQ,UAAU,KAAK,CAAC,eAAgB,QAAO;AAChE,MAAI,OAAO,IAAI,cAAc,EAAG,QAAO,MAAM,IAAI,cAAc,KAAK;AACpE,QAAM,MAAM,MAAM,QAAQ,OAAO,cAAc;AAC/C,MAAI,MAAM,KAAK,OAAO;AACtB,MAAI,CAAC,OAAO,MAAM,mBAAmB,OAAO,QAAQ,cAAc,YAAY;AAC5E,UAAM,UAAU,MAAM,QAAQ,UAAU,cAAc;AACtD,UAAM,SAAS,OAAO;AAAA,EACxB;AACA,SAAO,IAAI,gBAAgB,GAAG;AAC9B,SAAO;AACT;AAEA,eAAsB,wBACpB,OACA,UACA,SACA,OACkB;AAClB,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,QAAM,MAAM,MAAM,cAAc,SAAS,UAAU,OAAO,EAAE,iBAAiB,KAAK,CAAC;AACnF,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,aAAa,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;AAC3E,SAAO,kBAAkB,YAAY,GAAG,EAAE;AAC5C;AAEA,eAAsB,wBACpB,OACA,UACA,SACA,OACA,SACkB;AAClB,MAAI,UAAU,UAAa,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AAC/E,QAAM,MAAM,MAAM,cAAc,SAAS,UAAU,KAAK;AACxD,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,YAAY,kBAAkB,OAAO,GAAG;AAC9C,MAAI,cAAc,KAAM,QAAO;AAC/B,MAAI,uBAAuB,SAAS,QAAQ,IAAI,EAAG,QAAO;AAC1D,MAAI;AACF,WAAO,KAAK,MAAM,SAAS;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }
@@ -26,6 +26,16 @@ function findKey(obj, key) {
26
26
  }
27
27
  return null;
28
28
  }
29
+ function parseDecryptedFieldValue(decrypted) {
30
+ if (decrypted.length === 0) return decrypted;
31
+ const first = decrypted[0];
32
+ if (first !== "{" && first !== "[") return decrypted;
33
+ try {
34
+ return JSON.parse(decrypted);
35
+ } catch {
36
+ return decrypted;
37
+ }
38
+ }
29
39
  function isEncryptedPayload(value) {
30
40
  if (typeof value !== "string") return false;
31
41
  const parts = value.split(":");
@@ -201,11 +211,7 @@ class TenantDataEncryptionService {
201
211
  if (typeof value !== "string") continue;
202
212
  const decrypted = maybeDecrypt(value);
203
213
  if (decrypted === null) continue;
204
- try {
205
- clone[key] = JSON.parse(decrypted);
206
- } catch {
207
- clone[key] = decrypted;
208
- }
214
+ clone[key] = parseDecryptedFieldValue(decrypted);
209
215
  }
210
216
  return clone;
211
217
  }
@@ -247,6 +253,7 @@ class TenantDataEncryptionService {
247
253
  }
248
254
  }
249
255
  export {
250
- TenantDataEncryptionService
256
+ TenantDataEncryptionService,
257
+ parseDecryptedFieldValue
251
258
  };
252
259
  //# sourceMappingURL=tenantDataEncryptionService.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/encryption/tenantDataEncryptionService.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { decryptWithAesGcm, encryptWithAesGcm, hashForLookup } from './aes'\nimport { createKmsService, type KmsService, type TenantDek } from './kms'\nimport { isTenantDataEncryptionEnabled, isEncryptionDebugEnabled } from './toggles'\nimport { EncryptionMap } from '@open-mercato/core/modules/entities/data/entities'\n\nexport type EncryptedFieldRule = {\n field: string\n hashField?: string | null\n}\n\nexport type EncryptionMapRecord = {\n entityId: string\n fields: EncryptedFieldRule[]\n}\n\ntype MapCacheKey = {\n entityId: string\n tenantId: string | null\n organizationId: string | null\n}\n\nconst MAP_MISS_TTL_MS = 5 * 60 * 1000\n\nfunction cacheKey(key: MapCacheKey): string {\n return [\n 'encmap',\n key.entityId.toLowerCase(),\n key.tenantId ?? 'null',\n key.organizationId ?? 'null',\n ].join(':')\n}\n\nfunction debug(event: string, payload: Record<string, unknown>) {\n if (!isEncryptionDebugEnabled()) return\n try {\n // eslint-disable-next-line no-console\n console.debug(`${event} [tenant-encryption]`, payload)\n } catch {\n // ignore\n }\n}\n\nconst toSnakeCase = (value: string): string =>\n value.replace(/([A-Z])/g, '_$1').replace(/__/g, '_').toLowerCase()\n\nconst toCamelCase = (value: string): string =>\n value.replace(/_([a-z])/g, (_, c) => c.toUpperCase())\n\nfunction findKey(obj: Record<string, unknown>, key: string): string | null {\n const candidates = [key, toSnakeCase(key), toCamelCase(key)]\n for (const candidate of candidates) {\n if (Object.prototype.hasOwnProperty.call(obj, candidate)) return candidate\n }\n return null\n}\n\nfunction isEncryptedPayload(value: unknown): boolean {\n if (typeof value !== 'string') return false\n const parts = value.split(':')\n return parts.length === 4 && parts[3] === 'v1'\n}\n\nexport class TenantDataEncryptionService {\n private static globalMemoryCache = new Map<string, EncryptionMapRecord>()\n private static globalInflightMaps = new Map<string, Promise<EncryptionMapRecord | null>>()\n private static globalDekCache = new Map<string, TenantDek>()\n private static globalMissCache = new Map<string, number>()\n private readonly kms: KmsService\n private readonly cache?: CacheStrategy\n private readonly memoryCache = TenantDataEncryptionService.globalMemoryCache\n private readonly dekCache = TenantDataEncryptionService.globalDekCache\n private readonly inflightMaps = TenantDataEncryptionService.globalInflightMaps\n private readonly missCache = TenantDataEncryptionService.globalMissCache\n\n constructor(\n private em: EntityManager,\n opts?: { cache?: CacheStrategy; kms?: KmsService }\n ) {\n this.cache = opts?.cache\n this.kms = opts?.kms ?? createKmsService()\n }\n\n isEnabled(): boolean {\n return isTenantDataEncryptionEnabled() && this.kms.isHealthy()\n }\n\n async getDek(tenantId: string | null | undefined): Promise<TenantDek | null> {\n if (!tenantId) return null\n const cached = this.dekCache.get(tenantId)\n if (cached) return cached\n const dek = await this.kms.getTenantDek(tenantId)\n if (!dek) {\n debug('\uD83D\uDD0E dek.miss', { tenantId })\n } else {\n debug('\u2705 dek.hit', { tenantId })\n }\n if (dek) this.dekCache.set(tenantId, dek)\n return dek\n }\n\n private async resolveDekForEncrypt(tenantId: string | null): Promise<TenantDek | null> {\n const existing = await this.getDek(tenantId)\n if (existing || !tenantId) return existing ?? null\n if (typeof this.kms.createTenantDek !== 'function') return existing ?? null\n const created = await this.kms.createTenantDek(tenantId)\n if (created) this.dekCache.set(tenantId, created)\n return created ?? null\n }\n\n async createDek(tenantId: string): Promise<TenantDek | null> {\n const dek = await this.kms.createTenantDek(tenantId)\n if (dek) this.dekCache.set(tenantId, dek)\n return dek\n }\n\n private async fetchMap(key: MapCacheKey): Promise<EncryptionMapRecord | null> {\n // Bypass ORM lifecycle hooks to avoid recursive decrypt loops by querying directly.\n const conn: any = (this.em as any)?.getConnection?.()\n if (!conn || typeof conn.execute !== 'function') return null\n const sql = `\n select entity_id, fields_json\n from encryption_maps\n where entity_id = ?\n and tenant_id is not distinct from ?\n and organization_id is not distinct from ?\n and is_active = true\n and deleted_at is null\n limit 1\n `\n const rows = await conn.execute(sql, [key.entityId, key.tenantId ?? null, key.organizationId ?? null])\n const row = Array.isArray(rows) && rows.length ? rows[0] : null\n if (!row) return null\n return {\n entityId: row.entity_id || row.entityId || key.entityId,\n fields: Array.isArray(row.fields_json)\n ? (row.fields_json as EncryptedFieldRule[])\n : Array.isArray(row.fieldsJson)\n ? (row.fieldsJson as EncryptedFieldRule[])\n : [],\n }\n }\n\n private async getMap(key: MapCacheKey): Promise<EncryptionMapRecord | null> {\n const shouldSkipLookup = (tag: string) => {\n const expiresAt = this.missCache.get(tag)\n if (!expiresAt) return false\n if (expiresAt > Date.now()) return true\n this.missCache.delete(tag)\n return false\n }\n const recordMiss = (tag: string) => {\n this.missCache.set(tag, Date.now() + MAP_MISS_TTL_MS)\n }\n\n const candidates: MapCacheKey[] = [\n key,\n { entityId: key.entityId, tenantId: key.tenantId ?? null, organizationId: null },\n { entityId: key.entityId, tenantId: null, organizationId: null },\n ]\n for (const candidate of candidates) {\n const tag = cacheKey(candidate)\n if (shouldSkipLookup(tag)) continue\n if (this.inflightMaps.has(tag)) {\n const pending = this.inflightMaps.get(tag)!\n const resolved = await pending\n if (resolved) return resolved\n }\n const mem = this.memoryCache.get(tag)\n if (mem) return mem\n if (this.cache && typeof this.cache.get === 'function') {\n const cached = await this.cache.get(tag)\n if (cached) return cached as EncryptionMapRecord\n }\n const pending = this.fetchMap(candidate)\n this.inflightMaps.set(tag, pending)\n const loaded = await pending\n this.inflightMaps.delete(tag)\n if (!loaded) {\n recordMiss(tag)\n debug('\uD83D\uDD0D encmap.miss', {\n entityId: candidate.entityId,\n tenantId: candidate.tenantId,\n organizationId: candidate.organizationId,\n })\n continue\n }\n this.missCache.delete(tag)\n this.memoryCache.set(tag, loaded)\n if (this.cache && typeof this.cache.set === 'function') {\n await this.cache.set(tag, loaded, { ttl: 300 })\n }\n return loaded\n }\n return null\n }\n\n async invalidateMap(entityId: string, tenantId: string | null, organizationId: string | null): Promise<void> {\n const tag = cacheKey({ entityId, tenantId, organizationId })\n this.memoryCache.delete(tag)\n this.inflightMaps.delete(tag)\n this.missCache.delete(tag)\n if (this.cache && typeof (this.cache as any).delete === 'function') {\n await (this.cache as any).delete(tag)\n }\n }\n\n private encryptFields(\n obj: Record<string, unknown>,\n fields: EncryptedFieldRule[],\n dek: TenantDek\n ): Record<string, unknown> {\n const clone: Record<string, unknown> = { ...obj }\n for (const rule of fields) {\n const key = findKey(clone, rule.field)\n if (!key) continue\n const value = clone[key]\n if (value === null || value === undefined) continue\n // Avoid double-encrypting already encrypted payloads\n if (isEncryptedPayload(value)) continue\n const serialized = typeof value === 'string' ? value : JSON.stringify(value)\n const payload = encryptWithAesGcm(serialized, dek.key)\n clone[key] = payload.value\n if (rule.hashField) {\n const hashKey = findKey(clone, rule.hashField) ?? rule.hashField\n clone[hashKey] = hashForLookup(serialized)\n }\n }\n return clone\n }\n\n private decryptFields(\n obj: Record<string, unknown>,\n fields: EncryptedFieldRule[],\n dek: TenantDek\n ): Record<string, unknown> {\n const clone: Record<string, unknown> = { ...obj }\n const maybeDecrypt = (payload: string): string | null => {\n const first = decryptWithAesGcm(payload, dek.key)\n if (first === null) return null\n // Handle accidental double-encryption: if the first pass still looks like a v1 payload, try once more.\n const parts = first.split(':')\n if (parts.length === 4 && parts[3] === 'v1') {\n const second = decryptWithAesGcm(first, dek.key)\n return second ?? first\n }\n return first\n }\n for (const rule of fields) {\n const key = findKey(clone, rule.field)\n if (!key) continue\n const value = clone[key]\n if (typeof value !== 'string') continue\n const decrypted = maybeDecrypt(value)\n if (decrypted === null) continue\n try {\n clone[key] = JSON.parse(decrypted)\n } catch {\n clone[key] = decrypted\n }\n }\n return clone\n }\n\n async encryptEntityPayload(\n entityId: string,\n payload: Record<string, unknown>,\n tenantId: string | null | undefined,\n organizationId?: string | null\n ): Promise<Record<string, unknown>> {\n if (!this.isEnabled()) {\n debug('\u26AA\uFE0F encrypt.skip.disabled', { entityId, tenantId })\n return payload\n }\n const dek = await this.resolveDekForEncrypt(tenantId ?? null)\n if (!dek) {\n debug('\u26A0\uFE0F encrypt.skip.no-dek', { entityId, tenantId })\n return payload\n }\n const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })\n if (!map || !map.fields?.length) {\n debug('\u26AA\uFE0F encrypt.skip.no-map', { entityId, tenantId })\n return payload\n }\n debug('\uD83D\uDD12 encrypt_entity', { entityId, tenantId, organizationId, fields: map.fields.length })\n return this.encryptFields(payload, map.fields, dek)\n }\n\n async decryptEntityPayload(\n entityId: string,\n payload: Record<string, unknown>,\n tenantId: string | null | undefined,\n organizationId?: string | null\n ): Promise<Record<string, unknown>> {\n if (!isTenantDataEncryptionEnabled()) {\n debug('\u26AA\uFE0F decrypt.skip.disabled', { entityId, tenantId })\n return payload\n }\n const dek = await this.getDek(tenantId ?? null)\n if (!dek) {\n debug('\u26A0\uFE0F decrypt.skip.no-dek', { entityId, tenantId })\n return payload\n }\n const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })\n if (!map || !map.fields?.length) {\n debug('\u26AA\uFE0F decrypt.skip.no-map', { entityId, tenantId })\n return payload\n }\n debug('\uD83D\uDD13 decrypt_entity', { entityId, tenantId, organizationId, fields: map.fields.length })\n return this.decryptFields(payload, map.fields, dek)\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { decryptWithAesGcm, encryptWithAesGcm, hashForLookup } from './aes'\nimport { createKmsService, type KmsService, type TenantDek } from './kms'\nimport { isTenantDataEncryptionEnabled, isEncryptionDebugEnabled } from './toggles'\nimport { EncryptionMap } from '@open-mercato/core/modules/entities/data/entities'\n\nexport type EncryptedFieldRule = {\n field: string\n hashField?: string | null\n}\n\nexport type EncryptionMapRecord = {\n entityId: string\n fields: EncryptedFieldRule[]\n}\n\ntype MapCacheKey = {\n entityId: string\n tenantId: string | null\n organizationId: string | null\n}\n\nconst MAP_MISS_TTL_MS = 5 * 60 * 1000\n\nfunction cacheKey(key: MapCacheKey): string {\n return [\n 'encmap',\n key.entityId.toLowerCase(),\n key.tenantId ?? 'null',\n key.organizationId ?? 'null',\n ].join(':')\n}\n\nfunction debug(event: string, payload: Record<string, unknown>) {\n if (!isEncryptionDebugEnabled()) return\n try {\n // eslint-disable-next-line no-console\n console.debug(`${event} [tenant-encryption]`, payload)\n } catch {\n // ignore\n }\n}\n\nconst toSnakeCase = (value: string): string =>\n value.replace(/([A-Z])/g, '_$1').replace(/__/g, '_').toLowerCase()\n\nconst toCamelCase = (value: string): string =>\n value.replace(/_([a-z])/g, (_, c) => c.toUpperCase())\n\nfunction findKey(obj: Record<string, unknown>, key: string): string | null {\n const candidates = [key, toSnakeCase(key), toCamelCase(key)]\n for (const candidate of candidates) {\n if (Object.prototype.hasOwnProperty.call(obj, candidate)) return candidate\n }\n return null\n}\n\n/**\n * Decode a decrypted entity-field payload back into its original value.\n *\n * The encrypt path stores raw strings unwrapped and JSON-stringifies non-string\n * values. Blindly running `JSON.parse` on every decrypted value would coerce\n * text columns whose contents happen to be valid JSON primitives \u2014 e.g. the\n * string `\"123\"` \u2014 back into numbers/booleans, which then breaks string-typed\n * consumers (see issue #1734). Only restructure the value when the decrypted\n * payload is unambiguously a JSON object or array; otherwise return the raw\n * decrypted string. Numeric/boolean entity columns are not in any current\n * encryption map, so this is backward-compatible.\n */\nexport function parseDecryptedFieldValue(decrypted: string): unknown {\n if (decrypted.length === 0) return decrypted\n const first = decrypted[0]\n if (first !== '{' && first !== '[') return decrypted\n try {\n return JSON.parse(decrypted)\n } catch {\n return decrypted\n }\n}\n\nfunction isEncryptedPayload(value: unknown): boolean {\n if (typeof value !== 'string') return false\n const parts = value.split(':')\n return parts.length === 4 && parts[3] === 'v1'\n}\n\nexport class TenantDataEncryptionService {\n private static globalMemoryCache = new Map<string, EncryptionMapRecord>()\n private static globalInflightMaps = new Map<string, Promise<EncryptionMapRecord | null>>()\n private static globalDekCache = new Map<string, TenantDek>()\n private static globalMissCache = new Map<string, number>()\n private readonly kms: KmsService\n private readonly cache?: CacheStrategy\n private readonly memoryCache = TenantDataEncryptionService.globalMemoryCache\n private readonly dekCache = TenantDataEncryptionService.globalDekCache\n private readonly inflightMaps = TenantDataEncryptionService.globalInflightMaps\n private readonly missCache = TenantDataEncryptionService.globalMissCache\n\n constructor(\n private em: EntityManager,\n opts?: { cache?: CacheStrategy; kms?: KmsService }\n ) {\n this.cache = opts?.cache\n this.kms = opts?.kms ?? createKmsService()\n }\n\n isEnabled(): boolean {\n return isTenantDataEncryptionEnabled() && this.kms.isHealthy()\n }\n\n async getDek(tenantId: string | null | undefined): Promise<TenantDek | null> {\n if (!tenantId) return null\n const cached = this.dekCache.get(tenantId)\n if (cached) return cached\n const dek = await this.kms.getTenantDek(tenantId)\n if (!dek) {\n debug('\uD83D\uDD0E dek.miss', { tenantId })\n } else {\n debug('\u2705 dek.hit', { tenantId })\n }\n if (dek) this.dekCache.set(tenantId, dek)\n return dek\n }\n\n private async resolveDekForEncrypt(tenantId: string | null): Promise<TenantDek | null> {\n const existing = await this.getDek(tenantId)\n if (existing || !tenantId) return existing ?? null\n if (typeof this.kms.createTenantDek !== 'function') return existing ?? null\n const created = await this.kms.createTenantDek(tenantId)\n if (created) this.dekCache.set(tenantId, created)\n return created ?? null\n }\n\n async createDek(tenantId: string): Promise<TenantDek | null> {\n const dek = await this.kms.createTenantDek(tenantId)\n if (dek) this.dekCache.set(tenantId, dek)\n return dek\n }\n\n private async fetchMap(key: MapCacheKey): Promise<EncryptionMapRecord | null> {\n // Bypass ORM lifecycle hooks to avoid recursive decrypt loops by querying directly.\n const conn: any = (this.em as any)?.getConnection?.()\n if (!conn || typeof conn.execute !== 'function') return null\n const sql = `\n select entity_id, fields_json\n from encryption_maps\n where entity_id = ?\n and tenant_id is not distinct from ?\n and organization_id is not distinct from ?\n and is_active = true\n and deleted_at is null\n limit 1\n `\n const rows = await conn.execute(sql, [key.entityId, key.tenantId ?? null, key.organizationId ?? null])\n const row = Array.isArray(rows) && rows.length ? rows[0] : null\n if (!row) return null\n return {\n entityId: row.entity_id || row.entityId || key.entityId,\n fields: Array.isArray(row.fields_json)\n ? (row.fields_json as EncryptedFieldRule[])\n : Array.isArray(row.fieldsJson)\n ? (row.fieldsJson as EncryptedFieldRule[])\n : [],\n }\n }\n\n private async getMap(key: MapCacheKey): Promise<EncryptionMapRecord | null> {\n const shouldSkipLookup = (tag: string) => {\n const expiresAt = this.missCache.get(tag)\n if (!expiresAt) return false\n if (expiresAt > Date.now()) return true\n this.missCache.delete(tag)\n return false\n }\n const recordMiss = (tag: string) => {\n this.missCache.set(tag, Date.now() + MAP_MISS_TTL_MS)\n }\n\n const candidates: MapCacheKey[] = [\n key,\n { entityId: key.entityId, tenantId: key.tenantId ?? null, organizationId: null },\n { entityId: key.entityId, tenantId: null, organizationId: null },\n ]\n for (const candidate of candidates) {\n const tag = cacheKey(candidate)\n if (shouldSkipLookup(tag)) continue\n if (this.inflightMaps.has(tag)) {\n const pending = this.inflightMaps.get(tag)!\n const resolved = await pending\n if (resolved) return resolved\n }\n const mem = this.memoryCache.get(tag)\n if (mem) return mem\n if (this.cache && typeof this.cache.get === 'function') {\n const cached = await this.cache.get(tag)\n if (cached) return cached as EncryptionMapRecord\n }\n const pending = this.fetchMap(candidate)\n this.inflightMaps.set(tag, pending)\n const loaded = await pending\n this.inflightMaps.delete(tag)\n if (!loaded) {\n recordMiss(tag)\n debug('\uD83D\uDD0D encmap.miss', {\n entityId: candidate.entityId,\n tenantId: candidate.tenantId,\n organizationId: candidate.organizationId,\n })\n continue\n }\n this.missCache.delete(tag)\n this.memoryCache.set(tag, loaded)\n if (this.cache && typeof this.cache.set === 'function') {\n await this.cache.set(tag, loaded, { ttl: 300 })\n }\n return loaded\n }\n return null\n }\n\n async invalidateMap(entityId: string, tenantId: string | null, organizationId: string | null): Promise<void> {\n const tag = cacheKey({ entityId, tenantId, organizationId })\n this.memoryCache.delete(tag)\n this.inflightMaps.delete(tag)\n this.missCache.delete(tag)\n if (this.cache && typeof (this.cache as any).delete === 'function') {\n await (this.cache as any).delete(tag)\n }\n }\n\n private encryptFields(\n obj: Record<string, unknown>,\n fields: EncryptedFieldRule[],\n dek: TenantDek\n ): Record<string, unknown> {\n const clone: Record<string, unknown> = { ...obj }\n for (const rule of fields) {\n const key = findKey(clone, rule.field)\n if (!key) continue\n const value = clone[key]\n if (value === null || value === undefined) continue\n // Avoid double-encrypting already encrypted payloads\n if (isEncryptedPayload(value)) continue\n const serialized = typeof value === 'string' ? value : JSON.stringify(value)\n const payload = encryptWithAesGcm(serialized, dek.key)\n clone[key] = payload.value\n if (rule.hashField) {\n const hashKey = findKey(clone, rule.hashField) ?? rule.hashField\n clone[hashKey] = hashForLookup(serialized)\n }\n }\n return clone\n }\n\n private decryptFields(\n obj: Record<string, unknown>,\n fields: EncryptedFieldRule[],\n dek: TenantDek\n ): Record<string, unknown> {\n const clone: Record<string, unknown> = { ...obj }\n const maybeDecrypt = (payload: string): string | null => {\n const first = decryptWithAesGcm(payload, dek.key)\n if (first === null) return null\n // Handle accidental double-encryption: if the first pass still looks like a v1 payload, try once more.\n const parts = first.split(':')\n if (parts.length === 4 && parts[3] === 'v1') {\n const second = decryptWithAesGcm(first, dek.key)\n return second ?? first\n }\n return first\n }\n for (const rule of fields) {\n const key = findKey(clone, rule.field)\n if (!key) continue\n const value = clone[key]\n if (typeof value !== 'string') continue\n const decrypted = maybeDecrypt(value)\n if (decrypted === null) continue\n clone[key] = parseDecryptedFieldValue(decrypted)\n }\n return clone\n }\n\n async encryptEntityPayload(\n entityId: string,\n payload: Record<string, unknown>,\n tenantId: string | null | undefined,\n organizationId?: string | null\n ): Promise<Record<string, unknown>> {\n if (!this.isEnabled()) {\n debug('\u26AA\uFE0F encrypt.skip.disabled', { entityId, tenantId })\n return payload\n }\n const dek = await this.resolveDekForEncrypt(tenantId ?? null)\n if (!dek) {\n debug('\u26A0\uFE0F encrypt.skip.no-dek', { entityId, tenantId })\n return payload\n }\n const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })\n if (!map || !map.fields?.length) {\n debug('\u26AA\uFE0F encrypt.skip.no-map', { entityId, tenantId })\n return payload\n }\n debug('\uD83D\uDD12 encrypt_entity', { entityId, tenantId, organizationId, fields: map.fields.length })\n return this.encryptFields(payload, map.fields, dek)\n }\n\n async decryptEntityPayload(\n entityId: string,\n payload: Record<string, unknown>,\n tenantId: string | null | undefined,\n organizationId?: string | null\n ): Promise<Record<string, unknown>> {\n if (!isTenantDataEncryptionEnabled()) {\n debug('\u26AA\uFE0F decrypt.skip.disabled', { entityId, tenantId })\n return payload\n }\n const dek = await this.getDek(tenantId ?? null)\n if (!dek) {\n debug('\u26A0\uFE0F decrypt.skip.no-dek', { entityId, tenantId })\n return payload\n }\n const map = await this.getMap({ entityId, tenantId: tenantId ?? null, organizationId: organizationId ?? null })\n if (!map || !map.fields?.length) {\n debug('\u26AA\uFE0F decrypt.skip.no-map', { entityId, tenantId })\n return payload\n }\n debug('\uD83D\uDD13 decrypt_entity', { entityId, tenantId, organizationId, fields: map.fields.length })\n return this.decryptFields(payload, map.fields, dek)\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["pending"]
7
7
  }
@@ -1,4 +1,5 @@
1
1
  import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
2
+ const DEFAULT_SEARCH_MIN_TOKEN_LENGTH = 3;
2
3
  const DEFAULT_BLOCKLIST = ["password", "token", "secret", "hash"];
3
4
  function parseBoolean(raw, fallback) {
4
5
  return parseBooleanWithDefault(raw, fallback);
@@ -19,14 +20,19 @@ function parseHashAlgorithm(raw) {
19
20
  function resolveSearchConfig() {
20
21
  return {
21
22
  enabled: parseBoolean(process.env.OM_SEARCH_ENABLED, true),
22
- minTokenLength: parseNumber(process.env.OM_SEARCH_MIN_LEN, 3, 1),
23
+ minTokenLength: resolveSearchMinTokenLength(),
23
24
  enablePartials: parseBoolean(process.env.OM_SEARCH_ENABLE_PARTIAL, true),
24
25
  hashAlgorithm: parseHashAlgorithm(process.env.OM_SEARCH_HASH_ALGO),
25
26
  storeRawTokens: parseBoolean(process.env.OM_SEARCH_STORE_RAW_TOKENS, false),
26
27
  blocklistedFields: (process.env.OM_SEARCH_FIELD_BLOCKLIST ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).filter((value, index, arr) => arr.indexOf(value) === index).map((entry) => entry.toLowerCase()).concat(DEFAULT_BLOCKLIST).filter((value, index, arr) => arr.indexOf(value) === index)
27
28
  };
28
29
  }
30
+ function resolveSearchMinTokenLength() {
31
+ return parseNumber(process.env.OM_SEARCH_MIN_LEN, DEFAULT_SEARCH_MIN_TOKEN_LENGTH, 1);
32
+ }
29
33
  export {
30
- resolveSearchConfig
34
+ DEFAULT_SEARCH_MIN_TOKEN_LENGTH,
35
+ resolveSearchConfig,
36
+ resolveSearchMinTokenLength
31
37
  };
32
38
  //# sourceMappingURL=config.js.map
@@ -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'\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}\n\nconst DEFAULT_BLOCKLIST = ['password', 'token', 'secret', 'hash']\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 if (raw == null) return fallback\n const value = Number.parseInt(raw, 10)\n if (!Number.isFinite(value)) return fallback\n if (value < min) return fallback\n return value\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\nexport function resolveSearchConfig(): SearchConfig {\n return {\n enabled: parseBoolean(process.env.OM_SEARCH_ENABLED, true),\n minTokenLength: parseNumber(process.env.OM_SEARCH_MIN_LEN, 3, 1),\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: (process.env.OM_SEARCH_FIELD_BLOCKLIST ?? '')\n .split(',')\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0)\n .filter((value, index, arr) => arr.indexOf(value) === index)\n .map((entry) => entry.toLowerCase())\n .concat(DEFAULT_BLOCKLIST)\n .filter((value, index, arr) => arr.indexOf(value) === index),\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,+BAA+B;AAWxC,MAAM,oBAAoB,CAAC,YAAY,SAAS,UAAU,MAAM;AAEhE,SAAS,aAAa,KAAyB,UAA4B;AACzE,SAAO,wBAAwB,KAAK,QAAQ;AAC9C;AAEA,SAAS,YAAY,KAAyB,UAAkB,MAAM,GAAW;AAC/E,MAAI,OAAO,KAAM,QAAO;AACxB,QAAM,QAAQ,OAAO,SAAS,KAAK,EAAE;AACrC,MAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,MAAI,QAAQ,IAAK,QAAO;AACxB,SAAO;AACT;AAEA,SAAS,mBAAmB,KAAoD;AAC9E,QAAM,SAAS,OAAO,IAAI,KAAK,EAAE,YAAY;AAC7C,MAAI,UAAU,OAAQ,QAAO;AAC7B,MAAI,UAAU,MAAO,QAAO;AAC5B,SAAO;AACT;AAEO,SAAS,sBAAoC;AAClD,SAAO;AAAA,IACL,SAAS,aAAa,QAAQ,IAAI,mBAAmB,IAAI;AAAA,IACzD,gBAAgB,YAAY,QAAQ,IAAI,mBAAmB,GAAG,CAAC;AAAA,IAC/D,gBAAgB,aAAa,QAAQ,IAAI,0BAA0B,IAAI;AAAA,IACvE,eAAe,mBAAmB,QAAQ,IAAI,mBAAmB;AAAA,IACjE,gBAAgB,aAAa,QAAQ,IAAI,4BAA4B,KAAK;AAAA,IAC1E,oBAAoB,QAAQ,IAAI,6BAA6B,IAC1D,MAAM,GAAG,EACT,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAC3B,OAAO,CAAC,UAAU,MAAM,SAAS,CAAC,EAClC,OAAO,CAAC,OAAO,OAAO,QAAQ,IAAI,QAAQ,KAAK,MAAM,KAAK,EAC1D,IAAI,CAAC,UAAU,MAAM,YAAY,CAAC,EAClC,OAAO,iBAAiB,EACxB,OAAO,CAAC,OAAO,OAAO,QAAQ,IAAI,QAAQ,KAAK,MAAM,KAAK;AAAA,EAC/D;AACF;",
4
+ "sourcesContent": ["import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\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}\n\nexport const DEFAULT_SEARCH_MIN_TOKEN_LENGTH = 3\n\nconst DEFAULT_BLOCKLIST = ['password', 'token', 'secret', 'hash']\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 if (raw == null) return fallback\n const value = Number.parseInt(raw, 10)\n if (!Number.isFinite(value)) return fallback\n if (value < min) return fallback\n return value\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\nexport function resolveSearchConfig(): SearchConfig {\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: (process.env.OM_SEARCH_FIELD_BLOCKLIST ?? '')\n .split(',')\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0)\n .filter((value, index, arr) => arr.indexOf(value) === index)\n .map((entry) => entry.toLowerCase())\n .concat(DEFAULT_BLOCKLIST)\n .filter((value, index, arr) => arr.indexOf(value) === index),\n }\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": "AAAA,SAAS,+BAA+B;AAWjC,MAAM,kCAAkC;AAE/C,MAAM,oBAAoB,CAAC,YAAY,SAAS,UAAU,MAAM;AAEhE,SAAS,aAAa,KAAyB,UAA4B;AACzE,SAAO,wBAAwB,KAAK,QAAQ;AAC9C;AAEA,SAAS,YAAY,KAAyB,UAAkB,MAAM,GAAW;AAC/E,MAAI,OAAO,KAAM,QAAO;AACxB,QAAM,QAAQ,OAAO,SAAS,KAAK,EAAE;AACrC,MAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,MAAI,QAAQ,IAAK,QAAO;AACxB,SAAO;AACT;AAEA,SAAS,mBAAmB,KAAoD;AAC9E,QAAM,SAAS,OAAO,IAAI,KAAK,EAAE,YAAY;AAC7C,MAAI,UAAU,OAAQ,QAAO;AAC7B,MAAI,UAAU,MAAO,QAAO;AAC5B,SAAO;AACT;AAEO,SAAS,sBAAoC;AAClD,SAAO;AAAA,IACL,SAAS,aAAa,QAAQ,IAAI,mBAAmB,IAAI;AAAA,IACzD,gBAAgB,4BAA4B;AAAA,IAC5C,gBAAgB,aAAa,QAAQ,IAAI,0BAA0B,IAAI;AAAA,IACvE,eAAe,mBAAmB,QAAQ,IAAI,mBAAmB;AAAA,IACjE,gBAAgB,aAAa,QAAQ,IAAI,4BAA4B,KAAK;AAAA,IAC1E,oBAAoB,QAAQ,IAAI,6BAA6B,IAC1D,MAAM,GAAG,EACT,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAC3B,OAAO,CAAC,UAAU,MAAM,SAAS,CAAC,EAClC,OAAO,CAAC,OAAO,OAAO,QAAQ,IAAI,QAAQ,KAAK,MAAM,KAAK,EAC1D,IAAI,CAAC,UAAU,MAAM,YAAY,CAAC,EAClC,OAAO,iBAAiB,EACxB,OAAO,CAAC,OAAO,OAAO,QAAQ,IAAI,QAAQ,KAAK,MAAM,KAAK;AAAA,EAC/D;AACF;AAaO,SAAS,8BAAsC;AACpD,SAAO,YAAY,QAAQ,IAAI,mBAAmB,iCAAiC,CAAC;AACtF;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.5.1-develop.2975.ccbadc8198";
1
+ const APP_VERSION = "0.5.1-develop.2996.ce62fd491c";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.5.1-develop.2975.ccbadc8198'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.5.1-develop.2996.ce62fd491c'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
@@ -2,19 +2,21 @@ import { slugify } from "../../lib/slugify.js";
2
2
  const SIDEBAR_PREFERENCES_VERSION = 2;
3
3
  function normalizeSidebarSettings(settings) {
4
4
  if (!settings || typeof settings !== "object") {
5
- return { version: SIDEBAR_PREFERENCES_VERSION, groupOrder: [], groupLabels: {}, itemLabels: {}, hiddenItems: [] };
5
+ return { version: SIDEBAR_PREFERENCES_VERSION, groupOrder: [], groupLabels: {}, itemLabels: {}, hiddenItems: [], itemOrder: {} };
6
6
  }
7
7
  const version = typeof settings.version === "number" ? settings.version : SIDEBAR_PREFERENCES_VERSION;
8
8
  const groupOrder = Array.isArray(settings.groupOrder) ? settings.groupOrder.filter((v) => typeof v === "string") : [];
9
9
  const groupLabels = normalizeRecord(settings.groupLabels);
10
10
  const itemLabels = normalizeRecord(settings.itemLabels);
11
11
  const hiddenItems = normalizeStringArray(settings.hiddenItems);
12
+ const itemOrder = normalizeStringArrayRecord(settings.itemOrder);
12
13
  return {
13
14
  version,
14
15
  groupOrder,
15
16
  groupLabels,
16
17
  itemLabels,
17
- hiddenItems
18
+ hiddenItems,
19
+ itemOrder
18
20
  };
19
21
  }
20
22
  function normalizeRecord(record) {
@@ -26,6 +28,15 @@ function normalizeRecord(record) {
26
28
  }
27
29
  return out;
28
30
  }
31
+ function normalizeStringArrayRecord(record) {
32
+ if (!record || typeof record !== "object") return {};
33
+ const out = {};
34
+ for (const [key, value] of Object.entries(record)) {
35
+ const arr = normalizeStringArray(value);
36
+ if (arr.length > 0) out[key] = arr;
37
+ }
38
+ return out;
39
+ }
29
40
  function normalizeStringArray(values) {
30
41
  if (!Array.isArray(values)) return [];
31
42
  const seen = /* @__PURE__ */ new Set();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/navigation/sidebarPreferences.ts"],
4
- "sourcesContent": ["import { slugify } from '../../lib/slugify'\n\nexport const SIDEBAR_PREFERENCES_VERSION = 2\n\nexport type SidebarPreferencesSettings = {\n version: number\n groupOrder?: string[]\n groupLabels?: Record<string, string>\n itemLabels?: Record<string, string>\n hiddenItems?: string[]\n}\n\nexport type SidebarPreferencesPayload = {\n locale: string\n settings: SidebarPreferencesSettings\n}\n\nexport function normalizeSidebarSettings(settings?: SidebarPreferencesSettings | null): SidebarPreferencesSettings {\n if (!settings || typeof settings !== 'object') {\n return { version: SIDEBAR_PREFERENCES_VERSION, groupOrder: [], groupLabels: {}, itemLabels: {}, hiddenItems: [] }\n }\n const version = typeof settings.version === 'number' ? settings.version : SIDEBAR_PREFERENCES_VERSION\n const groupOrder = Array.isArray(settings.groupOrder) ? settings.groupOrder.filter((v): v is string => typeof v === 'string') : []\n const groupLabels = normalizeRecord(settings.groupLabels)\n const itemLabels = normalizeRecord(settings.itemLabels)\n const hiddenItems = normalizeStringArray(settings.hiddenItems)\n return {\n version,\n groupOrder,\n groupLabels,\n itemLabels,\n hiddenItems,\n }\n}\n\nfunction normalizeRecord(record: Record<string, unknown> | undefined): Record<string, string> {\n if (!record || typeof record !== 'object') return {}\n const out: Record<string, string> = {}\n for (const [key, value] of Object.entries(record)) {\n if (typeof value !== 'string') continue\n out[key] = value\n }\n return out\n}\n\nfunction normalizeStringArray(values: unknown): string[] {\n if (!Array.isArray(values)) return []\n const seen = new Set<string>()\n const out: string[] = []\n for (const value of values) {\n if (typeof value !== 'string') continue\n const trimmed = value.trim()\n if (!trimmed || seen.has(trimmed)) continue\n seen.add(trimmed)\n out.push(trimmed)\n }\n return out\n}\n\nexport function slugifySidebarId(source: string): string {\n return slugify(source, { allowedChars: '' }) || 'group'\n}\n"],
5
- "mappings": "AAAA,SAAS,eAAe;AAEjB,MAAM,8BAA8B;AAepC,SAAS,yBAAyB,UAA0E;AACjH,MAAI,CAAC,YAAY,OAAO,aAAa,UAAU;AAC7C,WAAO,EAAE,SAAS,6BAA6B,YAAY,CAAC,GAAG,aAAa,CAAC,GAAG,YAAY,CAAC,GAAG,aAAa,CAAC,EAAE;AAAA,EAClH;AACA,QAAM,UAAU,OAAO,SAAS,YAAY,WAAW,SAAS,UAAU;AAC1E,QAAM,aAAa,MAAM,QAAQ,SAAS,UAAU,IAAI,SAAS,WAAW,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ,IAAI,CAAC;AACjI,QAAM,cAAc,gBAAgB,SAAS,WAAW;AACxD,QAAM,aAAa,gBAAgB,SAAS,UAAU;AACtD,QAAM,cAAc,qBAAqB,SAAS,WAAW;AAC7D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,gBAAgB,QAAqE;AAC5F,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO,CAAC;AACnD,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,OAAO,UAAU,SAAU;AAC/B,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO;AACT;AAEA,SAAS,qBAAqB,QAA2B;AACvD,MAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AACpC,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAgB,CAAC;AACvB,aAAW,SAAS,QAAQ;AAC1B,QAAI,OAAO,UAAU,SAAU;AAC/B,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,CAAC,WAAW,KAAK,IAAI,OAAO,EAAG;AACnC,SAAK,IAAI,OAAO;AAChB,QAAI,KAAK,OAAO;AAAA,EAClB;AACA,SAAO;AACT;AAEO,SAAS,iBAAiB,QAAwB;AACvD,SAAO,QAAQ,QAAQ,EAAE,cAAc,GAAG,CAAC,KAAK;AAClD;",
4
+ "sourcesContent": ["import { slugify } from '../../lib/slugify'\n\nexport const SIDEBAR_PREFERENCES_VERSION = 2\n\nexport type SidebarPreferencesSettings = {\n version: number\n groupOrder?: string[]\n groupLabels?: Record<string, string>\n itemLabels?: Record<string, string>\n hiddenItems?: string[]\n /** Per-group ordered list of item keys. Missing items keep their natural position. */\n itemOrder?: Record<string, string[]>\n}\n\nexport type SidebarPreferencesPayload = {\n locale: string\n settings: SidebarPreferencesSettings\n}\n\nexport function normalizeSidebarSettings(settings?: SidebarPreferencesSettings | null): SidebarPreferencesSettings {\n if (!settings || typeof settings !== 'object') {\n return { version: SIDEBAR_PREFERENCES_VERSION, groupOrder: [], groupLabels: {}, itemLabels: {}, hiddenItems: [], itemOrder: {} }\n }\n const version = typeof settings.version === 'number' ? settings.version : SIDEBAR_PREFERENCES_VERSION\n const groupOrder = Array.isArray(settings.groupOrder) ? settings.groupOrder.filter((v): v is string => typeof v === 'string') : []\n const groupLabels = normalizeRecord(settings.groupLabels)\n const itemLabels = normalizeRecord(settings.itemLabels)\n const hiddenItems = normalizeStringArray(settings.hiddenItems)\n const itemOrder = normalizeStringArrayRecord(settings.itemOrder)\n return {\n version,\n groupOrder,\n groupLabels,\n itemLabels,\n hiddenItems,\n itemOrder,\n }\n}\n\nfunction normalizeRecord(record: Record<string, unknown> | undefined): Record<string, string> {\n if (!record || typeof record !== 'object') return {}\n const out: Record<string, string> = {}\n for (const [key, value] of Object.entries(record)) {\n if (typeof value !== 'string') continue\n out[key] = value\n }\n return out\n}\n\nfunction normalizeStringArrayRecord(record: Record<string, unknown> | undefined): Record<string, string[]> {\n if (!record || typeof record !== 'object') return {}\n const out: Record<string, string[]> = {}\n for (const [key, value] of Object.entries(record)) {\n const arr = normalizeStringArray(value)\n if (arr.length > 0) out[key] = arr\n }\n return out\n}\n\nfunction normalizeStringArray(values: unknown): string[] {\n if (!Array.isArray(values)) return []\n const seen = new Set<string>()\n const out: string[] = []\n for (const value of values) {\n if (typeof value !== 'string') continue\n const trimmed = value.trim()\n if (!trimmed || seen.has(trimmed)) continue\n seen.add(trimmed)\n out.push(trimmed)\n }\n return out\n}\n\nexport function slugifySidebarId(source: string): string {\n return slugify(source, { allowedChars: '' }) || 'group'\n}\n"],
5
+ "mappings": "AAAA,SAAS,eAAe;AAEjB,MAAM,8BAA8B;AAiBpC,SAAS,yBAAyB,UAA0E;AACjH,MAAI,CAAC,YAAY,OAAO,aAAa,UAAU;AAC7C,WAAO,EAAE,SAAS,6BAA6B,YAAY,CAAC,GAAG,aAAa,CAAC,GAAG,YAAY,CAAC,GAAG,aAAa,CAAC,GAAG,WAAW,CAAC,EAAE;AAAA,EACjI;AACA,QAAM,UAAU,OAAO,SAAS,YAAY,WAAW,SAAS,UAAU;AAC1E,QAAM,aAAa,MAAM,QAAQ,SAAS,UAAU,IAAI,SAAS,WAAW,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ,IAAI,CAAC;AACjI,QAAM,cAAc,gBAAgB,SAAS,WAAW;AACxD,QAAM,aAAa,gBAAgB,SAAS,UAAU;AACtD,QAAM,cAAc,qBAAqB,SAAS,WAAW;AAC7D,QAAM,YAAY,2BAA2B,SAAS,SAAS;AAC/D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,gBAAgB,QAAqE;AAC5F,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO,CAAC;AACnD,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,OAAO,UAAU,SAAU;AAC/B,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO;AACT;AAEA,SAAS,2BAA2B,QAAuE;AACzG,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO,CAAC;AACnD,QAAM,MAAgC,CAAC;AACvC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,UAAM,MAAM,qBAAqB,KAAK;AACtC,QAAI,IAAI,SAAS,EAAG,KAAI,GAAG,IAAI;AAAA,EACjC;AACA,SAAO;AACT;AAEA,SAAS,qBAAqB,QAA2B;AACvD,MAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AACpC,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAgB,CAAC;AACvB,aAAW,SAAS,QAAQ;AAC1B,QAAI,OAAO,UAAU,SAAU;AAC/B,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,CAAC,WAAW,KAAK,IAAI,OAAO,EAAG;AACnC,SAAK,IAAI,OAAO;AAChB,QAAI,KAAK,OAAO;AAAA,EAClB;AACA,SAAO;AACT;AAEO,SAAS,iBAAiB,QAAwB;AACvD,SAAO,QAAQ,QAAQ,EAAE,cAAc,GAAG,CAAC,KAAK;AAClD;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.5.1-develop.2975.ccbadc8198",
3
+ "version": "0.5.1-develop.2996.ce62fd491c",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -89,10 +89,10 @@
89
89
  }
90
90
  },
91
91
  "dependencies": {
92
- "@mikro-orm/core": "^7.0.10",
93
- "@mikro-orm/decorators": "^7.0.10",
94
- "@mikro-orm/postgresql": "^7.0.10",
95
- "@open-mercato/cache": "0.5.1-develop.2975.ccbadc8198",
92
+ "@mikro-orm/core": "^7.0.13",
93
+ "@mikro-orm/decorators": "^7.0.13",
94
+ "@mikro-orm/postgresql": "^7.0.13",
95
+ "@open-mercato/cache": "0.5.1-develop.2996.ce62fd491c",
96
96
  "dotenv": "^17.4.2",
97
97
  "rate-limiter-flexible": "^11.0.1",
98
98
  "reflect-metadata": "^0.2.2",
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  buildCustomFieldFiltersFromQuery,
3
+ decorateRecordWithCustomFields,
3
4
  extractAllCustomFieldEntries,
4
5
  loadCustomFieldDefinitionIndex,
5
6
  loadCustomFieldValues,
6
7
  splitCustomFieldPayload,
7
8
  } from '../custom-fields'
9
+ import type { CustomFieldDefinitionIndex, CustomFieldDefinitionSummary } from '../custom-fields'
8
10
  import { encryptWithAesGcm } from '../../encryption/aes'
9
11
 
10
12
  const mockEntityManager = (defs: any[]) => ({
@@ -145,9 +147,10 @@ describe('extractAllCustomFieldEntries', () => {
145
147
  })
146
148
 
147
149
  describe('loadCustomFieldValues (encryption)', () => {
148
- it('decrypts encrypted custom field payloads when definitions mark them encrypted', async () => {
150
+ it('decrypts encrypted text custom field payloads as strings', async () => {
149
151
  const dek = Buffer.alloc(32, 2).toString('base64')
150
- const encrypted = encryptWithAesGcm(JSON.stringify('secret-note'), dek).value
152
+ // Mirrors production encrypt path: strings stored unwrapped, non-strings JSON-stringified.
153
+ const encrypted = encryptWithAesGcm('secret-note', dek).value
151
154
  const em = {
152
155
  find: jest.fn().mockImplementation((_, where) => {
153
156
  if ((where as any).recordId) {
@@ -170,6 +173,129 @@ describe('loadCustomFieldValues (encryption)', () => {
170
173
  })
171
174
  expect(values['rec-1'].cf_note).toBe('secret-note')
172
175
  })
176
+
177
+ it('preserves numeric-looking text values as strings (regression: issue #1734)', async () => {
178
+ const dek = Buffer.alloc(32, 2).toString('base64')
179
+ const encrypted = encryptWithAesGcm('123', dek).value
180
+ const em = {
181
+ find: jest.fn().mockImplementation((_, where) => {
182
+ if ((where as any).recordId) {
183
+ return Promise.resolve([
184
+ { recordId: 'rec-1', fieldKey: 'note', organizationId: null, tenantId: 'tenant-1', valueText: encrypted, valueMultiline: null, valueInt: null, valueFloat: null, valueBool: null, deletedAt: null },
185
+ ])
186
+ }
187
+ return Promise.resolve([
188
+ { key: 'note', entityId: 'demo:entity', organizationId: null, tenantId: 'tenant-1', kind: 'text', configJson: { encrypted: true }, isActive: true },
189
+ ])
190
+ }),
191
+ }
192
+ const mockService = { isEnabled: () => true, getDek: async () => ({ key: dek }) }
193
+ const values = await loadCustomFieldValues({
194
+ em: em as any,
195
+ entityId: 'demo:entity',
196
+ recordIds: ['rec-1'],
197
+ tenantIdByRecord: { 'rec-1': 'tenant-1' },
198
+ encryptionService: mockService as any,
199
+ })
200
+ expect(values['rec-1'].cf_note).toBe('123')
201
+ expect(typeof values['rec-1'].cf_note).toBe('string')
202
+ })
203
+
204
+ it('still parses typed integer custom field payloads back to numbers', async () => {
205
+ const dek = Buffer.alloc(32, 2).toString('base64')
206
+ // Numeric kinds are JSON-stringified by encryptCustomFieldValue in production.
207
+ const encrypted = encryptWithAesGcm(JSON.stringify(42), dek).value
208
+ const em = {
209
+ find: jest.fn().mockImplementation((_, where) => {
210
+ if ((where as any).recordId) {
211
+ return Promise.resolve([
212
+ { recordId: 'rec-1', fieldKey: 'priority', organizationId: null, tenantId: 'tenant-1', valueText: encrypted, valueMultiline: null, valueInt: null, valueFloat: null, valueBool: null, deletedAt: null },
213
+ ])
214
+ }
215
+ return Promise.resolve([
216
+ { key: 'priority', entityId: 'demo:entity', organizationId: null, tenantId: 'tenant-1', kind: 'integer', configJson: { encrypted: true }, isActive: true },
217
+ ])
218
+ }),
219
+ }
220
+ const mockService = { isEnabled: () => true, getDek: async () => ({ key: dek }) }
221
+ const values = await loadCustomFieldValues({
222
+ em: em as any,
223
+ entityId: 'demo:entity',
224
+ recordIds: ['rec-1'],
225
+ tenantIdByRecord: { 'rec-1': 'tenant-1' },
226
+ encryptionService: mockService as any,
227
+ })
228
+ expect(values['rec-1'].cf_priority).toBe(42)
229
+ })
230
+ })
231
+
232
+ describe('decorateRecordWithCustomFields', () => {
233
+ const buildDefinition = (
234
+ overrides: Partial<CustomFieldDefinitionSummary> = {},
235
+ ): CustomFieldDefinitionSummary => ({
236
+ key: 'priority',
237
+ label: 'Priority',
238
+ kind: 'integer',
239
+ multi: false,
240
+ organizationId: null,
241
+ tenantId: null,
242
+ priority: 0,
243
+ updatedAt: 1,
244
+ ...overrides,
245
+ })
246
+
247
+ const buildIndex = (
248
+ entries: Array<[string, CustomFieldDefinitionSummary[]]>,
249
+ ): CustomFieldDefinitionIndex => new Map(entries)
250
+
251
+ it('returns the value in customValues and customFields when an active definition exists', () => {
252
+ const index = buildIndex([
253
+ ['priority', [buildDefinition()]],
254
+ ])
255
+
256
+ const result = decorateRecordWithCustomFields(
257
+ { cf_priority: 3 },
258
+ index,
259
+ { tenantId: 'tenant-1', organizationId: 'org-1' },
260
+ )
261
+
262
+ expect(result.customValues).toEqual({ priority: 3 })
263
+ expect(result.customFields).toEqual([
264
+ { key: 'priority', label: 'Priority', value: 3, kind: 'integer', multi: false },
265
+ ])
266
+ })
267
+
268
+ it('skips orphaned custom field values whose definition was deleted (regression for #1749)', () => {
269
+ const result = decorateRecordWithCustomFields(
270
+ { cf_my_test_key: 'leftover' },
271
+ buildIndex([]),
272
+ { tenantId: 'tenant-1', organizationId: 'org-1' },
273
+ )
274
+
275
+ expect(result.customValues).toBeNull()
276
+ expect(result.customFields).toEqual([])
277
+ })
278
+
279
+ it('keeps active fields and drops orphaned ones in mixed payloads', () => {
280
+ const index = buildIndex([
281
+ ['priority', [buildDefinition({ key: 'priority', label: 'Priority' })]],
282
+ ['severity', [buildDefinition({ key: 'severity', label: 'Severity', kind: 'text', priority: 1 })]],
283
+ ])
284
+
285
+ const result = decorateRecordWithCustomFields(
286
+ {
287
+ cf_priority: 5,
288
+ cf_severity: 'high',
289
+ cf_my_test_key: 'should-not-leak',
290
+ },
291
+ index,
292
+ { tenantId: 'tenant-1', organizationId: 'org-1' },
293
+ )
294
+
295
+ expect(result.customValues).toEqual({ priority: 5, severity: 'high' })
296
+ expect(result.customFields.map((entry) => entry.key)).toEqual(['priority', 'severity'])
297
+ expect(result.customFields.find((entry) => entry.key === 'my_test_key')).toBeUndefined()
298
+ })
173
299
  })
174
300
 
175
301
  describe('loadCustomFieldDefinitionIndex', () => {
@@ -427,20 +427,22 @@ export function decorateRecordWithCustomFields(
427
427
  const bareKey = prefixedKey.replace(/^cf_/, '')
428
428
  const normalizedKey = normalizeDefinitionKey(bareKey)
429
429
  if (!normalizedKey) return
430
- values[bareKey] = value
431
430
  const defsForKey = definitions.get(normalizedKey) ?? []
432
431
  const resolvedDef = selectDefinitionForRecord(defsForKey, organizationId, tenantId)
432
+ // Skip custom field values without active definitions to prevent orphaned fields
433
+ if (!resolvedDef) return
434
+ values[bareKey] = value
433
435
  const entry: CustomFieldDisplayEntry = {
434
436
  key: bareKey,
435
- label: resolvedDef?.label ?? bareKey,
437
+ label: resolvedDef.label ?? bareKey,
436
438
  value,
437
- kind: resolvedDef?.kind ?? null,
438
- multi: resolvedDef?.multi ?? Array.isArray(value),
439
+ kind: resolvedDef.kind ?? null,
440
+ multi: resolvedDef.multi ?? Array.isArray(value),
439
441
  }
440
442
  entries.push({
441
443
  entry,
442
- priority: resolvedDef?.priority ?? Number.MAX_SAFE_INTEGER,
443
- updatedAt: resolvedDef?.updatedAt ?? 0,
444
+ priority: resolvedDef.priority ?? Number.MAX_SAFE_INTEGER,
445
+ updatedAt: resolvedDef.updatedAt ?? 0,
444
446
  })
445
447
  })
446
448
 
@@ -582,7 +584,13 @@ export async function loadCustomFieldValues(opts: {
582
584
  const encrypted = Boolean(def?.configJson && (def as any).configJson?.encrypted)
583
585
  const value = valueFromRow(row)
584
586
  const decrypted = encrypted
585
- ? await decryptCustomFieldValue(value, resolvedTenantId ?? tenantId ?? null, getEncryptionService(), encryptionCache)
587
+ ? await decryptCustomFieldValue(
588
+ value,
589
+ resolvedTenantId ?? tenantId ?? null,
590
+ getEncryptionService(),
591
+ encryptionCache,
592
+ { kind: def?.kind ?? null },
593
+ )
586
594
  : value
587
595
  const existing = buckets.get(bucketKey)
588
596
  if (existing) {
@@ -60,4 +60,41 @@ describe('customFieldValues encryption helpers', () => {
60
60
  expect(await decryptCustomFieldValue('plain', 'tenant-1', disabledService)).toBe('plain')
61
61
  expect(await encryptCustomFieldValue('plain', null, disabledService)).toBe('plain')
62
62
  })
63
+
64
+ it('preserves text-typed values verbatim when kind is provided (regression: issue #1734)', async () => {
65
+ const service = {
66
+ isEnabled: () => true,
67
+ getDek: async () => ({ key: fixedKey }),
68
+ } as any
69
+ const cache = new Map<string | null, string | null>()
70
+
71
+ const numericText = await encryptCustomFieldValue('123', 'tenant-1', service, cache)
72
+ const decryptedAsText = await decryptCustomFieldValue(numericText, 'tenant-1', service, cache, { kind: 'text' })
73
+ expect(decryptedAsText).toBe('123')
74
+ expect(typeof decryptedAsText).toBe('string')
75
+
76
+ const booleanText = await encryptCustomFieldValue('true', 'tenant-1', service, cache)
77
+ expect(await decryptCustomFieldValue(booleanText, 'tenant-1', service, cache, { kind: 'multiline' })).toBe('true')
78
+ expect(await decryptCustomFieldValue(booleanText, 'tenant-1', service, cache, { kind: 'select' })).toBe('true')
79
+ expect(await decryptCustomFieldValue(booleanText, 'tenant-1', service, cache, { kind: 'currency' })).toBe('true')
80
+ expect(await decryptCustomFieldValue(booleanText, 'tenant-1', service, cache, { kind: 'dictionary' })).toBe('true')
81
+ expect(await decryptCustomFieldValue(booleanText, 'tenant-1', service, cache, { kind: 'email' })).toBe('true')
82
+ })
83
+
84
+ it('still parses typed kinds (integer/float/boolean) so legacy round-trip stays correct', async () => {
85
+ const service = {
86
+ isEnabled: () => true,
87
+ getDek: async () => ({ key: fixedKey }),
88
+ } as any
89
+ const cache = new Map<string | null, string | null>()
90
+
91
+ const encryptedInt = await encryptCustomFieldValue(42, 'tenant-1', service, cache)
92
+ expect(await decryptCustomFieldValue(encryptedInt, 'tenant-1', service, cache, { kind: 'integer' })).toBe(42)
93
+
94
+ const encryptedFloat = await encryptCustomFieldValue(3.14, 'tenant-1', service, cache)
95
+ expect(await decryptCustomFieldValue(encryptedFloat, 'tenant-1', service, cache, { kind: 'float' })).toBe(3.14)
96
+
97
+ const encryptedBool = await encryptCustomFieldValue(true, 'tenant-1', service, cache)
98
+ expect(await decryptCustomFieldValue(encryptedBool, 'tenant-1', service, cache, { kind: 'boolean' })).toBe(true)
99
+ })
63
100
  })
@@ -0,0 +1,109 @@
1
+ import { encryptWithAesGcm } from '../aes'
2
+ import {
3
+ TenantDataEncryptionService,
4
+ parseDecryptedFieldValue,
5
+ } from '../tenantDataEncryptionService'
6
+
7
+ const fixedKey = Buffer.alloc(32, 1).toString('base64')
8
+
9
+ describe('parseDecryptedFieldValue', () => {
10
+ it('keeps purely-numeric strings as strings (regression: issue #1734)', () => {
11
+ expect(parseDecryptedFieldValue('123')).toBe('123')
12
+ expect(parseDecryptedFieldValue('00042')).toBe('00042')
13
+ expect(parseDecryptedFieldValue('-9.5')).toBe('-9.5')
14
+ })
15
+
16
+ it('keeps boolean-like and null-like text as strings', () => {
17
+ expect(parseDecryptedFieldValue('true')).toBe('true')
18
+ expect(parseDecryptedFieldValue('false')).toBe('false')
19
+ expect(parseDecryptedFieldValue('null')).toBe('null')
20
+ })
21
+
22
+ it('keeps ordinary text untouched', () => {
23
+ expect(parseDecryptedFieldValue('Acme Corp')).toBe('Acme Corp')
24
+ expect(parseDecryptedFieldValue('')).toBe('')
25
+ })
26
+
27
+ it('parses JSON objects and arrays back to structured values', () => {
28
+ expect(parseDecryptedFieldValue('{"a":1,"b":"x"}')).toEqual({ a: 1, b: 'x' })
29
+ expect(parseDecryptedFieldValue('[1,2,3]')).toEqual([1, 2, 3])
30
+ expect(parseDecryptedFieldValue('[]')).toEqual([])
31
+ })
32
+
33
+ it('returns the raw text when the JSON-looking payload fails to parse', () => {
34
+ expect(parseDecryptedFieldValue('{not json')).toBe('{not json')
35
+ expect(parseDecryptedFieldValue('[broken')).toBe('[broken')
36
+ })
37
+ })
38
+
39
+ describe('TenantDataEncryptionService.decryptFields (issue #1734)', () => {
40
+ function makeService() {
41
+ type Anything = Record<string, unknown>
42
+ const service = new TenantDataEncryptionService({} as never) as unknown as {
43
+ decryptFields: (
44
+ obj: Anything,
45
+ fields: { field: string }[],
46
+ dek: { key: string },
47
+ ) => Anything
48
+ }
49
+ return service
50
+ }
51
+
52
+ function encrypt(value: string): string {
53
+ return encryptWithAesGcm(value, fixedKey).value as string
54
+ }
55
+
56
+ it('preserves a numeric-string display name through encrypt/decrypt round-trip', () => {
57
+ const service = makeService()
58
+ const obj = { display_name: encrypt('123') }
59
+ const out = service.decryptFields(obj, [{ field: 'display_name' }], { key: fixedKey } as never)
60
+ expect(out.display_name).toBe('123')
61
+ expect(typeof out.display_name).toBe('string')
62
+ })
63
+
64
+ it('preserves arbitrary text values through encrypt/decrypt round-trip', () => {
65
+ const service = makeService()
66
+ const obj = {
67
+ display_name: encrypt('Acme Corp'),
68
+ primary_email: encrypt('mail@example.com'),
69
+ }
70
+ const out = service.decryptFields(
71
+ obj,
72
+ [{ field: 'display_name' }, { field: 'primary_email' }],
73
+ { key: fixedKey } as never,
74
+ )
75
+ expect(out.display_name).toBe('Acme Corp')
76
+ expect(out.primary_email).toBe('mail@example.com')
77
+ })
78
+
79
+ it('still recovers JSON object payloads (audit_logs use case)', () => {
80
+ const service = makeService()
81
+ const payload = { actor: 'user-1', changes: { name: 'old → new' } }
82
+ const obj = { context_json: encrypt(JSON.stringify(payload)) }
83
+ const out = service.decryptFields(obj, [{ field: 'context_json' }], { key: fixedKey } as never)
84
+ expect(out.context_json).toEqual(payload)
85
+ })
86
+
87
+ it('still recovers JSON array payloads', () => {
88
+ const service = makeService()
89
+ const arr = [{ id: 1 }, { id: 2 }]
90
+ const obj = { thread_messages: encrypt(JSON.stringify(arr)) }
91
+ const out = service.decryptFields(obj, [{ field: 'thread_messages' }], { key: fixedKey } as never)
92
+ expect(out.thread_messages).toEqual(arr)
93
+ })
94
+
95
+ it('keeps boolean-like and null-like text strings as strings', () => {
96
+ const service = makeService()
97
+ const obj = {
98
+ display_name: encrypt('true'),
99
+ description: encrypt('null'),
100
+ }
101
+ const out = service.decryptFields(
102
+ obj,
103
+ [{ field: 'display_name' }, { field: 'description' }],
104
+ { key: fixedKey } as never,
105
+ )
106
+ expect(out.display_name).toBe('true')
107
+ expect(out.description).toBe('null')
108
+ })
109
+ })
@@ -2,6 +2,39 @@ import type { EntityManager } from '@mikro-orm/core'
2
2
  import { encryptWithAesGcm, decryptWithAesGcm } from './aes'
3
3
  import { TenantDataEncryptionService } from './tenantDataEncryptionService'
4
4
 
5
+ /**
6
+ * Custom field kinds that ALWAYS round-trip as a string. The encrypt path
7
+ * stores raw strings unwrapped, so blindly running `JSON.parse` on the
8
+ * decrypted payload coerces text values like `"123"` or `"true"` back into
9
+ * numbers/booleans (issue #1734). For these kinds, callers MUST pass the
10
+ * `kind` option so we keep the decrypted value as a string.
11
+ *
12
+ * Numeric (`integer`/`float`) and `boolean` kinds rely on JSON round-trip
13
+ * because the encrypt path JSON-stringifies the typed value before storage.
14
+ * Omitting the kind preserves legacy round-trip behavior for backward
15
+ * compatibility.
16
+ */
17
+ const STRING_TYPED_CUSTOM_FIELD_KINDS = new Set([
18
+ 'text',
19
+ 'multiline',
20
+ 'select',
21
+ 'currency',
22
+ 'dictionary',
23
+ 'email',
24
+ 'url',
25
+ 'string',
26
+ ])
27
+
28
+ export type DecryptCustomFieldOptions = {
29
+ /** Field kind, e.g. from `CustomFieldDef.kind`. When string-typed, the helper preserves the decrypted string verbatim. */
30
+ kind?: string | null
31
+ }
32
+
33
+ function shouldPreserveAsString(kind: string | null | undefined): boolean {
34
+ if (!kind) return false
35
+ return STRING_TYPED_CUSTOM_FIELD_KINDS.has(kind)
36
+ }
37
+
5
38
  const serviceCache = new WeakMap<EntityManager, TenantDataEncryptionService>()
6
39
 
7
40
  export function resolveTenantEncryptionService(
@@ -53,12 +86,14 @@ export async function decryptCustomFieldValue(
53
86
  tenantId: string | null | undefined,
54
87
  service: TenantDataEncryptionService | null,
55
88
  cache?: Map<string | null, string | null>,
89
+ options?: DecryptCustomFieldOptions,
56
90
  ): Promise<unknown> {
57
91
  if (value === undefined || value === null || typeof value !== 'string') return value
58
92
  const key = await resolveDekKey(service, tenantId, cache)
59
93
  if (!key) return value
60
94
  const decrypted = decryptWithAesGcm(value, key)
61
95
  if (decrypted === null) return value
96
+ if (shouldPreserveAsString(options?.kind ?? null)) return decrypted
62
97
  try {
63
98
  return JSON.parse(decrypted)
64
99
  } catch {
@@ -56,6 +56,29 @@ function findKey(obj: Record<string, unknown>, key: string): string | null {
56
56
  return null
57
57
  }
58
58
 
59
+ /**
60
+ * Decode a decrypted entity-field payload back into its original value.
61
+ *
62
+ * The encrypt path stores raw strings unwrapped and JSON-stringifies non-string
63
+ * values. Blindly running `JSON.parse` on every decrypted value would coerce
64
+ * text columns whose contents happen to be valid JSON primitives — e.g. the
65
+ * string `"123"` — back into numbers/booleans, which then breaks string-typed
66
+ * consumers (see issue #1734). Only restructure the value when the decrypted
67
+ * payload is unambiguously a JSON object or array; otherwise return the raw
68
+ * decrypted string. Numeric/boolean entity columns are not in any current
69
+ * encryption map, so this is backward-compatible.
70
+ */
71
+ export function parseDecryptedFieldValue(decrypted: string): unknown {
72
+ if (decrypted.length === 0) return decrypted
73
+ const first = decrypted[0]
74
+ if (first !== '{' && first !== '[') return decrypted
75
+ try {
76
+ return JSON.parse(decrypted)
77
+ } catch {
78
+ return decrypted
79
+ }
80
+ }
81
+
59
82
  function isEncryptedPayload(value: unknown): boolean {
60
83
  if (typeof value !== 'string') return false
61
84
  const parts = value.split(':')
@@ -254,11 +277,7 @@ export class TenantDataEncryptionService {
254
277
  if (typeof value !== 'string') continue
255
278
  const decrypted = maybeDecrypt(value)
256
279
  if (decrypted === null) continue
257
- try {
258
- clone[key] = JSON.parse(decrypted)
259
- } catch {
260
- clone[key] = decrypted
261
- }
280
+ clone[key] = parseDecryptedFieldValue(decrypted)
262
281
  }
263
282
  return clone
264
283
  }
@@ -0,0 +1,42 @@
1
+ import {
2
+ DEFAULT_SEARCH_MIN_TOKEN_LENGTH,
3
+ resolveSearchConfig,
4
+ resolveSearchMinTokenLength,
5
+ } from '../config'
6
+
7
+ describe('resolveSearchMinTokenLength', () => {
8
+ const originalValue = process.env.OM_SEARCH_MIN_LEN
9
+
10
+ afterEach(() => {
11
+ if (originalValue === undefined) {
12
+ delete process.env.OM_SEARCH_MIN_LEN
13
+ } else {
14
+ process.env.OM_SEARCH_MIN_LEN = originalValue
15
+ }
16
+ })
17
+
18
+ it('returns the default when OM_SEARCH_MIN_LEN is unset', () => {
19
+ delete process.env.OM_SEARCH_MIN_LEN
20
+ expect(resolveSearchMinTokenLength()).toBe(DEFAULT_SEARCH_MIN_TOKEN_LENGTH)
21
+ })
22
+
23
+ it('parses a positive integer', () => {
24
+ process.env.OM_SEARCH_MIN_LEN = '4'
25
+ expect(resolveSearchMinTokenLength()).toBe(4)
26
+ })
27
+
28
+ it('falls back to the default for non-numeric values', () => {
29
+ process.env.OM_SEARCH_MIN_LEN = 'abc'
30
+ expect(resolveSearchMinTokenLength()).toBe(DEFAULT_SEARCH_MIN_TOKEN_LENGTH)
31
+ })
32
+
33
+ it('falls back to the default for values below the floor', () => {
34
+ process.env.OM_SEARCH_MIN_LEN = '0'
35
+ expect(resolveSearchMinTokenLength()).toBe(DEFAULT_SEARCH_MIN_TOKEN_LENGTH)
36
+ })
37
+
38
+ it('keeps resolveSearchConfig().minTokenLength in sync', () => {
39
+ process.env.OM_SEARCH_MIN_LEN = '5'
40
+ expect(resolveSearchConfig().minTokenLength).toBe(resolveSearchMinTokenLength())
41
+ })
42
+ })
@@ -9,6 +9,8 @@ export type SearchConfig = {
9
9
  blocklistedFields: string[]
10
10
  }
11
11
 
12
+ export const DEFAULT_SEARCH_MIN_TOKEN_LENGTH = 3
13
+
12
14
  const DEFAULT_BLOCKLIST = ['password', 'token', 'secret', 'hash']
13
15
 
14
16
  function parseBoolean(raw: string | undefined, fallback: boolean): boolean {
@@ -33,7 +35,7 @@ function parseHashAlgorithm(raw: string | undefined): 'sha256' | 'sha1' | 'md5'
33
35
  export function resolveSearchConfig(): SearchConfig {
34
36
  return {
35
37
  enabled: parseBoolean(process.env.OM_SEARCH_ENABLED, true),
36
- minTokenLength: parseNumber(process.env.OM_SEARCH_MIN_LEN, 3, 1),
38
+ minTokenLength: resolveSearchMinTokenLength(),
37
39
  enablePartials: parseBoolean(process.env.OM_SEARCH_ENABLE_PARTIAL, true),
38
40
  hashAlgorithm: parseHashAlgorithm(process.env.OM_SEARCH_HASH_ALGO),
39
41
  storeRawTokens: parseBoolean(process.env.OM_SEARCH_STORE_RAW_TOKENS, false),
@@ -47,3 +49,18 @@ export function resolveSearchConfig(): SearchConfig {
47
49
  .filter((value, index, arr) => arr.indexOf(value) === index),
48
50
  }
49
51
  }
52
+
53
+ /**
54
+ * Browser-safe accessor for the minimum search token length.
55
+ *
56
+ * Why: client components (e.g. global search dialog) must mirror the server-side
57
+ * tokenizer's `minTokenLength` so the UI gates the request before hitting an
58
+ * empty result set. Pulling the value through this single helper keeps the env
59
+ * contract (`OM_SEARCH_MIN_LEN`) authoritative on both sides.
60
+ *
61
+ * How to apply: call from anywhere — server, client (when the host app exposes
62
+ * `OM_SEARCH_MIN_LEN` through `next.config.ts`'s `env` block), or tests.
63
+ */
64
+ export function resolveSearchMinTokenLength(): number {
65
+ return parseNumber(process.env.OM_SEARCH_MIN_LEN, DEFAULT_SEARCH_MIN_TOKEN_LENGTH, 1)
66
+ }
@@ -8,6 +8,8 @@ export type SidebarPreferencesSettings = {
8
8
  groupLabels?: Record<string, string>
9
9
  itemLabels?: Record<string, string>
10
10
  hiddenItems?: string[]
11
+ /** Per-group ordered list of item keys. Missing items keep their natural position. */
12
+ itemOrder?: Record<string, string[]>
11
13
  }
12
14
 
13
15
  export type SidebarPreferencesPayload = {
@@ -17,19 +19,21 @@ export type SidebarPreferencesPayload = {
17
19
 
18
20
  export function normalizeSidebarSettings(settings?: SidebarPreferencesSettings | null): SidebarPreferencesSettings {
19
21
  if (!settings || typeof settings !== 'object') {
20
- return { version: SIDEBAR_PREFERENCES_VERSION, groupOrder: [], groupLabels: {}, itemLabels: {}, hiddenItems: [] }
22
+ return { version: SIDEBAR_PREFERENCES_VERSION, groupOrder: [], groupLabels: {}, itemLabels: {}, hiddenItems: [], itemOrder: {} }
21
23
  }
22
24
  const version = typeof settings.version === 'number' ? settings.version : SIDEBAR_PREFERENCES_VERSION
23
25
  const groupOrder = Array.isArray(settings.groupOrder) ? settings.groupOrder.filter((v): v is string => typeof v === 'string') : []
24
26
  const groupLabels = normalizeRecord(settings.groupLabels)
25
27
  const itemLabels = normalizeRecord(settings.itemLabels)
26
28
  const hiddenItems = normalizeStringArray(settings.hiddenItems)
29
+ const itemOrder = normalizeStringArrayRecord(settings.itemOrder)
27
30
  return {
28
31
  version,
29
32
  groupOrder,
30
33
  groupLabels,
31
34
  itemLabels,
32
35
  hiddenItems,
36
+ itemOrder,
33
37
  }
34
38
  }
35
39
 
@@ -43,6 +47,16 @@ function normalizeRecord(record: Record<string, unknown> | undefined): Record<st
43
47
  return out
44
48
  }
45
49
 
50
+ function normalizeStringArrayRecord(record: Record<string, unknown> | undefined): Record<string, string[]> {
51
+ if (!record || typeof record !== 'object') return {}
52
+ const out: Record<string, string[]> = {}
53
+ for (const [key, value] of Object.entries(record)) {
54
+ const arr = normalizeStringArray(value)
55
+ if (arr.length > 0) out[key] = arr
56
+ }
57
+ return out
58
+ }
59
+
46
60
  function normalizeStringArray(values: unknown): string[] {
47
61
  if (!Array.isArray(values)) return []
48
62
  const seen = new Set<string>()