@open-mercato/shared 0.9.1-develop.7574.1.837288eec1 → 0.10.1-develop.7576.1.21d5b50435
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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +4 -4
- package/dist/lib/auth/server.js +1 -1
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/dynamicLoader.js +6 -46
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/bootstrap/factory.js +3 -4
- package/dist/lib/bootstrap/factory.js.map +2 -2
- package/dist/lib/catalog-visibility/index.js +0 -4
- package/dist/lib/catalog-visibility/index.js.map +2 -2
- package/dist/lib/commands/command-bus.js +13 -45
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/types.js.map +2 -2
- package/dist/lib/crud/exporters.js +1 -1
- package/dist/lib/crud/exporters.js.map +2 -2
- package/dist/lib/crud/ids.js +1 -13
- package/dist/lib/crud/ids.js.map +2 -2
- package/dist/lib/db/pg-errors.js +0 -14
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/localization/index.js +1 -9
- package/dist/lib/localization/index.js.map +2 -2
- package/dist/lib/localization/translatable-fields.js +0 -26
- package/dist/lib/localization/translatable-fields.js.map +2 -2
- package/dist/lib/number.js +0 -6
- package/dist/lib/number.js.map +2 -2
- package/dist/lib/openapi/generator.js +67 -84
- package/dist/lib/openapi/generator.js.map +2 -2
- package/dist/lib/query/engine.js +10 -45
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/join-utils.js +1 -1
- package/dist/lib/query/join-utils.js.map +2 -2
- package/dist/lib/query/types.js.map +2 -2
- package/dist/lib/url.js +0 -24
- package/dist/lib/url.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/body.js +3 -7
- package/dist/lib/webhooks/body.js.map +2 -2
- package/dist/lib/webhooks/index.js +0 -2
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/modules/integrations/types.js.map +2 -2
- package/dist/modules/notifications/types.js +0 -4
- package/dist/modules/notifications/types.js.map +3 -3
- package/dist/modules/payment_gateways/types.js +1 -14
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/search.js.map +2 -2
- package/dist/modules/widgets/injection-loader.js +3 -14
- package/dist/modules/widgets/injection-loader.js.map +2 -2
- package/package.json +3 -10
- package/src/lib/__tests__/number.test.ts +1 -24
- package/src/lib/auth/__tests__/server.test.ts +0 -14
- package/src/lib/auth/server.ts +1 -1
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.appModuleOverrides.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.cacheRecovery.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/factory.test.ts +6 -18
- package/src/lib/bootstrap/dynamicLoader.ts +5 -63
- package/src/lib/bootstrap/factory.ts +3 -8
- package/src/lib/bootstrap/types.ts +1 -5
- package/src/lib/catalog-visibility/index.ts +0 -2
- package/src/lib/commands/__tests__/command-bus.test.ts +1 -100
- package/src/lib/commands/command-bus.ts +10 -42
- package/src/lib/commands/command-interceptor.ts +0 -4
- package/src/lib/commands/types.ts +0 -14
- package/src/lib/crud/__tests__/exporters.test.ts +0 -18
- package/src/lib/crud/__tests__/ids.test.ts +1 -34
- package/src/lib/crud/exporters.ts +1 -1
- package/src/lib/crud/ids.ts +0 -24
- package/src/lib/db/__tests__/pg-errors.test.ts +1 -68
- package/src/lib/db/pg-errors.ts +0 -35
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +0 -23
- package/src/lib/localization/__tests__/translatable-fields.test.ts +0 -17
- package/src/lib/localization/index.ts +1 -8
- package/src/lib/localization/translatable-fields.ts +0 -46
- package/src/lib/number.ts +0 -13
- package/src/lib/openapi/generator.ts +77 -106
- package/src/lib/query/engine.ts +8 -58
- package/src/lib/query/join-utils.ts +1 -1
- package/src/lib/query/types.ts +1 -13
- package/src/lib/url.ts +0 -30
- package/src/lib/webhooks/__tests__/body.test.ts +0 -48
- package/src/lib/webhooks/body.ts +4 -11
- package/src/lib/webhooks/index.ts +0 -1
- package/src/modules/integrations/types.ts +0 -6
- package/src/modules/notifications/types.ts +0 -7
- package/src/modules/payment_gateways/__tests__/types.test.ts +0 -44
- package/src/modules/payment_gateways/types.ts +6 -52
- package/src/modules/registry.ts +0 -5
- package/src/modules/search.ts +0 -39
- package/src/modules/widgets/injection-loader.ts +3 -33
- package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js +0 -6
- package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js.map +0 -7
- package/dist/lib/bootstrap/next-subpath-resolve-hook.js +0 -62
- package/dist/lib/bootstrap/next-subpath-resolve-hook.js.map +0 -7
- package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js +0 -33
- package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js.map +0 -7
- package/dist/lib/catalog-visibility/hashEffectiveScope.js +0 -9
- package/dist/lib/catalog-visibility/hashEffectiveScope.js.map +0 -7
- package/dist/lib/db/advisoryLock.js +0 -172
- package/dist/lib/db/advisoryLock.js.map +0 -7
- package/dist/lib/query/overlap.js +0 -58
- package/dist/lib/query/overlap.js.map +0 -7
- package/dist/modules/notifications/safeHref.js +0 -20
- package/dist/modules/notifications/safeHref.js.map +0 -7
- package/src/lib/__tests__/url.cross-site.test.ts +0 -140
- package/src/lib/bootstrap/__tests__/dynamicLoader.externalSubpath.test.ts +0 -141
- package/src/lib/bootstrap/__tests__/next-subpath-resolve-hook.test.ts +0 -174
- package/src/lib/bootstrap/next-subpath-resolve-hook-worker.ts +0 -3
- package/src/lib/bootstrap/next-subpath-resolve-hook.ts +0 -130
- package/src/lib/catalog-visibility/__tests__/hashEffectiveScope.test.ts +0 -121
- package/src/lib/catalog-visibility/canonicalizeEffectiveScope.ts +0 -41
- package/src/lib/catalog-visibility/hashEffectiveScope.ts +0 -8
- package/src/lib/db/__tests__/advisoryLock.integration.test.ts +0 -343
- package/src/lib/db/__tests__/advisoryLock.test.ts +0 -564
- package/src/lib/db/advisoryLock.ts +0 -271
- package/src/lib/openapi/__tests__/optional-path-params.test.ts +0 -102
- package/src/lib/query/__tests__/empty-ids-filter.pg.test.ts +0 -89
- package/src/lib/query/__tests__/engine.empty-in-filter.test.ts +0 -79
- package/src/lib/query/__tests__/overlap.test.ts +0 -219
- package/src/lib/query/overlap.ts +0 -106
- package/src/modules/notifications/safeHref.ts +0 -19
- package/src/modules/widgets/__tests__/injection-loader.partitioned-registration.test.ts +0 -78
|
@@ -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"];
|
|
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', '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": "AACA,SAAS,WAAW;AAQpB,MAAM,YAAY,CAAC,MAAM,MAAM,MAAM,OAAO,MAAM,OAAO,MAAM,OAAO,QAAQ,SAAS,
|
|
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": "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",
|
|
6
6
|
"names": []
|
|
7
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\
|
|
5
|
-
"mappings": "
|
|
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;",
|
|
6
6
|
"names": ["SortDir"]
|
|
7
7
|
}
|
package/dist/lib/url.js
CHANGED
|
@@ -199,29 +199,6 @@ 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
|
-
}
|
|
225
202
|
function mapSecurityEmailUrlError(error, mapping) {
|
|
226
203
|
if (error instanceof AppOriginRejectedError) {
|
|
227
204
|
return { status: 400, body: { error: "Invalid request origin" } };
|
|
@@ -238,7 +215,6 @@ export {
|
|
|
238
215
|
assertAllowedAppOrigin,
|
|
239
216
|
getAppBaseUrl,
|
|
240
217
|
getSecurityEmailBaseUrl,
|
|
241
|
-
isCrossSiteBrowserRequest,
|
|
242
218
|
mapSecurityEmailUrlError,
|
|
243
219
|
resolveRequestOrigin,
|
|
244
220
|
toAbsoluteUrl,
|
package/dist/lib/url.js.map
CHANGED
|
@@ -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 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": "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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 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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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -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.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.10.1-develop.7576.1.21d5b50435';\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
|
|
15
|
+
async function readBoundedRequestBody(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 readBoundedRequestBytes(request, options) {
|
|
|
22
22
|
throw new WebhookBodyTooLargeError(maxBytes);
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
if (!request.body) return
|
|
25
|
+
if (!request.body) return "";
|
|
26
26
|
const reader = request.body.getReader();
|
|
27
27
|
const chunks = [];
|
|
28
28
|
let totalBytes = 0;
|
|
@@ -46,16 +46,12 @@ async function readBoundedRequestBytes(request, options) {
|
|
|
46
46
|
body.set(chunk, offset);
|
|
47
47
|
offset += chunk.byteLength;
|
|
48
48
|
}
|
|
49
|
-
return body;
|
|
50
|
-
}
|
|
51
|
-
async function readBoundedRequestBody(request, options) {
|
|
52
|
-
return new TextDecoder().decode(await readBoundedRequestBytes(request, options));
|
|
49
|
+
return new TextDecoder().decode(body);
|
|
53
50
|
}
|
|
54
51
|
export {
|
|
55
52
|
DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,
|
|
56
53
|
WebhookBodyTooLargeError,
|
|
57
54
|
readBoundedRequestBody,
|
|
58
|
-
readBoundedRequestBytes,
|
|
59
55
|
resolveWebhookBodyLimitBytes
|
|
60
56
|
};
|
|
61
57
|
//# 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
|
|
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,
|
|
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;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,7 +4,6 @@ import { generateWebhookSecret, parseWebhookSecret, isValidWebhookSecret } from
|
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_WEBHOOK_BODY_LIMIT_BYTES,
|
|
6
6
|
readBoundedRequestBody,
|
|
7
|
-
readBoundedRequestBytes,
|
|
8
7
|
resolveWebhookBodyLimitBytes,
|
|
9
8
|
WebhookBodyTooLargeError
|
|
10
9
|
} from "./body.js";
|
|
@@ -19,7 +18,6 @@ export {
|
|
|
19
18
|
isWebhookTimestampWithinTolerance,
|
|
20
19
|
parseWebhookSecret,
|
|
21
20
|
readBoundedRequestBody,
|
|
22
|
-
readBoundedRequestBytes,
|
|
23
21
|
resolveWebhookBodyLimitBytes,
|
|
24
22
|
signWebhookPayload,
|
|
25
23
|
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
|
|
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,
|
|
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;",
|
|
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
|
|
5
|
-
"mappings": "
|
|
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;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": [
|
|
4
|
-
"sourcesContent": [
|
|
5
|
-
"mappings": "
|
|
3
|
+
"sources": [],
|
|
4
|
+
"sourcesContent": [],
|
|
5
|
+
"mappings": "",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import { DEFAULT_WEBHOOK_BODY_LIMIT_BYTES } from "../../lib/webhooks/body.js";
|
|
2
|
-
class WebhookVerificationUnavailableError extends Error {
|
|
3
|
-
constructor(message = "Webhook verification is unavailable") {
|
|
4
|
-
super(message);
|
|
5
|
-
this.name = "WebhookVerificationUnavailableError";
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
2
|
const ADAPTER_REGISTRY_KEY = "__openMercatoPaymentGatewayAdapters__";
|
|
9
3
|
const WEBHOOK_REGISTRY_KEY = "__openMercatoPaymentGatewayWebhookHandlers__";
|
|
10
4
|
const DESCRIPTOR_REGISTRY_KEY = "__openMercatoPaymentGatewayDescriptors__";
|
|
@@ -68,9 +62,6 @@ function clearGatewayAdapters() {
|
|
|
68
62
|
getAdapterRegistry().clear();
|
|
69
63
|
}
|
|
70
64
|
function registerWebhookHandler(providerKey, handler, options) {
|
|
71
|
-
if (options?.rawBody !== void 0 && options.rawBody !== "text" && options.rawBody !== "bytes") {
|
|
72
|
-
throw new Error("[internal] Payment gateway webhook rawBody must be 'text' or 'bytes'");
|
|
73
|
-
}
|
|
74
65
|
if (options?.maxBodyBytes !== void 0 && (!Number.isSafeInteger(options.maxBodyBytes) || options.maxBodyBytes <= 0 || options.maxBodyBytes > DEFAULT_WEBHOOK_BODY_LIMIT_BYTES)) {
|
|
75
66
|
throw new Error(
|
|
76
67
|
`[internal] Payment gateway webhook maxBodyBytes must be a positive safe integer no greater than ${DEFAULT_WEBHOOK_BODY_LIMIT_BYTES}`
|
|
@@ -81,10 +72,7 @@ function registerWebhookHandler(providerKey, handler, options) {
|
|
|
81
72
|
handler,
|
|
82
73
|
queue: options?.queue,
|
|
83
74
|
readSessionIdHint: options?.readSessionIdHint,
|
|
84
|
-
|
|
85
|
-
maxBodyBytes: options?.maxBodyBytes,
|
|
86
|
-
rawBody: options?.rawBody,
|
|
87
|
-
formatResponse: options?.formatResponse
|
|
75
|
+
maxBodyBytes: options?.maxBodyBytes
|
|
88
76
|
});
|
|
89
77
|
return () => {
|
|
90
78
|
webhookHandlerRegistry.delete(providerKey);
|
|
@@ -113,7 +101,6 @@ function clearPaymentGatewayDescriptors() {
|
|
|
113
101
|
getDescriptorRegistry().clear();
|
|
114
102
|
}
|
|
115
103
|
export {
|
|
116
|
-
WebhookVerificationUnavailableError,
|
|
117
104
|
clearGatewayAdapters,
|
|
118
105
|
clearPaymentGatewayDescriptors,
|
|
119
106
|
clearWebhookHandlers,
|