@open-mercato/shared 0.9.1-develop.7575.1.8480826a9b → 0.10.1-develop.7577.1.477f2486e5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +4 -4
  3. package/dist/lib/auth/server.js +1 -1
  4. package/dist/lib/auth/server.js.map +2 -2
  5. package/dist/lib/bootstrap/dynamicLoader.js +46 -6
  6. package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
  7. package/dist/lib/bootstrap/factory.js +4 -3
  8. package/dist/lib/bootstrap/factory.js.map +2 -2
  9. package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js +6 -0
  10. package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js.map +7 -0
  11. package/dist/lib/bootstrap/next-subpath-resolve-hook.js +62 -0
  12. package/dist/lib/bootstrap/next-subpath-resolve-hook.js.map +7 -0
  13. package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js +33 -0
  14. package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js.map +7 -0
  15. package/dist/lib/catalog-visibility/hashEffectiveScope.js +9 -0
  16. package/dist/lib/catalog-visibility/hashEffectiveScope.js.map +7 -0
  17. package/dist/lib/catalog-visibility/index.js +4 -0
  18. package/dist/lib/catalog-visibility/index.js.map +2 -2
  19. package/dist/lib/commands/command-bus.js +45 -13
  20. package/dist/lib/commands/command-bus.js.map +2 -2
  21. package/dist/lib/commands/types.js.map +2 -2
  22. package/dist/lib/crud/exporters.js +1 -1
  23. package/dist/lib/crud/exporters.js.map +2 -2
  24. package/dist/lib/crud/ids.js +13 -1
  25. package/dist/lib/crud/ids.js.map +2 -2
  26. package/dist/lib/db/advisoryLock.js +172 -0
  27. package/dist/lib/db/advisoryLock.js.map +7 -0
  28. package/dist/lib/db/pg-errors.js +14 -0
  29. package/dist/lib/db/pg-errors.js.map +2 -2
  30. package/dist/lib/localization/index.js +9 -1
  31. package/dist/lib/localization/index.js.map +2 -2
  32. package/dist/lib/localization/translatable-fields.js +26 -0
  33. package/dist/lib/localization/translatable-fields.js.map +2 -2
  34. package/dist/lib/number.js +6 -0
  35. package/dist/lib/number.js.map +2 -2
  36. package/dist/lib/openapi/generator.js +84 -67
  37. package/dist/lib/openapi/generator.js.map +2 -2
  38. package/dist/lib/query/engine.js +45 -10
  39. package/dist/lib/query/engine.js.map +2 -2
  40. package/dist/lib/query/join-utils.js +1 -1
  41. package/dist/lib/query/join-utils.js.map +2 -2
  42. package/dist/lib/query/overlap.js +58 -0
  43. package/dist/lib/query/overlap.js.map +7 -0
  44. package/dist/lib/query/types.js.map +2 -2
  45. package/dist/lib/url.js +24 -0
  46. package/dist/lib/url.js.map +2 -2
  47. package/dist/lib/version.js +1 -1
  48. package/dist/lib/version.js.map +1 -1
  49. package/dist/lib/webhooks/body.js +7 -3
  50. package/dist/lib/webhooks/body.js.map +2 -2
  51. package/dist/lib/webhooks/index.js +2 -0
  52. package/dist/lib/webhooks/index.js.map +2 -2
  53. package/dist/modules/integrations/types.js.map +2 -2
  54. package/dist/modules/notifications/safeHref.js +20 -0
  55. package/dist/modules/notifications/safeHref.js.map +7 -0
  56. package/dist/modules/notifications/types.js +4 -0
  57. package/dist/modules/notifications/types.js.map +3 -3
  58. package/dist/modules/payment_gateways/types.js +14 -1
  59. package/dist/modules/payment_gateways/types.js.map +2 -2
  60. package/dist/modules/registry.js.map +2 -2
  61. package/dist/modules/search.js.map +2 -2
  62. package/dist/modules/widgets/injection-loader.js +14 -3
  63. package/dist/modules/widgets/injection-loader.js.map +2 -2
  64. package/package.json +10 -3
  65. package/src/lib/__tests__/number.test.ts +24 -1
  66. package/src/lib/__tests__/url.cross-site.test.ts +140 -0
  67. package/src/lib/auth/__tests__/server.test.ts +14 -0
  68. package/src/lib/auth/server.ts +1 -1
  69. package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +2 -2
  70. package/src/lib/bootstrap/__tests__/dynamicLoader.appModuleOverrides.test.ts +2 -2
  71. package/src/lib/bootstrap/__tests__/dynamicLoader.cacheRecovery.test.ts +2 -2
  72. package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +2 -2
  73. package/src/lib/bootstrap/__tests__/dynamicLoader.externalSubpath.test.ts +141 -0
  74. package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +2 -2
  75. package/src/lib/bootstrap/__tests__/factory.test.ts +18 -6
  76. package/src/lib/bootstrap/__tests__/next-subpath-resolve-hook.test.ts +174 -0
  77. package/src/lib/bootstrap/dynamicLoader.ts +63 -5
  78. package/src/lib/bootstrap/factory.ts +8 -3
  79. package/src/lib/bootstrap/next-subpath-resolve-hook-worker.ts +3 -0
  80. package/src/lib/bootstrap/next-subpath-resolve-hook.ts +130 -0
  81. package/src/lib/bootstrap/types.ts +5 -1
  82. package/src/lib/catalog-visibility/__tests__/hashEffectiveScope.test.ts +121 -0
  83. package/src/lib/catalog-visibility/canonicalizeEffectiveScope.ts +41 -0
  84. package/src/lib/catalog-visibility/hashEffectiveScope.ts +8 -0
  85. package/src/lib/catalog-visibility/index.ts +2 -0
  86. package/src/lib/commands/__tests__/command-bus.test.ts +100 -1
  87. package/src/lib/commands/command-bus.ts +42 -10
  88. package/src/lib/commands/command-interceptor.ts +4 -0
  89. package/src/lib/commands/types.ts +14 -0
  90. package/src/lib/crud/__tests__/exporters.test.ts +18 -0
  91. package/src/lib/crud/__tests__/ids.test.ts +34 -1
  92. package/src/lib/crud/exporters.ts +1 -1
  93. package/src/lib/crud/ids.ts +24 -0
  94. package/src/lib/db/__tests__/advisoryLock.integration.test.ts +343 -0
  95. package/src/lib/db/__tests__/advisoryLock.test.ts +564 -0
  96. package/src/lib/db/__tests__/pg-errors.test.ts +68 -1
  97. package/src/lib/db/advisoryLock.ts +271 -0
  98. package/src/lib/db/pg-errors.ts +35 -0
  99. package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +23 -0
  100. package/src/lib/localization/__tests__/translatable-fields.test.ts +17 -0
  101. package/src/lib/localization/index.ts +8 -1
  102. package/src/lib/localization/translatable-fields.ts +46 -0
  103. package/src/lib/number.ts +13 -0
  104. package/src/lib/openapi/__tests__/optional-path-params.test.ts +102 -0
  105. package/src/lib/openapi/generator.ts +106 -77
  106. package/src/lib/query/__tests__/empty-ids-filter.pg.test.ts +89 -0
  107. package/src/lib/query/__tests__/engine.empty-in-filter.test.ts +79 -0
  108. package/src/lib/query/__tests__/overlap.test.ts +219 -0
  109. package/src/lib/query/engine.ts +58 -8
  110. package/src/lib/query/join-utils.ts +1 -1
  111. package/src/lib/query/overlap.ts +106 -0
  112. package/src/lib/query/types.ts +13 -1
  113. package/src/lib/url.ts +30 -0
  114. package/src/lib/webhooks/__tests__/body.test.ts +48 -0
  115. package/src/lib/webhooks/body.ts +11 -4
  116. package/src/lib/webhooks/index.ts +1 -0
  117. package/src/modules/integrations/types.ts +6 -0
  118. package/src/modules/notifications/safeHref.ts +19 -0
  119. package/src/modules/notifications/types.ts +7 -0
  120. package/src/modules/payment_gateways/__tests__/types.test.ts +44 -0
  121. package/src/modules/payment_gateways/types.ts +52 -6
  122. package/src/modules/registry.ts +5 -0
  123. package/src/modules/search.ts +39 -0
  124. package/src/modules/widgets/__tests__/injection-loader.partitioned-registration.test.ts +78 -0
  125. package/src/modules/widgets/injection-loader.ts +33 -3
@@ -1,5 +1,5 @@
1
1
  import { sql } from "kysely";
2
- const VALID_OPS = ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "like", "ilike", "exists"];
2
+ const VALID_OPS = ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "like", "ilike", "exists", "overlap", "noverlap"];
3
3
  const normalizeField = (key) => key.startsWith("cf_") ? `cf:${key.slice(3)}` : key;
4
4
  const MAX_DNF_DISJUNCTS = 1e3;
