@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.
- package/AGENTS.md +0 -18
- package/dist/lib/auth/server.js +19 -7
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/crud/factory.js +0 -33
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/location/countries.js +4 -10
- package/dist/lib/location/countries.js.map +2 -2
- package/dist/lib/network.js +1 -20
- package/dist/lib/network.js.map +2 -2
- package/dist/lib/string.js +0 -5
- package/dist/lib/string.js.map +2 -2
- package/dist/lib/url-safety.js +7 -14
- package/dist/lib/url-safety.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/package.json +3 -4
- package/src/lib/__tests__/url-safety.test.ts +0 -28
- package/src/lib/auth/__tests__/server.test.ts +39 -0
- package/src/lib/auth/server.ts +38 -8
- package/src/lib/crud/factory.ts +0 -55
- package/src/lib/location/countries.ts +4 -15
- package/src/lib/network.ts +1 -28
- package/src/lib/string.ts +0 -8
- package/src/lib/url-safety.ts +14 -33
- package/src/modules/payment_gateways/types.ts +0 -2
- package/src/lib/__tests__/string.test.ts +0 -49
- package/src/lib/location/__tests__/countries.test.ts +0 -82
|
@@ -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
|
|
15
|
-
|
|
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
|
-
|
|
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\
|
|
5
|
-
"mappings": "AAAA,OAAO,cAAc;AAcrB,SAAS,YAAY,OAA0F;AAC7G,MAAI,MAAM,SAAS,SAAU,QAAO;AACpC,MAAI,CAAC,MAAM,UAAU,CAAC,aAAa,KAAK,MAAM,MAAM,EAAG,QAAO;AAC9D,MAAI,MAAM,WAAY,QAAO;AAC7B,MAAI,CAAC,MAAM,eAAe,CAAC,MAAM,YAAY,OAAQ,QAAO;AAC5D,MAAI,MAAM,YAAY,CAAC,MAAM,cAAe,QAAO;AACnD,SAAO;AACT;AAEA,MAAM,gBAA+B,SAClC,OAAO,WAAW,EAClB,IAAI,CAAC,WAAW;AAAA,EACf,MAAM,MAAM;AAAA,EACZ,MAAM,MAAM,YAAY,KAAK,IAAI;AACnC,EAAE;
|
|
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
|
}
|
package/dist/lib/network.js
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/lib/network.js.map
CHANGED
|
@@ -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
|
|
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,
|
|
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": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/string.js
CHANGED
|
@@ -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
|
};
|
package/dist/lib/string.js.map
CHANGED
|
@@ -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\
|
|
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;
|
|
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
|
}
|
package/dist/lib/url-safety.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { lookup } from "node:dns/promises";
|
|
2
2
|
import { isIP } from "node:net";
|
|
3
|
-
import { Agent
|
|
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
|
|
64
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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,
|
|
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,
|
|
5
|
-
"mappings": "AAAA,SAAS,cAAc;AACvB,SAAS,YAAY;AACrB,SAAS,
|
|
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
|
}
|
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.6.6
|
|
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": "
|
|
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
|
|
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
|
|
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 = {
|
package/src/lib/auth/server.ts
CHANGED
|
@@ -23,12 +23,29 @@ export type AuthContext = {
|
|
|
23
23
|
} | null
|
|
24
24
|
|
|
25
25
|
type CookieOverride = { applied: boolean; value: string | null }
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
366
|
+
return { auth: null, status: resolveUnauthenticatedStatus() }
|
|
337
367
|
}
|
|
338
368
|
const apiAuth = await resolveApiKeyAuth(apiKey)
|
|
339
369
|
if (!apiAuth) {
|
|
340
|
-
return { auth: null, status:
|
|
370
|
+
return { auth: null, status: resolveUnauthenticatedStatus() }
|
|
341
371
|
}
|
|
342
372
|
return {
|
|
343
373
|
auth: applySuperAdminScope(apiAuth, tenantCookie, orgCookie),
|