@open-mercato/shared 0.6.6-develop.6551.1.1511bfa890 → 0.6.6

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.
@@ -11,14 +11,9 @@ const RAW_COUNTRIES = registry.filter(isIsoAlpha2).map((entry) => ({
11
11
  code: entry.Subtag,
12
12
  name: entry.Description.join(", ")
13
13
  }));
14
- const SUPPLEMENTAL_COUNTRIES = [
15
- { code: "XK", name: "Kosovo" }
16
- ];
17
- const RAW_COUNTRY_CODES = new Set(RAW_COUNTRIES.map((entry) => entry.code));
18
- const ISO_COUNTRIES = [
19
- ...RAW_COUNTRIES,
20
- ...SUPPLEMENTAL_COUNTRIES.filter((entry) => !RAW_COUNTRY_CODES.has(entry.code))
21
- ].sort((a, b) => a.name.localeCompare(b.name, "en", { sensitivity: "base" }));
14
+ const ISO_COUNTRIES = [...RAW_COUNTRIES].sort(
15
+ (a, b) => a.name.localeCompare(b.name, "en", { sensitivity: "base" })
16
+ );
22
17
  const COUNTRY_PRIORITY = ["PL", "DE", "ES", "FR", "IT", "US", "GB", "CA"];
23
18
  const displayNameCache = /* @__PURE__ */ new Map();