5
5
  class AdvancedFilterComplexityError extends Error {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/query/join-utils.ts"],
4
- "sourcesContent": ["import type { Kysely } from 'kysely'\nimport { sql } from 'kysely'\nimport type { QueryOptions, QueryJoinEdge } from './types'\nimport type { FilterOp } from './types'\n\ntype AnyBuilder = any\n\nexport type NormalizedFilter = { field: string; op: FilterOp; value?: unknown; orGroup?: string; qualified?: string | null }\n\nconst VALID_OPS = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'like', 'ilike', 'exists'] as const\n\nconst normalizeField = (key: string) => (key.startsWith('cf_') ? `cf:${key.slice(3)}` : key)\n\ntype LeafClause = { field: string; op: FilterOp; value?: unknown }\n\n/**\n * Cap on DNF disjunct count to prevent adversarial / pathological filters from\n * exploding the in-memory normalization. `validateTreeLimits` already caps\n * editor input at 50 rules / 15 children per group / 3 levels of nesting, but\n * a hand-crafted URL or buggy preset could still bypass that. The cap mirrors\n * the editor's posture: fail loud, refuse to compile.\n */\nexport const MAX_DNF_DISJUNCTS = 1000\n\nexport class AdvancedFilterComplexityError extends Error {\n readonly code = 'ADVANCED_FILTER_TOO_COMPLEX'\n readonly disjunctCount: number\n constructor(disjunctCount: number) {\n super(\n `Advanced filter expanded to ${disjunctCount} disjuncts, exceeding the limit of ${MAX_DNF_DISJUNCTS}. ` +\n `Reduce the number of OR branches or simplify nested groups.`,\n )\n this.name = 'AdvancedFilterComplexityError'\n this.disjunctCount = disjunctCount\n }\n}\n\n/**\n * Expand a Mongo-style filter object (with arbitrary nesting of `$and`/`$or` and\n * implicit AND between sibling keys) into Disjunctive Normal Form: `Clause[][]`,\n * where the outer array is OR'd alternatives and each inner array is the AND'd\n * leaves of one alternative.\n *\n * Empty filter -> `[[]]` (one empty disjunct, treated as \"no constraint\").\n * Throws `AdvancedFilterComplexityError` if any intermediate cartesian product\n * exceeds `MAX_DNF_DISJUNCTS`.\n */\nfunction compileToDnf(filter: unknown): LeafClause[][] {\n if (filter === null || filter === undefined) return [[]]\n if (Array.isArray(filter) || typeof filter !== 'object') return [[]]\n const obj = filter as Record<string, unknown>\n const partialDnfs: LeafClause[][][] = []\n\n for (const [rawKey, rawVal] of Object.entries(obj)) {\n if (rawKey === '$and' && Array.isArray(rawVal)) {\n partialDnfs.push(cartesianAnd((rawVal as unknown[]).map(compileToDnf)))\n continue\n }\n if (rawKey === '$or' && Array.isArray(rawVal)) {\n const expanded = (rawVal as unknown[]).flatMap(compileToDnf)\n if (expanded.length > MAX_DNF_DISJUNCTS) {\n throw new AdvancedFilterComplexityError(expanded.length)\n }\n partialDnfs.push(expanded)\n continue\n }\n const field = normalizeField(rawKey)\n if (rawVal !== null && typeof rawVal === 'object' && !Array.isArray(rawVal)) {\n const clauses: LeafClause[] = []\n for (const [opKey, opVal] of Object.entries(rawVal as Record<string, unknown>)) {\n const op = (opKey.startsWith('$') ? opKey.slice(1) : opKey) as FilterOp\n if (VALID_OPS.includes(op as (typeof VALID_OPS)[number])) {\n clauses.push({ field, op, value: opVal })\n }\n }\n if (clauses.length > 0) partialDnfs.push([clauses])\n } else {\n partialDnfs.push([[{ field, op: 'eq' as FilterOp, value: rawVal }]])\n }\n }\n\n return cartesianAnd(partialDnfs)\n}\n\nfunction cartesianAnd(dnfs: LeafClause[][][]): LeafClause[][] {\n if (dnfs.length === 0) return [[]]\n let result: LeafClause[][] = [[]]\n for (const dnf of dnfs) {\n if (dnf.length === 0) {\n // An empty DNF means an unsatisfiable subexpression; the AND becomes unsatisfiable\n // too. Represent that as an empty outer array (no disjuncts).\n return []\n }\n // Predict the next size to fail loud before allocating millions of arrays.\n const projected = result.length * dnf.length\n if (projected > MAX_DNF_DISJUNCTS) {\n throw new AdvancedFilterComplexityError(projected)\n }\n const next: LeafClause[][] = []\n for (const left of result) {\n for (const right of dnf) {\n next.push([...left, ...right])\n }\n }\n result = next\n }\n return result\n}\n\nfunction clauseKey(c: LeafClause): string {\n return JSON.stringify([c.field, c.op, c.value])\n}\n\n/**\n * Lift clauses that appear in every disjunct into a \"common\" set so the engine\n * can treat them as regular ANDed base filters (preserving the search-tokens\n * optimization for like/ilike clauses) instead of duplicating them across OR\n * groups.\n */\nfunction liftCommonClauses(dnf: LeafClause[][]): { common: LeafClause[]; remaining: LeafClause[][] } {\n if (dnf.length <= 1) return { common: [], remaining: dnf }\n const counts = new Map<string, { clause: LeafClause; count: number }>()\n for (const disjunct of dnf) {\n const seen = new Set<string>()\n for (const c of disjunct) {\n const key = clauseKey(c)\n if (seen.has(key)) continue // count once per disjunct even if duplicated\n seen.add(key)\n const entry = counts.get(key)\n if (entry) entry.count += 1\n else counts.set(key, { clause: c, count: 1 })\n }\n }\n const total = dnf.length\n const commonKeys = new Set<string>()\n const common: LeafClause[] = []\n for (const [key, { clause, count }] of counts) {\n if (count === total) {\n commonKeys.add(key)\n common.push(clause)\n }\n }\n if (common.length === 0) return { common: [], remaining: dnf }\n const remaining = dnf.map((d) => d.filter((c) => !commonKeys.has(clauseKey(c))))\n return { common, remaining }\n}\n\nexport function normalizeFilters(filters?: QueryOptions['filters']): NormalizedFilter[] {\n if (!filters) return []\n if (Array.isArray(filters)) {\n return (filters as any[]).map((f) => ({\n ...f,\n field: normalizeField(String((f as any).field)),\n }))\n }\n const dnf = compileToDnf(filters)\n if (dnf.length === 0) return []\n const nonEmpty = dnf.filter((disjunct) => disjunct.length > 0)\n if (nonEmpty.length === 0) return []\n if (nonEmpty.length === 1) {\n return nonEmpty[0].map((c) => ({ field: c.field, op: c.op, value: c.value }))\n }\n const { common, remaining } = liftCommonClauses(nonEmpty)\n const out: NormalizedFilter[] = []\n for (const c of common) out.push({ field: c.field, op: c.op, value: c.value })\n // Drop disjuncts that became empty after lifting (they're already represented in `common`).\n const remainingNonEmpty = remaining.filter((d) => d.length > 0)\n if (remainingNonEmpty.length === 0) return out\n if (remainingNonEmpty.length === 1) {\n for (const c of remainingNonEmpty[0]) out.push({ field: c.field, op: c.op, value: c.value })\n return out\n }\n for (let i = 0; i < remainingNonEmpty.length; i++) {\n for (const c of remainingNonEmpty[i]) {\n out.push({ field: c.field, op: c.op, value: c.value, orGroup: `or_${i}` })\n }\n }\n return out\n}\n\nexport type ResolvedJoin = {\n alias: string\n table: string\n entityId?: string | null\n fromAlias: string\n fromField: string\n toField: string\n type: 'left' | 'inner'\n}\n\nexport type BaseFilter = NormalizedFilter & { qualified?: string | null }\nexport type JoinFilter = { alias: string; column: string; op: FilterOp; value?: unknown }\n\nexport function resolveJoins(\n baseTable: string,\n joins: QueryJoinEdge[] | null | undefined,\n resolveTable: (entityId: string) => string | null,\n): ResolvedJoin[] {\n if (!joins || joins.length === 0) return []\n const resolved: ResolvedJoin[] = []\n const seen = new Set<string>()\n for (const entry of joins) {\n if (!entry || typeof entry !== 'object') continue\n const alias = typeof entry.alias === 'string' ? entry.alias.trim() : ''\n if (!alias) continue\n if (seen.has(alias)) continue\n const table =\n entry.table ??\n (entry.entityId ? resolveTable(String(entry.entityId)) : null)\n if (!table) continue\n const fromField = entry.from?.field?.trim()\n const toField = entry.to?.field?.trim()\n if (!fromField || !toField) continue\n const fromAliasRaw = entry.from?.alias?.trim()\n const fromAlias = fromAliasRaw && fromAliasRaw.length > 0 ? fromAliasRaw : 'base'\n const type: 'left' | 'inner' = entry.type === 'inner' ? 'inner' : 'left'\n resolved.push({\n alias,\n table,\n entityId: entry.entityId ? String(entry.entityId) : null,\n fromAlias,\n fromField,\n toField,\n type,\n })\n seen.add(alias)\n }\n return resolved\n}\n\nexport function buildJoinChain(\n alias: string,\n joinMap: Map<string, ResolvedJoin>,\n baseTable: string,\n visited: Set<string> = new Set(),\n): ResolvedJoin[] {\n if (visited.has(alias)) {\n throw new Error(`QueryEngine: circular join reference detected for alias ${alias}`)\n }\n const cfg = joinMap.get(alias)\n if (!cfg) return []\n visited.add(alias)\n if (!cfg.fromAlias || cfg.fromAlias === 'base' || cfg.fromAlias === baseTable) {\n return [cfg]\n }\n const parentChain = buildJoinChain(cfg.fromAlias, joinMap, baseTable, visited)\n if (parentChain.length === 0) return []\n return [...parentChain, cfg]\n}\n\nexport function partitionFilters(\n baseTable: string,\n filters: NormalizedFilter[],\n joinMap: Map<string, ResolvedJoin>,\n): { baseFilters: BaseFilter[]; joinFilters: Map<string, JoinFilter[]> } {\n const baseFilters: BaseFilter[] = []\n const joinFilters = new Map<string, JoinFilter[]>()\n for (const filter of filters) {\n const field = String(filter.field)\n if (field.startsWith('cf:')) continue\n const parts = field.split('.')\n if (parts.length === 2) {\n const [aliasNameRaw, column] = parts\n const aliasName = aliasNameRaw || ''\n if (joinMap.has(aliasName)) {\n const list = joinFilters.get(aliasName) ?? []\n list.push({ alias: aliasName, column, op: filter.op, value: filter.value })\n joinFilters.set(aliasName, list)\n continue\n }\n if (aliasName === baseTable || aliasName === 'base') {\n baseFilters.push({\n field: column,\n op: filter.op,\n value: filter.value,\n qualified: `${baseTable}.${column}`,\n })\n continue\n }\n }\n baseFilters.push({ ...filter })\n }\n return { baseFilters, joinFilters }\n}\n\ntype ApplyJoinFiltersOptions = {\n db: Kysely<any>\n baseTable: string\n builder: AnyBuilder\n joinMap: Map<string, ResolvedJoin>\n joinFilters: Map<string, JoinFilter[]>\n aliasTables: Map<string, string>\n qualifyBase: (column: string) => string\n applyAliasScope: (builder: AnyBuilder, alias: string, table: string) => Promise<AnyBuilder> | AnyBuilder\n applyFilterOp: (builder: AnyBuilder, column: string, op: FilterOp, value?: unknown) => AnyBuilder\n applyJoinFilterOp?: (builder: AnyBuilder, filter: JoinFilter, qualified: string, join: ResolvedJoin, table: string) => Promise<{ applied: boolean; builder: AnyBuilder }> | { applied: boolean; builder: AnyBuilder }\n columnExists?: (table: string, column: string) => Promise<boolean> | boolean\n}\n\nexport async function applyJoinFilters({\n db,\n baseTable,\n builder,\n joinMap,\n joinFilters,\n aliasTables,\n qualifyBase,\n applyAliasScope,\n applyFilterOp,\n applyJoinFilterOp,\n columnExists,\n}: ApplyJoinFiltersOptions): Promise<AnyBuilder> {\n const resolveAliasName = (aliasName?: string | null) => {\n if (!aliasName || aliasName === 'base') return baseTable\n return aliasName\n }\n\n let nextBuilder = builder\n for (const [alias, filtersForAlias] of joinFilters.entries()) {\n const chain = buildJoinChain(alias, joinMap, baseTable)\n if (!chain.length) continue\n const first = chain[0]\n let sub: AnyBuilder = db.selectFrom(`${first.table} as ${first.alias}` as any).select(sql`1`.as('one'))\n sub = await applyAliasScope(sub, first.alias, first.table)\n const parentAlias = resolveAliasName(first.fromAlias)\n const parentRef = parentAlias === baseTable ? qualifyBase(first.fromField) : `${parentAlias}.${first.fromField}`\n sub = sub.whereRef(`${first.alias}.${first.toField}`, '=', parentRef)\n for (const cfg of chain.slice(1)) {\n const parent = resolveAliasName(cfg.fromAlias)\n const rightRef = parent === baseTable ? qualifyBase(cfg.fromField) : `${parent}.${cfg.fromField}`\n const joinFn = cfg.type === 'inner' ? 'innerJoin' : 'leftJoin'\n sub = (sub as any)[joinFn](`${cfg.table} as ${cfg.alias}`, (jb: any) =>\n jb.onRef(`${cfg.alias}.${cfg.toField}`, '=', rightRef))\n sub = await applyAliasScope(sub, cfg.alias, cfg.table)\n }\n let existsDirective: boolean | null = null\n for (const filter of filtersForAlias) {\n if (filter.op === 'exists') {\n if (filter.value === false) existsDirective = false\n else if (existsDirective === null) existsDirective = true\n continue\n }\n const join = joinMap.get(filter.alias)\n if (!join) continue\n const targetTable = aliasTables.get(filter.alias)\n if (!targetTable) continue\n if (columnExists) {\n const exists = await columnExists(targetTable, filter.column)\n if (!exists) continue\n }\n const qualified = `${filter.alias}.${filter.column}`\n if (applyJoinFilterOp) {\n const result = await applyJoinFilterOp(sub, filter, qualified, join, targetTable)\n if (result && result.applied) {\n sub = result.builder\n continue\n }\n if (result && result.builder) {\n sub = result.builder\n }\n }\n sub = applyFilterOp(sub, qualified, filter.op, filter.value)\n }\n if (existsDirective === false) {\n const capturedSub = sub\n nextBuilder = nextBuilder.where((eb: any) => eb.not(eb.exists(capturedSub)))\n } else {\n const capturedSub = sub\n nextBuilder = nextBuilder.where((eb: any) => eb.exists(capturedSub))\n }\n }\n return nextBuilder\n}\n"],
5
- "mappings": "AACA,SAAS,WAAW;AAQpB,MAAM,YAAY,CAAC,MAAM,MAAM,MAAM,OAAO,MAAM,OAAO,MAAM,OAAO,QAAQ,SAAS,QAAQ;AAE/F,MAAM,iBAAiB,CAAC,QAAiB,IAAI,WAAW,KAAK,IAAI,MAAM,IAAI,MAAM,CAAC,CAAC,KAAK;AAWjF,MAAM,oBAAoB;AAE1B,MAAM,sCAAsC,MAAM;AAAA,EAGvD,YAAY,eAAuB;AACjC;AAAA,MACE,+BAA+B,aAAa,sCAAsC,iBAAiB;AAAA,IAErG;AANF,SAAS,OAAO;AAOd,SAAK,OAAO;AACZ,SAAK,gBAAgB;AAAA,EACvB;AACF;AAYA,SAAS,aAAa,QAAiC;AACrD,MAAI,WAAW,QAAQ,WAAW,OAAW,QAAO,CAAC,CAAC,CAAC;AACvD,MAAI,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,SAAU,QAAO,CAAC,CAAC,CAAC;AACnE,QAAM,MAAM;AACZ,QAAM,cAAgC,CAAC;AAEvC,aAAW,CAAC,QAAQ,MAAM,KAAK,OAAO,QAAQ,GAAG,GAAG;AAClD,QAAI,WAAW,UAAU,MAAM,QAAQ,MAAM,GAAG;AAC9C,kBAAY,KAAK,aAAc,OAAqB,IAAI,YAAY,CAAC,CAAC;AACtE;AAAA,IACF;AACA,QAAI,WAAW,SAAS,MAAM,QAAQ,MAAM,GAAG;AAC7C,YAAM,WAAY,OAAqB,QAAQ,YAAY;AAC3D,UAAI,SAAS,SAAS,mBAAmB;AACvC,cAAM,IAAI,8BAA8B,SAAS,MAAM;AAAA,MACzD;AACA,kBAAY,KAAK,QAAQ;AACzB;AAAA,IACF;AACA,UAAM,QAAQ,eAAe,MAAM;AACnC,QAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC3E,YAAM,UAAwB,CAAC;AAC/B,iBAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,MAAiC,GAAG;AAC9E,cAAM,KAAM,MAAM,WAAW,GAAG,IAAI,MAAM,MAAM,CAAC,IAAI;AACrD,YAAI,UAAU,SAAS,EAAgC,GAAG;AACxD,kBAAQ,KAAK,EAAE,OAAO,IAAI,OAAO,MAAM,CAAC;AAAA,QAC1C;AAAA,MACF;AACA,UAAI,QAAQ,SAAS,EAAG,aAAY,KAAK,CAAC,OAAO,CAAC;AAAA,IACpD,OAAO;AACL,kBAAY,KAAK,CAAC,CAAC,EAAE,OAAO,IAAI,MAAkB,OAAO,OAAO,CAAC,CAAC,CAAC;AAAA,IACrE;AAAA,EACF;AAEA,SAAO,aAAa,WAAW;AACjC;AAEA,SAAS,aAAa,MAAwC;AAC5D,MAAI,KAAK,WAAW,EAAG,QAAO,CAAC,CAAC,CAAC;AACjC,MAAI,SAAyB,CAAC,CAAC,CAAC;AAChC,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,WAAW,GAAG;AAGpB,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,YAAY,OAAO,SAAS,IAAI;AACtC,QAAI,YAAY,mBAAmB;AACjC,YAAM,IAAI,8BAA8B,SAAS;AAAA,IACnD;AACA,UAAM,OAAuB,CAAC;AAC9B,eAAW,QAAQ,QAAQ;AACzB,iBAAW,SAAS,KAAK;AACvB,aAAK,KAAK,CAAC,GAAG,MAAM,GAAG,KAAK,CAAC;AAAA,MAC/B;AAAA,IACF;AACA,aAAS;AAAA,EACX;AACA,SAAO;AACT;AAEA,SAAS,UAAU,GAAuB;AACxC,SAAO,KAAK,UAAU,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC;AAChD;AAQA,SAAS,kBAAkB,KAA0E;AACnG,MAAI,IAAI,UAAU,EAAG,QAAO,EAAE,QAAQ,CAAC,GAAG,WAAW,IAAI;AACzD,QAAM,SAAS,oBAAI,IAAmD;AACtE,aAAW,YAAY,KAAK;AAC1B,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,KAAK,UAAU;AACxB,YAAM,MAAM,UAAU,CAAC;AACvB,UAAI,KAAK,IAAI,GAAG,EAAG;AACnB,WAAK,IAAI,GAAG;AACZ,YAAM,QAAQ,OAAO,IAAI,GAAG;AAC5B,UAAI,MAAO,OAAM,SAAS;AAAA,UACrB,QAAO,IAAI,KAAK,EAAE,QAAQ,GAAG,OAAO,EAAE,CAAC;AAAA,IAC9C;AAAA,EACF;AACA,QAAM,QAAQ,IAAI;AAClB,QAAM,aAAa,oBAAI,IAAY;AACnC,QAAM,SAAuB,CAAC;AAC9B,aAAW,CAAC,KAAK,EAAE,QAAQ,MAAM,CAAC,KAAK,QAAQ;AAC7C,QAAI,UAAU,OAAO;AACnB,iBAAW,IAAI,GAAG;AAClB,aAAO,KAAK,MAAM;AAAA,IACpB;AAAA,EACF;AACA,MAAI,OAAO,WAAW,EAAG,QAAO,EAAE,QAAQ,CAAC,GAAG,WAAW,IAAI;AAC7D,QAAM,YAAY,IAAI,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;AAC/E,SAAO,EAAE,QAAQ,UAAU;AAC7B;AAEO,SAAS,iBAAiB,SAAuD;AACtF,MAAI,CAAC,QAAS,QAAO,CAAC;AACtB,MAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,WAAQ,QAAkB,IAAI,CAAC,OAAO;AAAA,MACpC,GAAG;AAAA,MACH,OAAO,eAAe,OAAQ,EAAU,KAAK,CAAC;AAAA,IAChD,EAAE;AAAA,EACJ;AACA,QAAM,MAAM,aAAa,OAAO;AAChC,MAAI,IAAI,WAAW,EAAG,QAAO,CAAC;AAC9B,QAAM,WAAW,IAAI,OAAO,CAAC,aAAa,SAAS,SAAS,CAAC;AAC7D,MAAI,SAAS,WAAW,EAAG,QAAO,CAAC;AACnC,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO,SAAS,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,IAAI,OAAO,EAAE,MAAM,EAAE;AAAA,EAC9E;AACA,QAAM,EAAE,QAAQ,UAAU,IAAI,kBAAkB,QAAQ;AACxD,QAAM,MAA0B,CAAC;AACjC,aAAW,KAAK,OAAQ,KAAI,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,IAAI,OAAO,EAAE,MAAM,CAAC;AAE7E,QAAM,oBAAoB,UAAU,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC9D,MAAI,kBAAkB,WAAW,EAAG,QAAO;AAC3C,MAAI,kBAAkB,WAAW,GAAG;AAClC,eAAW,KAAK,kBAAkB,CAAC,EAAG,KAAI,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,IAAI,OAAO,EAAE,MAAM,CAAC;AAC3F,WAAO;AAAA,EACT;AACA,WAAS,IAAI,GAAG,IAAI,kBAAkB,QAAQ,KAAK;AACjD,eAAW,KAAK,kBAAkB,CAAC,GAAG;AACpC,UAAI,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,IAAI,OAAO,EAAE,OAAO,SAAS,MAAM,CAAC,GAAG,CAAC;AAAA,IAC3E;AAAA,EACF;AACA,SAAO;AACT;AAeO,SAAS,aACd,WACA,OACA,cACgB;AAChB,MAAI,CAAC,SAAS,MAAM,WAAW,EAAG,QAAO,CAAC;AAC1C,QAAM,WAA2B,CAAC;AAClC,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,SAAS,OAAO;AACzB,QAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AACzC,UAAM,QAAQ,OAAO,MAAM,UAAU,WAAW,MAAM,MAAM,KAAK,IAAI;AACrE,QAAI,CAAC,MAAO;AACZ,QAAI,KAAK,IAAI,KAAK,EAAG;AACrB,UAAM,QACJ,MAAM,UACL,MAAM,WAAW,aAAa,OAAO,MAAM,QAAQ,CAAC,IAAI;AAC3D,QAAI,CAAC,MAAO;AACZ,UAAM,YAAY,MAAM,MAAM,OAAO,KAAK;AAC1C,UAAM,UAAU,MAAM,IAAI,OAAO,KAAK;AACtC,QAAI,CAAC,aAAa,CAAC,QAAS;AAC5B,UAAM,eAAe,MAAM,MAAM,OAAO,KAAK;AAC7C,UAAM,YAAY,gBAAgB,aAAa,SAAS,IAAI,eAAe;AAC3E,UAAM,OAAyB,MAAM,SAAS,UAAU,UAAU;AAClE,aAAS,KAAK;AAAA,MACZ;AAAA,MACA;AAAA,MACA,UAAU,MAAM,WAAW,OAAO,MAAM,QAAQ,IAAI;AAAA,MACpD;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,SAAK,IAAI,KAAK;AAAA,EAChB;AACA,SAAO;AACT;AAEO,SAAS,eACd,OACA,SACA,WACA,UAAuB,oBAAI,IAAI,GACf;AAChB,MAAI,QAAQ,IAAI,KAAK,GAAG;AACtB,UAAM,IAAI,MAAM,2DAA2D,KAAK,EAAE;AAAA,EACpF;AACA,QAAM,MAAM,QAAQ,IAAI,KAAK;AAC7B,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,UAAQ,IAAI,KAAK;AACjB,MAAI,CAAC,IAAI,aAAa,IAAI,cAAc,UAAU,IAAI,cAAc,WAAW;AAC7E,WAAO,CAAC,GAAG;AAAA,EACb;AACA,QAAM,cAAc,eAAe,IAAI,WAAW,SAAS,WAAW,OAAO;AAC7E,MAAI,YAAY,WAAW,EAAG,QAAO,CAAC;AACtC,SAAO,CAAC,GAAG,aAAa,GAAG;AAC7B;AAEO,SAAS,iBACd,WACA,SACA,SACuE;AACvE,QAAM,cAA4B,CAAC;AACnC,QAAM,cAAc,oBAAI,IAA0B;AAClD,aAAW,UAAU,SAAS;AAC5B,UAAM,QAAQ,OAAO,OAAO,KAAK;AACjC,QAAI,MAAM,WAAW,KAAK,EAAG;AAC7B,UAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,QAAI,MAAM,WAAW,GAAG;AACtB,YAAM,CAAC,cAAc,MAAM,IAAI;AAC/B,YAAM,YAAY,gBAAgB;AAClC,UAAI,QAAQ,IAAI,SAAS,GAAG;AAC1B,cAAM,OAAO,YAAY,IAAI,SAAS,KAAK,CAAC;AAC5C,aAAK,KAAK,EAAE,OAAO,WAAW,QAAQ,IAAI,OAAO,IAAI,OAAO,OAAO,MAAM,CAAC;AAC1E,oBAAY,IAAI,WAAW,IAAI;AAC/B;AAAA,MACF;AACA,UAAI,cAAc,aAAa,cAAc,QAAQ;AACnD,oBAAY,KAAK;AAAA,UACf,OAAO;AAAA,UACP,IAAI,OAAO;AAAA,UACX,OAAO,OAAO;AAAA,UACd,WAAW,GAAG,SAAS,IAAI,MAAM;AAAA,QACnC,CAAC;AACD;AAAA,MACF;AAAA,IACF;AACA,gBAAY,KAAK,EAAE,GAAG,OAAO,CAAC;AAAA,EAChC;AACA,SAAO,EAAE,aAAa,YAAY;AACpC;AAgBA,eAAsB,iBAAiB;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAiD;AAC/C,QAAM,mBAAmB,CAAC,cAA8B;AACtD,QAAI,CAAC,aAAa,cAAc,OAAQ,QAAO;AAC/C,WAAO;AAAA,EACT;AAEA,MAAI,cAAc;AAClB,aAAW,CAAC,OAAO,eAAe,KAAK,YAAY,QAAQ,GAAG;AAC5D,UAAM,QAAQ,eAAe,OAAO,SAAS,SAAS;AACtD,QAAI,CAAC,MAAM,OAAQ;AACnB,UAAM,QAAQ,MAAM,CAAC;AACrB,QAAI,MAAkB,GAAG,WAAW,GAAG,MAAM,KAAK,OAAO,MAAM,KAAK,EAAS,EAAE,OAAO,OAAO,GAAG,KAAK,CAAC;AACtG,UAAM,MAAM,gBAAgB,KAAK,MAAM,OAAO,MAAM,KAAK;AACzD,UAAM,cAAc,iBAAiB,MAAM,SAAS;AACpD,UAAM,YAAY,gBAAgB,YAAY,YAAY,MAAM,SAAS,IAAI,GAAG,WAAW,IAAI,MAAM,SAAS;AAC9G,UAAM,IAAI,SAAS,GAAG,MAAM,KAAK,IAAI,MAAM,OAAO,IAAI,KAAK,SAAS;AACpE,eAAW,OAAO,MAAM,MAAM,CAAC,GAAG;AAChC,YAAM,SAAS,iBAAiB,IAAI,SAAS;AAC7C,YAAM,WAAW,WAAW,YAAY,YAAY,IAAI,SAAS,IAAI,GAAG,MAAM,IAAI,IAAI,SAAS;AAC/F,YAAM,SAAS,IAAI,SAAS,UAAU,cAAc;AACpD,YAAO,IAAY,MAAM,EAAE,GAAG,IAAI,KAAK,OAAO,IAAI,KAAK,IAAI,CAAC,OAC1D,GAAG,MAAM,GAAG,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC;AACxD,YAAM,MAAM,gBAAgB,KAAK,IAAI,OAAO,IAAI,KAAK;AAAA,IACvD;AACA,QAAI,kBAAkC;AACtC,eAAW,UAAU,iBAAiB;AACpC,UAAI,OAAO,OAAO,UAAU;AAC1B,YAAI,OAAO,UAAU,MAAO,mBAAkB;AAAA,iBACrC,oBAAoB,KAAM,mBAAkB;AACrD;AAAA,MACF;AACA,YAAM,OAAO,QAAQ,IAAI,OAAO,KAAK;AACrC,UAAI,CAAC,KAAM;AACX,YAAM,cAAc,YAAY,IAAI,OAAO,KAAK;AAChD,UAAI,CAAC,YAAa;AAClB,UAAI,cAAc;AAChB,cAAM,SAAS,MAAM,aAAa,aAAa,OAAO,MAAM;AAC5D,YAAI,CAAC,OAAQ;AAAA,MACf;AACA,YAAM,YAAY,GAAG,OAAO,KAAK,IAAI,OAAO,MAAM;AAClD,UAAI,mBAAmB;AACrB,cAAM,SAAS,MAAM,kBAAkB,KAAK,QAAQ,WAAW,MAAM,WAAW;AAChF,YAAI,UAAU,OAAO,SAAS;AAC5B,gBAAM,OAAO;AACb;AAAA,QACF;AACA,YAAI,UAAU,OAAO,SAAS;AAC5B,gBAAM,OAAO;AAAA,QACf;AAAA,MACF;AACA,YAAM,cAAc,KAAK,WAAW,OAAO,IAAI,OAAO,KAAK;AAAA,IAC7D;AACA,QAAI,oBAAoB,OAAO;AAC7B,YAAM,cAAc;AACpB,oBAAc,YAAY,MAAM,CAAC,OAAY,GAAG,IAAI,GAAG,OAAO,WAAW,CAAC,CAAC;AAAA,IAC7E,OAAO;AACL,YAAM,cAAc;AACpB,oBAAc,YAAY,MAAM,CAAC,OAAY,GAAG,OAAO,WAAW,CAAC;AAAA,IACrE;AAAA,EACF;AACA,SAAO;AACT;",
4
+ "sourcesContent": ["import type { Kysely } from 'kysely'\nimport { sql } from 'kysely'\nimport type { QueryOptions, QueryJoinEdge } from './types'\nimport type { FilterOp } from './types'\n\ntype AnyBuilder = any\n\nexport type NormalizedFilter = { field: string; op: FilterOp; value?: unknown; orGroup?: string; qualified?: string | null }\n\nconst VALID_OPS = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'like', 'ilike', 'exists', 'overlap', 'noverlap'] as const\n\nconst normalizeField = (key: string) => (key.startsWith('cf_') ? `cf:${key.slice(3)}` : key)\n\ntype LeafClause = { field: string; op: FilterOp; value?: unknown }\n\n/**\n * Cap on DNF disjunct count to prevent adversarial / pathological filters from\n * exploding the in-memory normalization. `validateTreeLimits` already caps\n * editor input at 50 rules / 15 children per group / 3 levels of nesting, but\n * a hand-crafted URL or buggy preset could still bypass that. The cap mirrors\n * the editor's posture: fail loud, refuse to compile.\n */\nexport const MAX_DNF_DISJUNCTS = 1000\n\nexport class AdvancedFilterComplexityError extends Error {\n readonly code = 'ADVANCED_FILTER_TOO_COMPLEX'\n readonly disjunctCount: number\n constructor(disjunctCount: number) {\n super(\n `Advanced filter expanded to ${disjunctCount} disjuncts, exceeding the limit of ${MAX_DNF_DISJUNCTS}. ` +\n `Reduce the number of OR branches or simplify nested groups.`,\n )\n this.name = 'AdvancedFilterComplexityError'\n this.disjunctCount = disjunctCount\n }\n}\n\n/**\n * Expand a Mongo-style filter object (with arbitrary nesting of `$and`/`$or` and\n * implicit AND between sibling keys) into Disjunctive Normal Form: `Clause[][]`,\n * where the outer array is OR'd alternatives and each inner array is the AND'd\n * leaves of one alternative.\n *\n * Empty filter -> `[[]]` (one empty disjunct, treated as \"no constraint\").\n * Throws `AdvancedFilterComplexityError` if any intermediate cartesian product\n * exceeds `MAX_DNF_DISJUNCTS`.\n */\nfunction compileToDnf(filter: unknown): LeafClause[][] {\n if (filter === null || filter === undefined) return [[]]\n if (Array.isArray(filter) || typeof filter !== 'object') return [[]]\n const obj = filter as Record<string, unknown>\n const partialDnfs: LeafClause[][][] = []\n\n for (const [rawKey, rawVal] of Object.entries(obj)) {\n if (rawKey === '$and' && Array.isArray(rawVal)) {\n partialDnfs.push(cartesianAnd((rawVal as unknown[]).map(compileToDnf)))\n continue\n }\n if (rawKey === '$or' && Array.isArray(rawVal)) {\n const expanded = (rawVal as unknown[]).flatMap(compileToDnf)\n if (expanded.length > MAX_DNF_DISJUNCTS) {\n throw new AdvancedFilterComplexityError(expanded.length)\n }\n partialDnfs.push(expanded)\n continue\n }\n const field = normalizeField(rawKey)\n if (rawVal !== null && typeof rawVal === 'object' && !Array.isArray(rawVal)) {\n const clauses: LeafClause[] = []\n for (const [opKey, opVal] of Object.entries(rawVal as Record<string, unknown>)) {\n const op = (opKey.startsWith('$') ? opKey.slice(1) : opKey) as FilterOp\n if (VALID_OPS.includes(op as (typeof VALID_OPS)[number])) {\n clauses.push({ field, op, value: opVal })\n }\n }\n if (clauses.length > 0) partialDnfs.push([clauses])\n } else {\n partialDnfs.push([[{ field, op: 'eq' as FilterOp, value: rawVal }]])\n }\n }\n\n return cartesianAnd(partialDnfs)\n}\n\nfunction cartesianAnd(dnfs: LeafClause[][][]): LeafClause[][] {\n if (dnfs.length === 0) return [[]]\n let result: LeafClause[][] = [[]]\n for (const dnf of dnfs) {\n if (dnf.length === 0) {\n // An empty DNF means an unsatisfiable subexpression; the AND becomes unsatisfiable\n // too. Represent that as an empty outer array (no disjuncts).\n return []\n }\n // Predict the next size to fail loud before allocating millions of arrays.\n const projected = result.length * dnf.length\n if (projected > MAX_DNF_DISJUNCTS) {\n throw new AdvancedFilterComplexityError(projected)\n }\n const next: LeafClause[][] = []\n for (const left of result) {\n for (const right of dnf) {\n next.push([...left, ...right])\n }\n }\n result = next\n }\n return result\n}\n\nfunction clauseKey(c: LeafClause): string {\n return JSON.stringify([c.field, c.op, c.value])\n}\n\n/**\n * Lift clauses that appear in every disjunct into a \"common\" set so the engine\n * can treat them as regular ANDed base filters (preserving the search-tokens\n * optimization for like/ilike clauses) instead of duplicating them across OR\n * groups.\n */\nfunction liftCommonClauses(dnf: LeafClause[][]): { common: LeafClause[]; remaining: LeafClause[][] } {\n if (dnf.length <= 1) return { common: [], remaining: dnf }\n const counts = new Map<string, { clause: LeafClause; count: number }>()\n for (const disjunct of dnf) {\n const seen = new Set<string>()\n for (const c of disjunct) {\n const key = clauseKey(c)\n if (seen.has(key)) continue // count once per disjunct even if duplicated\n seen.add(key)\n const entry = counts.get(key)\n if (entry) entry.count += 1\n else counts.set(key, { clause: c, count: 1 })\n }\n }\n const total = dnf.length\n const commonKeys = new Set<string>()\n const common: LeafClause[] = []\n for (const [key, { clause, count }] of counts) {\n if (count === total) {\n commonKeys.add(key)\n common.push(clause)\n }\n }\n if (common.length === 0) return { common: [], remaining: dnf }\n const remaining = dnf.map((d) => d.filter((c) => !commonKeys.has(clauseKey(c))))\n return { common, remaining }\n}\n\nexport function normalizeFilters(filters?: QueryOptions['filters']): NormalizedFilter[] {\n if (!filters) return []\n if (Array.isArray(filters)) {\n return (filters as any[]).map((f) => ({\n ...f,\n field: normalizeField(String((f as any).field)),\n }))\n }\n const dnf = compileToDnf(filters)\n if (dnf.length === 0) return []\n const nonEmpty = dnf.filter((disjunct) => disjunct.length > 0)\n if (nonEmpty.length === 0) return []\n if (nonEmpty.length === 1) {\n return nonEmpty[0].map((c) => ({ field: c.field, op: c.op, value: c.value }))\n }\n const { common, remaining } = liftCommonClauses(nonEmpty)\n const out: NormalizedFilter[] = []\n for (const c of common) out.push({ field: c.field, op: c.op, value: c.value })\n // Drop disjuncts that became empty after lifting (they're already represented in `common`).\n const remainingNonEmpty = remaining.filter((d) => d.length > 0)\n if (remainingNonEmpty.length === 0) return out\n if (remainingNonEmpty.length === 1) {\n for (const c of remainingNonEmpty[0]) out.push({ field: c.field, op: c.op, value: c.value })\n return out\n }\n for (let i = 0; i < remainingNonEmpty.length; i++) {\n for (const c of remainingNonEmpty[i]) {\n out.push({ field: c.field, op: c.op, value: c.value, orGroup: `or_${i}` })\n }\n }\n return out\n}\n\nexport type ResolvedJoin = {\n alias: string\n table: string\n entityId?: string | null\n fromAlias: string\n fromField: string\n toField: string\n type: 'left' | 'inner'\n}\n\nexport type BaseFilter = NormalizedFilter & { qualified?: string | null }\nexport type JoinFilter = { alias: string; column: string; op: FilterOp; value?: unknown }\n\nexport function resolveJoins(\n baseTable: string,\n joins: QueryJoinEdge[] | null | undefined,\n resolveTable: (entityId: string) => string | null,\n): ResolvedJoin[] {\n if (!joins || joins.length === 0) return []\n const resolved: ResolvedJoin[] = []\n const seen = new Set<string>()\n for (const entry of joins) {\n if (!entry || typeof entry !== 'object') continue\n const alias = typeof entry.alias === 'string' ? entry.alias.trim() : ''\n if (!alias) continue\n if (seen.has(alias)) continue\n const table =\n entry.table ??\n (entry.entityId ? resolveTable(String(entry.entityId)) : null)\n if (!table) continue\n const fromField = entry.from?.field?.trim()\n const toField = entry.to?.field?.trim()\n if (!fromField || !toField) continue\n const fromAliasRaw = entry.from?.alias?.trim()\n const fromAlias = fromAliasRaw && fromAliasRaw.length > 0 ? fromAliasRaw : 'base'\n const type: 'left' | 'inner' = entry.type === 'inner' ? 'inner' : 'left'\n resolved.push({\n alias,\n table,\n entityId: entry.entityId ? String(entry.entityId) : null,\n fromAlias,\n fromField,\n toField,\n type,\n })\n seen.add(alias)\n }\n return resolved\n}\n\nexport function buildJoinChain(\n alias: string,\n joinMap: Map<string, ResolvedJoin>,\n baseTable: string,\n visited: Set<string> = new Set(),\n): ResolvedJoin[] {\n if (visited.has(alias)) {\n throw new Error(`QueryEngine: circular join reference detected for alias ${alias}`)\n }\n const cfg = joinMap.get(alias)\n if (!cfg) return []\n visited.add(alias)\n if (!cfg.fromAlias || cfg.fromAlias === 'base' || cfg.fromAlias === baseTable) {\n return [cfg]\n }\n const parentChain = buildJoinChain(cfg.fromAlias, joinMap, baseTable, visited)\n if (parentChain.length === 0) return []\n return [...parentChain, cfg]\n}\n\nexport function partitionFilters(\n baseTable: string,\n filters: NormalizedFilter[],\n joinMap: Map<string, ResolvedJoin>,\n): { baseFilters: BaseFilter[]; joinFilters: Map<string, JoinFilter[]> } {\n const baseFilters: BaseFilter[] = []\n const joinFilters = new Map<string, JoinFilter[]>()\n for (const filter of filters) {\n const field = String(filter.field)\n if (field.startsWith('cf:')) continue\n const parts = field.split('.')\n if (parts.length === 2) {\n const [aliasNameRaw, column] = parts\n const aliasName = aliasNameRaw || ''\n if (joinMap.has(aliasName)) {\n const list = joinFilters.get(aliasName) ?? []\n list.push({ alias: aliasName, column, op: filter.op, value: filter.value })\n joinFilters.set(aliasName, list)\n continue\n }\n if (aliasName === baseTable || aliasName === 'base') {\n baseFilters.push({\n field: column,\n op: filter.op,\n value: filter.value,\n qualified: `${baseTable}.${column}`,\n })\n continue\n }\n }\n baseFilters.push({ ...filter })\n }\n return { baseFilters, joinFilters }\n}\n\ntype ApplyJoinFiltersOptions = {\n db: Kysely<any>\n baseTable: string\n builder: AnyBuilder\n joinMap: Map<string, ResolvedJoin>\n joinFilters: Map<string, JoinFilter[]>\n aliasTables: Map<string, string>\n qualifyBase: (column: string) => string\n applyAliasScope: (builder: AnyBuilder, alias: string, table: string) => Promise<AnyBuilder> | AnyBuilder\n applyFilterOp: (builder: AnyBuilder, column: string, op: FilterOp, value?: unknown) => AnyBuilder\n applyJoinFilterOp?: (builder: AnyBuilder, filter: JoinFilter, qualified: string, join: ResolvedJoin, table: string) => Promise<{ applied: boolean; builder: AnyBuilder }> | { applied: boolean; builder: AnyBuilder }\n columnExists?: (table: string, column: string) => Promise<boolean> | boolean\n}\n\nexport async function applyJoinFilters({\n db,\n baseTable,\n builder,\n joinMap,\n joinFilters,\n aliasTables,\n qualifyBase,\n applyAliasScope,\n applyFilterOp,\n applyJoinFilterOp,\n columnExists,\n}: ApplyJoinFiltersOptions): Promise<AnyBuilder> {\n const resolveAliasName = (aliasName?: string | null) => {\n if (!aliasName || aliasName === 'base') return baseTable\n return aliasName\n }\n\n let nextBuilder = builder\n for (const [alias, filtersForAlias] of joinFilters.entries()) {\n const chain = buildJoinChain(alias, joinMap, baseTable)\n if (!chain.length) continue\n const first = chain[0]\n let sub: AnyBuilder = db.selectFrom(`${first.table} as ${first.alias}` as any).select(sql`1`.as('one'))\n sub = await applyAliasScope(sub, first.alias, first.table)\n const parentAlias = resolveAliasName(first.fromAlias)\n const parentRef = parentAlias === baseTable ? qualifyBase(first.fromField) : `${parentAlias}.${first.fromField}`\n sub = sub.whereRef(`${first.alias}.${first.toField}`, '=', parentRef)\n for (const cfg of chain.slice(1)) {\n const parent = resolveAliasName(cfg.fromAlias)\n const rightRef = parent === baseTable ? qualifyBase(cfg.fromField) : `${parent}.${cfg.fromField}`\n const joinFn = cfg.type === 'inner' ? 'innerJoin' : 'leftJoin'\n sub = (sub as any)[joinFn](`${cfg.table} as ${cfg.alias}`, (jb: any) =>\n jb.onRef(`${cfg.alias}.${cfg.toField}`, '=', rightRef))\n sub = await applyAliasScope(sub, cfg.alias, cfg.table)\n }\n let existsDirective: boolean | null = null\n for (const filter of filtersForAlias) {\n if (filter.op === 'exists') {\n if (filter.value === false) existsDirective = false\n else if (existsDirective === null) existsDirective = true\n continue\n }\n const join = joinMap.get(filter.alias)\n if (!join) continue\n const targetTable = aliasTables.get(filter.alias)\n if (!targetTable) continue\n if (columnExists) {\n const exists = await columnExists(targetTable, filter.column)\n if (!exists) continue\n }\n const qualified = `${filter.alias}.${filter.column}`\n if (applyJoinFilterOp) {\n const result = await applyJoinFilterOp(sub, filter, qualified, join, targetTable)\n if (result && result.applied) {\n sub = result.builder\n continue\n }\n if (result && result.builder) {\n sub = result.builder\n }\n }\n sub = applyFilterOp(sub, qualified, filter.op, filter.value)\n }\n if (existsDirective === false) {\n const capturedSub = sub\n nextBuilder = nextBuilder.where((eb: any) => eb.not(eb.exists(capturedSub)))\n } else {\n const capturedSub = sub\n nextBuilder = nextBuilder.where((eb: any) => eb.exists(capturedSub))\n }\n }\n return nextBuilder\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,58 @@
1
+ import { sql } from "kysely";
2
+ const LITERAL_SAFE_DOC_KEY = /^[A-Za-z0-9_:.-]{1,128}$/;
3
+ function normalizeOverlapValues(value) {
4
+ const list = Array.isArray(value) ? value : value === void 0 || value === null ? [] : [value];
5
+ const out = [];
6
+ const seen = /* @__PURE__ */ new Set();
7
+ for (const entry of list) {
8
+ if (entry === void 0 || entry === null) continue;
9
+ const normalized = String(entry);
10
+ if (seen.has(normalized)) continue;
11
+ seen.add(normalized);
12
+ out.push(normalized);
13
+ }
14
+ return out;
15
+ }
16
+ function jsonbDocKeyExpression(docColumnRef, key) {
17
+ const keyExpr = LITERAL_SAFE_DOC_KEY.test(key) ? sql.lit(key) : sql`${key}`;
18
+ return sql`(${sql.ref(docColumnRef)} -> ${keyExpr})`;
19
+ }
20
+ function buildJsonbOverlapPredicate(docColumnRef, key, value) {
21
+ const values = normalizeOverlapValues(value);
22
+ if (!values.length) return sql`false`;
23
+ return sql`${jsonbDocKeyExpression(docColumnRef, key)} ?| ${values}::text[]`;
24
+ }
25
+ function buildJsonbNoOverlapPredicate(docColumnRef, key, value) {
26
+ const values = normalizeOverlapValues(value);
27
+ const keyExpr = jsonbDocKeyExpression(docColumnRef, key);
28
+ const isArray = sql`jsonb_typeof(${keyExpr}) = 'array'`;
29
+ if (!values.length) return isArray;
30
+ return sql`(${isArray} and not (${keyExpr} ?| ${values}::text[]))`;
31
+ }
32
+ function buildArrayColumnOverlapPredicate(column, value) {
33
+ const values = normalizeOverlapValues(value);
34
+ if (!values.length) return sql`false`;
35
+ const columnExpr = typeof column === "string" ? sql.ref(column) : column;
36
+ return sql`${columnExpr} && ${values}`;
37
+ }
38
+ function buildArrayColumnNoOverlapPredicate(column, value) {
39
+ const values = normalizeOverlapValues(value);
40
+ const columnExpr = typeof column === "string" ? sql.ref(column) : column;
41
+ if (!values.length) return sql`${columnExpr} is not null`;
42
+ return sql`not (${columnExpr} && ${values})`;
43
+ }
44
+ function buildScalarOverlapPredicate(expression, value) {
45
+ const values = normalizeOverlapValues(value);
46
+ if (!values.length) return sql`false`;
47
+ return sql`${expression} in (${sql.join(values.map((entry) => sql`${entry}`), sql`, `)})`;
48
+ }
49
+ export {
50
+ buildArrayColumnNoOverlapPredicate,
51
+ buildArrayColumnOverlapPredicate,
52
+ buildJsonbNoOverlapPredicate,
53
+ buildJsonbOverlapPredicate,
54
+ buildScalarOverlapPredicate,
55
+ jsonbDocKeyExpression,
56
+ normalizeOverlapValues
57
+ };
58
+ //# sourceMappingURL=overlap.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/query/overlap.ts"],
4
+ "sourcesContent": ["import { sql, type RawBuilder } from 'kysely'\n\nconst LITERAL_SAFE_DOC_KEY = /^[A-Za-z0-9_:.-]{1,128}$/\n\n/**\n * Normalizes the right-hand side of an `overlap` filter into a de-duplicated list of strings.\n *\n * `null`/`undefined` entries are dropped; a scalar is treated as a one-element list. An empty\n * result means the filter matches nothing \u2014 every `overlap` builder compiles it to `false`\n * rather than dropping the predicate, so a caller that computes an empty inclusion set can\n * never widen a query by accident.\n */\nexport function normalizeOverlapValues(value: unknown): string[] {\n const list: readonly unknown[] = Array.isArray(value)\n ? value\n : value === undefined || value === null\n ? []\n : [value]\n const out: string[] = []\n const seen = new Set<string>()\n for (const entry of list) {\n if (entry === undefined || entry === null) continue\n const normalized = String(entry)\n if (seen.has(normalized)) continue\n seen.add(normalized)\n out.push(normalized)\n }\n return out\n}\n\n/**\n * `(<docColumn> -> '<key>')` \u2014 the jsonb sub-document of an index document.\n *\n * The key is emitted as an escaped SQL literal when it is a plain identifier so the expression is\n * textually identical to a GIN expression index such as `((doc -> 'scope_keys'))` and the planner\n * can match it. Any other key falls back to a bound parameter (still correct, just not indexable).\n */\nexport function jsonbDocKeyExpression(docColumnRef: string, key: string): RawBuilder<unknown> {\n const keyExpr = LITERAL_SAFE_DOC_KEY.test(key) ? sql.lit(key) : sql`${key}`\n return sql`(${sql.ref(docColumnRef)} -> ${keyExpr})`\n}\n\n/**\n * jsonb \"has any of\" over a string-array key of an index document:\n * `(<docColumn> -> '<key>') ?| $n::text[]`. The values are bound as one array parameter.\n * A missing key or a JSON `null` evaluates to SQL NULL, i.e. the row does not match.\n */\nexport function buildJsonbOverlapPredicate(docColumnRef: string, key: string, value: unknown): RawBuilder<boolean> {\n const values = normalizeOverlapValues(value)\n if (!values.length) return sql<boolean>`false`\n return sql<boolean>`${jsonbDocKeyExpression(docColumnRef, key)} ?| ${values}::text[]`\n}\n\n/**\n * jsonb \"has none of\" over a string-array key of an index document (`noverlap`):\n * `(jsonb_typeof(<expr>) = 'array' and not (<expr> ?| $n::text[]))`.\n *\n * Only a document whose key holds a JSON array can match: a missing key, a JSON `null` or a\n * scalar never satisfies it, so an unindexed document can never slip through an exclusion.\n * An empty value list excludes nothing (only the array check remains).\n */\nexport function buildJsonbNoOverlapPredicate(docColumnRef: string, key: string, value: unknown): RawBuilder<boolean> {\n const values = normalizeOverlapValues(value)\n const keyExpr = jsonbDocKeyExpression(docColumnRef, key)\n const isArray = sql<boolean>`jsonb_typeof(${keyExpr}) = 'array'`\n if (!values.length) return isArray\n return sql<boolean>`(${isArray} and not (${keyExpr} ?| ${values}::text[]))`\n}\n\n/**\n * Postgres array overlap for array-typed columns: `<column> && $n`. The bound array takes its\n * element type from the column, so `text[]`, `varchar[]` and `uuid[]` columns all work.\n */\nexport function buildArrayColumnOverlapPredicate(\n column: string | RawBuilder<unknown>,\n value: unknown,\n): RawBuilder<boolean> {\n const values = normalizeOverlapValues(value)\n if (!values.length) return sql<boolean>`false`\n const columnExpr = typeof column === 'string' ? sql.ref(column) : column\n return sql<boolean>`${columnExpr} && ${values}`\n}\n\n/**\n * Postgres array \"has none of\" for array-typed columns (`noverlap`): `not (<column> && $n)`.\n * A NULL column never matches; an empty value list excludes nothing (`<column> is not null`).\n */\nexport function buildArrayColumnNoOverlapPredicate(\n column: string | RawBuilder<unknown>,\n value: unknown,\n): RawBuilder<boolean> {\n const values = normalizeOverlapValues(value)\n const columnExpr = typeof column === 'string' ? sql.ref(column) : column\n if (!values.length) return sql<boolean>`${columnExpr} is not null`\n return sql<boolean>`not (${columnExpr} && ${values})`\n}\n\n/**\n * `overlap` over a scalar expression evaluated once per stored value (custom-field value rows):\n * the record matches when any of its values is one of the requested ones \u2014 `<expr> in (...)`.\n */\nexport function buildScalarOverlapPredicate(expression: RawBuilder<unknown>, value: unknown): RawBuilder<boolean> {\n const values = normalizeOverlapValues(value)\n if (!values.length) return sql<boolean>`false`\n return sql<boolean>`${expression} in (${sql.join(values.map((entry) => sql`${entry}`), sql`, `)})`\n}\n"],
5
+ "mappings": "AAAA,SAAS,WAA4B;AAErC,MAAM,uBAAuB;AAUtB,SAAS,uBAAuB,OAA0B;AAC/D,QAAM,OAA2B,MAAM,QAAQ,KAAK,IAChD,QACA,UAAU,UAAa,UAAU,OAC/B,CAAC,IACD,CAAC,KAAK;AACZ,QAAM,MAAgB,CAAC;AACvB,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,SAAS,MAAM;AACxB,QAAI,UAAU,UAAa,UAAU,KAAM;AAC3C,UAAM,aAAa,OAAO,KAAK;AAC/B,QAAI,KAAK,IAAI,UAAU,EAAG;AAC1B,SAAK,IAAI,UAAU;AACnB,QAAI,KAAK,UAAU;AAAA,EACrB;AACA,SAAO;AACT;AASO,SAAS,sBAAsB,cAAsB,KAAkC;AAC5F,QAAM,UAAU,qBAAqB,KAAK,GAAG,IAAI,IAAI,IAAI,GAAG,IAAI,MAAM,GAAG;AACzE,SAAO,OAAO,IAAI,IAAI,YAAY,CAAC,OAAO,OAAO;AACnD;AAOO,SAAS,2BAA2B,cAAsB,KAAa,OAAqC;AACjH,QAAM,SAAS,uBAAuB,KAAK;AAC3C,MAAI,CAAC,OAAO,OAAQ,QAAO;AAC3B,SAAO,MAAe,sBAAsB,cAAc,GAAG,CAAC,OAAO,MAAM;AAC7E;AAUO,SAAS,6BAA6B,cAAsB,KAAa,OAAqC;AACnH,QAAM,SAAS,uBAAuB,KAAK;AAC3C,QAAM,UAAU,sBAAsB,cAAc,GAAG;AACvD,QAAM,UAAU,mBAA4B,OAAO;AACnD,MAAI,CAAC,OAAO,OAAQ,QAAO;AAC3B,SAAO,OAAgB,OAAO,aAAa,OAAO,OAAO,MAAM;AACjE;AAMO,SAAS,iCACd,QACA,OACqB;AACrB,QAAM,SAAS,uBAAuB,KAAK;AAC3C,MAAI,CAAC,OAAO,OAAQ,QAAO;AAC3B,QAAM,aAAa,OAAO,WAAW,WAAW,IAAI,IAAI,MAAM,IAAI;AAClE,SAAO,MAAe,UAAU,OAAO,MAAM;AAC/C;AAMO,SAAS,mCACd,QACA,OACqB;AACrB,QAAM,SAAS,uBAAuB,KAAK;AAC3C,QAAM,aAAa,OAAO,WAAW,WAAW,IAAI,IAAI,MAAM,IAAI;AAClE,MAAI,CAAC,OAAO,OAAQ,QAAO,MAAe,UAAU;AACpD,SAAO,WAAoB,UAAU,OAAO,MAAM;AACpD;AAMO,SAAS,4BAA4B,YAAiC,OAAqC;AAChH,QAAM,SAAS,uBAAuB,KAAK;AAC3C,MAAI,CAAC,OAAO,OAAQ,QAAO;AAC3B,SAAO,MAAe,UAAU,QAAQ,IAAI,KAAK,OAAO,IAAI,CAAC,UAAU,MAAM,KAAK,EAAE,GAAG,OAAO,CAAC;AACjG;",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/query/types.ts"],
4
- "sourcesContent": ["import type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { Profiler } from '../profiler'\nimport type { ResolvedCustomFieldDefinitions } from '../crud/custom-field-definition-index'\n\nexport type FilterOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'like' | 'ilike' | 'exists'\n\nexport enum SortDir {\n Asc = 'asc',\n Desc = 'desc',\n}\n\nexport type FieldSelector = string // base field or custom field key (prefixed with 'cf:')\n\nexport type Filter = {\n field: FieldSelector\n op: FilterOp\n value?: any\n}\n\nexport type Sort = { field: FieldSelector; dir?: SortDir }\n\nexport type Page = { page?: number; pageSize?: number }\n\n// Mongo/Medusa-style filter operators (typed)\nexport type WhereOps<T> = {\n $eq?: T\n $ne?: T | null\n $gt?: T extends number | Date ? T : never\n $gte?: T extends number | Date ? T : never\n $lt?: T extends number | Date ? T : never\n $lte?: T extends number | Date ? T : never\n $in?: T[]\n $nin?: T[]\n $like?: T extends string ? string : never\n $ilike?: T extends string ? string : never\n $exists?: boolean\n}\n\n// A field filter can be a direct value (equals) or ops object\nexport type WhereValue<T = any> = T | WhereOps<T>\n\n// Generic shape for object filters. If you have a typed map of field\u2192type,\n// pass it as the generic to get end-to-end typing.\n// Example: Where<{\n// id: string; title: string; created_at: Date; 'cf:severity': number\n// }>\nexport type Where<Fields extends Record<string, any> = Record<string, any>> =\n Partial<{ [K in keyof Fields]: WhereValue<Fields[K]> }> & Record<string, WhereValue>\n\nexport type QueryCustomFieldJoin = {\n fromField: string\n toField: string\n type?: 'left' | 'inner'\n}\n\nexport type QueryCustomFieldSource = {\n entityId: EntityId\n table?: string\n alias?: string\n recordIdColumn?: string\n join?: QueryCustomFieldJoin\n tenantField?: string\n organizationField?: string\n}\n\nexport type QueryJoinEdge = {\n alias: string\n table?: string\n entityId?: EntityId\n from: {\n alias?: string\n field: string\n }\n to: {\n field: string\n }\n type?: 'left' | 'inner'\n}\n\n/**\n * Optional context for query-level UMES extensions.\n * When provided, the query engine will execute sync lifecycle events\n * (querying/queried) and apply query-enabled enrichers.\n */\nexport type QueryExtensionsConfig = {\n userId?: string\n container?: unknown\n userFeatures?: string[]\n resolve?: <T = unknown>(name: string) => T\n}\n\nexport type QueryOptions = {\n fields?: FieldSelector[] // base fields and/or 'cf:<key>' for custom fields\n includeExtensions?: boolean | string[] // include all registered extensions or only specific ones by entity id\n includeCustomFields?: boolean | string[] // include all CFs or specific keys\n // Accept classic array syntax or Mongo-style object syntax\n filters?: Filter[] | Where\n sort?: Sort[]\n page?: Page\n organizationId?: string // enforce multi-tenant scope\n tenantId?: string // enforce tenant scope\n // Optional list of organization ids to scope results. Takes precedence over organizationId.\n organizationIds?: string[]\n /**\n * When true, the engine does not apply default `organization_id` / `tenant_id` equality guards.\n *\n * Callers MUST encode full visibility in `filters` (for example with `$or` of scoped branches)\n * and MUST fail closed when the authenticated principal lacks a resolvable tenant/org, otherwise\n * queries return cross-tenant rows.\n *\n * When this flag is set, the hybrid query engine delegates to the basic engine. The basic engine\n * still applies `cf:*` filters/sorts, but `search_tokens` fulltext filtering, the JSONB index read\n * path, and the vector-search branch are BYPASSED. Only use this on entities whose scoping does\n * not match the standard `organization_id = X AND tenant_id = Y` shape.\n */\n omitAutomaticTenantOrgScope?: boolean\n // Soft-delete behavior: when false (default), rows with non-null deleted_at\n // are excluded if the base table has that column. Set true to include them.\n withDeleted?: boolean\n customFieldSources?: QueryCustomFieldSource[]\n joins?: QueryJoinEdge[]\n profiler?: Profiler\n // When true, suppress automatic reindex scheduling triggered by coverage gap detection.\n // Used by the search indexing pipeline to prevent feedback loops where indexing triggers\n // re-indexing indefinitely.\n skipAutoReindex?: boolean\n /**\n * Force routing this query to custom-entity doc storage (`custom_entities_storage`)\n * instead of classifying the entity automatically. Automatic classification routes\n * ids backed by a registered ORM table to that base table, so surfaces that manage\n * doc records for ids that are ALSO table-backed (e.g. the entities records browser\n * reading a module-declared custom entity such as `example:todo`) must set this flag.\n * Honored by the hybrid query engine only; `BasicQueryEngine` has no doc-storage\n * reader and ignores it.\n */\n forceCustomEntityStorage?: boolean\n // Optional UMES query extensions context. When provided, the engine will\n // emit sync lifecycle events and apply query-level enrichers.\n extensions?: QueryExtensionsConfig\n}\n\nexport type PartialIndexWarning = {\n entity: EntityId\n entityLabel?: string | null\n baseCount?: number | null\n indexedCount?: number | null\n scope?: 'scoped' | 'global'\n}\n\nexport type EncryptedSortRowCapWarning = {\n entity: EntityId\n sortFields: string[]\n maxRows: number\n /**\n * Exact when `meta.listCountCapWarning` is absent; a floor when it is\n * present (the list total itself was bounded at the cap).\n */\n totalMatched: number\n}\n\n/**\n * Present when the list COUNT was bounded at `cap` matching rows\n * (`OM_LIST_COUNT_CAP`): `total` is a floor, not an exact value. Surfaced on\n * CRUD list payloads as `totalIsCapped: true`.\n */\nexport type ListCountCapWarning = {\n entity: EntityId\n cap: number\n}\n\nexport type QueryResultMeta = {\n partialIndexWarning?: PartialIndexWarning\n encryptedSortRowCapWarning?: EncryptedSortRowCapWarning\n listCountCapWarning?: ListCountCapWarning\n}\n\nexport type QueryResult<T = any> = {\n items: T[]\n page: number\n pageSize: number\n total: number\n meta?: QueryResultMeta\n /**\n * Custom-field definitions the engine resolved while building this result\n * (only present when `includeCustomFields: true`). Lets the CRUD factory\n * decorate list rows without reloading definitions from the DB (issue #2133).\n * Internal contract \u2014 additive and optional; callers must treat absence as a\n * cue to load definitions themselves.\n */\n customFieldDefinitions?: ResolvedCustomFieldDefinitions\n}\n\nexport interface QueryEngine {\n query<T = any>(entity: EntityId, opts?: QueryOptions): Promise<QueryResult<T>>\n}\n"],
5
- "mappings": "AAMO,IAAK,UAAL,kBAAKA,aAAL;AACL,EAAAA,SAAA,SAAM;AACN,EAAAA,SAAA,UAAO;AAFG,SAAAA;AAAA,GAAA;",
4
+ "sourcesContent": ["import type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { Profiler } from '../profiler'\nimport type { ResolvedCustomFieldDefinitions } from '../crud/custom-field-definition-index'\n\n/**\n * `overlap` (additive, 2026-10-05): \"has any of\" over a multi-valued field. An index-document\n * string array compiles to jsonb `?|`, a `cf:*` key to any-value-in-set, and an array-typed base\n * column to `&&`. An empty value list matches nothing.\n *\n * `noverlap` (additive, 2026-10-05): \"has none of\" \u2014 the negation of `overlap` on an index-document\n * string array or an array-typed base column. Only a present array can match (a missing/null key or\n * NULL column never does); an empty value list excludes nothing. Unsupported on `cf:*` keys, where\n * it compiles to `false`.\n */\nexport type FilterOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'like' | 'ilike' | 'exists' | 'overlap' | 'noverlap'\n\nexport enum SortDir {\n Asc = 'asc',\n Desc = 'desc',\n}\n\nexport type FieldSelector = string // base field or custom field key (prefixed with 'cf:')\n\nexport type Filter = {\n field: FieldSelector\n op: FilterOp\n value?: any\n}\n\nexport type Sort = { field: FieldSelector; dir?: SortDir }\n\nexport type Page = { page?: number; pageSize?: number }\n\n// Mongo/Medusa-style filter operators (typed)\nexport type WhereOps<T> = {\n $eq?: T\n $ne?: T | null\n $gt?: T extends number | Date ? T : never\n $gte?: T extends number | Date ? T : never\n $lt?: T extends number | Date ? T : never\n $lte?: T extends number | Date ? T : never\n $in?: T[]\n $nin?: T[]\n $like?: T extends string ? string : never\n $ilike?: T extends string ? string : never\n $exists?: boolean\n $overlap?: T extends readonly (infer Element)[] ? Element[] : T[]\n $noverlap?: T extends readonly (infer Element)[] ? Element[] : T[]\n}\n\n// A field filter can be a direct value (equals) or ops object\nexport type WhereValue<T = any> = T | WhereOps<T>\n\n// Generic shape for object filters. If you have a typed map of field\u2192type,\n// pass it as the generic to get end-to-end typing.\n// Example: Where<{\n// id: string; title: string; created_at: Date; 'cf:severity': number\n// }>\nexport type Where<Fields extends Record<string, any> = Record<string, any>> =\n Partial<{ [K in keyof Fields]: WhereValue<Fields[K]> }> & Record<string, WhereValue>\n\nexport type QueryCustomFieldJoin = {\n fromField: string\n toField: string\n type?: 'left' | 'inner'\n}\n\nexport type QueryCustomFieldSource = {\n entityId: EntityId\n table?: string\n alias?: string\n recordIdColumn?: string\n join?: QueryCustomFieldJoin\n tenantField?: string\n organizationField?: string\n}\n\nexport type QueryJoinEdge = {\n alias: string\n table?: string\n entityId?: EntityId\n from: {\n alias?: string\n field: string\n }\n to: {\n field: string\n }\n type?: 'left' | 'inner'\n}\n\n/**\n * Optional context for query-level UMES extensions.\n * When provided, the query engine will execute sync lifecycle events\n * (querying/queried) and apply query-enabled enrichers.\n */\nexport type QueryExtensionsConfig = {\n userId?: string\n container?: unknown\n userFeatures?: string[]\n resolve?: <T = unknown>(name: string) => T\n}\n\nexport type QueryOptions = {\n fields?: FieldSelector[] // base fields and/or 'cf:<key>' for custom fields\n includeExtensions?: boolean | string[] // include all registered extensions or only specific ones by entity id\n includeCustomFields?: boolean | string[] // include all CFs or specific keys\n // Accept classic array syntax or Mongo-style object syntax\n filters?: Filter[] | Where\n sort?: Sort[]\n page?: Page\n organizationId?: string // enforce multi-tenant scope\n tenantId?: string // enforce tenant scope\n // Optional list of organization ids to scope results. Takes precedence over organizationId.\n organizationIds?: string[]\n /**\n * When true, the engine does not apply default `organization_id` / `tenant_id` equality guards.\n *\n * Callers MUST encode full visibility in `filters` (for example with `$or` of scoped branches)\n * and MUST fail closed when the authenticated principal lacks a resolvable tenant/org, otherwise\n * queries return cross-tenant rows.\n *\n * When this flag is set, the hybrid query engine delegates to the basic engine. The basic engine\n * still applies `cf:*` filters/sorts, but `search_tokens` fulltext filtering, the JSONB index read\n * path, and the vector-search branch are BYPASSED. Only use this on entities whose scoping does\n * not match the standard `organization_id = X AND tenant_id = Y` shape.\n */\n omitAutomaticTenantOrgScope?: boolean\n // Soft-delete behavior: when false (default), rows with non-null deleted_at\n // are excluded if the base table has that column. Set true to include them.\n withDeleted?: boolean\n customFieldSources?: QueryCustomFieldSource[]\n joins?: QueryJoinEdge[]\n profiler?: Profiler\n // When true, suppress automatic reindex scheduling triggered by coverage gap detection.\n // Used by the search indexing pipeline to prevent feedback loops where indexing triggers\n // re-indexing indefinitely.\n skipAutoReindex?: boolean\n /**\n * Force routing this query to custom-entity doc storage (`custom_entities_storage`)\n * instead of classifying the entity automatically. Automatic classification routes\n * ids backed by a registered ORM table to that base table, so surfaces that manage\n * doc records for ids that are ALSO table-backed (e.g. the entities records browser\n * reading a module-declared custom entity such as `example:todo`) must set this flag.\n * Honored by the hybrid query engine only; `BasicQueryEngine` has no doc-storage\n * reader and ignores it.\n */\n forceCustomEntityStorage?: boolean\n // Optional UMES query extensions context. When provided, the engine will\n // emit sync lifecycle events and apply query-level enrichers.\n extensions?: QueryExtensionsConfig\n}\n\nexport type PartialIndexWarning = {\n entity: EntityId\n entityLabel?: string | null\n baseCount?: number | null\n indexedCount?: number | null\n scope?: 'scoped' | 'global'\n}\n\nexport type EncryptedSortRowCapWarning = {\n entity: EntityId\n sortFields: string[]\n maxRows: number\n /**\n * Exact when `meta.listCountCapWarning` is absent; a floor when it is\n * present (the list total itself was bounded at the cap).\n */\n totalMatched: number\n}\n\n/**\n * Present when the list COUNT was bounded at `cap` matching rows\n * (`OM_LIST_COUNT_CAP`): `total` is a floor, not an exact value. Surfaced on\n * CRUD list payloads as `totalIsCapped: true`.\n */\nexport type ListCountCapWarning = {\n entity: EntityId\n cap: number\n}\n\nexport type QueryResultMeta = {\n partialIndexWarning?: PartialIndexWarning\n encryptedSortRowCapWarning?: EncryptedSortRowCapWarning\n listCountCapWarning?: ListCountCapWarning\n}\n\nexport type QueryResult<T = any> = {\n items: T[]\n page: number\n pageSize: number\n total: number\n meta?: QueryResultMeta\n /**\n * Custom-field definitions the engine resolved while building this result\n * (only present when `includeCustomFields: true`). Lets the CRUD factory\n * decorate list rows without reloading definitions from the DB (issue #2133).\n * Internal contract \u2014 additive and optional; callers must treat absence as a\n * cue to load definitions themselves.\n */\n customFieldDefinitions?: ResolvedCustomFieldDefinitions\n}\n\nexport interface QueryEngine {\n query<T = any>(entity: EntityId, opts?: QueryOptions): Promise<QueryResult<T>>\n}\n"],
5
+ "mappings": "AAgBO,IAAK,UAAL,kBAAKA,aAAL;AACL,EAAAA,SAAA,SAAM;AACN,EAAAA,SAAA,UAAO;AAFG,SAAAA;AAAA,GAAA;",
6
6
  "names": ["SortDir"]
7
7
  }
package/dist/lib/url.js CHANGED
@@ -199,6 +199,29 @@ function toSecurityEmailUrl(input, path, env = process.env) {
199
199
  const base = getSecurityEmailBaseUrl(input, env);
200
200
  return `${base}/${path.replace(/^\/+/, "")}`;
201
201
  }
202
+ function isCrossSiteBrowserRequest(req, env = process.env) {
203
+ const rawFetchSite = req.headers.get("sec-fetch-site");
204
+ const fetchSite = rawFetchSite?.trim().toLowerCase() ?? null;
205
+ if (fetchSite === "cross-site") return true;
206
+ if (fetchSite === "same-origin" || fetchSite === "none") return false;
207
+ const rawOrigin = req.headers.get("origin");
208
+ if (rawOrigin === null) return rawFetchSite !== null;
209
+ const origin = normalizeOrigin(rawOrigin);
210
+ if (!origin) return true;
211
+ const allowedOrigins = readAllowedOrigins(env);
212
+ if (allowedOrigins.has(origin)) return false;
213
+ if (shouldAllowLoopbackOrigin(origin, allowedOrigins, env)) return false;
214
+ const forwardedProto = readFirstHeaderValue(req.headers.get("x-forwarded-proto"));
215
+ const protocol = forwardedProto ? forwardedProto.endsWith(":") ? forwardedProto : `${forwardedProto}:` : new URL(origin).protocol;
216
+ const host = req.headers.get("host");
217
+ const forwardedHost = req.headers.get("x-forwarded-host");
218
+ if (originFromHost(protocol, forwardedHost) === origin) return false;
219
+ if (originFromHost(protocol, host) === origin) return false;
220
+ if (env.NODE_ENV !== "test") {
221
+ logger.warn("Cross-site request rejected", { origin, host, forwardedHost });
222
+ }
223
+ return true;
224
+ }
202
225
  function mapSecurityEmailUrlError(error, mapping) {
203
226
  if (error instanceof AppOriginRejectedError) {
204
227
  return { status: 400, body: { error: "Invalid request origin" } };
@@ -215,6 +238,7 @@ export {
215
238
  assertAllowedAppOrigin,
216
239
  getAppBaseUrl,
217
240
  getSecurityEmailBaseUrl,
241
+ isCrossSiteBrowserRequest,
218
242
  mapSecurityEmailUrlError,
219
243
  resolveRequestOrigin,
220
244
  toAbsoluteUrl,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/url.ts"],
4
- "sourcesContent": ["import { createLogger } from './logger'\n\nconst logger = createLogger('shared').child({ component: 'origin-check' })\n\nconst DEFAULT_DEV_APP_URL = 'http://localhost:3000'\n\ntype EnvLike = Record<string, string | undefined> & {\n APP_URL?: string\n NEXT_PUBLIC_APP_URL?: string\n APP_ALLOWED_ORIGINS?: string\n NODE_ENV?: string\n}\ntype RequestInput = Request | string | undefined\n\nexport type SecurityEmailUrlErrorMapping = {\n scope: string\n configMessage: string\n}\n\nexport class AppOriginConfigurationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AppOriginConfigurationError'\n }\n}\n\nexport class AppOriginRejectedError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AppOriginRejectedError'\n }\n}\n\nfunction parseBaseUrl(raw: string | undefined): URL | null {\n const value = raw?.trim()\n if (!value) return null\n try {\n const url = new URL(value)\n url.hash = ''\n url.search = ''\n return url\n } catch {\n return null\n }\n}\n\nfunction normalizeBaseUrl(raw: string | undefined): string | null {\n const url = parseBaseUrl(raw)\n return url ? url.toString().replace(/\\/$/, '') : null\n}\n\nfunction normalizeOrigin(raw: string | undefined): string | null {\n const url = parseBaseUrl(raw)\n return url ? url.origin : null\n}\n\nfunction readCsv(value: string | undefined): string[] {\n return (value ?? '')\n .split(',')\n .map((item) => item.trim())\n .filter((item) => item.length > 0)\n}\n\nfunction readAllowedOrigins(env: EnvLike): Set<string> {\n const origins = new Set<string>()\n for (const raw of [env.APP_URL, env.NEXT_PUBLIC_APP_URL, ...readCsv(env.APP_ALLOWED_ORIGINS)]) {\n const origin = normalizeOrigin(raw)\n if (origin) origins.add(origin)\n }\n return origins\n}\n\nfunction readFirstHeaderValue(value: string | null): string | null {\n const first = value?.split(',')[0]?.trim()\n return first && first.length > 0 ? first : null\n}\n\nfunction originFromHost(protocol: string, rawHost: string | null): string | null {\n const host = readFirstHeaderValue(rawHost)\n if (!host) return null\n return normalizeOrigin(`${protocol}//${host}`)\n}\n\ntype RequestOriginCandidates = {\n urlOrigin: string | null\n headerOrigins: Set<string>\n}\n\nfunction readRequestOriginCandidates(input: RequestInput): RequestOriginCandidates {\n const candidates: RequestOriginCandidates = {\n urlOrigin: null,\n headerOrigins: new Set<string>(),\n }\n if (!input) return candidates\n\n const requestUrl = typeof input === 'string' ? input : input.url\n let parsedUrl: URL\n try {\n parsedUrl = new URL(requestUrl)\n } catch {\n return candidates\n }\n\n candidates.urlOrigin = normalizeOrigin(parsedUrl.origin)\n if (typeof input === 'string') return candidates\n\n const forwardedProto = readFirstHeaderValue(input.headers.get('x-forwarded-proto')) ?? parsedUrl.protocol.replace(/:$/, '')\n const protocol = forwardedProto.endsWith(':') ? forwardedProto : `${forwardedProto}:`\n const hostOrigin = originFromHost(protocol, input.headers.get('host'))\n const forwardedHostOrigin = originFromHost(protocol, input.headers.get('x-forwarded-host'))\n if (hostOrigin) candidates.headerOrigins.add(hostOrigin)\n if (forwardedHostOrigin) candidates.headerOrigins.add(forwardedHostOrigin)\n return candidates\n}\n\nfunction requestOrigins(input: RequestInput): Set<string> {\n const origins = new Set<string>()\n const { urlOrigin, headerOrigins } = readRequestOriginCandidates(input)\n if (urlOrigin) origins.add(urlOrigin)\n for (const origin of headerOrigins) origins.add(origin)\n return origins\n}\n\nfunction logOriginDebugContext(\n input: RequestInput,\n allowedOrigins: Set<string>,\n rejectedOrigin?: string,\n level: 'error' | 'warn' = 'error',\n nodeEnv?: string,\n): void {\n if (level === 'warn' && nodeEnv === 'test') return\n const log = (msg: string, fields: Record<string, unknown>) =>\n level === 'warn' ? logger.warn(msg, fields) : logger.error(msg, fields)\n\n if (typeof input === 'string') {\n log('Origin check rejected string input', {\n requestUrl: input,\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n return\n }\n\n if (!input) {\n log('Origin check rejected empty input', {\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n return\n }\n\n log('Origin check rejected request', {\n requestUrl: input.url,\n host: input.headers.get('host'),\n forwardedHost: input.headers.get('x-forwarded-host'),\n forwardedProto: input.headers.get('x-forwarded-proto'),\n derivedOrigins: Array.from(requestOrigins(input)),\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n}\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'\n}\n\nfunction isLoopbackOrigin(origin: string): boolean {\n try {\n return isLoopbackHostname(new URL(origin).hostname)\n } catch {\n return false\n }\n}\n\nfunction normalizeOriginPort(url: URL): string {\n if (url.port) return url.port\n if (url.protocol === 'https:') return '443'\n if (url.protocol === 'http:') return '80'\n return ''\n}\n\nfunction isEquivalentLoopbackOrigin(origin: string, allowedOrigin: string): boolean {\n try {\n const candidateUrl = new URL(origin)\n const allowedUrl = new URL(allowedOrigin)\n if (!isLoopbackHostname(candidateUrl.hostname) || !isLoopbackHostname(allowedUrl.hostname)) {\n return false\n }\n return normalizeOriginPort(candidateUrl) === normalizeOriginPort(allowedUrl)\n } catch {\n return false\n }\n}\n\nfunction shouldAllowLoopbackOrigin(origin: string, allowedOrigins: Set<string>, env: EnvLike): boolean {\n if (!isLoopbackOrigin(origin)) return false\n for (const allowedOrigin of allowedOrigins) {\n if (!isLoopbackOrigin(allowedOrigin)) continue\n if (env.NODE_ENV !== 'production') return true\n if (isEquivalentLoopbackOrigin(origin, allowedOrigin)) return true\n }\n return false\n}\n\nexport function assertAllowedAppOrigin(input: RequestInput, env: EnvLike = process.env): void {\n const allowedOrigins = readAllowedOrigins(env)\n if (allowedOrigins.size === 0) {\n if (env.NODE_ENV === 'production') {\n logOriginDebugContext(input, allowedOrigins)\n throw new AppOriginConfigurationError('APP_URL must be configured in production')\n }\n return\n }\n const { urlOrigin, headerOrigins } = readRequestOriginCandidates(input)\n const hasAllowedHeaderOrigin = Array.from(headerOrigins).some((origin) => allowedOrigins.has(origin))\n\n for (const origin of headerOrigins) {\n if (allowedOrigins.has(origin)) continue\n if (shouldAllowLoopbackOrigin(origin, allowedOrigins, env)) continue\n logOriginDebugContext(input, allowedOrigins, origin, 'warn', env.NODE_ENV)\n throw new AppOriginRejectedError('Request origin is not allowed')\n }\n\n if (!urlOrigin) return\n if (allowedOrigins.has(urlOrigin)) return\n if (shouldAllowLoopbackOrigin(urlOrigin, allowedOrigins, env)) return\n if (isLoopbackOrigin(urlOrigin) && hasAllowedHeaderOrigin) return\n\n logOriginDebugContext(input, allowedOrigins, urlOrigin, 'warn', env.NODE_ENV)\n throw new AppOriginRejectedError('Request origin is not allowed')\n}\n\nexport function resolveRequestOrigin(req: Request): string {\n const url = new URL(req.url)\n const proto = req.headers.get('x-forwarded-proto') || url.protocol.replace(':', '')\n const host = req.headers.get('x-forwarded-host') || req.headers.get('host') || url.host\n return `${proto}://${host}`\n}\n\nexport function getAppBaseUrl(req: Request): string {\n return (\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n resolveRequestOrigin(req)\n )\n}\n\nexport function toAbsoluteUrl(req: Request, path: string): string {\n return new URL(path, getAppBaseUrl(req)).toString()\n}\n\nexport function getSecurityEmailBaseUrl(input?: RequestInput, env: EnvLike = process.env): string {\n const configuredAppUrl = normalizeBaseUrl(env.APP_URL)\n if (!configuredAppUrl) {\n if (env.NODE_ENV === 'production') {\n throw new AppOriginConfigurationError('APP_URL must be configured in production')\n }\n return DEFAULT_DEV_APP_URL\n }\n\n assertAllowedAppOrigin(input, env)\n return configuredAppUrl\n}\n\nexport function toSecurityEmailUrl(input: RequestInput, path: string, env: EnvLike = process.env): string {\n const base = getSecurityEmailBaseUrl(input, env)\n return `${base}/${path.replace(/^\\/+/, '')}`\n}\n\nexport function mapSecurityEmailUrlError(\n error: unknown,\n mapping: SecurityEmailUrlErrorMapping,\n): { status: number; body: { error: string } } | null {\n if (error instanceof AppOriginRejectedError) {\n return { status: 400, body: { error: 'Invalid request origin' } }\n }\n if (error instanceof AppOriginConfigurationError) {\n logger.error('APP_URL is required in production', { scope: mapping.scope })\n return { status: 500, body: { error: mapping.configMessage } }\n }\n return null\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createLogger } from './logger'\n\nconst logger = createLogger('shared').child({ component: 'origin-check' })\n\nconst DEFAULT_DEV_APP_URL = 'http://localhost:3000'\n\ntype EnvLike = Record<string, string | undefined> & {\n APP_URL?: string\n NEXT_PUBLIC_APP_URL?: string\n APP_ALLOWED_ORIGINS?: string\n NODE_ENV?: string\n}\ntype RequestInput = Request | string | undefined\n\nexport type SecurityEmailUrlErrorMapping = {\n scope: string\n configMessage: string\n}\n\nexport class AppOriginConfigurationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AppOriginConfigurationError'\n }\n}\n\nexport class AppOriginRejectedError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AppOriginRejectedError'\n }\n}\n\nfunction parseBaseUrl(raw: string | undefined): URL | null {\n const value = raw?.trim()\n if (!value) return null\n try {\n const url = new URL(value)\n url.hash = ''\n url.search = ''\n return url\n } catch {\n return null\n }\n}\n\nfunction normalizeBaseUrl(raw: string | undefined): string | null {\n const url = parseBaseUrl(raw)\n return url ? url.toString().replace(/\\/$/, '') : null\n}\n\nfunction normalizeOrigin(raw: string | undefined): string | null {\n const url = parseBaseUrl(raw)\n return url ? url.origin : null\n}\n\nfunction readCsv(value: string | undefined): string[] {\n return (value ?? '')\n .split(',')\n .map((item) => item.trim())\n .filter((item) => item.length > 0)\n}\n\nfunction readAllowedOrigins(env: EnvLike): Set<string> {\n const origins = new Set<string>()\n for (const raw of [env.APP_URL, env.NEXT_PUBLIC_APP_URL, ...readCsv(env.APP_ALLOWED_ORIGINS)]) {\n const origin = normalizeOrigin(raw)\n if (origin) origins.add(origin)\n }\n return origins\n}\n\nfunction readFirstHeaderValue(value: string | null): string | null {\n const first = value?.split(',')[0]?.trim()\n return first && first.length > 0 ? first : null\n}\n\nfunction originFromHost(protocol: string, rawHost: string | null): string | null {\n const host = readFirstHeaderValue(rawHost)\n if (!host) return null\n return normalizeOrigin(`${protocol}//${host}`)\n}\n\ntype RequestOriginCandidates = {\n urlOrigin: string | null\n headerOrigins: Set<string>\n}\n\nfunction readRequestOriginCandidates(input: RequestInput): RequestOriginCandidates {\n const candidates: RequestOriginCandidates = {\n urlOrigin: null,\n headerOrigins: new Set<string>(),\n }\n if (!input) return candidates\n\n const requestUrl = typeof input === 'string' ? input : input.url\n let parsedUrl: URL\n try {\n parsedUrl = new URL(requestUrl)\n } catch {\n return candidates\n }\n\n candidates.urlOrigin = normalizeOrigin(parsedUrl.origin)\n if (typeof input === 'string') return candidates\n\n const forwardedProto = readFirstHeaderValue(input.headers.get('x-forwarded-proto')) ?? parsedUrl.protocol.replace(/:$/, '')\n const protocol = forwardedProto.endsWith(':') ? forwardedProto : `${forwardedProto}:`\n const hostOrigin = originFromHost(protocol, input.headers.get('host'))\n const forwardedHostOrigin = originFromHost(protocol, input.headers.get('x-forwarded-host'))\n if (hostOrigin) candidates.headerOrigins.add(hostOrigin)\n if (forwardedHostOrigin) candidates.headerOrigins.add(forwardedHostOrigin)\n return candidates\n}\n\nfunction requestOrigins(input: RequestInput): Set<string> {\n const origins = new Set<string>()\n const { urlOrigin, headerOrigins } = readRequestOriginCandidates(input)\n if (urlOrigin) origins.add(urlOrigin)\n for (const origin of headerOrigins) origins.add(origin)\n return origins\n}\n\nfunction logOriginDebugContext(\n input: RequestInput,\n allowedOrigins: Set<string>,\n rejectedOrigin?: string,\n level: 'error' | 'warn' = 'error',\n nodeEnv?: string,\n): void {\n if (level === 'warn' && nodeEnv === 'test') return\n const log = (msg: string, fields: Record<string, unknown>) =>\n level === 'warn' ? logger.warn(msg, fields) : logger.error(msg, fields)\n\n if (typeof input === 'string') {\n log('Origin check rejected string input', {\n requestUrl: input,\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n return\n }\n\n if (!input) {\n log('Origin check rejected empty input', {\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n return\n }\n\n log('Origin check rejected request', {\n requestUrl: input.url,\n host: input.headers.get('host'),\n forwardedHost: input.headers.get('x-forwarded-host'),\n forwardedProto: input.headers.get('x-forwarded-proto'),\n derivedOrigins: Array.from(requestOrigins(input)),\n rejectedOrigin: rejectedOrigin ?? null,\n allowedOrigins: Array.from(allowedOrigins),\n })\n}\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'\n}\n\nfunction isLoopbackOrigin(origin: string): boolean {\n try {\n return isLoopbackHostname(new URL(origin).hostname)\n } catch {\n return false\n }\n}\n\nfunction normalizeOriginPort(url: URL): string {\n if (url.port) return url.port\n if (url.protocol === 'https:') return '443'\n if (url.protocol === 'http:') return '80'\n return ''\n}\n\nfunction isEquivalentLoopbackOrigin(origin: string, allowedOrigin: string): boolean {\n try {\n const candidateUrl = new URL(origin)\n const allowedUrl = new URL(allowedOrigin)\n if (!isLoopbackHostname(candidateUrl.hostname) || !isLoopbackHostname(allowedUrl.hostname)) {\n return false\n }\n return normalizeOriginPort(candidateUrl) === normalizeOriginPort(allowedUrl)\n } catch {\n return false\n }\n}\n\nfunction shouldAllowLoopbackOrigin(origin: string, allowedOrigins: Set<string>, env: EnvLike): boolean {\n if (!isLoopbackOrigin(origin)) return false\n for (const allowedOrigin of allowedOrigins) {\n if (!isLoopbackOrigin(allowedOrigin)) continue\n if (env.NODE_ENV !== 'production') return true\n if (isEquivalentLoopbackOrigin(origin, allowedOrigin)) return true\n }\n return false\n}\n\nexport function assertAllowedAppOrigin(input: RequestInput, env: EnvLike = process.env): void {\n const allowedOrigins = readAllowedOrigins(env)\n if (allowedOrigins.size === 0) {\n if (env.NODE_ENV === 'production') {\n logOriginDebugContext(input, allowedOrigins)\n throw new AppOriginConfigurationError('APP_URL must be configured in production')\n }\n return\n }\n const { urlOrigin, headerOrigins } = readRequestOriginCandidates(input)\n const hasAllowedHeaderOrigin = Array.from(headerOrigins).some((origin) => allowedOrigins.has(origin))\n\n for (const origin of headerOrigins) {\n if (allowedOrigins.has(origin)) continue\n if (shouldAllowLoopbackOrigin(origin, allowedOrigins, env)) continue\n logOriginDebugContext(input, allowedOrigins, origin, 'warn', env.NODE_ENV)\n throw new AppOriginRejectedError('Request origin is not allowed')\n }\n\n if (!urlOrigin) return\n if (allowedOrigins.has(urlOrigin)) return\n if (shouldAllowLoopbackOrigin(urlOrigin, allowedOrigins, env)) return\n if (isLoopbackOrigin(urlOrigin) && hasAllowedHeaderOrigin) return\n\n logOriginDebugContext(input, allowedOrigins, urlOrigin, 'warn', env.NODE_ENV)\n throw new AppOriginRejectedError('Request origin is not allowed')\n}\n\nexport function resolveRequestOrigin(req: Request): string {\n const url = new URL(req.url)\n const proto = req.headers.get('x-forwarded-proto') || url.protocol.replace(':', '')\n const host = req.headers.get('x-forwarded-host') || req.headers.get('host') || url.host\n return `${proto}://${host}`\n}\n\nexport function getAppBaseUrl(req: Request): string {\n return (\n process.env.NEXT_PUBLIC_APP_URL ||\n process.env.APP_URL ||\n resolveRequestOrigin(req)\n )\n}\n\nexport function toAbsoluteUrl(req: Request, path: string): string {\n return new URL(path, getAppBaseUrl(req)).toString()\n}\n\nexport function getSecurityEmailBaseUrl(input?: RequestInput, env: EnvLike = process.env): string {\n const configuredAppUrl = normalizeBaseUrl(env.APP_URL)\n if (!configuredAppUrl) {\n if (env.NODE_ENV === 'production') {\n throw new AppOriginConfigurationError('APP_URL must be configured in production')\n }\n return DEFAULT_DEV_APP_URL\n }\n\n assertAllowedAppOrigin(input, env)\n return configuredAppUrl\n}\n\nexport function toSecurityEmailUrl(input: RequestInput, path: string, env: EnvLike = process.env): string {\n const base = getSecurityEmailBaseUrl(input, env)\n return `${base}/${path.replace(/^\\/+/, '')}`\n}\n\nexport function isCrossSiteBrowserRequest(req: Request, env: EnvLike = process.env): boolean {\n const rawFetchSite = req.headers.get('sec-fetch-site')\n const fetchSite = rawFetchSite?.trim().toLowerCase() ?? null\n if (fetchSite === 'cross-site') return true\n if (fetchSite === 'same-origin' || fetchSite === 'none') return false\n\n const rawOrigin = req.headers.get('origin')\n if (rawOrigin === null) return rawFetchSite !== null\n const origin = normalizeOrigin(rawOrigin)\n if (!origin) return true\n\n const allowedOrigins = readAllowedOrigins(env)\n if (allowedOrigins.has(origin)) return false\n if (shouldAllowLoopbackOrigin(origin, allowedOrigins, env)) return false\n\n const forwardedProto = readFirstHeaderValue(req.headers.get('x-forwarded-proto'))\n const protocol = forwardedProto\n ? (forwardedProto.endsWith(':') ? forwardedProto : `${forwardedProto}:`)\n : new URL(origin).protocol\n const host = req.headers.get('host')\n const forwardedHost = req.headers.get('x-forwarded-host')\n if (originFromHost(protocol, forwardedHost) === origin) return false\n if (originFromHost(protocol, host) === origin) return false\n\n if (env.NODE_ENV !== 'test') {\n logger.warn('Cross-site request rejected', { origin, host, forwardedHost })\n }\n return true\n}\n\nexport function mapSecurityEmailUrlError(\n error: unknown,\n mapping: SecurityEmailUrlErrorMapping,\n): { status: number; body: { error: string } } | null {\n if (error instanceof AppOriginRejectedError) {\n return { status: 400, body: { error: 'Invalid request origin' } }\n }\n if (error instanceof AppOriginConfigurationError) {\n logger.error('APP_URL is required in production', { scope: mapping.scope })\n return { status: 500, body: { error: mapping.configMessage } }\n }\n return null\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,eAAe,CAAC;AAEzE,MAAM,sBAAsB;AAerB,MAAM,oCAAoC,MAAM;AAAA,EACrD,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,+BAA+B,MAAM;AAAA,EAChD,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEA,SAAS,aAAa,KAAqC;AACzD,QAAM,QAAQ,KAAK,KAAK;AACxB,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,KAAK;AACzB,QAAI,OAAO;AACX,QAAI,SAAS;AACb,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,iBAAiB,KAAwC;AAChE,QAAM,MAAM,aAAa,GAAG;AAC5B,SAAO,MAAM,IAAI,SAAS,EAAE,QAAQ,OAAO,EAAE,IAAI;AACnD;AAEA,SAAS,gBAAgB,KAAwC;AAC/D,QAAM,MAAM,aAAa,GAAG;AAC5B,SAAO,MAAM,IAAI,SAAS;AAC5B;AAEA,SAAS,QAAQ,OAAqC;AACpD,UAAQ,SAAS,IACd,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,EACzB,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC;AACrC;AAEA,SAAS,mBAAmB,KAA2B;AACrD,QAAM,UAAU,oBAAI,IAAY;AAChC,aAAW,OAAO,CAAC,IAAI,SAAS,IAAI,qBAAqB,GAAG,QAAQ,IAAI,mBAAmB,CAAC,GAAG;AAC7F,UAAM,SAAS,gBAAgB,GAAG;AAClC,QAAI,OAAQ,SAAQ,IAAI,MAAM;AAAA,EAChC;AACA,SAAO;AACT;AAEA,SAAS,qBAAqB,OAAqC;AACjE,QAAM,QAAQ,OAAO,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AACzC,SAAO,SAAS,MAAM,SAAS,IAAI,QAAQ;AAC7C;AAEA,SAAS,eAAe,UAAkB,SAAuC;AAC/E,QAAM,OAAO,qBAAqB,OAAO;AACzC,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,gBAAgB,GAAG,QAAQ,KAAK,IAAI,EAAE;AAC/C;AAOA,SAAS,4BAA4B,OAA8C;AACjF,QAAM,aAAsC;AAAA,IAC1C,WAAW;AAAA,IACX,eAAe,oBAAI,IAAY;AAAA,EACjC;AACA,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,aAAa,OAAO,UAAU,WAAW,QAAQ,MAAM;AAC7D,MAAI;AACJ,MAAI;AACF,gBAAY,IAAI,IAAI,UAAU;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,aAAW,YAAY,gBAAgB,UAAU,MAAM;AACvD,MAAI,OAAO,UAAU,SAAU,QAAO;AAEtC,QAAM,iBAAiB,qBAAqB,MAAM,QAAQ,IAAI,mBAAmB,CAAC,KAAK,UAAU,SAAS,QAAQ,MAAM,EAAE;AAC1H,QAAM,WAAW,eAAe,SAAS,GAAG,IAAI,iBAAiB,GAAG,cAAc;AAClF,QAAM,aAAa,eAAe,UAAU,MAAM,QAAQ,IAAI,MAAM,CAAC;AACrE,QAAM,sBAAsB,eAAe,UAAU,MAAM,QAAQ,IAAI,kBAAkB,CAAC;AAC1F,MAAI,WAAY,YAAW,cAAc,IAAI,UAAU;AACvD,MAAI,oBAAqB,YAAW,cAAc,IAAI,mBAAmB;AACzE,SAAO;AACT;AAEA,SAAS,eAAe,OAAkC;AACxD,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,EAAE,WAAW,cAAc,IAAI,4BAA4B,KAAK;AACtE,MAAI,UAAW,SAAQ,IAAI,SAAS;AACpC,aAAW,UAAU,cAAe,SAAQ,IAAI,MAAM;AACtD,SAAO;AACT;AAEA,SAAS,sBACP,OACA,gBACA,gBACA,QAA0B,SAC1B,SACM;AACN,MAAI,UAAU,UAAU,YAAY,OAAQ;AAC5C,QAAM,MAAM,CAAC,KAAa,WACxB,UAAU,SAAS,OAAO,KAAK,KAAK,MAAM,IAAI,OAAO,MAAM,KAAK,MAAM;AAExE,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI,sCAAsC;AAAA,MACxC,YAAY;AAAA,MACZ,gBAAgB,kBAAkB;AAAA,MAClC,gBAAgB,MAAM,KAAK,cAAc;AAAA,IAC3C,CAAC;AACD;AAAA,EACF;AAEA,MAAI,CAAC,OAAO;AACV,QAAI,qCAAqC;AAAA,MACvC,gBAAgB,kBAAkB;AAAA,MAClC,gBAAgB,MAAM,KAAK,cAAc;AAAA,IAC3C,CAAC;AACD;AAAA,EACF;AAEA,MAAI,iCAAiC;AAAA,IACnC,YAAY,MAAM;AAAA,IAClB,MAAM,MAAM,QAAQ,IAAI,MAAM;AAAA,IAC9B,eAAe,MAAM,QAAQ,IAAI,kBAAkB;AAAA,IACnD,gBAAgB,MAAM,QAAQ,IAAI,mBAAmB;AAAA,IACrD,gBAAgB,MAAM,KAAK,eAAe,KAAK,CAAC;AAAA,IAChD,gBAAgB,kBAAkB;AAAA,IAClC,gBAAgB,MAAM,KAAK,cAAc;AAAA,EAC3C,CAAC;AACH;AAEA,SAAS,mBAAmB,UAA2B;AACrD,SAAO,aAAa,eAAe,aAAa,eAAe,aAAa;AAC9E;AAEA,SAAS,iBAAiB,QAAyB;AACjD,MAAI;AACF,WAAO,mBAAmB,IAAI,IAAI,MAAM,EAAE,QAAQ;AAAA,EACpD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,oBAAoB,KAAkB;AAC7C,MAAI,IAAI,KAAM,QAAO,IAAI;AACzB,MAAI,IAAI,aAAa,SAAU,QAAO;AACtC,MAAI,IAAI,aAAa,QAAS,QAAO;AACrC,SAAO;AACT;AAEA,SAAS,2BAA2B,QAAgB,eAAgC;AAClF,MAAI;AACF,UAAM,eAAe,IAAI,IAAI,MAAM;AACnC,UAAM,aAAa,IAAI,IAAI,aAAa;AACxC,QAAI,CAAC,mBAAmB,aAAa,QAAQ,KAAK,CAAC,mBAAmB,WAAW,QAAQ,GAAG;AAC1F,aAAO;AAAA,IACT;AACA,WAAO,oBAAoB,YAAY,MAAM,oBAAoB,UAAU;AAAA,EAC7E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,0BAA0B,QAAgB,gBAA6B,KAAuB;AACrG,MAAI,CAAC,iBAAiB,MAAM,EAAG,QAAO;AACtC,aAAW,iBAAiB,gBAAgB;AAC1C,QAAI,CAAC,iBAAiB,aAAa,EAAG;AACtC,QAAI,IAAI,aAAa,aAAc,QAAO;AAC1C,QAAI,2BAA2B,QAAQ,aAAa,EAAG,QAAO;AAAA,EAChE;AACA,SAAO;AACT;AAEO,SAAS,uBAAuB,OAAqB,MAAe,QAAQ,KAAW;AAC5F,QAAM,iBAAiB,mBAAmB,GAAG;AAC7C,MAAI,eAAe,SAAS,GAAG;AAC7B,QAAI,IAAI,aAAa,cAAc;AACjC,4BAAsB,OAAO,cAAc;AAC3C,YAAM,IAAI,4BAA4B,0CAA0C;AAAA,IAClF;AACA;AAAA,EACF;AACA,QAAM,EAAE,WAAW,cAAc,IAAI,4BAA4B,KAAK;AACtE,QAAM,yBAAyB,MAAM,KAAK,aAAa,EAAE,KAAK,CAAC,WAAW,eAAe,IAAI,MAAM,CAAC;AAEpG,aAAW,UAAU,eAAe;AAClC,QAAI,eAAe,IAAI,MAAM,EAAG;AAChC,QAAI,0BAA0B,QAAQ,gBAAgB,GAAG,EAAG;AAC5D,0BAAsB,OAAO,gBAAgB,QAAQ,QAAQ,IAAI,QAAQ;AACzE,UAAM,IAAI,uBAAuB,+BAA+B;AAAA,EAClE;AAEA,MAAI,CAAC,UAAW;AAChB,MAAI,eAAe,IAAI,SAAS,EAAG;AACnC,MAAI,0BAA0B,WAAW,gBAAgB,GAAG,EAAG;AAC/D,MAAI,iBAAiB,SAAS,KAAK,uBAAwB;AAE3D,wBAAsB,OAAO,gBAAgB,WAAW,QAAQ,IAAI,QAAQ;AAC5E,QAAM,IAAI,uBAAuB,+BAA+B;AAClE;AAEO,SAAS,qBAAqB,KAAsB;AACzD,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,QAAQ,IAAI,QAAQ,IAAI,mBAAmB,KAAK,IAAI,SAAS,QAAQ,KAAK,EAAE;AAClF,QAAM,OAAO,IAAI,QAAQ,IAAI,kBAAkB,KAAK,IAAI,QAAQ,IAAI,MAAM,KAAK,IAAI;AACnF,SAAO,GAAG,KAAK,MAAM,IAAI;AAC3B;AAEO,SAAS,cAAc,KAAsB;AAClD,SACE,QAAQ,IAAI,uBACZ,QAAQ,IAAI,WACZ,qBAAqB,GAAG;AAE5B;AAEO,SAAS,cAAc,KAAc,MAAsB;AAChE,SAAO,IAAI,IAAI,MAAM,cAAc,GAAG,CAAC,EAAE,SAAS;AACpD;AAEO,SAAS,wBAAwB,OAAsB,MAAe,QAAQ,KAAa;AAChG,QAAM,mBAAmB,iBAAiB,IAAI,OAAO;AACrD,MAAI,CAAC,kBAAkB;AACrB,QAAI,IAAI,aAAa,cAAc;AACjC,YAAM,IAAI,4BAA4B,0CAA0C;AAAA,IAClF;AACA,WAAO;AAAA,EACT;AAEA,yBAAuB,OAAO,GAAG;AACjC,SAAO;AACT;AAEO,SAAS,mBAAmB,OAAqB,MAAc,MAAe,QAAQ,KAAa;AACxG,QAAM,OAAO,wBAAwB,OAAO,GAAG;AAC/C,SAAO,GAAG,IAAI,IAAI,KAAK,QAAQ,QAAQ,EAAE,CAAC;AAC5C;AAEO,SAAS,0BAA0B,KAAc,MAAe,QAAQ,KAAc;AAC3F,QAAM,eAAe,IAAI,QAAQ,IAAI,gBAAgB;AACrD,QAAM,YAAY,cAAc,KAAK,EAAE,YAAY,KAAK;AACxD,MAAI,cAAc,aAAc,QAAO;AACvC,MAAI,cAAc,iBAAiB,cAAc,OAAQ,QAAO;AAEhE,QAAM,YAAY,IAAI,QAAQ,IAAI,QAAQ;AAC1C,MAAI,cAAc,KAAM,QAAO,iBAAiB;AAChD,QAAM,SAAS,gBAAgB,SAAS;AACxC,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,iBAAiB,mBAAmB,GAAG;AAC7C,MAAI,eAAe,IAAI,MAAM,EAAG,QAAO;AACvC,MAAI,0BAA0B,QAAQ,gBAAgB,GAAG,EAAG,QAAO;AAEnE,QAAM,iBAAiB,qBAAqB,IAAI,QAAQ,IAAI,mBAAmB,CAAC;AAChF,QAAM,WAAW,iBACZ,eAAe,SAAS,GAAG,IAAI,iBAAiB,GAAG,cAAc,MAClE,IAAI,IAAI,MAAM,EAAE;AACpB,QAAM,OAAO,IAAI,QAAQ,IAAI,MAAM;AACnC,QAAM,gBAAgB,IAAI,QAAQ,IAAI,kBAAkB;AACxD,MAAI,eAAe,UAAU,aAAa,MAAM,OAAQ,QAAO;AAC/D,MAAI,eAAe,UAAU,IAAI,MAAM,OAAQ,QAAO;AAEtD,MAAI,IAAI,aAAa,QAAQ;AAC3B,WAAO,KAAK,+BAA+B,EAAE,QAAQ,MAAM,cAAc,CAAC;AAAA,EAC5E;AACA,SAAO;AACT;AAEO,SAAS,yBACd,OACA,SACoD;AACpD,MAAI,iBAAiB,wBAAwB;AAC3C,WAAO,EAAE,QAAQ,KAAK,MAAM,EAAE,OAAO,yBAAyB,EAAE;AAAA,EAClE;AACA,MAAI,iBAAiB,6BAA6B;AAChD,WAAO,MAAM,qCAAqC,EAAE,OAAO,QAAQ,MAAM,CAAC;AAC1E,WAAO,EAAE,QAAQ,KAAK,MAAM,EAAE,OAAO,QAAQ,cAAc,EAAE;AAAA,EAC/D;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.9.1-develop.7575.1.8480826a9b";
1
+ const APP_VERSION = "0.10.1-develop.7577.1.477f2486e5";
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.9.1-develop.7575.1.8480826a9b';\nexport const appVersion = APP_VERSION;\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.10.1-develop.7577.1.477f2486e5';\nexport const appVersion = APP_VERSION;\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
@@ -12,7 +12,7 @@ function resolveWebhookBodyLimitBytes(raw = typeof process === "undefined" ? voi
12
12
  const parsed = parseNumberWithDefault(raw, fallback, { min: 1 });
13
13
  return Number.isSafeInteger(parsed) ? parsed : fallback;
14
14
  }
15
- async function readBoundedRequestBody(request, options) {
15
+ async function readBoundedRequestBytes(request, options) {
16
16
  const configuredLimit = options?.maxBytes ?? resolveWebhookBodyLimitBytes();
17
17
  const maxBytes = Number.isSafeInteger(configuredLimit) && configuredLimit > 0 ? configuredLimit : DEFAULT_WEBHOOK_BODY_LIMIT_BYTES;
18
18
  const declaredLength = request.headers.get("content-length")?.trim();
@@ -22,7 +22,7 @@ async function readBoundedRequestBody(request, options) {
22
22
  throw new WebhookBodyTooLargeError(maxBytes);
23
23
  }
24
24
  }
25
- if (!request.body) return "";
25
+ if (!request.body) return new Uint8Array(0);
26
26
  const reader = request.body.getReader();
27
27
  const chunks = [];
28
28
  let totalBytes = 0;
@@ -46,12 +46,16 @@ async function readBoundedRequestBody(request, options) {
46
46
  body.set(chunk, offset);
47
47
  offset += chunk.byteLength;
48
48
  }
49
- return new TextDecoder().decode(body);
49
+ return body;
50
+ }
51
+ async function readBoundedRequestBody(request, options) {
52
+ return new TextDecoder().decode(await readBoundedRequestBytes(request, options));
50
53
  }
51
54
  export {
52
55
  DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,
53
56
  WebhookBodyTooLargeError,
54
57
  readBoundedRequestBody,
58
+ readBoundedRequestBytes,
55
59
  resolveWebhookBodyLimitBytes
56
60
  };
57
61
  //# sourceMappingURL=body.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/webhooks/body.ts"],
4
- "sourcesContent": ["import { parseNumberWithDefault } from '../number'\n\nexport const DEFAULT_WEBHOOK_BODY_LIMIT_BYTES = 1024 * 1024\n\nexport class WebhookBodyTooLargeError extends Error {\n readonly limitBytes: number\n\n constructor(limitBytes: number) {\n super(`Webhook body exceeds the ${limitBytes}-byte limit`)\n this.name = 'WebhookBodyTooLargeError'\n this.limitBytes = limitBytes\n }\n}\n\nexport function resolveWebhookBodyLimitBytes(\n raw = typeof process === 'undefined' ? undefined : process.env.OM_WEBHOOK_MAX_BODY_BYTES,\n fallbackBytes = DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,\n): number {\n const fallback = Number.isSafeInteger(fallbackBytes) && fallbackBytes > 0\n ? fallbackBytes\n : DEFAULT_WEBHOOK_BODY_LIMIT_BYTES\n const parsed = parseNumberWithDefault(raw, fallback, { min: 1 })\n return Number.isSafeInteger(parsed) ? parsed : fallback\n}\n\nexport async function readBoundedRequestBody(\n request: Request,\n options?: { maxBytes?: number },\n): Promise<string> {\n const configuredLimit = options?.maxBytes ?? resolveWebhookBodyLimitBytes()\n const maxBytes = Number.isSafeInteger(configuredLimit) && configuredLimit > 0\n ? configuredLimit\n : DEFAULT_WEBHOOK_BODY_LIMIT_BYTES\n const declaredLength = request.headers.get('content-length')?.trim()\n if (declaredLength && /^\\d+$/.test(declaredLength)) {\n const declaredBytes = Number(declaredLength)\n if (!Number.isSafeInteger(declaredBytes) || declaredBytes > maxBytes) {\n throw new WebhookBodyTooLargeError(maxBytes)\n }\n }\n\n if (!request.body) return ''\n\n const reader = request.body.getReader()\n const chunks: Uint8Array[] = []\n let totalBytes = 0\n\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n totalBytes += value.byteLength\n if (totalBytes > maxBytes) {\n await reader.cancel().catch(() => undefined)\n throw new WebhookBodyTooLargeError(maxBytes)\n }\n chunks.push(value)\n }\n } finally {\n reader.releaseLock()\n }\n\n const body = new Uint8Array(totalBytes)\n let offset = 0\n for (const chunk of chunks) {\n body.set(chunk, offset)\n offset += chunk.byteLength\n }\n return new TextDecoder().decode(body)\n}\n"],
5
- "mappings": "AAAA,SAAS,8BAA8B;AAEhC,MAAM,mCAAmC,OAAO;AAEhD,MAAM,iCAAiC,MAAM;AAAA,EAGlD,YAAY,YAAoB;AAC9B,UAAM,4BAA4B,UAAU,aAAa;AACzD,SAAK,OAAO;AACZ,SAAK,aAAa;AAAA,EACpB;AACF;AAEO,SAAS,6BACd,MAAM,OAAO,YAAY,cAAc,SAAY,QAAQ,IAAI,2BAC/D,gBAAgB,kCACR;AACR,QAAM,WAAW,OAAO,cAAc,aAAa,KAAK,gBAAgB,IACpE,gBACA;AACJ,QAAM,SAAS,uBAAuB,KAAK,UAAU,EAAE,KAAK,EAAE,CAAC;AAC/D,SAAO,OAAO,cAAc,MAAM,IAAI,SAAS;AACjD;AAEA,eAAsB,uBACpB,SACA,SACiB;AACjB,QAAM,kBAAkB,SAAS,YAAY,6BAA6B;AAC1E,QAAM,WAAW,OAAO,cAAc,eAAe,KAAK,kBAAkB,IACxE,kBACA;AACJ,QAAM,iBAAiB,QAAQ,QAAQ,IAAI,gBAAgB,GAAG,KAAK;AACnE,MAAI,kBAAkB,QAAQ,KAAK,cAAc,GAAG;AAClD,UAAM,gBAAgB,OAAO,cAAc;AAC3C,QAAI,CAAC,OAAO,cAAc,aAAa,KAAK,gBAAgB,UAAU;AACpE,YAAM,IAAI,yBAAyB,QAAQ;AAAA,IAC7C;AAAA,EACF;AAEA,MAAI,CAAC,QAAQ,KAAM,QAAO;AAE1B,QAAM,SAAS,QAAQ,KAAK,UAAU;AACtC,QAAM,SAAuB,CAAC;AAC9B,MAAI,aAAa;AAEjB,MAAI;AACF,WAAO,MAAM;AACX,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,UAAI,KAAM;AACV,oBAAc,MAAM;AACpB,UAAI,aAAa,UAAU;AACzB,cAAM,OAAO,OAAO,EAAE,MAAM,MAAM,MAAS;AAC3C,cAAM,IAAI,yBAAyB,QAAQ;AAAA,MAC7C;AACA,aAAO,KAAK,KAAK;AAAA,IACnB;AAAA,EACF,UAAE;AACA,WAAO,YAAY;AAAA,EACrB;AAEA,QAAM,OAAO,IAAI,WAAW,UAAU;AACtC,MAAI,SAAS;AACb,aAAW,SAAS,QAAQ;AAC1B,SAAK,IAAI,OAAO,MAAM;AACtB,cAAU,MAAM;AAAA,EAClB;AACA,SAAO,IAAI,YAAY,EAAE,OAAO,IAAI;AACtC;",
4
+ "sourcesContent": ["import { parseNumberWithDefault } from '../number'\n\nexport const DEFAULT_WEBHOOK_BODY_LIMIT_BYTES = 1024 * 1024\n\nexport class WebhookBodyTooLargeError extends Error {\n readonly limitBytes: number\n\n constructor(limitBytes: number) {\n super(`Webhook body exceeds the ${limitBytes}-byte limit`)\n this.name = 'WebhookBodyTooLargeError'\n this.limitBytes = limitBytes\n }\n}\n\nexport function resolveWebhookBodyLimitBytes(\n raw = typeof process === 'undefined' ? undefined : process.env.OM_WEBHOOK_MAX_BODY_BYTES,\n fallbackBytes = DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,\n): number {\n const fallback = Number.isSafeInteger(fallbackBytes) && fallbackBytes > 0\n ? fallbackBytes\n : DEFAULT_WEBHOOK_BODY_LIMIT_BYTES\n const parsed = parseNumberWithDefault(raw, fallback, { min: 1 })\n return Number.isSafeInteger(parsed) ? parsed : fallback\n}\n\nexport async function readBoundedRequestBytes(\n request: Request,\n options?: { maxBytes?: number },\n): Promise<Uint8Array> {\n const configuredLimit = options?.maxBytes ?? resolveWebhookBodyLimitBytes()\n const maxBytes = Number.isSafeInteger(configuredLimit) && configuredLimit > 0\n ? configuredLimit\n : DEFAULT_WEBHOOK_BODY_LIMIT_BYTES\n const declaredLength = request.headers.get('content-length')?.trim()\n if (declaredLength && /^\\d+$/.test(declaredLength)) {\n const declaredBytes = Number(declaredLength)\n if (!Number.isSafeInteger(declaredBytes) || declaredBytes > maxBytes) {\n throw new WebhookBodyTooLargeError(maxBytes)\n }\n }\n\n if (!request.body) return new Uint8Array(0)\n\n const reader = request.body.getReader()\n const chunks: Uint8Array[] = []\n let totalBytes = 0\n\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n totalBytes += value.byteLength\n if (totalBytes > maxBytes) {\n await reader.cancel().catch(() => undefined)\n throw new WebhookBodyTooLargeError(maxBytes)\n }\n chunks.push(value)\n }\n } finally {\n reader.releaseLock()\n }\n\n const body = new Uint8Array(totalBytes)\n let offset = 0\n for (const chunk of chunks) {\n body.set(chunk, offset)\n offset += chunk.byteLength\n }\n return body\n}\n\nexport async function readBoundedRequestBody(\n request: Request,\n options?: { maxBytes?: number },\n): Promise<string> {\n return new TextDecoder().decode(await readBoundedRequestBytes(request, options))\n}\n"],
5
+ "mappings": "AAAA,SAAS,8BAA8B;AAEhC,MAAM,mCAAmC,OAAO;AAEhD,MAAM,iCAAiC,MAAM;AAAA,EAGlD,YAAY,YAAoB;AAC9B,UAAM,4BAA4B,UAAU,aAAa;AACzD,SAAK,OAAO;AACZ,SAAK,aAAa;AAAA,EACpB;AACF;AAEO,SAAS,6BACd,MAAM,OAAO,YAAY,cAAc,SAAY,QAAQ,IAAI,2BAC/D,gBAAgB,kCACR;AACR,QAAM,WAAW,OAAO,cAAc,aAAa,KAAK,gBAAgB,IACpE,gBACA;AACJ,QAAM,SAAS,uBAAuB,KAAK,UAAU,EAAE,KAAK,EAAE,CAAC;AAC/D,SAAO,OAAO,cAAc,MAAM,IAAI,SAAS;AACjD;AAEA,eAAsB,wBACpB,SACA,SACqB;AACrB,QAAM,kBAAkB,SAAS,YAAY,6BAA6B;AAC1E,QAAM,WAAW,OAAO,cAAc,eAAe,KAAK,kBAAkB,IACxE,kBACA;AACJ,QAAM,iBAAiB,QAAQ,QAAQ,IAAI,gBAAgB,GAAG,KAAK;AACnE,MAAI,kBAAkB,QAAQ,KAAK,cAAc,GAAG;AAClD,UAAM,gBAAgB,OAAO,cAAc;AAC3C,QAAI,CAAC,OAAO,cAAc,aAAa,KAAK,gBAAgB,UAAU;AACpE,YAAM,IAAI,yBAAyB,QAAQ;AAAA,IAC7C;AAAA,EACF;AAEA,MAAI,CAAC,QAAQ,KAAM,QAAO,IAAI,WAAW,CAAC;AAE1C,QAAM,SAAS,QAAQ,KAAK,UAAU;AACtC,QAAM,SAAuB,CAAC;AAC9B,MAAI,aAAa;AAEjB,MAAI;AACF,WAAO,MAAM;AACX,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,UAAI,KAAM;AACV,oBAAc,MAAM;AACpB,UAAI,aAAa,UAAU;AACzB,cAAM,OAAO,OAAO,EAAE,MAAM,MAAM,MAAS;AAC3C,cAAM,IAAI,yBAAyB,QAAQ;AAAA,MAC7C;AACA,aAAO,KAAK,KAAK;AAAA,IACnB;AAAA,EACF,UAAE;AACA,WAAO,YAAY;AAAA,EACrB;AAEA,QAAM,OAAO,IAAI,WAAW,UAAU;AACtC,MAAI,SAAS;AACb,aAAW,SAAS,QAAQ;AAC1B,SAAK,IAAI,OAAO,MAAM;AACtB,cAAU,MAAM;AAAA,EAClB;AACA,SAAO;AACT;AAEA,eAAsB,uBACpB,SACA,SACiB;AACjB,SAAO,IAAI,YAAY,EAAE,OAAO,MAAM,wBAAwB,SAAS,OAAO,CAAC;AACjF;",
6
6
  "names": []
7
7
  }
@@ -4,6 +4,7 @@ import { generateWebhookSecret, parseWebhookSecret, isValidWebhookSecret } from
4
4
  import {
5
5
  DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,
6
6
  readBoundedRequestBody,
7
+ readBoundedRequestBytes,
7
8
  resolveWebhookBodyLimitBytes,
8
9
  WebhookBodyTooLargeError
9
10
  } from "./body.js";
@@ -18,6 +19,7 @@ export {
18
19
  isWebhookTimestampWithinTolerance,
19
20
  parseWebhookSecret,
20
21
  readBoundedRequestBody,
22
+ readBoundedRequestBytes,
21
23
  resolveWebhookBodyLimitBytes,
22
24
  signWebhookPayload,
23
25
  verifyWebhookSignature
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/webhooks/index.ts"],
4
- "sourcesContent": ["export { signWebhookPayload, buildWebhookHeaders, generateMessageId } from './sign'\nexport { WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, isWebhookTimestampWithinTolerance, verifyWebhookSignature } from './verify'\nexport { generateWebhookSecret, parseWebhookSecret, isValidWebhookSecret } from './secrets'\nexport {\n DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,\n readBoundedRequestBody,\n resolveWebhookBodyLimitBytes,\n WebhookBodyTooLargeError,\n} from './body'\nexport type { StandardWebhookHeaders, WebhookSigningKey, WebhookVerificationResult, StandardWebhookPayload } from './types'\nexport type {\n InboundWebhookRequest,\n WebhookSourceCredentialField,\n WebhookSourceConfig,\n WebhookHandlerMeta,\n WebhookHandlerPayload,\n WebhookHandlerContext,\n WebhookHandler,\n WebhookHandlerRegistryEntry,\n WebhookHandlerResult,\n WebhookIngestionStatus,\n} from './inbound-types'\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB,qBAAqB,yBAAyB;AAC3E,SAAS,qCAAqC,mCAAmC,8BAA8B;AAC/G,SAAS,uBAAuB,oBAAoB,4BAA4B;AAChF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;",
4
+ "sourcesContent": ["export { signWebhookPayload, buildWebhookHeaders, generateMessageId } from './sign'\nexport { WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, isWebhookTimestampWithinTolerance, verifyWebhookSignature } from './verify'\nexport { generateWebhookSecret, parseWebhookSecret, isValidWebhookSecret } from './secrets'\nexport {\n DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,\n readBoundedRequestBody,\n readBoundedRequestBytes,\n resolveWebhookBodyLimitBytes,\n WebhookBodyTooLargeError,\n} from './body'\nexport type { StandardWebhookHeaders, WebhookSigningKey, WebhookVerificationResult, StandardWebhookPayload } from './types'\nexport type {\n InboundWebhookRequest,\n WebhookSourceCredentialField,\n WebhookSourceConfig,\n WebhookHandlerMeta,\n WebhookHandlerPayload,\n WebhookHandlerContext,\n WebhookHandler,\n WebhookHandlerRegistryEntry,\n WebhookHandlerResult,\n WebhookIngestionStatus,\n} from './inbound-types'\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB,qBAAqB,yBAAyB;AAC3E,SAAS,qCAAqC,mCAAmC,8BAA8B;AAC/G,SAAS,uBAAuB,oBAAoB,4BAA4B;AAChF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/integrations/types.ts"],
4
- "sourcesContent": ["export type IntegrationScope = {\n organizationId: string\n tenantId: string\n /**\n * Optional per-user secret scoping. When set, credential lookups and writes\n * are scoped to a single user \u2014 used by per-user channels (Gmail, IMAP)\n * so two users on the same tenant don't share one row.\n *\n * When `undefined`/`null`, behaviour is unchanged \u2014 tenant-wide credentials\n * (e.g. shared Stripe/Akeneo API keys) keep working exactly as before.\n *\n * Stored on `IntegrationCredentials.user_id` (added 2026-05-26 by the email\n * integration spec). Backed by the partial unique index\n * `integration_credentials_user_lookup_idx` across integration, organization,\n * tenant, and user where `user_id IS NOT NULL AND deleted_at IS NULL`.\n */\n userId?: string | null\n}\n\nexport type IntegrationCategory =\n | 'payment'\n | 'shipping'\n | 'data_sync'\n | 'communication'\n | 'webhook'\n | 'storage'\n | 'other'\n\nexport type IntegrationHubId =\n | 'payment_gateways'\n | 'shipping_carriers'\n | 'data_sync'\n | 'communication_channels'\n | 'webhook_endpoints'\n | 'storage_hubs'\n | string\n\nexport type CredentialFieldType =\n | 'text'\n | 'secret'\n | 'select'\n | 'boolean'\n | 'url'\n | 'oauth'\n | 'ssh_keypair'\n\nexport interface CredentialFieldOption {\n value: string\n label: string\n}\n\nexport interface CredentialFieldVisibleWhen {\n field: string\n equals: string | number | boolean\n}\n\nexport interface IntegrationCredentialWebhookHelp {\n kind: 'webhook_setup'\n title: string\n summary: string\n endpointPath: string\n dashboardPathLabel: string\n steps: string[]\n events?: string[]\n localDevelopment?: {\n tunnelCommand: string\n publicUrlExample: string\n note?: string\n }\n}\n\nexport interface IntegrationCredentialFieldBase {\n key: string\n label: string\n required?: boolean\n placeholder?: string\n helpText?: string\n helpDetails?: IntegrationCredentialWebhookHelp\n visibleWhen?: CredentialFieldVisibleWhen\n}\n\nexport interface IntegrationCredentialFieldText extends IntegrationCredentialFieldBase {\n type: Extract<CredentialFieldType, 'text' | 'secret' | 'url'>\n}\n\nexport interface IntegrationCredentialFieldBoolean extends IntegrationCredentialFieldBase {\n type: Extract<CredentialFieldType, 'boolean'>\n}\n\nexport interface IntegrationCredentialFieldSelect extends IntegrationCredentialFieldBase {\n type: Extract<CredentialFieldType, 'select'>\n options: CredentialFieldOption[]\n}\n\nexport interface IntegrationCredentialFieldOauth extends IntegrationCredentialFieldBase {\n type: Extract<CredentialFieldType, 'oauth'>\n authUrl?: string\n tokenUrl?: string\n scopes?: string[]\n clientIdField?: string\n clientSecretField?: string\n}\n\nexport interface IntegrationCredentialFieldSshKeypair extends IntegrationCredentialFieldBase {\n type: Extract<CredentialFieldType, 'ssh_keypair'>\n algorithm?: 'ed25519' | 'rsa'\n rsaBits?: 2048 | 3072 | 4096\n}\n\nexport type IntegrationCredentialField =\n | IntegrationCredentialFieldText\n | IntegrationCredentialFieldBoolean\n | IntegrationCredentialFieldSelect\n | IntegrationCredentialFieldOauth\n | IntegrationCredentialFieldSshKeypair\n\nexport interface IntegrationCredentialsSchema {\n fields: IntegrationCredentialField[]\n}\n\nexport interface IntegrationHealthCheckConfig {\n service: string\n}\n\nexport interface ApiVersionDefinition {\n id: string\n label: string\n status: 'stable' | 'deprecated' | 'experimental'\n default?: boolean\n changelog?: string\n deprecatedAt?: string\n sunsetAt?: string\n migrationGuide?: string\n}\n\nexport interface IntegrationBundle {\n id: string\n title: string\n description: string\n icon?: string\n package?: string\n version?: string\n author?: string\n credentials: IntegrationCredentialsSchema\n healthCheck?: IntegrationHealthCheckConfig\n}\n\nexport interface IntegrationDetailPageConfig {\n /**\n * UMES widget spot rendered on the integration detail page.\n * Widgets registered here can render inline blocks, grouped panels,\n * or additional tabs via `placement.kind`.\n */\n widgetSpotId?: string\n /**\n * Built-in tabs to hide for this integration detail page.\n * Provider modules can replace these with injected tabs using the same spot.\n */\n hiddenTabs?: IntegrationDetailBuiltInTab[]\n}\n\nexport type IntegrationDetailBuiltInTab =\n | 'credentials'\n | 'version'\n | 'health'\n | 'logs'\n | 'data-sync-schedule'\n\nexport interface IntegrationDefaultStateConfig {\n isEnabled?: boolean\n}\n\nexport interface IntegrationDefinition {\n id: string\n title: string\n icon?: string\n buildExternalUrl?: (externalId: string) => string\n bundleId?: string\n apiVersions?: ApiVersionDefinition[]\n description?: string\n category?: IntegrationCategory | string\n hub?: IntegrationHubId\n providerKey?: string\n docsUrl?: string\n package?: string\n version?: string\n author?: string\n company?: string\n license?: string\n tags?: string[]\n detailPage?: IntegrationDetailPageConfig\n defaultState?: IntegrationDefaultStateConfig\n credentials?: IntegrationCredentialsSchema\n healthCheck?: IntegrationHealthCheckConfig\n}\n\nexport interface ExternalIdEnrichment {\n _integrations: Record<string, ExternalIdMapping>\n}\n\nexport interface ExternalIdMapping {\n externalId: string\n externalUrl?: string\n lastSyncedAt?: string\n syncStatus: 'synced' | 'pending' | 'error' | 'not_synced'\n}\n\ntype IntegrationRegistryState = {\n integrations: Map<string, IntegrationDefinition>\n bundles: Map<string, IntegrationBundle>\n}\n\nconst GLOBAL_INTEGRATION_REGISTRY_KEY = '__openMercatoIntegrationRegistry__' as const\n\ntype GlobalIntegrationRegistry = typeof globalThis & {\n [GLOBAL_INTEGRATION_REGISTRY_KEY]?: IntegrationRegistryState\n}\n\nfunction getIntegrationRegistryState(): IntegrationRegistryState {\n const globalRegistry = globalThis as GlobalIntegrationRegistry\n if (!globalRegistry[GLOBAL_INTEGRATION_REGISTRY_KEY]) {\n globalRegistry[GLOBAL_INTEGRATION_REGISTRY_KEY] = {\n integrations: new Map<string, IntegrationDefinition>(),\n bundles: new Map<string, IntegrationBundle>(),\n }\n }\n return globalRegistry[GLOBAL_INTEGRATION_REGISTRY_KEY]\n}\n\nexport function registerIntegration(definition: IntegrationDefinition): void {\n getIntegrationRegistryState().integrations.set(definition.id, definition)\n}\n\nexport function registerIntegrations(definitions: IntegrationDefinition[]): void {\n const registry = getIntegrationRegistryState().integrations\n for (const definition of definitions) {\n registry.set(definition.id, definition)\n }\n}\n\nexport function registerBundle(bundle: IntegrationBundle): void {\n getIntegrationRegistryState().bundles.set(bundle.id, bundle)\n}\n\nexport function registerBundles(bundles: IntegrationBundle[]): void {\n const registry = getIntegrationRegistryState().bundles\n for (const bundle of bundles) {\n registry.set(bundle.id, bundle)\n }\n}\n\nexport function clearRegisteredIntegrations(): void {\n const registry = getIntegrationRegistryState()\n registry.integrations.clear()\n registry.bundles.clear()\n}\n\nexport function getIntegration(integrationId: string): IntegrationDefinition | undefined {\n return getIntegrationRegistryState().integrations.get(integrationId)\n}\n\nexport function getAllIntegrations(): IntegrationDefinition[] {\n return Array.from(getIntegrationRegistryState().integrations.values())\n}\n\nexport function getBundle(bundleId: string): IntegrationBundle | undefined {\n return getIntegrationRegistryState().bundles.get(bundleId)\n}\n\nexport function getAllBundles(): IntegrationBundle[] {\n return Array.from(getIntegrationRegistryState().bundles.values())\n}\n\nexport function getBundleIntegrations(bundleId: string): IntegrationDefinition[] {\n return Array.from(getIntegrationRegistryState().integrations.values()).filter((integration) => integration.bundleId === bundleId)\n}\n\nexport function resolveIntegrationCredentialsSchema(integrationId: string): IntegrationCredentialsSchema | undefined {\n const registry = getIntegrationRegistryState()\n const definition = registry.integrations.get(integrationId)\n if (!definition) return undefined\n\n if (definition.credentials && definition.credentials.fields.length > 0) {\n return definition.credentials\n }\n\n if (!definition.bundleId) return definition.credentials\n return registry.bundles.get(definition.bundleId)?.credentials\n}\n\nexport function getIntegrationTitle(integrationId: string): string {\n return getIntegrationRegistryState().integrations.get(integrationId)?.title ?? integrationId\n}\n\nexport const LEGACY_INTEGRATION_DETAIL_TABS_SPOT_ID = 'integrations.detail:tabs'\n\nexport function buildIntegrationDetailWidgetSpotId(integrationId: string): string {\n return `integrations.detail:${integrationId}`\n}\n"],
5
- "mappings": "AAoNA,MAAM,kCAAkC;AAMxC,SAAS,8BAAwD;AAC/D,QAAM,iBAAiB;AACvB,MAAI,CAAC,eAAe,+BAA+B,GAAG;AACpD,mBAAe,+BAA+B,IAAI;AAAA,MAChD,cAAc,oBAAI,IAAmC;AAAA,MACrD,SAAS,oBAAI,IAA+B;AAAA,IAC9C;AAAA,EACF;AACA,SAAO,eAAe,+BAA+B;AACvD;AAEO,SAAS,oBAAoB,YAAyC;AAC3E,8BAA4B,EAAE,aAAa,IAAI,WAAW,IAAI,UAAU;AAC1E;AAEO,SAAS,qBAAqB,aAA4C;AAC/E,QAAM,WAAW,4BAA4B,EAAE;AAC/C,aAAW,cAAc,aAAa;AACpC,aAAS,IAAI,WAAW,IAAI,UAAU;AAAA,EACxC;AACF;AAEO,SAAS,eAAe,QAAiC;AAC9D,8BAA4B,EAAE,QAAQ,IAAI,OAAO,IAAI,MAAM;AAC7D;AAEO,SAAS,gBAAgB,SAAoC;AAClE,QAAM,WAAW,4BAA4B,EAAE;AAC/C,aAAW,UAAU,SAAS;AAC5B,aAAS,IAAI,OAAO,IAAI,MAAM;AAAA,EAChC;AACF;AAEO,SAAS,8BAAoC;AAClD,QAAM,WAAW,4BAA4B;AAC7C,WAAS,aAAa,MAAM;AAC5B,WAAS,QAAQ,MAAM;AACzB;AAEO,SAAS,eAAe,eAA0D;AACvF,SAAO,4BAA4B,EAAE,aAAa,IAAI,aAAa;AACrE;AAEO,SAAS,qBAA8C;AAC5D,SAAO,MAAM,KAAK,4BAA4B,EAAE,aAAa,OAAO,CAAC;AACvE;AAEO,SAAS,UAAU,UAAiD;AACzE,SAAO,4BAA4B,EAAE,QAAQ,IAAI,QAAQ;AAC3D;AAEO,SAAS,gBAAqC;AACnD,SAAO,MAAM,KAAK,4BAA4B,EAAE,QAAQ,OAAO,CAAC;AAClE;AAEO,SAAS,sBAAsB,UAA2C;AAC/E,SAAO,MAAM,KAAK,4BAA4B,EAAE,aAAa,OAAO,CAAC,EAAE,OAAO,CAAC,gBAAgB,YAAY,aAAa,QAAQ;AAClI;AAEO,SAAS,oCAAoC,eAAiE;AACnH,QAAM,WAAW,4BAA4B;AAC7C,QAAM,aAAa,SAAS,aAAa,IAAI,aAAa;AAC1D,MAAI,CAAC,WAAY,QAAO;AAExB,MAAI,WAAW,eAAe,WAAW,YAAY,OAAO,SAAS,GAAG;AACtE,WAAO,WAAW;AAAA,EACpB;AAEA,MAAI,CAAC,WAAW,SAAU,QAAO,WAAW;AAC5C,SAAO,SAAS,QAAQ,IAAI,WAAW,QAAQ,GAAG;AACpD;AAEO,SAAS,oBAAoB,eAA+B;AACjE,SAAO,4BAA4B,EAAE,aAAa,IAAI,aAAa,GAAG,SAAS;AACjF;AAEO,MAAM,yCAAyC;AAE/C,SAAS,mCAAmC,eAA+B;AAChF,SAAO,uBAAuB,aAAa;AAC7C;",
4
+ "sourcesContent": ["export type IntegrationScope = {\n organizationId: string\n tenantId: string\n /**\n * Optional per-user secret scoping. When set, credential lookups and writes\n * are scoped to a single user \u2014 used by per-user channels (Gmail, IMAP)\n * so two users on the same tenant don't share one row.\n *\n * When `undefined`/`null`, behaviour is unchanged \u2014 tenant-wide credentials\n * (e.g. shared Stripe/Akeneo API keys) keep working exactly as before.\n *\n * Stored on `IntegrationCredentials.user_id` (added 2026-05-26 by the email\n * integration spec). Backed by the partial unique index\n * `integration_credentials_user_lookup_idx` across integration, organization,\n * tenant, and user where `user_id IS NOT NULL AND deleted_at IS NULL`.\n */\n userId?: string | null\n}\n\nexport type IntegrationCategory =\n | 'payment'\n | 'shipping'\n | 'data_sync'\n | 'communication'\n | 'webhook'\n | 'storage'\n | 'other'\n\nexport type IntegrationHubId =\n | 'payment_gateways'\n | 'shipping_carriers'\n | 'data_sync'\n | 'communication_channels'\n | 'webhook_endpoints'\n | 'storage_hubs'\n | string\n\nexport type CredentialFieldType =\n | 'text'\n | 'secret'\n | 'select'\n | 'boolean'\n | 'url'\n | 'oauth'\n | 'ssh_keypair'\n\nexport interface CredentialFieldOption {\n value: string\n label: string\n}\n\nexport interface CredentialFieldVisibleWhen {\n field: string\n equals: string | number | boolean\n}\n\nexport interface IntegrationCredentialWebhookHelp {\n kind: 'webhook_setup'\n title: string\n summary: string\n endpointPath: string\n dashboardPathLabel: string\n steps: string[]\n events?: string[]\n localDevelopment?: {\n tunnelCommand: string\n publicUrlExample: string\n note?: string\n }\n}\n\nexport interface IntegrationCredentialFieldBase {\n key: string\n label: string\n required?: boolean\n placeholder?: string\n helpText?: string\n helpDetails?: IntegrationCredentialWebhookHelp\n visibleWhen?: CredentialFieldVisibleWhen\n}\n\nexport interface IntegrationCredentialFieldText extends IntegrationCredentialFieldBase {\n type: Extract<CredentialFieldType, 'text' | 'secret' | 'url'>\n}\n\nexport interface IntegrationCredentialFieldBoolean extends IntegrationCredentialFieldBase {\n type: Extract<CredentialFieldType, 'boolean'>\n}\n\nexport interface IntegrationCredentialFieldSelect extends IntegrationCredentialFieldBase {\n type: Extract<CredentialFieldType, 'select'>\n options: CredentialFieldOption[]\n}\n\nexport interface IntegrationCredentialFieldOauth extends IntegrationCredentialFieldBase {\n type: Extract<CredentialFieldType, 'oauth'>\n authUrl?: string\n tokenUrl?: string\n scopes?: string[]\n clientIdField?: string\n clientSecretField?: string\n}\n\nexport interface IntegrationCredentialFieldSshKeypair extends IntegrationCredentialFieldBase {\n type: Extract<CredentialFieldType, 'ssh_keypair'>\n algorithm?: 'ed25519' | 'rsa'\n rsaBits?: 2048 | 3072 | 4096\n}\n\nexport type IntegrationCredentialField =\n | IntegrationCredentialFieldText\n | IntegrationCredentialFieldBoolean\n | IntegrationCredentialFieldSelect\n | IntegrationCredentialFieldOauth\n | IntegrationCredentialFieldSshKeypair\n\nexport interface IntegrationCredentialsSchema {\n fields: IntegrationCredentialField[]\n}\n\nexport interface IntegrationHealthCheckConfig {\n service: string\n}\n\nexport interface ApiVersionDefinition {\n id: string\n label: string\n status: 'stable' | 'deprecated' | 'experimental'\n default?: boolean\n changelog?: string\n deprecatedAt?: string\n sunsetAt?: string\n migrationGuide?: string\n}\n\nexport interface IntegrationBundle {\n id: string\n title: string\n description: string\n icon?: string\n package?: string\n version?: string\n author?: string\n credentials: IntegrationCredentialsSchema\n healthCheck?: IntegrationHealthCheckConfig\n}\n\nexport interface IntegrationDetailPageConfig {\n /**\n * UMES widget spot rendered on the integration detail page.\n * Widgets registered here can render inline blocks, grouped panels,\n * or additional tabs via `placement.kind`.\n */\n widgetSpotId?: string\n /**\n * Built-in tabs to hide for this integration detail page.\n * Provider modules can replace these with injected tabs using the same spot.\n */\n hiddenTabs?: IntegrationDetailBuiltInTab[]\n /**\n * Detail-page tab that holds the provider's Connect / Reconnect controls (a built-in tab id or\n * an injected tab id). The OAuth reauth banner links to `?tab=<connectTabId>` while that tab is\n * visible and the viewer holds `integrations.credentials.manage`.\n */\n connectTabId?: string\n}\n\nexport type IntegrationDetailBuiltInTab =\n | 'credentials'\n | 'version'\n | 'health'\n | 'logs'\n | 'data-sync-schedule'\n\nexport interface IntegrationDefaultStateConfig {\n isEnabled?: boolean\n}\n\nexport interface IntegrationDefinition {\n id: string\n title: string\n icon?: string\n buildExternalUrl?: (externalId: string) => string\n bundleId?: string\n apiVersions?: ApiVersionDefinition[]\n description?: string\n category?: IntegrationCategory | string\n hub?: IntegrationHubId\n providerKey?: string\n docsUrl?: string\n package?: string\n version?: string\n author?: string\n company?: string\n license?: string\n tags?: string[]\n detailPage?: IntegrationDetailPageConfig\n defaultState?: IntegrationDefaultStateConfig\n credentials?: IntegrationCredentialsSchema\n healthCheck?: IntegrationHealthCheckConfig\n}\n\nexport interface ExternalIdEnrichment {\n _integrations: Record<string, ExternalIdMapping>\n}\n\nexport interface ExternalIdMapping {\n externalId: string\n externalUrl?: string\n lastSyncedAt?: string\n syncStatus: 'synced' | 'pending' | 'error' | 'not_synced'\n}\n\ntype IntegrationRegistryState = {\n integrations: Map<string, IntegrationDefinition>\n bundles: Map<string, IntegrationBundle>\n}\n\nconst GLOBAL_INTEGRATION_REGISTRY_KEY = '__openMercatoIntegrationRegistry__' as const\n\ntype GlobalIntegrationRegistry = typeof globalThis & {\n [GLOBAL_INTEGRATION_REGISTRY_KEY]?: IntegrationRegistryState\n}\n\nfunction getIntegrationRegistryState(): IntegrationRegistryState {\n const globalRegistry = globalThis as GlobalIntegrationRegistry\n if (!globalRegistry[GLOBAL_INTEGRATION_REGISTRY_KEY]) {\n globalRegistry[GLOBAL_INTEGRATION_REGISTRY_KEY] = {\n integrations: new Map<string, IntegrationDefinition>(),\n bundles: new Map<string, IntegrationBundle>(),\n }\n }\n return globalRegistry[GLOBAL_INTEGRATION_REGISTRY_KEY]\n}\n\nexport function registerIntegration(definition: IntegrationDefinition): void {\n getIntegrationRegistryState().integrations.set(definition.id, definition)\n}\n\nexport function registerIntegrations(definitions: IntegrationDefinition[]): void {\n const registry = getIntegrationRegistryState().integrations\n for (const definition of definitions) {\n registry.set(definition.id, definition)\n }\n}\n\nexport function registerBundle(bundle: IntegrationBundle): void {\n getIntegrationRegistryState().bundles.set(bundle.id, bundle)\n}\n\nexport function registerBundles(bundles: IntegrationBundle[]): void {\n const registry = getIntegrationRegistryState().bundles\n for (const bundle of bundles) {\n registry.set(bundle.id, bundle)\n }\n}\n\nexport function clearRegisteredIntegrations(): void {\n const registry = getIntegrationRegistryState()\n registry.integrations.clear()\n registry.bundles.clear()\n}\n\nexport function getIntegration(integrationId: string): IntegrationDefinition | undefined {\n return getIntegrationRegistryState().integrations.get(integrationId)\n}\n\nexport function getAllIntegrations(): IntegrationDefinition[] {\n return Array.from(getIntegrationRegistryState().integrations.values())\n}\n\nexport function getBundle(bundleId: string): IntegrationBundle | undefined {\n return getIntegrationRegistryState().bundles.get(bundleId)\n}\n\nexport function getAllBundles(): IntegrationBundle[] {\n return Array.from(getIntegrationRegistryState().bundles.values())\n}\n\nexport function getBundleIntegrations(bundleId: string): IntegrationDefinition[] {\n return Array.from(getIntegrationRegistryState().integrations.values()).filter((integration) => integration.bundleId === bundleId)\n}\n\nexport function resolveIntegrationCredentialsSchema(integrationId: string): IntegrationCredentialsSchema | undefined {\n const registry = getIntegrationRegistryState()\n const definition = registry.integrations.get(integrationId)\n if (!definition) return undefined\n\n if (definition.credentials && definition.credentials.fields.length > 0) {\n return definition.credentials\n }\n\n if (!definition.bundleId) return definition.credentials\n return registry.bundles.get(definition.bundleId)?.credentials\n}\n\nexport function getIntegrationTitle(integrationId: string): string {\n return getIntegrationRegistryState().integrations.get(integrationId)?.title ?? integrationId\n}\n\nexport const LEGACY_INTEGRATION_DETAIL_TABS_SPOT_ID = 'integrations.detail:tabs'\n\nexport function buildIntegrationDetailWidgetSpotId(integrationId: string): string {\n return `integrations.detail:${integrationId}`\n}\n"],
5
+ "mappings": "AA0NA,MAAM,kCAAkC;AAMxC,SAAS,8BAAwD;AAC/D,QAAM,iBAAiB;AACvB,MAAI,CAAC,eAAe,+BAA+B,GAAG;AACpD,mBAAe,+BAA+B,IAAI;AAAA,MAChD,cAAc,oBAAI,IAAmC;AAAA,MACrD,SAAS,oBAAI,IAA+B;AAAA,IAC9C;AAAA,EACF;AACA,SAAO,eAAe,+BAA+B;AACvD;AAEO,SAAS,oBAAoB,YAAyC;AAC3E,8BAA4B,EAAE,aAAa,IAAI,WAAW,IAAI,UAAU;AAC1E;AAEO,SAAS,qBAAqB,aAA4C;AAC/E,QAAM,WAAW,4BAA4B,EAAE;AAC/C,aAAW,cAAc,aAAa;AACpC,aAAS,IAAI,WAAW,IAAI,UAAU;AAAA,EACxC;AACF;AAEO,SAAS,eAAe,QAAiC;AAC9D,8BAA4B,EAAE,QAAQ,IAAI,OAAO,IAAI,MAAM;AAC7D;AAEO,SAAS,gBAAgB,SAAoC;AAClE,QAAM,WAAW,4BAA4B,EAAE;AAC/C,aAAW,UAAU,SAAS;AAC5B,aAAS,IAAI,OAAO,IAAI,MAAM;AAAA,EAChC;AACF;AAEO,SAAS,8BAAoC;AAClD,QAAM,WAAW,4BAA4B;AAC7C,WAAS,aAAa,MAAM;AAC5B,WAAS,QAAQ,MAAM;AACzB;AAEO,SAAS,eAAe,eAA0D;AACvF,SAAO,4BAA4B,EAAE,aAAa,IAAI,aAAa;AACrE;AAEO,SAAS,qBAA8C;AAC5D,SAAO,MAAM,KAAK,4BAA4B,EAAE,aAAa,OAAO,CAAC;AACvE;AAEO,SAAS,UAAU,UAAiD;AACzE,SAAO,4BAA4B,EAAE,QAAQ,IAAI,QAAQ;AAC3D;AAEO,SAAS,gBAAqC;AACnD,SAAO,MAAM,KAAK,4BAA4B,EAAE,QAAQ,OAAO,CAAC;AAClE;AAEO,SAAS,sBAAsB,UAA2C;AAC/E,SAAO,MAAM,KAAK,4BAA4B,EAAE,aAAa,OAAO,CAAC,EAAE,OAAO,CAAC,gBAAgB,YAAY,aAAa,QAAQ;AAClI;AAEO,SAAS,oCAAoC,eAAiE;AACnH,QAAM,WAAW,4BAA4B;AAC7C,QAAM,aAAa,SAAS,aAAa,IAAI,aAAa;AAC1D,MAAI,CAAC,WAAY,QAAO;AAExB,MAAI,WAAW,eAAe,WAAW,YAAY,OAAO,SAAS,GAAG;AACtE,WAAO,WAAW;AAAA,EACpB;AAEA,MAAI,CAAC,WAAW,SAAU,QAAO,WAAW;AAC5C,SAAO,SAAS,QAAQ,IAAI,WAAW,QAAQ,GAAG;AACpD;AAEO,SAAS,oBAAoB,eAA+B;AACjE,SAAO,4BAA4B,EAAE,aAAa,IAAI,aAAa,GAAG,SAAS;AACjF;AAEO,MAAM,yCAAyC;AAE/C,SAAS,mCAAmC,eAA+B;AAChF,SAAO,uBAAuB,aAAa;AAC7C;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,20 @@
1
+ const SENTINEL_ORIGIN = "https://notifications.open-mercato.local";
2
+ const CONTROL_OR_BACKSLASH = /[\u0000-\u001F\u007F\\]/;
3
+ function isSafeNotificationHref(href) {
4
+ if (!href.startsWith("/") || href.startsWith("//")) {
5
+ return false;
6
+ }
7
+ if (CONTROL_OR_BACKSLASH.test(href)) {
8
+ return false;
9
+ }
10
+ try {
11
+ const parsed = new URL(href, SENTINEL_ORIGIN);
12
+ return parsed.origin === SENTINEL_ORIGIN && !parsed.pathname.startsWith("//");
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+ export {
18
+ isSafeNotificationHref
19
+ };
20
+ //# sourceMappingURL=safeHref.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/notifications/safeHref.ts"],
4
+ "sourcesContent": ["const SENTINEL_ORIGIN = 'https://notifications.open-mercato.local'\nconst CONTROL_OR_BACKSLASH = /[\\u0000-\\u001F\\u007F\\\\]/\n\nexport function isSafeNotificationHref(href: string): boolean {\n if (!href.startsWith('/') || href.startsWith('//')) {\n return false\n }\n\n if (CONTROL_OR_BACKSLASH.test(href)) {\n return false\n }\n\n try {\n const parsed = new URL(href, SENTINEL_ORIGIN)\n return parsed.origin === SENTINEL_ORIGIN && !parsed.pathname.startsWith('//')\n } catch {\n return false\n }\n}\n"],
5
+ "mappings": "AAAA,MAAM,kBAAkB;AACxB,MAAM,uBAAuB;AAEtB,SAAS,uBAAuB,MAAuB;AAC5D,MAAI,CAAC,KAAK,WAAW,GAAG,KAAK,KAAK,WAAW,IAAI,GAAG;AAClD,WAAO;AAAA,EACT;AAEA,MAAI,qBAAqB,KAAK,IAAI,GAAG;AACnC,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,MAAM,eAAe;AAC5C,WAAO,OAAO,WAAW,mBAAmB,CAAC,OAAO,SAAS,WAAW,IAAI;AAAA,EAC9E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
6
+ "names": []
7
+ }
@@ -1 +1,5 @@
1
+ const NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE = "notification_action_already_executed";
2
+ export {
3
+ NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE
4
+ };
1
5
  //# sourceMappingURL=types.js.map