24
19
  function getDisplayNames(locale) {
@@ -42,8 +37,7 @@ function resolveCountryName(code, options = {}) {
42
37
  if (!displayNames) return fallback;
43
38
  try {
44
39
  const label = displayNames.of(normalized);
45
- if (typeof label !== "string" || label === normalized) return fallback;
46
- return label;
40
+ return typeof label === "string" ? label : fallback;
47
41
  } catch {
48
42
  return fallback;
49
43
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/location/countries.ts"],
4
- "sourcesContent": ["import registry from 'language-subtag-registry/data/json/registry.json'\n\ntype RegistryEntry = {\n Type: string\n Subtag?: string\n Description?: string[]\n Deprecated?: string\n}\n\nexport type IsoCountry = {\n code: string\n name: string\n}\n\nfunction isIsoAlpha2(entry: RegistryEntry): entry is RegistryEntry & { Subtag: string; Description: string[] } {\n if (entry.Type !== 'region') return false\n if (!entry.Subtag || !/^[A-Z]{2}$/.test(entry.Subtag)) return false\n if (entry.Deprecated) return false\n if (!entry.Description || !entry.Description.length) return false\n if (entry.Description[0] === 'Private use') return false\n return true\n}\n\nconst RAW_COUNTRIES: IsoCountry[] = (registry as RegistryEntry[])\n .filter(isIsoAlpha2)\n .map((entry) => ({\n code: entry.Subtag,\n name: entry.Description.join(', '),\n }))\n\n// Codes in common use that the language-subtag registry does not list as region subtags.\n// XK is user-assigned rather than ISO 3166-1 assigned, so it never appears in the registry.\nconst SUPPLEMENTAL_COUNTRIES: IsoCountry[] = [\n { code: 'XK', name: 'Kosovo' },\n]\n\nconst RAW_COUNTRY_CODES = new Set(RAW_COUNTRIES.map((entry) => entry.code))\n\nexport const ISO_COUNTRIES: IsoCountry[] = [\n ...RAW_COUNTRIES,\n ...SUPPLEMENTAL_COUNTRIES.filter((entry) => !RAW_COUNTRY_CODES.has(entry.code)),\n].sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }))\n\nexport const COUNTRY_PRIORITY: string[] = ['PL', 'DE', 'ES', 'FR', 'IT', 'US', 'GB', 'CA']\n\nconst displayNameCache = new Map<string, Intl.DisplayNames>()\n\nfunction getDisplayNames(locale: string): Intl.DisplayNames | null {\n if (typeof Intl === 'undefined' || typeof Intl.DisplayNames === 'undefined') return null\n const key = locale || 'en'\n let instance = displayNameCache.get(key)\n if (!instance) {\n try {\n instance = new Intl.DisplayNames([key], { type: 'region' })\n displayNameCache.set(key, instance)\n } catch {\n return null\n }\n }\n return instance\n}\n\nexport function resolveCountryName(code: string, options: { locale?: string } = {}): string {\n const normalized = code.toUpperCase()\n const fallback = ISO_COUNTRIES.find((entry) => entry.code === normalized)?.name ?? normalized\n const displayNames = getDisplayNames(options.locale ?? 'en')\n if (!displayNames) return fallback\n try {\n const label = displayNames.of(normalized as any)\n // Intl echoes the raw code back when its ICU data cannot resolve the region.\n if (typeof label !== 'string' || label === normalized) return fallback\n return label\n } catch {\n return fallback\n }\n}\n\nexport function buildCountryOptions(options: {\n locale?: string\n priority?: string[]\n transformLabel?: (code: string, defaultLabel: string) => string\n} = {}): Array<{ code: string; label: string }> {\n const { locale, transformLabel } = options\n const priority = (options.priority ?? COUNTRY_PRIORITY).map((code) => code.toUpperCase())\n const prioritySet = new Set(priority)\n const labelFor = (code: string) => {\n const base = resolveCountryName(code, { locale })\n return transformLabel ? transformLabel(code, base) : base\n }\n\n const byCode = new Map(ISO_COUNTRIES.map((entry) => [entry.code, entry]))\n\n const prioritized = priority\n .map((code) => {\n const base = byCode.get(code)\n if (!base) return null\n return { code, label: labelFor(code) }\n })\n .filter((entry): entry is { code: string; label: string } => !!entry)\n .sort((a, b) => a.label.localeCompare(b.label, locale ?? 'en', { sensitivity: 'base' }))\n\n const remaining = ISO_COUNTRIES\n .filter((entry) => !prioritySet.has(entry.code))\n .map((entry) => ({ code: entry.code, label: labelFor(entry.code) }))\n .sort((a, b) => a.label.localeCompare(b.label, locale ?? 'en', { sensitivity: 'base' }))\n\n return [...prioritized, ...remaining]\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import registry from 'language-subtag-registry/data/json/registry.json'\n\ntype RegistryEntry = {\n Type: string\n Subtag?: string\n Description?: string[]\n Deprecated?: string\n}\n\nexport type IsoCountry = {\n code: string\n name: string\n}\n\nfunction isIsoAlpha2(entry: RegistryEntry): entry is RegistryEntry & { Subtag: string; Description: string[] } {\n if (entry.Type !== 'region') return false\n if (!entry.Subtag || !/^[A-Z]{2}$/.test(entry.Subtag)) return false\n if (entry.Deprecated) return false\n if (!entry.Description || !entry.Description.length) return false\n if (entry.Description[0] === 'Private use') return false\n return true\n}\n\nconst RAW_COUNTRIES: IsoCountry[] = (registry as RegistryEntry[])\n .filter(isIsoAlpha2)\n .map((entry) => ({\n code: entry.Subtag,\n name: entry.Description.join(', '),\n }))\n\nexport const ISO_COUNTRIES: IsoCountry[] = [...RAW_COUNTRIES].sort((a, b) =>\n a.name.localeCompare(b.name, 'en', { sensitivity: 'base' })\n)\n\nexport const COUNTRY_PRIORITY: string[] = ['PL', 'DE', 'ES', 'FR', 'IT', 'US', 'GB', 'CA']\n\nconst displayNameCache = new Map<string, Intl.DisplayNames>()\n\nfunction getDisplayNames(locale: string): Intl.DisplayNames | null {\n if (typeof Intl === 'undefined' || typeof Intl.DisplayNames === 'undefined') return null\n const key = locale || 'en'\n let instance = displayNameCache.get(key)\n if (!instance) {\n try {\n instance = new Intl.DisplayNames([key], { type: 'region' })\n displayNameCache.set(key, instance)\n } catch {\n return null\n }\n }\n return instance\n}\n\nexport function resolveCountryName(code: string, options: { locale?: string } = {}): string {\n const normalized = code.toUpperCase()\n const fallback = ISO_COUNTRIES.find((entry) => entry.code === normalized)?.name ?? normalized\n const displayNames = getDisplayNames(options.locale ?? 'en')\n if (!displayNames) return fallback\n try {\n const label = displayNames.of(normalized as any)\n return typeof label === 'string' ? label : fallback\n } catch {\n return fallback\n }\n}\n\nexport function buildCountryOptions(options: {\n locale?: string\n priority?: string[]\n transformLabel?: (code: string, defaultLabel: string) => string\n} = {}): Array<{ code: string; label: string }> {\n const { locale, transformLabel } = options\n const priority = (options.priority ?? COUNTRY_PRIORITY).map((code) => code.toUpperCase())\n const prioritySet = new Set(priority)\n const labelFor = (code: string) => {\n const base = resolveCountryName(code, { locale })\n return transformLabel ? transformLabel(code, base) : base\n }\n\n const byCode = new Map(ISO_COUNTRIES.map((entry) => [entry.code, entry]))\n\n const prioritized = priority\n .map((code) => {\n const base = byCode.get(code)\n if (!base) return null\n return { code, label: labelFor(code) }\n })\n .filter((entry): entry is { code: string; label: string } => !!entry)\n .sort((a, b) => a.label.localeCompare(b.label, locale ?? 'en', { sensitivity: 'base' }))\n\n const remaining = ISO_COUNTRIES\n .filter((entry) => !prioritySet.has(entry.code))\n .map((entry) => ({ code: entry.code, label: labelFor(entry.code) }))\n .sort((a, b) => a.label.localeCompare(b.label, locale ?? 'en', { sensitivity: 'base' }))\n\n return [...prioritized, ...remaining]\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -69,30 +69,11 @@ function isPrivateIPv6(address) {
69
69
  if (segments.slice(0, 7).every((segment) => segment === 0) && segments[7] === 1) return true;
70
70
  if (isIPv4CompatibleIPv6(segments)) return true;
71
71
  const embedded = embeddedIPv4FromIPv6(segments);
72
- if (embedded) return isPrivateIPv4(embedded);
72
+ if (embedded && isPrivateIPv4(embedded)) return true;
73
73
  const [a] = segments;
74
74
  if ((a & 65024) === 64512) return true;
75
75
  if ((a & 65472) === 65152) return true;
76
76
  if ((a & 65280) === 65280) return true;
77
- if (a === 256 && segments[1] === 0 && segments[2] === 0 && segments[3] === 0) {
78
- return true;
79
- }
80
- if (a === 8193 && (segments[1] & 65024) === 0) {
81
- return !isGloballyReachableIetfProtocolAssignment(segments);
82
- }
83
- if (a === 8193 && segments[1] === 3512) return true;
84
- if ((a & 65520) === 16368) return true;
85
- if ((a & 57344) !== 8192) return true;
86
- return false;
87
- }
88
- function isGloballyReachableIetfProtocolAssignment(segments) {
89
- const [, b, c, d, e, f, g, h] = segments;
90
- const trailingZero = c === 0 && d === 0 && e === 0 && f === 0 && g === 0;
91
- if (b === 1 && trailingZero && h >= 1 && h <= 3) return true;
92
- if (b === 3) return true;
93
- if (b === 4 && c === 274) return true;
94
- if ((b & 65520) === 32) return true;
95
- if ((b & 65520) === 48) return true;
96
77
  return false;
97
78
  }
98
79
  function expandIPv6(address) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/network.ts"],
4
- "sourcesContent": ["import { isIP } from 'node:net'\n\nconst BLOCKED_HOSTNAMES = new Set([\n 'localhost',\n 'ip6-localhost',\n 'ip6-loopback',\n 'broadcasthost',\n])\n\nexport function isPrivateIpAddress(address: string): boolean {\n const family = isIP(address)\n if (family === 4) return isPrivateIPv4(address)\n if (family === 6) return isPrivateIPv6(address)\n return false\n}\n\nexport function isBlockedHostname(hostname: string): boolean {\n const normalized = normalizeHostname(hostname)\n if (!normalized) return true\n if (BLOCKED_HOSTNAMES.has(normalized)) return true\n if (normalized.endsWith('.localhost')) return true\n if (normalized.endsWith('.internal')) return true\n if (normalized.endsWith('.local')) return true\n return false\n}\n\nexport function isPrivateUrl(rawUrl: string): boolean {\n const urlToParse = rawUrl.replace(/\\[([0-9a-fA-F:]+)%25[^\\]]*\\]/g, '[$1]')\n let url: URL\n try {\n url = new URL(urlToParse)\n } catch {\n return false\n }\n\n const hostname = normalizeHostname(url.hostname)\n if (isBlockedHostname(hostname)) return true\n return isPrivateIpAddress(hostname)\n}\n\nfunction normalizeHostname(hostname: string): string {\n let normalized = hostname.trim().toLowerCase()\n if (normalized.startsWith('[') && normalized.endsWith(']')) {\n normalized = normalized.slice(1, -1)\n }\n while (normalized.endsWith('.')) {\n normalized = normalized.slice(0, -1)\n }\n return normalized\n}\n\nexport function isPrivateIPv4(address: string): boolean {\n const parts = address.split('.')\n if (parts.length !== 4) return false\n const octets = parts.map((part) => Number(part))\n if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) return false\n\n const [a, b, c] = octets\n if (a === 0) return true\n if (a === 10) return true\n if (a === 100 && b >= 64 && b <= 127) return true\n if (a === 127) return true\n if (a === 169 && b === 254) return true\n if (a === 172 && b >= 16 && b <= 31) return true\n if (a === 192 && b === 0 && (c === 0 || c === 2)) return true\n if (a === 192 && b === 168) return true\n if (a === 198 && (b === 18 || b === 19)) return true\n if (a === 198 && b === 51 && c === 100) return true\n if (a === 203 && b === 0 && c === 113) return true\n if (a >= 224) return true\n return false\n}\n\nexport function isPrivateIPv6(address: string): boolean {\n const segments = expandIPv6(address)\n if (!segments) return false\n\n if (segments.every((segment) => segment === 0)) return true\n if (segments.slice(0, 7).every((segment) => segment === 0) && segments[7] === 1) return true\n if (isIPv4CompatibleIPv6(segments)) return true\n\n const embedded = embeddedIPv4FromIPv6(segments)\n if (embedded) return isPrivateIPv4(embedded)\n\n const [a] = segments\n if ((a & 0xfe00) === 0xfc00) return true\n if ((a & 0xffc0) === 0xfe80) return true\n if ((a & 0xff00) === 0xff00) return true\n if (\n a === 0x0100 &&\n segments[1] === 0 &&\n segments[2] === 0 &&\n segments[3] === 0\n ) {\n return true\n }\n if (a === 0x2001 && (segments[1] & 0xfe00) === 0) {\n return !isGloballyReachableIetfProtocolAssignment(segments)\n }\n if (a === 0x2001 && segments[1] === 0x0db8) return true\n if ((a & 0xfff0) === 0x3ff0) return true\n // Except for the explicitly handled IPv4 translation ranges above, globally\n // routable unicast IPv6 addresses are allocated from 2000::/3.\n if ((a & 0xe000) !== 0x2000) return true\n return false\n}\n\nfunction isGloballyReachableIetfProtocolAssignment(segments: number[]): boolean {\n const [, b, c, d, e, f, g, h] = segments\n const trailingZero = c === 0 && d === 0 && e === 0 && f === 0 && g === 0\n if (b === 0x0001 && trailingZero && h >= 1 && h <= 3) return true\n if (b === 0x0003) return true\n if (b === 0x0004 && c === 0x0112) return true\n if ((b & 0xfff0) === 0x0020) return true\n if ((b & 0xfff0) === 0x0030) return true\n return false\n}\n\nfunction expandIPv6(address: string): number[] | null {\n let normalized = address.toLowerCase()\n if (normalized.includes('.')) {\n const lastColon = normalized.lastIndexOf(':')\n if (lastColon === -1) return null\n const ipv4Segments = ipv4ToHexSegments(normalized.slice(lastColon + 1))\n if (!ipv4Segments) return null\n normalized = `${normalized.slice(0, lastColon)}:${ipv4Segments[0].toString(16)}:${ipv4Segments[1].toString(16)}`\n }\n\n const doubleColonParts = normalized.split('::')\n if (doubleColonParts.length > 2) return null\n\n const head = doubleColonParts[0] ? doubleColonParts[0].split(':') : []\n const tail = doubleColonParts[1] ? doubleColonParts[1].split(':') : []\n const headSegments = parseIPv6SegmentList(head)\n const tailSegments = parseIPv6SegmentList(tail)\n if (!headSegments || !tailSegments) return null\n\n if (doubleColonParts.length === 1) {\n return headSegments.length === 8 ? headSegments : null\n }\n\n const missing = 8 - headSegments.length - tailSegments.length\n if (missing < 1) return null\n return [\n ...headSegments,\n ...Array.from({ length: missing }, () => 0),\n ...tailSegments,\n ]\n}\n\nfunction parseIPv6SegmentList(parts: string[]): number[] | null {\n const segments: number[] = []\n for (const part of parts) {\n if (!/^[0-9a-f]{1,4}$/.test(part)) return null\n const value = Number.parseInt(part, 16)\n if (!Number.isInteger(value) || value < 0 || value > 0xffff) return null\n segments.push(value)\n }\n return segments\n}\n\nfunction ipv4ToHexSegments(address: string): [number, number] | null {\n const octets = address.split('.').map((part) => Number(part))\n if (octets.length !== 4) return null\n if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) return null\n return [\n (octets[0] << 8) + octets[1],\n (octets[2] << 8) + octets[3],\n ]\n}\n\nfunction embeddedIPv4FromIPv6(segments: number[]): string | null {\n const firstFiveZero = segments.slice(0, 5).every((segment) => segment === 0)\n const isMapped = firstFiveZero && segments[5] === 0xffff\n const isNat64 = segments[0] === 0x0064\n && segments[1] === 0xff9b\n && segments.slice(2, 6).every((segment) => segment === 0)\n const isSixToFour = segments[0] === 0x2002\n\n if (isMapped || isNat64) {\n return ipv4FromHexSegments(segments[6], segments[7])\n }\n if (isSixToFour) {\n return ipv4FromHexSegments(segments[1], segments[2])\n }\n return null\n}\n\nfunction isIPv4CompatibleIPv6(segments: number[]): boolean {\n return segments.slice(0, 6).every((segment) => segment === 0)\n}\n\nfunction ipv4FromHexSegments(high: number, low: number): string {\n return [\n (high >> 8) & 0xff,\n high & 0xff,\n (low >> 8) & 0xff,\n low & 0xff,\n ].join('.')\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { isIP } from 'node:net'\n\nconst BLOCKED_HOSTNAMES = new Set([\n 'localhost',\n 'ip6-localhost',\n 'ip6-loopback',\n 'broadcasthost',\n])\n\nexport function isPrivateIpAddress(address: string): boolean {\n const family = isIP(address)\n if (family === 4) return isPrivateIPv4(address)\n if (family === 6) return isPrivateIPv6(address)\n return false\n}\n\nexport function isBlockedHostname(hostname: string): boolean {\n const normalized = normalizeHostname(hostname)\n if (!normalized) return true\n if (BLOCKED_HOSTNAMES.has(normalized)) return true\n if (normalized.endsWith('.localhost')) return true\n if (normalized.endsWith('.internal')) return true\n if (normalized.endsWith('.local')) return true\n return false\n}\n\nexport function isPrivateUrl(rawUrl: string): boolean {\n const urlToParse = rawUrl.replace(/\\[([0-9a-fA-F:]+)%25[^\\]]*\\]/g, '[$1]')\n let url: URL\n try {\n url = new URL(urlToParse)\n } catch {\n return false\n }\n\n const hostname = normalizeHostname(url.hostname)\n if (isBlockedHostname(hostname)) return true\n return isPrivateIpAddress(hostname)\n}\n\nfunction normalizeHostname(hostname: string): string {\n let normalized = hostname.trim().toLowerCase()\n if (normalized.startsWith('[') && normalized.endsWith(']')) {\n normalized = normalized.slice(1, -1)\n }\n while (normalized.endsWith('.')) {\n normalized = normalized.slice(0, -1)\n }\n return normalized\n}\n\nexport function isPrivateIPv4(address: string): boolean {\n const parts = address.split('.')\n if (parts.length !== 4) return false\n const octets = parts.map((part) => Number(part))\n if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) return false\n\n const [a, b, c] = octets\n if (a === 0) return true\n if (a === 10) return true\n if (a === 100 && b >= 64 && b <= 127) return true\n if (a === 127) return true\n if (a === 169 && b === 254) return true\n if (a === 172 && b >= 16 && b <= 31) return true\n if (a === 192 && b === 0 && (c === 0 || c === 2)) return true\n if (a === 192 && b === 168) return true\n if (a === 198 && (b === 18 || b === 19)) return true\n if (a === 198 && b === 51 && c === 100) return true\n if (a === 203 && b === 0 && c === 113) return true\n if (a >= 224) return true\n return false\n}\n\nexport function isPrivateIPv6(address: string): boolean {\n const segments = expandIPv6(address)\n if (!segments) return false\n\n if (segments.every((segment) => segment === 0)) return true\n if (segments.slice(0, 7).every((segment) => segment === 0) && segments[7] === 1) return true\n if (isIPv4CompatibleIPv6(segments)) return true\n\n const embedded = embeddedIPv4FromIPv6(segments)\n if (embedded && isPrivateIPv4(embedded)) return true\n\n const [a] = segments\n if ((a & 0xfe00) === 0xfc00) return true\n if ((a & 0xffc0) === 0xfe80) return true\n if ((a & 0xff00) === 0xff00) return true\n return false\n}\n\nfunction expandIPv6(address: string): number[] | null {\n let normalized = address.toLowerCase()\n if (normalized.includes('.')) {\n const lastColon = normalized.lastIndexOf(':')\n if (lastColon === -1) return null\n const ipv4Segments = ipv4ToHexSegments(normalized.slice(lastColon + 1))\n if (!ipv4Segments) return null\n normalized = `${normalized.slice(0, lastColon)}:${ipv4Segments[0].toString(16)}:${ipv4Segments[1].toString(16)}`\n }\n\n const doubleColonParts = normalized.split('::')\n if (doubleColonParts.length > 2) return null\n\n const head = doubleColonParts[0] ? doubleColonParts[0].split(':') : []\n const tail = doubleColonParts[1] ? doubleColonParts[1].split(':') : []\n const headSegments = parseIPv6SegmentList(head)\n const tailSegments = parseIPv6SegmentList(tail)\n if (!headSegments || !tailSegments) return null\n\n if (doubleColonParts.length === 1) {\n return headSegments.length === 8 ? headSegments : null\n }\n\n const missing = 8 - headSegments.length - tailSegments.length\n if (missing < 1) return null\n return [\n ...headSegments,\n ...Array.from({ length: missing }, () => 0),\n ...tailSegments,\n ]\n}\n\nfunction parseIPv6SegmentList(parts: string[]): number[] | null {\n const segments: number[] = []\n for (const part of parts) {\n if (!/^[0-9a-f]{1,4}$/.test(part)) return null\n const value = Number.parseInt(part, 16)\n if (!Number.isInteger(value) || value < 0 || value > 0xffff) return null\n segments.push(value)\n }\n return segments\n}\n\nfunction ipv4ToHexSegments(address: string): [number, number] | null {\n const octets = address.split('.').map((part) => Number(part))\n if (octets.length !== 4) return null\n if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) return null\n return [\n (octets[0] << 8) + octets[1],\n (octets[2] << 8) + octets[3],\n ]\n}\n\nfunction embeddedIPv4FromIPv6(segments: number[]): string | null {\n const firstFiveZero = segments.slice(0, 5).every((segment) => segment === 0)\n const isMapped = firstFiveZero && segments[5] === 0xffff\n const isNat64 = segments[0] === 0x0064\n && segments[1] === 0xff9b\n && segments.slice(2, 6).every((segment) => segment === 0)\n const isSixToFour = segments[0] === 0x2002\n\n if (isMapped || isNat64) {\n return ipv4FromHexSegments(segments[6], segments[7])\n }\n if (isSixToFour) {\n return ipv4FromHexSegments(segments[1], segments[2])\n }\n return null\n}\n\nfunction isIPv4CompatibleIPv6(segments: number[]): boolean {\n return segments.slice(0, 6).every((segment) => segment === 0)\n}\n\nfunction ipv4FromHexSegments(high: number, low: number): string {\n return [\n (high >> 8) & 0xff,\n high & 0xff,\n (low >> 8) & 0xff,\n low & 0xff,\n ].join('.')\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY;AAErB,MAAM,oBAAoB,oBAAI,IAAI;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,SAAS,mBAAmB,SAA0B;AAC3D,QAAM,SAAS,KAAK,OAAO;AAC3B,MAAI,WAAW,EAAG,QAAO,cAAc,OAAO;AAC9C,MAAI,WAAW,EAAG,QAAO,cAAc,OAAO;AAC9C,SAAO;AACT;AAEO,SAAS,kBAAkB,UAA2B;AAC3D,QAAM,aAAa,kBAAkB,QAAQ;AAC7C,MAAI,CAAC,WAAY,QAAO;AACxB,MAAI,kBAAkB,IAAI,UAAU,EAAG,QAAO;AAC9C,MAAI,WAAW,SAAS,YAAY,EAAG,QAAO;AAC9C,MAAI,WAAW,SAAS,WAAW,EAAG,QAAO;AAC7C,MAAI,WAAW,SAAS,QAAQ,EAAG,QAAO;AAC1C,SAAO;AACT;AAEO,SAAS,aAAa,QAAyB;AACpD,QAAM,aAAa,OAAO,QAAQ,iCAAiC,MAAM;AACzE,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,UAAU;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,kBAAkB,IAAI,QAAQ;AAC/C,MAAI,kBAAkB,QAAQ,EAAG,QAAO;AACxC,SAAO,mBAAmB,QAAQ;AACpC;AAEA,SAAS,kBAAkB,UAA0B;AACnD,MAAI,aAAa,SAAS,KAAK,EAAE,YAAY;AAC7C,MAAI,WAAW,WAAW,GAAG,KAAK,WAAW,SAAS,GAAG,GAAG;AAC1D,iBAAa,WAAW,MAAM,GAAG,EAAE;AAAA,EACrC;AACA,SAAO,WAAW,SAAS,GAAG,GAAG;AAC/B,iBAAa,WAAW,MAAM,GAAG,EAAE;AAAA,EACrC;AACA,SAAO;AACT;AAEO,SAAS,cAAc,SAA0B;AACtD,QAAM,QAAQ,QAAQ,MAAM,GAAG;AAC/B,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,SAAS,MAAM,IAAI,CAAC,SAAS,OAAO,IAAI,CAAC;AAC/C,MAAI,OAAO,KAAK,CAAC,MAAM,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,KAAK,IAAI,GAAG,EAAG,QAAO;AAEzE,QAAM,CAAC,GAAG,GAAG,CAAC,IAAI;AAClB,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,MAAM,GAAI,QAAO;AACrB,MAAI,MAAM,OAAO,KAAK,MAAM,KAAK,IAAK,QAAO;AAC7C,MAAI,MAAM,IAAK,QAAO;AACtB,MAAI,MAAM,OAAO,MAAM,IAAK,QAAO;AACnC,MAAI,MAAM,OAAO,KAAK,MAAM,KAAK,GAAI,QAAO;AAC5C,MAAI,MAAM,OAAO,MAAM,MAAM,MAAM,KAAK,MAAM,GAAI,QAAO;AACzD,MAAI,MAAM,OAAO,MAAM,IAAK,QAAO;AACnC,MAAI,MAAM,QAAQ,MAAM,MAAM,MAAM,IAAK,QAAO;AAChD,MAAI,MAAM,OAAO,MAAM,MAAM,MAAM,IAAK,QAAO;AAC/C,MAAI,MAAM,OAAO,MAAM,KAAK,MAAM,IAAK,QAAO;AAC9C,MAAI,KAAK,IAAK,QAAO;AACrB,SAAO;AACT;AAEO,SAAS,cAAc,SAA0B;AACtD,QAAM,WAAW,WAAW,OAAO;AACnC,MAAI,CAAC,SAAU,QAAO;AAEtB,MAAI,SAAS,MAAM,CAAC,YAAY,YAAY,CAAC,EAAG,QAAO;AACvD,MAAI,SAAS,MAAM,GAAG,CAAC,EAAE,MAAM,CAAC,YAAY,YAAY,CAAC,KAAK,SAAS,CAAC,MAAM,EAAG,QAAO;AACxF,MAAI,qBAAqB,QAAQ,EAAG,QAAO;AAE3C,QAAM,WAAW,qBAAqB,QAAQ;AAC9C,MAAI,YAAY,cAAc,QAAQ,EAAG,QAAO;AAEhD,QAAM,CAAC,CAAC,IAAI;AACZ,OAAK,IAAI,WAAY,MAAQ,QAAO;AACpC,OAAK,IAAI,WAAY,MAAQ,QAAO;AACpC,OAAK,IAAI,WAAY,MAAQ,QAAO;AACpC,SAAO;AACT;AAEA,SAAS,WAAW,SAAkC;AACpD,MAAI,aAAa,QAAQ,YAAY;AACrC,MAAI,WAAW,SAAS,GAAG,GAAG;AAC5B,UAAM,YAAY,WAAW,YAAY,GAAG;AAC5C,QAAI,cAAc,GAAI,QAAO;AAC7B,UAAM,eAAe,kBAAkB,WAAW,MAAM,YAAY,CAAC,CAAC;AACtE,QAAI,CAAC,aAAc,QAAO;AAC1B,iBAAa,GAAG,WAAW,MAAM,GAAG,SAAS,CAAC,IAAI,aAAa,CAAC,EAAE,SAAS,EAAE,CAAC,IAAI,aAAa,CAAC,EAAE,SAAS,EAAE,CAAC;AAAA,EAChH;AAEA,QAAM,mBAAmB,WAAW,MAAM,IAAI;AAC9C,MAAI,iBAAiB,SAAS,EAAG,QAAO;AAExC,QAAM,OAAO,iBAAiB,CAAC,IAAI,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;AACrE,QAAM,OAAO,iBAAiB,CAAC,IAAI,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;AACrE,QAAM,eAAe,qBAAqB,IAAI;AAC9C,QAAM,eAAe,qBAAqB,IAAI;AAC9C,MAAI,CAAC,gBAAgB,CAAC,aAAc,QAAO;AAE3C,MAAI,iBAAiB,WAAW,GAAG;AACjC,WAAO,aAAa,WAAW,IAAI,eAAe;AAAA,EACpD;AAEA,QAAM,UAAU,IAAI,aAAa,SAAS,aAAa;AACvD,MAAI,UAAU,EAAG,QAAO;AACxB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAG,MAAM,KAAK,EAAE,QAAQ,QAAQ,GAAG,MAAM,CAAC;AAAA,IAC1C,GAAG;AAAA,EACL;AACF;AAEA,SAAS,qBAAqB,OAAkC;AAC9D,QAAM,WAAqB,CAAC;AAC5B,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,kBAAkB,KAAK,IAAI,EAAG,QAAO;AAC1C,UAAM,QAAQ,OAAO,SAAS,MAAM,EAAE;AACtC,QAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAQ,MAAQ,QAAO;AACpE,aAAS,KAAK,KAAK;AAAA,EACrB;AACA,SAAO;AACT;AAEA,SAAS,kBAAkB,SAA0C;AACnE,QAAM,SAAS,QAAQ,MAAM,GAAG,EAAE,IAAI,CAAC,SAAS,OAAO,IAAI,CAAC;AAC5D,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,MAAI,OAAO,KAAK,CAAC,UAAU,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAQ,GAAG,EAAG,QAAO;AACzF,SAAO;AAAA,KACJ,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC;AAAA,KAC1B,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC;AAAA,EAC7B;AACF;AAEA,SAAS,qBAAqB,UAAmC;AAC/D,QAAM,gBAAgB,SAAS,MAAM,GAAG,CAAC,EAAE,MAAM,CAAC,YAAY,YAAY,CAAC;AAC3E,QAAM,WAAW,iBAAiB,SAAS,CAAC,MAAM;AAClD,QAAM,UAAU,SAAS,CAAC,MAAM,OAC3B,SAAS,CAAC,MAAM,SAChB,SAAS,MAAM,GAAG,CAAC,EAAE,MAAM,CAAC,YAAY,YAAY,CAAC;AAC1D,QAAM,cAAc,SAAS,CAAC,MAAM;AAEpC,MAAI,YAAY,SAAS;AACvB,WAAO,oBAAoB,SAAS,CAAC,GAAG,SAAS,CAAC,CAAC;AAAA,EACrD;AACA,MAAI,aAAa;AACf,WAAO,oBAAoB,SAAS,CAAC,GAAG,SAAS,CAAC,CAAC;AAAA,EACrD;AACA,SAAO;AACT;AAEA,SAAS,qBAAqB,UAA6B;AACzD,SAAO,SAAS,MAAM,GAAG,CAAC,EAAE,MAAM,CAAC,YAAY,YAAY,CAAC;AAC9D;AAEA,SAAS,oBAAoB,MAAc,KAAqB;AAC9D,SAAO;AAAA,IACJ,QAAQ,IAAK;AAAA,IACd,OAAO;AAAA,IACN,OAAO,IAAK;AAAA,IACb,MAAM;AAAA,EACR,EAAE,KAAK,GAAG;AACZ;",
6
6
  "names": []
7
7
  }
@@ -4,12 +4,7 @@ function trimToUndefined(value) {
4
4
  const trimmed = value.trim();
5
5
  return trimmed.length > 0 ? trimmed : void 0;
6
6
  }
7
- function parseCommaSeparatedList(value) {
8
- if (typeof value !== "string") return [];
9
- return value.split(",").map((entry) => entry.trim()).filter(Boolean);
10
- }
11
7
  export {
12
- parseCommaSeparatedList,
13
8
  toOptionalString,
14
9
  trimToUndefined
15
10
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/string.ts"],
4
- "sourcesContent": ["export { toOptionalString } from './string/coerce'\n\nexport function trimToUndefined(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport function parseCommaSeparatedList(value: string | null | undefined): string[] {\n if (typeof value !== 'string') return []\n return value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n}\n\n"],
5
- "mappings": "AAAA,SAAS,wBAAwB;AAE1B,SAAS,gBAAgB,OAAoC;AAClE,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;AAEO,SAAS,wBAAwB,OAA4C;AAClF,MAAI,OAAO,UAAU,SAAU,QAAO,CAAC;AACvC,SAAO,MACJ,MAAM,GAAG,EACT,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAC3B,OAAO,OAAO;AACnB;",
4
+ "sourcesContent": ["export { toOptionalString } from './string/coerce'\n\nexport function trimToUndefined(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\n"],
5
+ "mappings": "AAAA,SAAS,wBAAwB;AAE1B,SAAS,gBAAgB,OAAoC;AAClE,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import { lookup } from "node:dns/promises";
2
2
  import { isIP } from "node:net";
3
- import { Agent, fetch as undiciFetch } from "undici";
3
+ import { Agent } from "undici";
4
4
  import { isBlockedHostname, isPrivateIpAddress } from "./network.js";
5
5
  class UnsafeOutboundUrlError extends Error {
6
6
  constructor(reason, message) {
@@ -60,14 +60,12 @@ async function resolveSafeOutboundUrl(rawUrl, options = {}) {
60
60
  const subject = options.subject ?? "URL";
61
61
  const factory = options.errorFactory ?? defaultErrorFactory;
62
62
  const { url, hostname } = parseOutboundUrl(rawUrl, options);
63
- if (options.allowPrivate && !options.pinAllowedPrivate) {
64
- return { url, hostname, addresses: null };
65
- }
66
- if (!options.allowPrivate && isBlockedHostname(hostname)) {
63
+ if (options.allowPrivate) return { url, hostname, addresses: null };
64
+ if (isBlockedHostname(hostname)) {
67
65
  throw factory("blocked_hostname", `${subject} host "${hostname}" is not allowed`);
68
66
  }
69
67
  if (isIP(hostname)) {
70
- if (!options.allowPrivate && isPrivateIpAddress(hostname)) {
68
+ if (isPrivateIpAddress(hostname)) {
71
69
  throw factory(
72
70
  "private_ip_literal",
73
71
  `${subject} host "${hostname}" resolves to a private or reserved IP range`
@@ -95,7 +93,7 @@ async function resolveSafeOutboundUrl(rawUrl, options = {}) {
95
93
  );
96
94
  }
97
95
  for (const record of addresses) {
98
- if (!options.allowPrivate && isPrivateIpAddress(record.address)) {
96
+ if (isPrivateIpAddress(record.address)) {
99
97
  throw factory(
100
98
  "private_ip_resolved",
101
99
  `${subject} host "${hostname}" resolves to a private or reserved IP address (${record.address})`
@@ -124,15 +122,14 @@ async function safeOutboundFetch(rawUrl, init = {}, options = {}) {
124
122
  }
125
123
  });
126
124
  try {
127
- const pinnedFetch = options.pinnedFetchImpl ?? undiciFetch;
128
- return await pinnedFetch(rawUrl, { ...mergedInit, dispatcher });
125
+ return await globalThis.fetch(rawUrl, { ...mergedInit, dispatcher });
129
126
  } finally {
130
127
  dispatcher.close().catch(() => {
131
128
  });
132
129
  }
133
130
  }
134
131
  function createPinnedDnsLookup(expectedHostname, pinned) {
135
- return (host, opts, cb) => {
132
+ return (host, _opts, cb) => {
136
133
  if (host !== expectedHostname) {
137
134
  const err = new Error(
138
135
  `Refusing DNS lookup for unexpected host "${host}" (expected "${expectedHostname}")`
@@ -141,10 +138,6 @@ function createPinnedDnsLookup(expectedHostname, pinned) {
141
138
  cb(err, "", 0);
142
139
  return;
143
140
  }
144
- if (opts.all) {
145
- cb(null, [pinned]);
146
- return;
147
- }
148
141
  cb(null, pinned.address, pinned.family);
149
142
  };
150
143
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/url-safety.ts"],
4
- "sourcesContent": ["import { lookup } from 'node:dns/promises'\nimport { isIP } from 'node:net'\nimport { Agent, fetch as undiciFetch, type Dispatcher } from 'undici'\nimport { isBlockedHostname, isPrivateIpAddress } from './network'\n\nexport type UrlSafetyReason =\n | 'invalid_url'\n | 'forbidden_protocol'\n | 'credentials_in_url'\n | 'missing_host'\n | 'blocked_hostname'\n | 'private_ip_literal'\n | 'private_ip_resolved'\n | 'dns_resolution_failed'\n | 'dns_resolution_empty'\n\nexport class UnsafeOutboundUrlError extends Error {\n public readonly reason: UrlSafetyReason\n\n constructor(reason: UrlSafetyReason, message?: string) {\n super(message ?? `Outbound URL rejected: ${reason}`)\n this.name = 'UnsafeOutboundUrlError'\n this.reason = reason\n }\n}\n\nconst ALLOWED_PROTOCOLS = new Set(['http:', 'https:'])\n\nexport type ParsedOutboundUrl = {\n url: URL\n hostname: string\n}\n\nexport type UrlSafetyErrorFactory = (reason: UrlSafetyReason, message: string) => Error\n\nconst defaultErrorFactory: UrlSafetyErrorFactory = (reason, message) =>\n new UnsafeOutboundUrlError(reason, message)\n\nexport type ParseOutboundUrlOptions = {\n errorFactory?: UrlSafetyErrorFactory\n subject?: string\n}\n\nexport function parseOutboundUrl(\n rawUrl: string,\n options: ParseOutboundUrlOptions = {},\n): ParsedOutboundUrl {\n const subject = options.subject ?? 'URL'\n const factory = options.errorFactory ?? defaultErrorFactory\n let url: URL\n try {\n url = new URL(rawUrl)\n } catch {\n throw factory('invalid_url', `${subject} is not a valid URL`)\n }\n if (!ALLOWED_PROTOCOLS.has(url.protocol)) {\n throw factory(\n 'forbidden_protocol',\n `${subject} protocol \"${url.protocol.replace(':', '')}\" is not allowed; use http or https`,\n )\n }\n if (url.username || url.password) {\n throw factory('credentials_in_url', `${subject} must not embed basic-auth credentials`)\n }\n let hostname = url.hostname.trim().toLowerCase()\n if (!hostname) {\n throw factory('missing_host', `${subject} must include a hostname`)\n }\n if (hostname.startsWith('[') && hostname.endsWith(']')) {\n hostname = hostname.slice(1, -1)\n }\n return { url, hostname }\n}\n\nexport type HostLookup = (\n hostname: string,\n) => Promise<ReadonlyArray<{ address: string; family: number }>>\n\nexport type AssertStaticUrlOptions = ParseOutboundUrlOptions & {\n allowPrivate?: boolean\n}\n\nexport function assertStaticallySafeOutboundUrl(\n rawUrl: string,\n options: AssertStaticUrlOptions = {},\n): void {\n const subject = options.subject ?? 'URL'\n const factory = options.errorFactory ?? defaultErrorFactory\n const { hostname } = parseOutboundUrl(rawUrl, options)\n if (options.allowPrivate) return\n\n if (isBlockedHostname(hostname)) {\n throw factory('blocked_hostname', `${subject} host \"${hostname}\" is not allowed`)\n }\n if (isIP(hostname) && isPrivateIpAddress(hostname)) {\n throw factory(\n 'private_ip_literal',\n `${subject} host \"${hostname}\" resolves to a private or reserved IP range`,\n )\n }\n}\n\nexport type AssertSafeOutboundUrlOptions = AssertStaticUrlOptions & {\n lookupHost?: HostLookup\n /** Resolve and return DNS answers even when private addresses are explicitly allowed. */\n pinAllowedPrivate?: boolean\n}\n\nexport async function assertSafeOutboundUrl(\n rawUrl: string,\n options: AssertSafeOutboundUrlOptions = {},\n): Promise<void> {\n await resolveSafeOutboundUrl(rawUrl, options)\n}\n\nexport type ResolvedHostAddress = { address: string; family: number }\n\nexport type ResolveSafeOutboundUrlResult = {\n url: URL\n hostname: string\n /**\n * The validated DNS records, in lookup order. `null` when the hostname is an IP literal\n * (no DNS lookup performed) or when `allowPrivate` short-circuited validation without\n * `pinAllowedPrivate`.\n */\n addresses: ReadonlyArray<ResolvedHostAddress> | null\n}\n\n/**\n * Validates an outbound URL exactly like `assertSafeOutboundUrl()` and additionally returns\n * the resolved DNS records so the caller can pin the subsequent connection to the same\n * address. This is what `safeOutboundFetch()` uses internally to defeat DNS rebinding \u2014\n * call it directly only if you need to drive the fetch yourself.\n */\nexport async function resolveSafeOutboundUrl(\n rawUrl: string,\n options: AssertSafeOutboundUrlOptions = {},\n): Promise<ResolveSafeOutboundUrlResult> {\n const subject = options.subject ?? 'URL'\n const factory = options.errorFactory ?? defaultErrorFactory\n const { url, hostname } = parseOutboundUrl(rawUrl, options)\n if (options.allowPrivate && !options.pinAllowedPrivate) {\n return { url, hostname, addresses: null }\n }\n\n if (!options.allowPrivate && isBlockedHostname(hostname)) {\n throw factory('blocked_hostname', `${subject} host \"${hostname}\" is not allowed`)\n }\n\n if (isIP(hostname)) {\n if (!options.allowPrivate && isPrivateIpAddress(hostname)) {\n throw factory(\n 'private_ip_literal',\n `${subject} host \"${hostname}\" resolves to a private or reserved IP range`,\n )\n }\n return { url, hostname, addresses: null }\n }\n\n const resolver: HostLookup =\n options.lookupHost ??\n (async (host) => {\n const records = await lookup(host, { all: true, verbatim: true })\n return records\n })\n\n let addresses: ReadonlyArray<ResolvedHostAddress>\n try {\n addresses = await resolver(hostname)\n } catch (error) {\n throw factory(\n 'dns_resolution_failed',\n `${subject} host \"${hostname}\" could not be resolved: ${\n error instanceof Error ? error.message : 'lookup failed'\n }`,\n )\n }\n\n if (!addresses || addresses.length === 0) {\n throw factory(\n 'dns_resolution_empty',\n `${subject} host \"${hostname}\" has no DNS A/AAAA records`,\n )\n }\n\n for (const record of addresses) {\n if (!options.allowPrivate && isPrivateIpAddress(record.address)) {\n throw factory(\n 'private_ip_resolved',\n `${subject} host \"${hostname}\" resolves to a private or reserved IP address (${record.address})`,\n )\n }\n }\n\n return { url, hostname, addresses }\n}\n\nexport type SafeOutboundFetchOptions = AssertSafeOutboundUrlOptions & {\n /**\n * Test/seam injection. When provided, `safeOutboundFetch` calls `fetchImpl(url, init)` after\n * URL validation instead of using the global `fetch` with a DNS-pinned dispatcher. Tests do\n * not actually open sockets, so DNS pinning is unnecessary and would just complicate mocking.\n */\n fetchImpl?: typeof fetch\n /** Test seam for the dispatcher-compatible fetch used by DNS-pinned requests. */\n pinnedFetchImpl?: PinnedFetch\n}\n\nexport type PinnedFetch = (\n rawUrl: string,\n init: RequestInit & { dispatcher: Dispatcher },\n) => Promise<Response>\n\n/**\n * Validates an outbound URL and performs `fetch()` with the connection pinned to a\n * pre-validated IP address, so DNS cannot be re-resolved between validation and connect\n * (DNS rebinding). Always defaults to `redirect: 'manual'` \u2014 callers MUST decide what to\n * do with 3xx responses (re-validate the redirect target before following).\n *\n * For IP literal hosts no DNS pinning is performed because there is no DNS lookup to defeat.\n * Callers that allow private hostnames can set `pinAllowedPrivate` to retain DNS pinning.\n */\nexport async function safeOutboundFetch(\n rawUrl: string,\n init: RequestInit = {},\n options: SafeOutboundFetchOptions = {},\n): Promise<Response> {\n const { url, hostname, addresses } = await resolveSafeOutboundUrl(rawUrl, options)\n void url\n\n const mergedInit: RequestInit = {\n redirect: 'manual',\n ...init,\n }\n\n const fetchImpl = options.fetchImpl\n if (fetchImpl) {\n return fetchImpl(rawUrl, mergedInit)\n }\n\n if (!addresses || addresses.length === 0) {\n return globalThis.fetch(rawUrl, mergedInit)\n }\n\n const dispatcher: Dispatcher = new Agent({\n connect: {\n lookup: createPinnedDnsLookup(hostname, addresses[0]),\n },\n })\n\n try {\n const pinnedFetch = options.pinnedFetchImpl ?? (undiciFetch as unknown as PinnedFetch)\n return await pinnedFetch(rawUrl, { ...mergedInit, dispatcher })\n } finally {\n dispatcher.close().catch(() => {})\n }\n}\n\nexport type PinnedDnsLookup = (\n host: string,\n opts: { all?: boolean },\n cb: (\n err: NodeJS.ErrnoException | null,\n address: string | ResolvedHostAddress[],\n family?: number,\n ) => void,\n) => void\n\n/**\n * Builds a `dns.lookup`-shaped callback that always returns the supplied pre-validated\n * address for the expected hostname, and refuses to resolve any other hostname. Used as\n * `Agent.connect.lookup` to bind an outbound TCP connect to an IP that has already been\n * validated, so attacker-controlled DNS cannot rebind between validation and connect.\n */\nexport function createPinnedDnsLookup(\n expectedHostname: string,\n pinned: ResolvedHostAddress,\n): PinnedDnsLookup {\n return (host, opts, cb) => {\n if (host !== expectedHostname) {\n const err: NodeJS.ErrnoException = new Error(\n `Refusing DNS lookup for unexpected host \"${host}\" (expected \"${expectedHostname}\")`,\n )\n err.code = 'EREFUSED'\n cb(err, '', 0)\n return\n }\n if (opts.all) {\n cb(null, [pinned])\n return\n }\n cb(null, pinned.address, pinned.family)\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { lookup } from 'node:dns/promises'\nimport { isIP } from 'node:net'\nimport { Agent, type Dispatcher } from 'undici'\nimport { isBlockedHostname, isPrivateIpAddress } from './network'\n\nexport type UrlSafetyReason =\n | 'invalid_url'\n | 'forbidden_protocol'\n | 'credentials_in_url'\n | 'missing_host'\n | 'blocked_hostname'\n | 'private_ip_literal'\n | 'private_ip_resolved'\n | 'dns_resolution_failed'\n | 'dns_resolution_empty'\n\nexport class UnsafeOutboundUrlError extends Error {\n public readonly reason: UrlSafetyReason\n\n constructor(reason: UrlSafetyReason, message?: string) {\n super(message ?? `Outbound URL rejected: ${reason}`)\n this.name = 'UnsafeOutboundUrlError'\n this.reason = reason\n }\n}\n\nconst ALLOWED_PROTOCOLS = new Set(['http:', 'https:'])\n\nexport type ParsedOutboundUrl = {\n url: URL\n hostname: string\n}\n\nexport type UrlSafetyErrorFactory = (reason: UrlSafetyReason, message: string) => Error\n\nconst defaultErrorFactory: UrlSafetyErrorFactory = (reason, message) =>\n new UnsafeOutboundUrlError(reason, message)\n\nexport type ParseOutboundUrlOptions = {\n errorFactory?: UrlSafetyErrorFactory\n subject?: string\n}\n\nexport function parseOutboundUrl(\n rawUrl: string,\n options: ParseOutboundUrlOptions = {},\n): ParsedOutboundUrl {\n const subject = options.subject ?? 'URL'\n const factory = options.errorFactory ?? defaultErrorFactory\n let url: URL\n try {\n url = new URL(rawUrl)\n } catch {\n throw factory('invalid_url', `${subject} is not a valid URL`)\n }\n if (!ALLOWED_PROTOCOLS.has(url.protocol)) {\n throw factory(\n 'forbidden_protocol',\n `${subject} protocol \"${url.protocol.replace(':', '')}\" is not allowed; use http or https`,\n )\n }\n if (url.username || url.password) {\n throw factory('credentials_in_url', `${subject} must not embed basic-auth credentials`)\n }\n let hostname = url.hostname.trim().toLowerCase()\n if (!hostname) {\n throw factory('missing_host', `${subject} must include a hostname`)\n }\n if (hostname.startsWith('[') && hostname.endsWith(']')) {\n hostname = hostname.slice(1, -1)\n }\n return { url, hostname }\n}\n\nexport type HostLookup = (\n hostname: string,\n) => Promise<ReadonlyArray<{ address: string; family: number }>>\n\nexport type AssertStaticUrlOptions = ParseOutboundUrlOptions & {\n allowPrivate?: boolean\n}\n\nexport function assertStaticallySafeOutboundUrl(\n rawUrl: string,\n options: AssertStaticUrlOptions = {},\n): void {\n const subject = options.subject ?? 'URL'\n const factory = options.errorFactory ?? defaultErrorFactory\n const { hostname } = parseOutboundUrl(rawUrl, options)\n if (options.allowPrivate) return\n\n if (isBlockedHostname(hostname)) {\n throw factory('blocked_hostname', `${subject} host \"${hostname}\" is not allowed`)\n }\n if (isIP(hostname) && isPrivateIpAddress(hostname)) {\n throw factory(\n 'private_ip_literal',\n `${subject} host \"${hostname}\" resolves to a private or reserved IP range`,\n )\n }\n}\n\nexport type AssertSafeOutboundUrlOptions = AssertStaticUrlOptions & {\n lookupHost?: HostLookup\n}\n\nexport async function assertSafeOutboundUrl(\n rawUrl: string,\n options: AssertSafeOutboundUrlOptions = {},\n): Promise<void> {\n await resolveSafeOutboundUrl(rawUrl, options)\n}\n\nexport type ResolvedHostAddress = { address: string; family: number }\n\nexport type ResolveSafeOutboundUrlResult = {\n url: URL\n hostname: string\n /**\n * The validated DNS records, in lookup order. `null` when the hostname is an IP literal\n * (no DNS lookup performed) or when `allowPrivate` short-circuited validation.\n */\n addresses: ReadonlyArray<ResolvedHostAddress> | null\n}\n\n/**\n * Validates an outbound URL exactly like `assertSafeOutboundUrl()` and additionally returns\n * the resolved DNS records so the caller can pin the subsequent connection to the same\n * address. This is what `safeOutboundFetch()` uses internally to defeat DNS rebinding \u2014\n * call it directly only if you need to drive the fetch yourself.\n */\nexport async function resolveSafeOutboundUrl(\n rawUrl: string,\n options: AssertSafeOutboundUrlOptions = {},\n): Promise<ResolveSafeOutboundUrlResult> {\n const subject = options.subject ?? 'URL'\n const factory = options.errorFactory ?? defaultErrorFactory\n const { url, hostname } = parseOutboundUrl(rawUrl, options)\n if (options.allowPrivate) return { url, hostname, addresses: null }\n\n if (isBlockedHostname(hostname)) {\n throw factory('blocked_hostname', `${subject} host \"${hostname}\" is not allowed`)\n }\n\n if (isIP(hostname)) {\n if (isPrivateIpAddress(hostname)) {\n throw factory(\n 'private_ip_literal',\n `${subject} host \"${hostname}\" resolves to a private or reserved IP range`,\n )\n }\n return { url, hostname, addresses: null }\n }\n\n const resolver: HostLookup =\n options.lookupHost ??\n (async (host) => {\n const records = await lookup(host, { all: true, verbatim: true })\n return records\n })\n\n let addresses: ReadonlyArray<ResolvedHostAddress>\n try {\n addresses = await resolver(hostname)\n } catch (error) {\n throw factory(\n 'dns_resolution_failed',\n `${subject} host \"${hostname}\" could not be resolved: ${\n error instanceof Error ? error.message : 'lookup failed'\n }`,\n )\n }\n\n if (!addresses || addresses.length === 0) {\n throw factory(\n 'dns_resolution_empty',\n `${subject} host \"${hostname}\" has no DNS A/AAAA records`,\n )\n }\n\n for (const record of addresses) {\n if (isPrivateIpAddress(record.address)) {\n throw factory(\n 'private_ip_resolved',\n `${subject} host \"${hostname}\" resolves to a private or reserved IP address (${record.address})`,\n )\n }\n }\n\n return { url, hostname, addresses }\n}\n\nexport type SafeOutboundFetchOptions = AssertSafeOutboundUrlOptions & {\n /**\n * Test/seam injection. When provided, `safeOutboundFetch` calls `fetchImpl(url, init)` after\n * URL validation instead of using the global `fetch` with a DNS-pinned dispatcher. Tests do\n * not actually open sockets, so DNS pinning is unnecessary and would just complicate mocking.\n */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Validates an outbound URL and performs `fetch()` with the connection pinned to a\n * pre-validated IP address, so DNS cannot be re-resolved between validation and connect\n * (DNS rebinding). Always defaults to `redirect: 'manual'` \u2014 callers MUST decide what to\n * do with 3xx responses (re-validate the redirect target before following).\n *\n * For IP literal hosts and `allowPrivate=true`, no DNS pinning is performed because there\n * is no DNS lookup to defeat.\n */\nexport async function safeOutboundFetch(\n rawUrl: string,\n init: RequestInit = {},\n options: SafeOutboundFetchOptions = {},\n): Promise<Response> {\n const { url, hostname, addresses } = await resolveSafeOutboundUrl(rawUrl, options)\n void url\n\n const mergedInit: RequestInit = {\n redirect: 'manual',\n ...init,\n }\n\n const fetchImpl = options.fetchImpl\n if (fetchImpl) {\n return fetchImpl(rawUrl, mergedInit)\n }\n\n if (!addresses || addresses.length === 0) {\n return globalThis.fetch(rawUrl, mergedInit)\n }\n\n const dispatcher: Dispatcher = new Agent({\n connect: {\n lookup: createPinnedDnsLookup(hostname, addresses[0]),\n },\n })\n\n try {\n return await globalThis.fetch(rawUrl, { ...mergedInit, dispatcher } as RequestInit & {\n dispatcher: Dispatcher\n })\n } finally {\n dispatcher.close().catch(() => {})\n }\n}\n\nexport type PinnedDnsLookup = (\n host: string,\n opts: unknown,\n cb: (err: NodeJS.ErrnoException | null, address: string, family: number) => void,\n) => void\n\n/**\n * Builds a `dns.lookup`-shaped callback that always returns the supplied pre-validated\n * address for the expected hostname, and refuses to resolve any other hostname. Used as\n * `Agent.connect.lookup` to bind an outbound TCP connect to an IP that has already been\n * validated, so attacker-controlled DNS cannot rebind between validation and connect.\n */\nexport function createPinnedDnsLookup(\n expectedHostname: string,\n pinned: ResolvedHostAddress,\n): PinnedDnsLookup {\n return (host, _opts, cb) => {\n if (host !== expectedHostname) {\n const err: NodeJS.ErrnoException = new Error(\n `Refusing DNS lookup for unexpected host \"${host}\" (expected \"${expectedHostname}\")`,\n )\n err.code = 'EREFUSED'\n cb(err, '', 0)\n return\n }\n cb(null, pinned.address, pinned.family)\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.6-develop.6551.1.1511bfa890";
1
+ const APP_VERSION = "0.6.6";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.6-develop.6551.1.1511bfa890'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.6'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/payment_gateways/types.ts"],
4
- "sourcesContent": ["// \u2500\u2500 Unified Payment Status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type UnifiedPaymentStatus =\n | 'pending'\n | 'authorized'\n | 'captured'\n | 'partially_captured'\n | 'refunded'\n | 'partially_refunded'\n | 'cancelled'\n | 'failed'\n | 'expired'\n | 'unknown'\n\n// \u2500\u2500 GatewayAdapter Interface \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface GatewayAdapter {\n readonly providerKey: string\n\n /** Create a payment session / payment intent */\n createSession(input: CreateSessionInput): Promise<CreateSessionResult>\n\n /** Capture an authorized payment */\n capture(input: CaptureInput): Promise<CaptureResult>\n\n /** Refund a captured payment (full or partial) */\n refund(input: RefundInput): Promise<RefundResult>\n\n /** Cancel / void an authorized payment before capture */\n cancel(input: CancelInput): Promise<CancelResult>\n\n /** Get current payment status from provider */\n getStatus(input: GetStatusInput): Promise<GatewayPaymentStatus>\n\n /** Verify and parse an inbound webhook event */\n verifyWebhook(input: VerifyWebhookInput): Promise<WebhookEvent>\n\n /** Map provider status to unified status */\n mapStatus(providerStatus: string, eventType?: string): UnifiedPaymentStatus\n}\n\nexport type PaymentGatewayDescriptorFieldType =\n | 'text'\n | 'number'\n | 'select'\n | 'multiselect'\n | 'boolean'\n | 'textarea'\n | 'secret'\n | 'url'\n\nexport interface PaymentGatewayDescriptorField {\n key: string\n label: string\n type: PaymentGatewayDescriptorFieldType\n description?: string\n required?: boolean\n options?: Array<{ value: string; label: string }>\n}\n\nexport type PaymentGatewayRendererType = 'embedded' | 'redirect'\n\nexport interface PaymentGatewayRendererDescriptor {\n key: string\n label: string\n type: PaymentGatewayRendererType\n description?: string\n supportedPaymentTypes?: '*' | string[]\n settingsFields?: PaymentGatewayDescriptorField[]\n}\n\nexport interface PaymentGatewayPresentationRequest {\n mode?: 'auto' | PaymentGatewayRendererType\n rendererKey?: string\n rendererSettings?: Record<string, unknown>\n}\n\nexport interface PaymentGatewayDescriptor {\n providerKey: string\n label: string\n sessionConfig?: {\n fields?: PaymentGatewayDescriptorField[]\n supportedCurrencies?: '*' | string[]\n supportedPaymentTypes?: Array<{ value: string; label: string }>\n defaultRendererKey?: string\n renderers?: PaymentGatewayRendererDescriptor[]\n /**\n * @deprecated Use `renderers` and `defaultRendererKey`.\n */\n presentation?: 'embedded' | 'redirect' | 'either'\n /**\n * @deprecated Use `renderers`.\n */\n embeddedRenderers?: string[]\n }\n}\n\nexport interface EmbeddedPaymentGatewayClientSession {\n type: 'embedded'\n rendererKey: string\n payload?: Record<string, unknown>\n settings?: Record<string, unknown>\n}\n\nexport interface RedirectPaymentGatewayClientSession {\n type: 'redirect'\n redirectUrl: string\n target?: 'self' | 'top'\n}\n\nexport type PaymentGatewayClientSession =\n | EmbeddedPaymentGatewayClientSession\n | RedirectPaymentGatewayClientSession\n\n// \u2500\u2500 Input / Output Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CreateSessionInput {\n orderId?: string\n paymentId: string\n /** Stable operation key for provider-native session creation idempotency. */\n idempotencyKey?: string\n tenantId: string\n organizationId: string\n amount: number\n currencyCode: string\n captureMethod?: 'automatic' | 'manual'\n paymentTypes?: string[]\n description?: string\n successUrl?: string\n cancelUrl?: string\n metadata?: Record<string, unknown>\n credentials: Record<string, unknown>\n lineItems?: SessionLineItem[]\n presentation?: PaymentGatewayPresentationRequest\n}\n\nexport interface SessionLineItem {\n name: string\n quantity: number\n unitAmount: number\n currencyCode: string\n}\n\nexport interface CreateSessionResult {\n sessionId: string\n clientSecret?: string\n redirectUrl?: string\n status: UnifiedPaymentStatus\n providerData?: Record<string, unknown>\n clientSession?: PaymentGatewayClientSession\n}\n\nexport interface CaptureInput {\n sessionId: string\n amount?: number\n credentials: Record<string, unknown>\n metadata?: Record<string, unknown>\n}\n\nexport interface CaptureResult {\n status: UnifiedPaymentStatus\n capturedAmount: number\n providerData?: Record<string, unknown>\n}\n\nexport interface RefundInput {\n sessionId: string\n amount?: number\n reason?: string\n credentials: Record<string, unknown>\n metadata?: Record<string, unknown>\n}\n\nexport interface RefundResult {\n refundId: string\n status: UnifiedPaymentStatus\n refundedAmount: number\n providerData?: Record<string, unknown>\n}\n\nexport interface CancelInput {\n sessionId: string\n reason?: string\n credentials: Record<string, unknown>\n}\n\nexport interface CancelResult {\n status: UnifiedPaymentStatus\n providerData?: Record<string, unknown>\n}\n\nexport interface GetStatusInput {\n sessionId: string\n credentials: Record<string, unknown>\n}\n\nexport interface GatewayPaymentStatus {\n status: UnifiedPaymentStatus\n amount: number\n amountReceived: number\n currencyCode: string\n providerData?: Record<string, unknown>\n}\n\nexport interface VerifyWebhookInput {\n rawBody: string | Buffer\n headers: Record<string, string | string[] | undefined>\n credentials: Record<string, unknown>\n}\n\nexport interface WebhookEvent {\n eventType: string\n eventId: string\n data: Record<string, unknown>\n idempotencyKey: string\n timestamp: Date\n}\n\n// \u2500\u2500 Webhook Handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface WebhookHandlerRegistration {\n handler: (input: VerifyWebhookInput) => Promise<WebhookEvent>\n queue?: string\n readSessionIdHint?: (payload: Record<string, unknown> | null) => string | null\n}\n\n// \u2500\u2500 Adapter Registry Options \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface RegisterAdapterOptions {\n version?: string\n}\n\n// \u2500\u2500 Registries \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ADAPTER_REGISTRY_KEY = '__openMercatoPaymentGatewayAdapters__'\nconst WEBHOOK_REGISTRY_KEY = '__openMercatoPaymentGatewayWebhookHandlers__'\nconst DESCRIPTOR_REGISTRY_KEY = '__openMercatoPaymentGatewayDescriptors__'\n\nfunction getAdapterRegistry(): Map<string, GatewayAdapter> {\n const globalState = globalThis as typeof globalThis & {\n [ADAPTER_REGISTRY_KEY]?: Map<string, GatewayAdapter>\n }\n if (!globalState[ADAPTER_REGISTRY_KEY]) {\n globalState[ADAPTER_REGISTRY_KEY] = new Map<string, GatewayAdapter>()\n }\n return globalState[ADAPTER_REGISTRY_KEY]\n}\n\nfunction getWebhookHandlerRegistry(): Map<string, WebhookHandlerRegistration> {\n const globalState = globalThis as typeof globalThis & {\n [WEBHOOK_REGISTRY_KEY]?: Map<string, WebhookHandlerRegistration>\n }\n if (!globalState[WEBHOOK_REGISTRY_KEY]) {\n globalState[WEBHOOK_REGISTRY_KEY] = new Map<string, WebhookHandlerRegistration>()\n }\n return globalState[WEBHOOK_REGISTRY_KEY]\n}\n\nfunction getDescriptorRegistry(): Map<string, PaymentGatewayDescriptor> {\n const globalState = globalThis as typeof globalThis & {\n [DESCRIPTOR_REGISTRY_KEY]?: Map<string, PaymentGatewayDescriptor>\n }\n if (!globalState[DESCRIPTOR_REGISTRY_KEY]) {\n globalState[DESCRIPTOR_REGISTRY_KEY] = new Map<string, PaymentGatewayDescriptor>()\n }\n return globalState[DESCRIPTOR_REGISTRY_KEY]\n}\n\nfunction adapterKey(providerKey: string, version?: string): string {\n return version ? `${providerKey}:${version}` : providerKey\n}\n\nexport function registerGatewayAdapter(adapter: GatewayAdapter, options?: RegisterAdapterOptions): () => void {\n const adapterRegistry = getAdapterRegistry()\n const key = adapterKey(adapter.providerKey, options?.version)\n adapterRegistry.set(key, adapter)\n if (options?.version) {\n // Also register as default if no default exists\n if (!adapterRegistry.has(adapter.providerKey)) {\n adapterRegistry.set(adapter.providerKey, adapter)\n }\n }\n return () => {\n adapterRegistry.delete(key)\n }\n}\n\nexport function getGatewayAdapter(providerKey: string, version?: string): GatewayAdapter | undefined {\n const adapterRegistry = getAdapterRegistry()\n if (version) {\n return adapterRegistry.get(adapterKey(providerKey, version)) ?? adapterRegistry.get(providerKey)\n }\n return adapterRegistry.get(providerKey)\n}\n\nexport function listGatewayAdapters(): GatewayAdapter[] {\n const adapterRegistry = getAdapterRegistry()\n const seen = new Set<string>()\n const result: GatewayAdapter[] = []\n for (const [key, adapter] of adapterRegistry) {\n if (!key.includes(':') && !seen.has(adapter.providerKey)) {\n seen.add(adapter.providerKey)\n result.push(adapter)\n }\n }\n return result\n}\n\nexport function clearGatewayAdapters(): void {\n getAdapterRegistry().clear()\n}\n\nexport function registerWebhookHandler(\n providerKey: string,\n handler: (input: VerifyWebhookInput) => Promise<WebhookEvent>,\n options?: {\n queue?: string\n readSessionIdHint?: (payload: Record<string, unknown> | null) => string | null\n },\n): () => void {\n const webhookHandlerRegistry = getWebhookHandlerRegistry()\n webhookHandlerRegistry.set(providerKey, {\n handler,\n queue: options?.queue,\n readSessionIdHint: options?.readSessionIdHint,\n })\n return () => {\n webhookHandlerRegistry.delete(providerKey)\n }\n}\n\nexport function getWebhookHandler(providerKey: string): WebhookHandlerRegistration | undefined {\n return getWebhookHandlerRegistry().get(providerKey)\n}\n\nexport function clearWebhookHandlers(): void {\n getWebhookHandlerRegistry().clear()\n}\n\nexport function registerPaymentGatewayDescriptor(descriptor: PaymentGatewayDescriptor): () => void {\n const descriptorRegistry = getDescriptorRegistry()\n descriptorRegistry.set(descriptor.providerKey, descriptor)\n return () => {\n descriptorRegistry.delete(descriptor.providerKey)\n }\n}\n\nexport function getPaymentGatewayDescriptor(providerKey: string): PaymentGatewayDescriptor | undefined {\n return getDescriptorRegistry().get(providerKey)\n}\n\nexport function listPaymentGatewayDescriptors(): PaymentGatewayDescriptor[] {\n return Array.from(getDescriptorRegistry().values()).sort((left, right) => left.label.localeCompare(right.label))\n}\n\nexport function clearPaymentGatewayDescriptors(): void {\n getDescriptorRegistry().clear()\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// \u2500\u2500 Unified Payment Status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type UnifiedPaymentStatus =\n | 'pending'\n | 'authorized'\n | 'captured'\n | 'partially_captured'\n | 'refunded'\n | 'partially_refunded'\n | 'cancelled'\n | 'failed'\n | 'expired'\n | 'unknown'\n\n// \u2500\u2500 GatewayAdapter Interface \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface GatewayAdapter {\n readonly providerKey: string\n\n /** Create a payment session / payment intent */\n createSession(input: CreateSessionInput): Promise<CreateSessionResult>\n\n /** Capture an authorized payment */\n capture(input: CaptureInput): Promise<CaptureResult>\n\n /** Refund a captured payment (full or partial) */\n refund(input: RefundInput): Promise<RefundResult>\n\n /** Cancel / void an authorized payment before capture */\n cancel(input: CancelInput): Promise<CancelResult>\n\n /** Get current payment status from provider */\n getStatus(input: GetStatusInput): Promise<GatewayPaymentStatus>\n\n /** Verify and parse an inbound webhook event */\n verifyWebhook(input: VerifyWebhookInput): Promise<WebhookEvent>\n\n /** Map provider status to unified status */\n mapStatus(providerStatus: string, eventType?: string): UnifiedPaymentStatus\n}\n\nexport type PaymentGatewayDescriptorFieldType =\n | 'text'\n | 'number'\n | 'select'\n | 'multiselect'\n | 'boolean'\n | 'textarea'\n | 'secret'\n | 'url'\n\nexport interface PaymentGatewayDescriptorField {\n key: string\n label: string\n type: PaymentGatewayDescriptorFieldType\n description?: string\n required?: boolean\n options?: Array<{ value: string; label: string }>\n}\n\nexport type PaymentGatewayRendererType = 'embedded' | 'redirect'\n\nexport interface PaymentGatewayRendererDescriptor {\n key: string\n label: string\n type: PaymentGatewayRendererType\n description?: string\n supportedPaymentTypes?: '*' | string[]\n settingsFields?: PaymentGatewayDescriptorField[]\n}\n\nexport interface PaymentGatewayPresentationRequest {\n mode?: 'auto' | PaymentGatewayRendererType\n rendererKey?: string\n rendererSettings?: Record<string, unknown>\n}\n\nexport interface PaymentGatewayDescriptor {\n providerKey: string\n label: string\n sessionConfig?: {\n fields?: PaymentGatewayDescriptorField[]\n supportedCurrencies?: '*' | string[]\n supportedPaymentTypes?: Array<{ value: string; label: string }>\n defaultRendererKey?: string\n renderers?: PaymentGatewayRendererDescriptor[]\n /**\n * @deprecated Use `renderers` and `defaultRendererKey`.\n */\n presentation?: 'embedded' | 'redirect' | 'either'\n /**\n * @deprecated Use `renderers`.\n */\n embeddedRenderers?: string[]\n }\n}\n\nexport interface EmbeddedPaymentGatewayClientSession {\n type: 'embedded'\n rendererKey: string\n payload?: Record<string, unknown>\n settings?: Record<string, unknown>\n}\n\nexport interface RedirectPaymentGatewayClientSession {\n type: 'redirect'\n redirectUrl: string\n target?: 'self' | 'top'\n}\n\nexport type PaymentGatewayClientSession =\n | EmbeddedPaymentGatewayClientSession\n | RedirectPaymentGatewayClientSession\n\n// \u2500\u2500 Input / Output Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface CreateSessionInput {\n orderId?: string\n paymentId: string\n tenantId: string\n organizationId: string\n amount: number\n currencyCode: string\n captureMethod?: 'automatic' | 'manual'\n paymentTypes?: string[]\n description?: string\n successUrl?: string\n cancelUrl?: string\n metadata?: Record<string, unknown>\n credentials: Record<string, unknown>\n lineItems?: SessionLineItem[]\n presentation?: PaymentGatewayPresentationRequest\n}\n\nexport interface SessionLineItem {\n name: string\n quantity: number\n unitAmount: number\n currencyCode: string\n}\n\nexport interface CreateSessionResult {\n sessionId: string\n clientSecret?: string\n redirectUrl?: string\n status: UnifiedPaymentStatus\n providerData?: Record<string, unknown>\n clientSession?: PaymentGatewayClientSession\n}\n\nexport interface CaptureInput {\n sessionId: string\n amount?: number\n credentials: Record<string, unknown>\n metadata?: Record<string, unknown>\n}\n\nexport interface CaptureResult {\n status: UnifiedPaymentStatus\n capturedAmount: number\n providerData?: Record<string, unknown>\n}\n\nexport interface RefundInput {\n sessionId: string\n amount?: number\n reason?: string\n credentials: Record<string, unknown>\n metadata?: Record<string, unknown>\n}\n\nexport interface RefundResult {\n refundId: string\n status: UnifiedPaymentStatus\n refundedAmount: number\n providerData?: Record<string, unknown>\n}\n\nexport interface CancelInput {\n sessionId: string\n reason?: string\n credentials: Record<string, unknown>\n}\n\nexport interface CancelResult {\n status: UnifiedPaymentStatus\n providerData?: Record<string, unknown>\n}\n\nexport interface GetStatusInput {\n sessionId: string\n credentials: Record<string, unknown>\n}\n\nexport interface GatewayPaymentStatus {\n status: UnifiedPaymentStatus\n amount: number\n amountReceived: number\n currencyCode: string\n providerData?: Record<string, unknown>\n}\n\nexport interface VerifyWebhookInput {\n rawBody: string | Buffer\n headers: Record<string, string | string[] | undefined>\n credentials: Record<string, unknown>\n}\n\nexport interface WebhookEvent {\n eventType: string\n eventId: string\n data: Record<string, unknown>\n idempotencyKey: string\n timestamp: Date\n}\n\n// \u2500\u2500 Webhook Handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface WebhookHandlerRegistration {\n handler: (input: VerifyWebhookInput) => Promise<WebhookEvent>\n queue?: string\n readSessionIdHint?: (payload: Record<string, unknown> | null) => string | null\n}\n\n// \u2500\u2500 Adapter Registry Options \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface RegisterAdapterOptions {\n version?: string\n}\n\n// \u2500\u2500 Registries \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst ADAPTER_REGISTRY_KEY = '__openMercatoPaymentGatewayAdapters__'\nconst WEBHOOK_REGISTRY_KEY = '__openMercatoPaymentGatewayWebhookHandlers__'\nconst DESCRIPTOR_REGISTRY_KEY = '__openMercatoPaymentGatewayDescriptors__'\n\nfunction getAdapterRegistry(): Map<string, GatewayAdapter> {\n const globalState = globalThis as typeof globalThis & {\n [ADAPTER_REGISTRY_KEY]?: Map<string, GatewayAdapter>\n }\n if (!globalState[ADAPTER_REGISTRY_KEY]) {\n globalState[ADAPTER_REGISTRY_KEY] = new Map<string, GatewayAdapter>()\n }\n return globalState[ADAPTER_REGISTRY_KEY]\n}\n\nfunction getWebhookHandlerRegistry(): Map<string, WebhookHandlerRegistration> {\n const globalState = globalThis as typeof globalThis & {\n [WEBHOOK_REGISTRY_KEY]?: Map<string, WebhookHandlerRegistration>\n }\n if (!globalState[WEBHOOK_REGISTRY_KEY]) {\n globalState[WEBHOOK_REGISTRY_KEY] = new Map<string, WebhookHandlerRegistration>()\n }\n return globalState[WEBHOOK_REGISTRY_KEY]\n}\n\nfunction getDescriptorRegistry(): Map<string, PaymentGatewayDescriptor> {\n const globalState = globalThis as typeof globalThis & {\n [DESCRIPTOR_REGISTRY_KEY]?: Map<string, PaymentGatewayDescriptor>\n }\n if (!globalState[DESCRIPTOR_REGISTRY_KEY]) {\n globalState[DESCRIPTOR_REGISTRY_KEY] = new Map<string, PaymentGatewayDescriptor>()\n }\n return globalState[DESCRIPTOR_REGISTRY_KEY]\n}\n\nfunction adapterKey(providerKey: string, version?: string): string {\n return version ? `${providerKey}:${version}` : providerKey\n}\n\nexport function registerGatewayAdapter(adapter: GatewayAdapter, options?: RegisterAdapterOptions): () => void {\n const adapterRegistry = getAdapterRegistry()\n const key = adapterKey(adapter.providerKey, options?.version)\n adapterRegistry.set(key, adapter)\n if (options?.version) {\n // Also register as default if no default exists\n if (!adapterRegistry.has(adapter.providerKey)) {\n adapterRegistry.set(adapter.providerKey, adapter)\n }\n }\n return () => {\n adapterRegistry.delete(key)\n }\n}\n\nexport function getGatewayAdapter(providerKey: string, version?: string): GatewayAdapter | undefined {\n const adapterRegistry = getAdapterRegistry()\n if (version) {\n return adapterRegistry.get(adapterKey(providerKey, version)) ?? adapterRegistry.get(providerKey)\n }\n return adapterRegistry.get(providerKey)\n}\n\nexport function listGatewayAdapters(): GatewayAdapter[] {\n const adapterRegistry = getAdapterRegistry()\n const seen = new Set<string>()\n const result: GatewayAdapter[] = []\n for (const [key, adapter] of adapterRegistry) {\n if (!key.includes(':') && !seen.has(adapter.providerKey)) {\n seen.add(adapter.providerKey)\n result.push(adapter)\n }\n }\n return result\n}\n\nexport function clearGatewayAdapters(): void {\n getAdapterRegistry().clear()\n}\n\nexport function registerWebhookHandler(\n providerKey: string,\n handler: (input: VerifyWebhookInput) => Promise<WebhookEvent>,\n options?: {\n queue?: string\n readSessionIdHint?: (payload: Record<string, unknown> | null) => string | null\n },\n): () => void {\n const webhookHandlerRegistry = getWebhookHandlerRegistry()\n webhookHandlerRegistry.set(providerKey, {\n handler,\n queue: options?.queue,\n readSessionIdHint: options?.readSessionIdHint,\n })\n return () => {\n webhookHandlerRegistry.delete(providerKey)\n }\n}\n\nexport function getWebhookHandler(providerKey: string): WebhookHandlerRegistration | undefined {\n return getWebhookHandlerRegistry().get(providerKey)\n}\n\nexport function clearWebhookHandlers(): void {\n getWebhookHandlerRegistry().clear()\n}\n\nexport function registerPaymentGatewayDescriptor(descriptor: PaymentGatewayDescriptor): () => void {\n const descriptorRegistry = getDescriptorRegistry()\n descriptorRegistry.set(descriptor.providerKey, descriptor)\n return () => {\n descriptorRegistry.delete(descriptor.providerKey)\n }\n}\n\nexport function getPaymentGatewayDescriptor(providerKey: string): PaymentGatewayDescriptor | undefined {\n return getDescriptorRegistry().get(providerKey)\n}\n\nexport function listPaymentGatewayDescriptors(): PaymentGatewayDescriptor[] {\n return Array.from(getDescriptorRegistry().values()).sort((left, right) => left.label.localeCompare(right.label))\n}\n\nexport function clearPaymentGatewayDescriptors(): void {\n getDescriptorRegistry().clear()\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.6.6-develop.6551.1.1511bfa890",
3
+ "version": "0.6.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -97,7 +97,7 @@
97
97
  "@mikro-orm/core": "^7.1.5",
98
98
  "@mikro-orm/decorators": "^7.1.5",
99
99
  "@mikro-orm/postgresql": "^7.1.5",
100
- "@open-mercato/cache": "0.6.6-develop.6551.1.1511bfa890",
100
+ "@open-mercato/cache": "0.6.6",
101
101
  "@types/sanitize-html": "^2.16.1",
102
102
  "dotenv": "^17.4.2",
103
103
  "pino": "^10.3.1",
@@ -120,6 +120,5 @@
120
120
  "type": "git",
121
121
  "url": "https://github.com/open-mercato/open-mercato",
122
122
  "directory": "packages/shared"
123
- },
124
- "stableVersion": "0.6.5"
123
+ }
125
124
  }
@@ -228,16 +228,6 @@ describe('url-safety — createPinnedDnsLookup', () => {
228
228
  expect(cb).toHaveBeenCalledWith(null, '93.184.216.34', 4)
229
229
  })
230
230
 
231
- it('returns an address array when Node requests all lookup results', () => {
232
- const pinned = { address: '93.184.216.34', family: 4 }
233
- const lookup = createPinnedDnsLookup('good.example', pinned)
234
- const cb = jest.fn()
235
-
236
- lookup('good.example', { all: true }, cb)
237
-
238
- expect(cb).toHaveBeenCalledWith(null, [pinned])
239
- })
240
-
241
231
  it('refuses to resolve a different hostname (defeats redirect to private host via Host header)', () => {
242
232
  const lookup = createPinnedDnsLookup('good.example', { address: '93.184.216.34', family: 4 })
243
233
  const cb = jest.fn()
@@ -315,22 +305,4 @@ describe('url-safety — safeOutboundFetch', () => {
315
305
  )
316
306
  expect(lookupHost).toHaveBeenCalledTimes(1)
317
307
  })
318
-
319
- it('uses the dispatcher-compatible fetch implementation for pinned requests', async () => {
320
- const pinnedFetchImpl = jest.fn(async () => new Response('ok', { status: 200 }))
321
- const lookupHost = jest.fn(async () => [{ address: '93.184.216.34', family: 4 }])
322
-
323
- const response = await safeOutboundFetch(
324
- 'https://good.example/hook',
325
- {},
326
- { lookupHost, pinnedFetchImpl },
327
- )
328
-
329
- expect(response.status).toBe(200)
330
- expect(pinnedFetchImpl).toHaveBeenCalledTimes(1)
331
- expect(pinnedFetchImpl).toHaveBeenCalledWith(
332
- 'https://good.example/hook',
333
- expect.objectContaining({ dispatcher: expect.any(Object), redirect: 'manual' }),
334
- )
335
- })
336
308
  })
@@ -82,6 +82,45 @@ describe('auth server integrity checks', () => {
82
82
  expect(resolveCanonicalStaffAuthContext).toHaveBeenCalledWith(em, auth)
83
83
  })
84
84
 
85
+ it('reports a transient DB failure as "error", never "invalid" (issue #4176 — no mass logout)', async () => {
86
+ const { resolveAuthFromRequestDetailed, resolveAuthFromCookiesDetailed } = await import('@open-mercato/shared/lib/auth/server')
87
+ const auth = {
88
+ sub: '11111111-1111-4111-8111-111111111111',
89
+ tenantId: '22222222-2222-4222-8222-222222222222',
90
+ orgId: '33333333-3333-4333-8333-333333333333',
91
+ roles: [],
92
+ }
93
+
94
+ verifyJwt.mockReturnValue(auth)
95
+ // A DB blip / pool exhaustion / timeout throws from the canonical lookup.
96
+ resolveCanonicalStaffAuthContext.mockRejectedValue(new Error('connection pool exhausted'))
97
+
98
+ const request = new Request('https://example.test/api/test', {
99
+ headers: { cookie: 'auth_token=jwt-token' },
100
+ })
101
+
102
+ await expect(resolveAuthFromRequestDetailed(request)).resolves.toEqual({ auth: null, status: 'error' })
103
+
104
+ cookieStore.get.mockImplementation((name: string) => {
105
+ if (name === 'auth_token') return { value: 'jwt-token' }
106
+ return undefined
107
+ })
108
+ await expect(resolveAuthFromCookiesDetailed()).resolves.toEqual({ auth: null, status: 'error' })
109
+ })
110
+
111
+ it('still reports a genuinely bad token as "invalid" when it throws during verify', async () => {
112
+ const { resolveAuthFromRequestDetailed } = await import('@open-mercato/shared/lib/auth/server')
113
+ verifyJwt.mockImplementation(() => {
114
+ throw new Error('malformed token')
115
+ })
116
+
117
+ const request = new Request('https://example.test/api/test', {
118
+ headers: { cookie: 'auth_token=jwt-token' },
119
+ })
120
+
121
+ await expect(resolveAuthFromRequestDetailed(request)).resolves.toEqual({ auth: null, status: 'invalid' })
122
+ })
123
+
85
124
  it('replaces stale JWT roles with canonical roles from the database', async () => {
86
125
  const { getAuthFromRequest } = await import('@open-mercato/shared/lib/auth/server')
87
126
  const jwtAuth = {
@@ -23,12 +23,29 @@ export type AuthContext = {
23
23
  } | null
24
24
 
25
25
  type CookieOverride = { applied: boolean; value: string | null }
26
- type AuthResolutionStatus = 'authenticated' | 'missing' | 'invalid'
26
+ // 'error' means auth could not be determined because of a transient/unexpected
27
+ // failure (DB unavailable, connection-pool exhaustion, statement timeout, …) —
28
+ // NOT because the token is genuinely invalid. Callers MUST NOT clear the user's
29
+ // session cookies for 'error'; doing so would log everyone out at once during a
30
+ // shared infrastructure blip (issue #4176).
31
+ type AuthResolutionStatus = 'authenticated' | 'missing' | 'invalid' | 'error'
27
32
  type AuthResolution = {
28
33
  auth: AuthContext
29
34
  status: AuthResolutionStatus
30
35
  }
31
36
 
37
+ // Thrown by `resolveCanonicalInteractiveAuthContext` when the canonical auth
38
+ // lookup itself fails unexpectedly (as opposed to resolving to a genuine
39
+ // "not authorized" null). Kept distinct so resolvers can map it to the
40
+ // non-destructive 'error' status instead of the cookie-clearing 'invalid'.
41
+ export class AuthResolutionUnavailableError extends Error {
42
+ constructor(cause?: unknown) {
43
+ super('Auth resolution unavailable')
44
+ this.name = 'AuthResolutionUnavailableError'
45
+ if (cause !== undefined) (this as { cause?: unknown }).cause = cause
46
+ }
47
+ }
48
+
32
49
  // Symbol-keyed trusted auth context. Set on synthetic Request objects by
33
50
  // callers that have already authenticated (e.g. the AI in-process operation
34
51
  // runner) so downstream auth resolution short-circuits without re-running
@@ -263,8 +280,12 @@ async function resolveCanonicalInteractiveAuthContext(auth: AuthContext): Promis
263
280
  const container = await createRequestContainer()
264
281
  const em = container.resolve('em') as EntityManager
265
282
  return await resolveCanonicalStaffAuthContext(em, auth)
266
- } catch {
267
- return null
283
+ } catch (err) {
284
+ // A thrown error here means we could not check the token against the
285
+ // database (DB down, pool exhausted, timeout). This is NOT the same as the
286
+ // token being invalid — surface it so callers keep the session intact and
287
+ // fail the request transiently instead of force-logging the user out.
288
+ throw new AuthResolutionUnavailableError(err)
268
289
  }
269
290
  }
270
291
 
@@ -284,7 +305,8 @@ export async function resolveAuthFromCookiesDetailed(): Promise<AuthResolution>
284
305
  auth: applySuperAdminScope(canonicalAuth, tenantCookie, orgCookie),
285
306
  status: 'authenticated',
286
307
  }
287
- } catch {
308
+ } catch (err) {
309
+ if (err instanceof AuthResolutionUnavailableError) return { auth: null, status: 'error' }
288
310
  return { auth: null, status: 'invalid' }
289
311
  }
290
312
  }
@@ -307,6 +329,7 @@ export async function resolveAuthFromRequestDetailed(req: Request): Promise<Auth
307
329
  const authHeader = (req.headers.get('authorization') || '').trim()
308
330
  let token: string | undefined
309
331
  let hadInvalidInteractiveToken = false
332
+ let hadUnavailableResolution = false
310
333
  if (authHeader.toLowerCase().startsWith('bearer ')) token = authHeader.slice(7).trim()
311
334
  if (!token) {
312
335
  const match = cookieHeader.match(/(?:^|;\s*)auth_token=([^;]+)/)
@@ -326,18 +349,25 @@ export async function resolveAuthFromRequestDetailed(req: Request): Promise<Auth
326
349
  }
327
350
  hadInvalidInteractiveToken = true
328
351
  }
329
- } catch {
330
- hadInvalidInteractiveToken = true
352
+ } catch (err) {
353
+ // Transient/unexpected canonical-resolution failures must not invalidate
354
+ // the token — surface them so we return 'error' (retryable) rather than
355
+ // clearing the session cookies.
356
+ if (err instanceof AuthResolutionUnavailableError) hadUnavailableResolution = true
357
+ else hadInvalidInteractiveToken = true
331
358
  }
332
359
  }
333
360
 
361
+ const resolveUnauthenticatedStatus = (): AuthResolutionStatus =>
362
+ hadUnavailableResolution ? 'error' : hadInvalidInteractiveToken ? 'invalid' : 'missing'
363
+
334
364
  const apiKey = extractApiKey(req)
335
365
  if (!apiKey) {
336
- return { auth: null, status: hadInvalidInteractiveToken ? 'invalid' : 'missing' }
366
+ return { auth: null, status: resolveUnauthenticatedStatus() }
337
367
  }
338
368
  const apiAuth = await resolveApiKeyAuth(apiKey)
339
369
  if (!apiAuth) {
340
- return { auth: null, status: hadInvalidInteractiveToken ? 'invalid' : 'missing' }
370
+ return { auth: null, status: resolveUnauthenticatedStatus() }
341
371
  }
342
372
  return {
343
373
  auth: applySuperAdminScope(apiAuth, tenantCookie, orgCookie),