@hostwebhook/node-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/code-runner.d.ts +20 -0
  2. package/dist/code-runner.js +138 -0
  3. package/dist/contratos.d.ts +121 -0
  4. package/dist/contratos.js +24 -0
  5. package/dist/dto/output-node.dto.d.ts +19 -0
  6. package/dist/dto/output-node.dto.js +96 -0
  7. package/dist/ensure-meta.d.ts +22 -0
  8. package/dist/ensure-meta.js +35 -0
  9. package/dist/execute-with-iteration.d.ts +18 -0
  10. package/dist/execute-with-iteration.js +66 -0
  11. package/dist/filter-utils.d.ts +22 -0
  12. package/dist/filter-utils.js +178 -0
  13. package/dist/handler-helpers.d.ts +21 -0
  14. package/dist/handler-helpers.js +53 -0
  15. package/dist/index.d.ts +51 -0
  16. package/dist/index.js +73 -0
  17. package/dist/log-metadata.d.ts +191 -0
  18. package/dist/log-metadata.js +375 -0
  19. package/dist/node-dispatch.registry.d.ts +32 -0
  20. package/dist/node-dispatch.registry.js +45 -0
  21. package/dist/node-executors.d.ts +299 -0
  22. package/dist/node-executors.js +555 -0
  23. package/dist/node-lifecycle.d.ts +399 -0
  24. package/dist/node-lifecycle.js +782 -0
  25. package/dist/normalize-nodes.d.ts +18 -0
  26. package/dist/normalize-nodes.js +22 -0
  27. package/dist/output-node-ref.schema.d.ts +82 -0
  28. package/dist/output-node-ref.schema.js +90 -0
  29. package/dist/output-webhook-scope.d.ts +36 -0
  30. package/dist/output-webhook-scope.js +42 -0
  31. package/dist/payload-preview.d.ts +10 -0
  32. package/dist/payload-preview.js +39 -0
  33. package/dist/pipeline.constants.d.ts +29 -0
  34. package/dist/pipeline.constants.js +51 -0
  35. package/dist/pre-request-pool.d.ts +58 -0
  36. package/dist/pre-request-pool.js +308 -0
  37. package/dist/pre-request-runner-source.d.ts +28 -0
  38. package/dist/pre-request-runner-source.js +411 -0
  39. package/dist/regex-de-inquilino.d.ts +15 -0
  40. package/dist/regex-de-inquilino.js +98 -0
  41. package/dist/request-context.d.ts +18 -0
  42. package/dist/request-context.js +34 -0
  43. package/dist/retry-transient.d.ts +54 -0
  44. package/dist/retry-transient.js +67 -0
  45. package/dist/retry-utils.d.ts +17 -0
  46. package/dist/retry-utils.js +23 -0
  47. package/dist/schema-validator-utils.d.ts +9 -0
  48. package/dist/schema-validator-utils.js +140 -0
  49. package/dist/ssrf-guard.d.ts +202 -0
  50. package/dist/ssrf-guard.js +917 -0
  51. package/dist/swallow.d.ts +52 -0
  52. package/dist/swallow.js +55 -0
  53. package/dist/template-render.d.ts +33 -0
  54. package/dist/template-render.js +43 -0
  55. package/dist/try-parse.d.ts +41 -0
  56. package/dist/try-parse.js +69 -0
  57. package/dist/workspace-payloads.d.ts +66 -0
  58. package/dist/workspace-payloads.js +496 -0
  59. package/package.json +35 -0
@@ -0,0 +1,917 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.URI_SCHEMES = exports.URI_GUARD_FAMILY = void 0;
37
+ exports.assertSafeMongoUri = assertSafeMongoUri;
38
+ exports.maskMongoUri = maskMongoUri;
39
+ exports.assertSafeMongoUriUnlessTunneled = assertSafeMongoUriUnlessTunneled;
40
+ exports.assertSafePostgresUri = assertSafePostgresUri;
41
+ exports.assertSafePostgresUriUnlessTunneled = assertSafePostgresUriUnlessTunneled;
42
+ exports.isGuardedUriCredential = isGuardedUriCredential;
43
+ exports.assertUriSchemeMatchesType = assertUriSchemeMatchesType;
44
+ exports.assertSafeCredentialUri = assertSafeCredentialUri;
45
+ exports.assertSafeCredentialUriUnlessTunneled = assertSafeCredentialUriUnlessTunneled;
46
+ exports.maskPostgresUri = maskPostgresUri;
47
+ exports.parseFirstMongoHost = parseFirstMongoHost;
48
+ exports.rewriteMongoUriHost = rewriteMongoUriHost;
49
+ exports.parseFirstPostgresHost = parseFirstPostgresHost;
50
+ exports.baseDeLaUri = baseDeLaUri;
51
+ exports.rewritePostgresUriDatabase = rewritePostgresUriDatabase;
52
+ exports.rewritePostgresUriHost = rewritePostgresUriHost;
53
+ exports.isPrivateAddress = isPrivateAddress;
54
+ exports.assertSafeHttpUrl = assertSafeHttpUrl;
55
+ exports.agentesDeEntrega = agentesDeEntrega;
56
+ exports.despachadorDeSalida = despachadorDeSalida;
57
+ exports.fetchSeguro = fetchSeguro;
58
+ const common_1 = require("@nestjs/common");
59
+ const node_dns_1 = require("node:dns");
60
+ const node_net_1 = require("node:net");
61
+ const http = __importStar(require("node:http"));
62
+ const https = __importStar(require("node:https"));
63
+ const undici_1 = require("undici");
64
+ /**
65
+ * SSRF guard for user-supplied connection URIs (MongoDB, HTTP outbound, etc).
66
+ *
67
+ * Refuses any host that resolves to an internal / private / loopback IP
68
+ * range. Without this, a tenant could point a connection string at our
69
+ * own infra (`mongodb://localhost:27017/admin`, the metadata service IP
70
+ * `169.254.169.254` on cloud VMs, an internal `10.0.x.y` Mongo, etc.)
71
+ * and use the credential decrypt + connect path to read or write into
72
+ * resources that don't belong to them. We also re-resolve at runtime
73
+ * so a DNS-rebinding attack — host that resolves to a public IP at
74
+ * save time and a private one at use time — gets caught.
75
+ */
76
+ const PRIVATE_V4_RANGES = [
77
+ /^10\./,
78
+ /^127\./,
79
+ /^169\.254\./,
80
+ /^172\.(1[6-9]|2\d|3[0-1])\./,
81
+ /^192\.168\./,
82
+ /^0\./,
83
+ ];
84
+ /**
85
+ * The IPv6 ranges that must never be a connection target, as real CIDRs.
86
+ *
87
+ * This used to be a list of string prefixes — `['::1', 'fc', 'fd', 'fe80']`
88
+ * matched with `startsWith`. That let two things through. Link-local is
89
+ * fe80::/10, i.e. fe80 through febf, so every `fe90:`/`fea0:`/`feb0:`
90
+ * address read as public. And any spelling that did not literally begin
91
+ * with those characters missed entirely — `0:0:0:0:0:0:0:1` is the expanded
92
+ * form of `::1` and passed the check.
93
+ */
94
+ const PRIVATE_V6_CIDRS = [
95
+ ['::', 128], // unspecified — routes to localhost on most stacks
96
+ ['::1', 128], // loopback
97
+ ['fc00::', 7], // unique local (covers the old 'fc' and 'fd' prefixes)
98
+ ['fe80::', 10], // link-local
99
+ ];
100
+ const BLOCKED_HOSTNAMES = new Set([
101
+ 'localhost',
102
+ 'localhost.localdomain',
103
+ 'ip6-localhost',
104
+ 'ip6-loopback',
105
+ 'metadata.google.internal',
106
+ 'metadata',
107
+ ]);
108
+ /**
109
+ * Expand any IPv6 spelling to its 128-bit value, so ranges can be tested by
110
+ * containment instead of by how the address happens to be written.
111
+ * Returns null when the text is not a well-formed IPv6 literal.
112
+ */
113
+ function ipv6ToBigInt(ip) {
114
+ let s = ip.toLowerCase();
115
+ // Zone index (fe80::1%eth0) names an interface, not part of the address.
116
+ const zone = s.indexOf('%');
117
+ if (zone >= 0)
118
+ s = s.slice(0, zone);
119
+ // A trailing dotted-quad is the low 32 bits written in IPv4 notation.
120
+ const v4 = /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(s);
121
+ if (v4) {
122
+ const octets = v4[1].split('.').map(Number);
123
+ if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255))
124
+ return null;
125
+ const hi = ((octets[0] << 8) | octets[1]).toString(16);
126
+ const lo = ((octets[2] << 8) | octets[3]).toString(16);
127
+ s = s.slice(0, v4.index) + hi + ':' + lo;
128
+ }
129
+ const halves = s.split('::');
130
+ if (halves.length > 2)
131
+ return null;
132
+ const head = halves[0] ? halves[0].split(':') : [];
133
+ const tail = halves.length === 2 && halves[1] ? halves[1].split(':') : [];
134
+ let groups;
135
+ if (halves.length === 2) {
136
+ const missing = 8 - head.length - tail.length;
137
+ if (missing < 0)
138
+ return null;
139
+ groups = [...head, ...Array(missing).fill('0'), ...tail];
140
+ }
141
+ else {
142
+ if (head.length !== 8)
143
+ return null;
144
+ groups = head;
145
+ }
146
+ let out = 0n;
147
+ for (const g of groups) {
148
+ if (!/^[0-9a-f]{1,4}$/.test(g))
149
+ return null;
150
+ out = (out << 16n) | BigInt(parseInt(g, 16));
151
+ }
152
+ return out;
153
+ }
154
+ /** Is `addr` inside `base/bits`? */
155
+ function inV6Cidr(addr, base, bits) {
156
+ const baseInt = ipv6ToBigInt(base);
157
+ if (baseInt === null)
158
+ return false;
159
+ const mask = bits === 0 ? 0n : ((1n << BigInt(bits)) - 1n) << BigInt(128 - bits);
160
+ return (addr & mask) === (baseInt & mask);
161
+ }
162
+ function isPrivateV4(ip) {
163
+ return PRIVATE_V4_RANGES.some((re) => re.test(ip));
164
+ }
165
+ function intToIpv4(n) {
166
+ return [
167
+ (n >>> 24) & 0xff,
168
+ (n >>> 16) & 0xff,
169
+ (n >>> 8) & 0xff,
170
+ n & 0xff,
171
+ ].join('.');
172
+ }
173
+ function isPrivateIp(ip) {
174
+ const family = (0, node_net_1.isIP)(ip);
175
+ if (family === 4)
176
+ return isPrivateV4(ip);
177
+ if (family !== 6)
178
+ return false;
179
+ const addr = ipv6ToBigInt(ip);
180
+ // node's isIP accepted it but this parser did not. Refuse rather than
181
+ // treat an address we cannot read as safe.
182
+ if (addr === null)
183
+ return true;
184
+ // IPv4-mapped (::ffff:a.b.c.d) and IPv4-compatible (::a.b.c.d) carry an
185
+ // IPv4 address in their low 32 bits and reach exactly that address, so
186
+ // they have to face the IPv4 test. This is done on the parsed value, not
187
+ // on the text: `::ffff:127.0.0.1` and `::ffff:7f00:1` are the same
188
+ // address, and node's URL parser hands back the second spelling — so
189
+ // unwrapping by regex would have missed the very form the platform sees.
190
+ const high96 = addr >> 32n;
191
+ const low32 = Number(addr & 0xffffffffn);
192
+ // `::` and `::1` sit in ::/96 too, but they are addresses in their own
193
+ // right rather than embedded IPv4 — the CIDR list below owns them.
194
+ if (high96 === 0xffffn || (high96 === 0n && low32 > 1)) {
195
+ return isPrivateV4(intToIpv4(low32));
196
+ }
197
+ return PRIVATE_V6_CIDRS.some(([base, bits]) => inV6Cidr(addr, base, bits));
198
+ }
199
+ /**
200
+ * Parse the bare hostnames + the SRV flag out of a `mongodb://` or
201
+ * `mongodb+srv://` URI. Returns array of hostnames (no port) plus a
202
+ * boolean telling the caller whether to use SRV-style DNS lookup.
203
+ *
204
+ * `mongodb+srv://` URIs name a SINGLE hostname that itself has no
205
+ * A/AAAA record — Atlas-style. The SRV record at
206
+ * `_mongodb._tcp.<host>` returns the actual cluster nodes that we
207
+ * have to validate. Legacy `mongodb://` URIs may have a comma-
208
+ * separated host list for replica sets.
209
+ */
210
+ function extractMongoHosts(uri) {
211
+ const match = /^mongodb(\+srv)?:\/\/(?:[^@/]*@)?([^/?]+)/i.exec(uri);
212
+ if (!match)
213
+ throw new common_1.ForbiddenException('Connection string is not a valid mongodb:// URI');
214
+ const srv = !!match[1];
215
+ const hostList = match[2];
216
+ const hosts = hostList
217
+ .split(',')
218
+ .map((h) => h.trim())
219
+ .map((h) => {
220
+ // Bracket-aware: `[::1]:27017` is an IPv6 literal plus a port, and the
221
+ // brackets are what say where the address ends. Strip the port only
222
+ // once they are gone — and never from a bare IPv6 literal, or `::1`
223
+ // would lose its last group and stop looking like an address at all.
224
+ const bracketed = /^\[([^\]]+)\](?::\d+)?$/.exec(h);
225
+ if (bracketed)
226
+ return bracketed[1];
227
+ if ((0, node_net_1.isIP)(h) === 6)
228
+ return h;
229
+ return h.replace(/:\d+$/, '');
230
+ })
231
+ .filter(Boolean);
232
+ return { hosts, srv };
233
+ }
234
+ /**
235
+ * Resolve a hostname to its real cluster nodes. For `mongodb+srv://`
236
+ * the host itself (e.g. `cluster0.abc123.mongodb.net`) typically has
237
+ * NO A record — Atlas only publishes the SRV at
238
+ * `_mongodb._tcp.<host>`. Look that up first; fall back to a direct
239
+ * A/AAAA lookup on the bare hostname for legacy `mongodb://` URIs and
240
+ * self-hosted clusters that publish SRV anyway.
241
+ */
242
+ async function resolveMongoHost(host, srv) {
243
+ const ips = [];
244
+ if (srv) {
245
+ try {
246
+ const srvRecords = await node_dns_1.promises.resolveSrv(`_mongodb._tcp.${host}`);
247
+ const targetHosts = srvRecords.map((r) => r.name);
248
+ for (const target of targetHosts) {
249
+ const v4 = await node_dns_1.promises.resolve4(target).catch(() => []);
250
+ const v6 = await node_dns_1.promises.resolve6(target).catch(() => []);
251
+ ips.push(...v4, ...v6);
252
+ }
253
+ // SRV worked — return what we got, even if the per-node A/AAAA
254
+ // lookups all failed (mongoose driver will surface a clearer
255
+ // error than us at connect time).
256
+ return ips;
257
+ }
258
+ catch {
259
+ // SRV lookup failed — fall through to direct A/AAAA below. Some
260
+ // self-hosted setups use mongodb+srv:// for the URI shape but
261
+ // don't actually publish SRV records.
262
+ }
263
+ }
264
+ const v4 = await node_dns_1.promises.resolve4(host).catch(() => []);
265
+ const v6 = await node_dns_1.promises.resolve6(host).catch(() => []);
266
+ return [...v4, ...v6];
267
+ }
268
+ /**
269
+ * Throw if the URI's host(s) point at private / loopback / link-local
270
+ * addresses, OR if their DNS resolves there. mongodb+srv URIs are
271
+ * handled via SRV record lookup (the `<host>.mongodb.net` pattern
272
+ * Atlas uses doesn't have A/AAAA records — only the SRV at
273
+ * `_mongodb._tcp.<host>` does).
274
+ */
275
+ async function assertSafeMongoUri(uri) {
276
+ const { hosts, srv } = extractMongoHosts(uri);
277
+ for (const host of hosts) {
278
+ const lowered = host.toLowerCase();
279
+ if (BLOCKED_HOSTNAMES.has(lowered)) {
280
+ throw new common_1.ForbiddenException(`Connection to ${host} is not allowed`);
281
+ }
282
+ // Literal IP in the URI — check directly, no DNS needed.
283
+ if ((0, node_net_1.isIP)(host)) {
284
+ if (isPrivateIp(host)) {
285
+ throw new common_1.ForbiddenException(`Connection to private IP ${host} is not allowed`);
286
+ }
287
+ continue;
288
+ }
289
+ // DNS lookup — guards against rebinding attacks (host resolves to
290
+ // a public IP at save time and a private one when we actually
291
+ // connect later). For mongodb+srv URIs we resolve via SRV first.
292
+ let resolved;
293
+ try {
294
+ resolved = await resolveMongoHost(host, srv);
295
+ }
296
+ catch {
297
+ throw new common_1.ForbiddenException(`Could not resolve host ${host}`);
298
+ }
299
+ if (resolved.length === 0) {
300
+ throw new common_1.ForbiddenException(`Could not resolve host ${host}${srv ? ' (no SRV record at _mongodb._tcp.' + host + ' and no A record on the host either)' : ''}`);
301
+ }
302
+ for (const ip of resolved) {
303
+ if (isPrivateIp(ip)) {
304
+ throw new common_1.ForbiddenException(`Host ${host} resolves to private IP ${ip} — refusing to connect`);
305
+ }
306
+ }
307
+ }
308
+ }
309
+ /** Mask the credentials portion of a Mongo URI for logs / UI. */
310
+ function maskMongoUri(uri) {
311
+ return uri.replace(/^(mongodb(?:\+srv)?:\/\/)([^:]+):([^@]+)@/i, '$1$2:***@');
312
+ }
313
+ /**
314
+ * Tunnel-aware variant: when `tunnelId` is set, the connection is going to
315
+ * route through a customer-installed agent (`hostwh expose-tcp`), so the
316
+ * URI's host is the customer's local network — `localhost` / `127.0.0.1` /
317
+ * `192.168.x.x` are all expected and safe (they're THEIR localhost, not
318
+ * ours). Skip the SSRF check entirely.
319
+ *
320
+ * Without a tunnel, behave exactly like assertSafeMongoUri.
321
+ */
322
+ async function assertSafeMongoUriUnlessTunneled(uri, tunnelId) {
323
+ if (tunnelId)
324
+ return;
325
+ await assertSafeMongoUri(uri);
326
+ }
327
+ /**
328
+ * Parse the host out of a `postgres://` or `postgresql://` URI. Postgres
329
+ * URIs are simpler than mongo — single host, optional port, no SRV.
330
+ */
331
+ function extractPostgresHost(uri) {
332
+ let url;
333
+ try {
334
+ url = new URL(uri);
335
+ }
336
+ catch {
337
+ throw new common_1.ForbiddenException('Connection string is not a valid postgres:// URI');
338
+ }
339
+ if (url.protocol !== 'postgres:' && url.protocol !== 'postgresql:') {
340
+ throw new common_1.ForbiddenException('Connection string is not a valid postgres:// URI');
341
+ }
342
+ if (!url.hostname) {
343
+ throw new common_1.ForbiddenException('Connection string is missing a host');
344
+ }
345
+ // WHATWG URL keeps the brackets on an IPv6 host (`[::1]`), and with them
346
+ // the string is not an IP literal any more — it fell through to the DNS
347
+ // path, where it could only ever fail to resolve. Strip them so an IPv6
348
+ // postgres host is actually classified.
349
+ return url.hostname.replace(/^\[(.+)\]$/, '$1');
350
+ }
351
+ /**
352
+ * Throw if the postgres URI's host points at private / loopback /
353
+ * link-local addresses, OR if its DNS resolves there. Mirrors
354
+ * assertSafeMongoUri but with the simpler postgres URI grammar.
355
+ */
356
+ async function assertSafePostgresUri(uri) {
357
+ const host = extractPostgresHost(uri);
358
+ const lowered = host.toLowerCase();
359
+ if (BLOCKED_HOSTNAMES.has(lowered)) {
360
+ throw new common_1.ForbiddenException(`Connection to ${host} is not allowed`);
361
+ }
362
+ if ((0, node_net_1.isIP)(host)) {
363
+ if (isPrivateIp(host)) {
364
+ throw new common_1.ForbiddenException(`Connection to private IP ${host} is not allowed`);
365
+ }
366
+ return;
367
+ }
368
+ let resolved;
369
+ try {
370
+ const v4 = await node_dns_1.promises.resolve4(host).catch(() => []);
371
+ const v6 = await node_dns_1.promises.resolve6(host).catch(() => []);
372
+ resolved = [...v4, ...v6];
373
+ }
374
+ catch {
375
+ throw new common_1.ForbiddenException(`Could not resolve host ${host}`);
376
+ }
377
+ if (resolved.length === 0) {
378
+ throw new common_1.ForbiddenException(`Could not resolve host ${host}`);
379
+ }
380
+ for (const ip of resolved) {
381
+ if (isPrivateIp(ip)) {
382
+ throw new common_1.ForbiddenException(`Host ${host} resolves to private IP ${ip} — refusing to connect`);
383
+ }
384
+ }
385
+ }
386
+ /**
387
+ * Tunnel-aware variant for postgres — same semantics as the mongo one.
388
+ * When tunnelId is set, the URI describes the customer's own network
389
+ * reachable through their `hostwh expose --tcp` agent, so private IPs
390
+ * are intentional and we skip the SSRF check.
391
+ */
392
+ async function assertSafePostgresUriUnlessTunneled(uri, tunnelId) {
393
+ if (tunnelId)
394
+ return;
395
+ await assertSafePostgresUri(uri);
396
+ }
397
+ exports.URI_GUARD_FAMILY = {
398
+ mongodb: 'mongo',
399
+ memory_mongodb: 'mongo',
400
+ postgres: 'postgres',
401
+ };
402
+ /** True when this credential type carries a connection string we must guard. */
403
+ function isGuardedUriCredential(type) {
404
+ return type in exports.URI_GUARD_FAMILY;
405
+ }
406
+ /**
407
+ * Los esquemas que puede llevar la URI de cada familia.
408
+ *
409
+ * Va aquí, junto a `URI_GUARD_FAMILY`, y no en un `if` suelto dentro del
410
+ * servicio: el tipo `Record<UriGuardFamily, …>` obliga a que una familia
411
+ * nueva declare los suyos o no compila, que es la misma disciplina que ya
412
+ * aplica el registro de guardias.
413
+ */
414
+ exports.URI_SCHEMES = {
415
+ mongo: ['mongodb:', 'mongodb+srv:'],
416
+ postgres: ['postgres:', 'postgresql:'],
417
+ };
418
+ /** Cómo se llama cada familia cuando hay que decírselo a una persona. */
419
+ const NOMBRE_DE_FAMILIA = {
420
+ mongo: 'MongoDB',
421
+ postgres: 'PostgreSQL',
422
+ };
423
+ /**
424
+ * El esquema tal y como está escrito, sin construir un `URL`.
425
+ *
426
+ * Se lee a mano a propósito: `new URL()` acepta cosas que aquí no queremos
427
+ * dar por buenas y, sobre todo, LANZA con una cadena sin esquema, y ese
428
+ * fallo hay que contarlo con nuestras palabras y no con las del motor.
429
+ */
430
+ function esquemaDe(uri) {
431
+ /* Se exige el `://` entero, no sólo los dos puntos: sin eso, `localhost:27017`
432
+ hacía pasar `localhost` por esquema y el error hablaba de un inexistente
433
+ `localhost://` en vez de decir que ahí falta el esquema. Lo cazó su test. */
434
+ const encontrado = /^([a-zA-Z][a-zA-Z0-9+.-]*):[/][/]/.exec(uri.trim());
435
+ return encontrado ? `${encontrado[1].toLowerCase()}:` : null;
436
+ }
437
+ /**
438
+ * Que la cadena de conexión sea del producto que dice el tipo de credencial.
439
+ *
440
+ * Faltaba, y no se notaba porque los dos sitios que miraban la URI se
441
+ * rinden antes en el caso más común:
442
+ *
443
+ * - el guardia SSRF vuelve en cuanto la credencial lleva `tunnelId`, porque
444
+ * entonces la URI describe la red del propio cliente;
445
+ * - la comprobación de la base de Mongo vuelve si no se tecleó ninguna.
446
+ *
447
+ * Con las dos condiciones a la vez —una credencial con túnel y sin base, que
448
+ * es justo lo que sale del flujo de túneles— NADIE llegaba a mirar la cadena.
449
+ * Una `postgres://` se guardaba como MongoDB sin una palabra, y el error
450
+ * aparecía mucho después dentro de un nodo, en boca del driver:
451
+ * «Connection string is not a valid mongodb:// URI», que no nombra lo único
452
+ * que hacía falta saber: que eso era de Postgres.
453
+ *
454
+ * Por eso esto NO mira el túnel ni la base: corre siempre.
455
+ */
456
+ function assertUriSchemeMatchesType(type, uri) {
457
+ const familia = exports.URI_GUARD_FAMILY[type];
458
+ if (!familia)
459
+ return;
460
+ const esperados = exports.URI_SCHEMES[familia];
461
+ const comoSeEscribe = esperados.map((e) => `${e}//`).join(' or ');
462
+ const esquema = esquemaDe(uri);
463
+ if (!esquema) {
464
+ throw new common_1.BadRequestException(`That connection string has no scheme — a ${NOMBRE_DE_FAMILIA[familia]} credential starts with ${comoSeEscribe}`);
465
+ }
466
+ if (esperados.includes(esquema))
467
+ return;
468
+ /* Si el esquema es de otra familia que conocemos, se dice CUÁL y a dónde
469
+ llevarlo. Un «no vale» a secas deja a quien lo pegó buscando el error en
470
+ la contraseña o en el host, que es donde nadie lo tenía. */
471
+ const otra = Object.keys(exports.URI_SCHEMES).find((f) => f !== familia && exports.URI_SCHEMES[f].includes(esquema));
472
+ if (otra) {
473
+ throw new common_1.BadRequestException(`That is a ${NOMBRE_DE_FAMILIA[otra]} connection string (${esquema}//), but this credential is ${NOMBRE_DE_FAMILIA[familia]}. Create it as a ${NOMBRE_DE_FAMILIA[otra]} credential instead.`);
474
+ }
475
+ throw new common_1.BadRequestException(`"${esquema}//" is not a ${NOMBRE_DE_FAMILIA[familia]} connection string — expected ${comoSeEscribe}`);
476
+ }
477
+ /** Apply the right guard for a credential type. No-op for unguarded types. */
478
+ async function assertSafeCredentialUri(type, uri) {
479
+ const family = exports.URI_GUARD_FAMILY[type];
480
+ if (!family)
481
+ return;
482
+ if (family === 'mongo')
483
+ await assertSafeMongoUri(uri);
484
+ else
485
+ await assertSafePostgresUri(uri);
486
+ }
487
+ /** As above, but a tunnelled credential describes the customer's own network. */
488
+ async function assertSafeCredentialUriUnlessTunneled(type, uri, tunnelId) {
489
+ if (tunnelId)
490
+ return;
491
+ await assertSafeCredentialUri(type, uri);
492
+ }
493
+ /** Mask the password portion of a postgres URI for logs / UI. */
494
+ function maskPostgresUri(uri) {
495
+ return uri.replace(/^(postgres(?:ql)?:\/\/)([^:]+):([^@]+)@/i, '$1$2:***@');
496
+ }
497
+ /**
498
+ * Parse the first host:port pair out of a `mongodb://` or
499
+ * `mongodb+srv://` URI. Used by the TCP forwarder + URI rewriter to
500
+ * know what target to register and what host:port to substitute with
501
+ * the local forwarder address.
502
+ *
503
+ * Returns the bare host (no port) plus the explicit port — defaulting
504
+ * to 27017 for `mongodb://` and to a synthetic 27017 for `mongodb+srv://`
505
+ * (SRV resolution is bypassed entirely when tunnelled, the agent talks
506
+ * to its target directly).
507
+ */
508
+ function parseFirstMongoHost(uri) {
509
+ const match = /^mongodb(\+srv)?:\/\/(?:[^@/]*@)?([^/?]+)/i.exec(uri);
510
+ if (!match)
511
+ throw new Error('Connection string is not a valid mongodb:// URI');
512
+ const isSrv = !!match[1];
513
+ const firstHost = match[2]
514
+ .split(',')[0]
515
+ .trim()
516
+ .replace(/^\[/, '')
517
+ .replace(/\].*$/, '');
518
+ const portIdx = firstHost.lastIndexOf(':');
519
+ if (portIdx > 0 && /^\d+$/.test(firstHost.slice(portIdx + 1))) {
520
+ return {
521
+ host: firstHost.slice(0, portIdx),
522
+ port: Number(firstHost.slice(portIdx + 1)),
523
+ };
524
+ }
525
+ return { host: firstHost, port: isSrv ? 27017 : 27017 };
526
+ }
527
+ /**
528
+ * Replace the host(s) section of a Mongo URI with a single host:port.
529
+ * Preserves user:pass, path (database), and querystring. Forces the
530
+ * scheme to `mongodb://` (drops `+srv`) because the rewritten URI
531
+ * points at our local forwarder, not an SRV-resolvable hostname.
532
+ *
533
+ * Also drops the `tls`, `ssl`, and `replicaSet` query params — they
534
+ * would cause the driver to negotiate TLS with our local plaintext
535
+ * forwarder. The agent talks to its real Atlas with TLS on its end.
536
+ */
537
+ function rewriteMongoUriHost(uri, host, port) {
538
+ // Match: scheme://[user:pass@]hosts[/db?query]
539
+ const m = /^mongodb(?:\+srv)?:\/\/(?:([^@/]*)@)?([^/?]+)(\/[^?]*)?(\?.*)?$/i.exec(uri);
540
+ if (!m)
541
+ throw new Error('Connection string is not a valid mongodb:// URI');
542
+ const userInfo = m[1];
543
+ const path = m[3] ?? '';
544
+ let query = m[4] ?? '';
545
+ if (query) {
546
+ // Drop tls/ssl/replicaSet — incompatible with our plaintext forwarder.
547
+ const params = new URLSearchParams(query.slice(1));
548
+ for (const k of [
549
+ 'tls',
550
+ 'ssl',
551
+ 'replicaSet',
552
+ 'tlsCAFile',
553
+ 'tlsCertificateKeyFile',
554
+ ]) {
555
+ params.delete(k);
556
+ }
557
+ const remaining = params.toString();
558
+ query = remaining ? `?${remaining}` : '';
559
+ }
560
+ const auth = userInfo ? `${userInfo}@` : '';
561
+ return `mongodb://${auth}${host}:${port}${path}${query}`;
562
+ }
563
+ /**
564
+ * Parse the first host:port pair out of a `postgres://` /
565
+ * `postgresql://` URI. Same role as `parseFirstMongoHost` — the TCP
566
+ * forwarder needs to know what host:port to register against the
567
+ * tunnel, and the URI rewriter needs to know what to swap out.
568
+ *
569
+ * Postgres URIs only ever name one host (no comma list, no `+srv`
570
+ * sibling), but we still parse the userinfo correctly so passwords
571
+ * containing `@` (URL-encoded as `%40`) don't get treated as host
572
+ * delimiters.
573
+ */
574
+ function parseFirstPostgresHost(uri) {
575
+ const match = /^postgres(?:ql)?:\/\/(?:[^@/]*@)?([^/?]+)/i.exec(uri);
576
+ if (!match)
577
+ throw new Error('Connection string is not a valid postgres:// URI');
578
+ const firstHost = match[1].trim().replace(/^\[/, '').replace(/\].*$/, '');
579
+ const portIdx = firstHost.lastIndexOf(':');
580
+ if (portIdx > 0 && /^\d+$/.test(firstHost.slice(portIdx + 1))) {
581
+ return {
582
+ host: firstHost.slice(0, portIdx),
583
+ port: Number(firstHost.slice(portIdx + 1)),
584
+ };
585
+ }
586
+ return { host: firstHost, port: 5432 };
587
+ }
588
+ /**
589
+ * Replace the host:port of a Postgres URI with the local forwarder
590
+ * address. Same idea as `rewriteMongoUriHost`: the local forwarder is
591
+ * plaintext (the agent restores TLS on its end of the wire), so any
592
+ * SSL-related query params would cause the driver to negotiate TLS
593
+ * against the wrong webhook and fail. Drop them.
594
+ *
595
+ * Preserves userinfo + path (the database name lives in the path) +
596
+ * remaining querystring. Also forces scheme to `postgres://` (drops
597
+ * `postgresql://` to one canonical form).
598
+ */
599
+ /**
600
+ * Qué base declara una URI de Postgres en su ruta, o `null` si no declara
601
+ * ninguna —en ese caso el driver cae a la base con el nombre del usuario—.
602
+ *
603
+ * Se decodifica porque `rewritePostgresUriDatabase` codifica al escribir: sin
604
+ * esto, comparar «lo que había» con «lo que se pide» diría que `mi base` y
605
+ * `mi%20base` son distintas y registraría un cambio que no existe.
606
+ */
607
+ function baseDeLaUri(uri) {
608
+ const m = /^postgres(?:ql)?:\/\/(?:[^@/]*@)?[^/?]+\/([^?]*)/i.exec(uri);
609
+ if (!m || !m[1])
610
+ return null;
611
+ try {
612
+ return decodeURIComponent(m[1]) || null;
613
+ }
614
+ catch {
615
+ return m[1] || null;
616
+ }
617
+ }
618
+ /**
619
+ * Cambiar la base de datos de una URI de Postgres — la que va en la ruta.
620
+ *
621
+ * En Mongo la base se elige con `client.db(nombre)` sobre una conexión ya
622
+ * abierta. En Postgres **no existe eso**: la base se decide al conectarse y
623
+ * viene en la URI, así que respetar el `databaseName` de un nodo obliga a
624
+ * reescribirla aquí. Ésa es la razón de que `databaseName` estuviera muerto en
625
+ * `postgres-actions.service.ts` — se calculaba y no se podía aplicar sin esto.
626
+ *
627
+ * Se conserva el esquema tal cual llegó (`postgres://` o `postgresql://`), el
628
+ * userinfo, el host y el querystring; sólo cambia la ruta. El nombre se
629
+ * codifica porque una base puede llevar caracteres que en una URI significan
630
+ * otra cosa, y sin codificar `mi base` o `a/b` producirían una URI distinta de
631
+ * la que se pidió.
632
+ *
633
+ * Con `database` vacío devuelve la URI intacta: quien llama decide si hay algo
634
+ * que aplicar.
635
+ */
636
+ function rewritePostgresUriDatabase(uri, database) {
637
+ const nombre = (database ?? '').trim();
638
+ if (!nombre)
639
+ return uri;
640
+ const m = /^(postgres(?:ql)?):\/\/(?:([^@/]*)@)?([^/?]+)(\/[^?]*)?(\?.*)?$/i.exec(uri);
641
+ if (!m)
642
+ throw new Error('Connection string is not a valid postgres:// URI');
643
+ const esquema = m[1];
644
+ const auth = m[2] ? `${m[2]}@` : '';
645
+ const hostPuerto = m[3];
646
+ const query = m[5] ?? '';
647
+ return `${esquema}://${auth}${hostPuerto}/${encodeURIComponent(nombre)}${query}`;
648
+ }
649
+ function rewritePostgresUriHost(uri, host, port) {
650
+ const m = /^postgres(?:ql)?:\/\/(?:([^@/]*)@)?([^/?]+)(\/[^?]*)?(\?.*)?$/i.exec(uri);
651
+ if (!m)
652
+ throw new Error('Connection string is not a valid postgres:// URI');
653
+ const userInfo = m[1];
654
+ const path = m[3] ?? '';
655
+ let query = m[4] ?? '';
656
+ if (query) {
657
+ const params = new URLSearchParams(query.slice(1));
658
+ for (const k of [
659
+ 'sslmode',
660
+ 'sslrootcert',
661
+ 'sslcert',
662
+ 'sslkey',
663
+ 'sslpassword',
664
+ 'sslcrl',
665
+ 'sslcrldir',
666
+ ]) {
667
+ params.delete(k);
668
+ }
669
+ const remaining = params.toString();
670
+ query = remaining ? `?${remaining}` : '';
671
+ }
672
+ const auth = userInfo ? `${userInfo}@` : '';
673
+ return `postgres://${auth}${host}:${port}${path}${query}`;
674
+ }
675
+ /* ── Salida HTTP ────────────────────────────────────────────────────────── */
676
+ /**
677
+ * El guard para las URLs de entrega, que hasta ahora no tenía ninguno.
678
+ *
679
+ * ## El fallo que cierra
680
+ *
681
+ * `webhook.targetUrl` llegaba al POST saliente sin tocar. Su única validación
682
+ * era `@IsUrl({ require_tld: false })`, y el validador instalado acepta una IP
683
+ * pelada sin lista negra, así que loopback, link-local y privadas pasaban.
684
+ * `fallbackUrls` llevaba sólo `@IsString({ each: true })` y llegaba al mismo
685
+ * sitio sin ninguna comprobación de URL. Este fichero existía, pero estaba
686
+ * cableado únicamente a las rutas de credenciales Mongo y Postgres.
687
+ *
688
+ * Con `validateStatus: () => true` la respuesta interna se conservaba, se
689
+ * persistía en el registro de entrega y —en modo `sync`— volvía inline a quien
690
+ * publicó. Eso es descubrimiento de red interna con lectura de vuelta, desde
691
+ * una cuenta de alta libre.
692
+ *
693
+ * ## Por qué se puede bloquear sin romper a nadie
694
+ *
695
+ * La entrega a la red local del cliente ya tiene su propio camino: el
696
+ * procesador comprueba `hasTunnel(webhookId)` y entrega por el túnel; el POST
697
+ * directo es la rama `else`. Así que el destino privado legítimo va por
698
+ * `hostwh expose` y no por aquí.
699
+ */
700
+ /** Sólo estos esquemas. `file:`, `gopher:` y compañía no son destinos de
701
+ * entrega, y algunos son primitivas de lectura local. */
702
+ const ESQUEMAS_DE_ENTREGA = new Set(['http:', 'https:']);
703
+ /** Igual que `isPrivateIp`, expuesto para quien tenga ya la IP resuelta. */
704
+ function isPrivateAddress(ip) {
705
+ return isPrivateIp(ip);
706
+ }
707
+ /**
708
+ * Rechaza una URL de entrega que apunte adentro.
709
+ *
710
+ * Comprueba esquema, nombres bloqueados, IP literal y —cuando es un nombre— lo
711
+ * que resuelve AHORA. Esa resolución es la foto previa; lo que de verdad ata la
712
+ * dirección comprobada a la dirección marcada es el hook de `agentesDeEntrega`,
713
+ * porque entre esta consulta y el socket cabe un cambio de DNS.
714
+ */
715
+ async function assertSafeHttpUrl(rawUrl) {
716
+ let url;
717
+ try {
718
+ url = new URL(rawUrl);
719
+ }
720
+ catch {
721
+ throw new common_1.ForbiddenException(`Delivery URL is not a valid URL: ${rawUrl}`);
722
+ }
723
+ if (!ESQUEMAS_DE_ENTREGA.has(url.protocol)) {
724
+ throw new common_1.ForbiddenException(`Delivery URL scheme "${url.protocol}" is not allowed — use http or https`);
725
+ }
726
+ // WHATWG URL conserva los corchetes de un host IPv6; con ellos la cadena no
727
+ // es una IP literal y caería al camino de DNS.
728
+ const host = url.hostname.replace(/^\[(.+)\]$/, '$1');
729
+ if (!host)
730
+ throw new common_1.ForbiddenException('Delivery URL is missing a host');
731
+ if (BLOCKED_HOSTNAMES.has(host.toLowerCase())) {
732
+ throw new common_1.ForbiddenException(`Delivery to ${host} is not allowed`);
733
+ }
734
+ if ((0, node_net_1.isIP)(host)) {
735
+ if (isPrivateIp(host)) {
736
+ throw new common_1.ForbiddenException(`Delivery to private IP ${host} is not allowed`);
737
+ }
738
+ return;
739
+ }
740
+ const v4 = await node_dns_1.promises.resolve4(host).catch(() => []);
741
+ const v6 = await node_dns_1.promises.resolve6(host).catch(() => []);
742
+ const resueltas = [...v4, ...v6];
743
+ if (resueltas.length === 0) {
744
+ throw new common_1.ForbiddenException(`Could not resolve delivery host ${host}`);
745
+ }
746
+ for (const ip of resueltas) {
747
+ if (isPrivateIp(ip)) {
748
+ throw new common_1.ForbiddenException(`Delivery host ${host} resolves to private IP ${ip} — refusing to connect`);
749
+ }
750
+ }
751
+ }
752
+ /**
753
+ * Agentes cuyo `lookup` vuelve a comprobar cada dirección resuelta.
754
+ *
755
+ * Ésta es la parte que cierra el rebinding: entre la comprobación de arriba y
756
+ * el socket cabe un cambio de DNS, así que la dirección se valida **en el
757
+ * momento de marcarla**, dentro del propio `lookup`.
758
+ *
759
+ * OJO con lo que este `lookup` NO cubre: sólo corre cuando hay un nombre que
760
+ * resolver. Si el host ya es una IP literal, Node se salta el DNS y esto no
761
+ * llega a ejecutarse — de ahí `conectorQueRechazaPrivadas`, más abajo, que
762
+ * mira todas las conexiones. Aquí decía que valía «para cada salto de una
763
+ * redirección»; no era verdad, y por ahí se colaba un 302 hacia una literal.
764
+ *
765
+ * Se comprueba aquí y no resolviendo a mano para conectar a la IP: conectar a
766
+ * la IP obliga a rehacer SNI y la cabecera Host, y eso rompe cualquier destino
767
+ * en hosting compartido o detrás de un CDN.
768
+ */
769
+ function lookupQueRechazaPrivadas(hostname, options, callback) {
770
+ node_dns_1.lookup(hostname, options, (err, address, family) => {
771
+ if (err)
772
+ return callback(err, address, family);
773
+ const direcciones = Array.isArray(address)
774
+ ? address.map((a) => a.address)
775
+ : [String(address)];
776
+ const adentro = direcciones.find((ip) => isPrivateIp(ip));
777
+ if (adentro) {
778
+ return callback(Object.assign(new Error(`refusing to connect to private address ${adentro} for ${hostname}`), { code: 'ESSRFBLOCKED' }), address, family);
779
+ }
780
+ callback(null, address, family);
781
+ });
782
+ }
783
+ /**
784
+ * Mira el host de UNA conexión, y lanza si es una privada literal.
785
+ *
786
+ * ## El agujero que tapa, que es el mismo que ya tenía `fetch`
787
+ *
788
+ * `lookupQueRechazaPrivadas` sólo corre cuando hay un nombre que resolver. Si
789
+ * el host ya es una IP literal, Node se salta el DNS entero y el `lookup` no
790
+ * llega a ejecutarse. Para `fetch` eso lo cierra `conectorQueRechazaPrivadas`
791
+ * (undici), y desde entonces este fichero decía que estaba cubierto — pero
792
+ * **sólo para undici**. Los agentes de `node:http`, que son los que usan axios
793
+ * y por tanto el nodo HTTP y el procesador de entregas, se quedaron sin
794
+ * equivalente.
795
+ *
796
+ * Por ahí entra lo de siempre:
797
+ *
798
+ * el atacante pide https://loquesea.suyo/x (público, pasa el chequeo)
799
+ * su servidor responde 302 Location: http://169.254.169.254/latest/...
800
+ *
801
+ * y ese salto abre su propia conexión, contra una literal, sin DNS. También
802
+ * entra un endpoint escrito a mano que YA sea una IP privada en cualquier
803
+ * cliente que use estos agentes sin llamar antes a `assertSafeHttpUrl`.
804
+ *
805
+ * ## Por qué envolver `createConnection` y no pasar una opción
806
+ *
807
+ * `http.Agent` no tiene un gancho de «antes de conectar»: `lookup` es lo más
808
+ * parecido y ya vimos que no basta. `createConnection` es el único punto por
809
+ * el que pasan TODAS las conexiones del agente, con o sin nombre, y es
810
+ * público y documentado. Se envuelve el del prototipo en vez de reescribirlo
811
+ * para no perder el manejo de sesiones TLS de `https.Agent`.
812
+ */
813
+ function conConectorQueRechazaPrivadas(agente) {
814
+ const original = agente.createConnection.bind(agente);
815
+ agente.createConnection = ((opciones, callback) => {
816
+ // Node conserva los corchetes de un host IPv6; con ellos la cadena no es
817
+ // una IP literal y el `isIP` diría que no. Mismo detalle que en undici.
818
+ const host = String(opciones?.host ?? '').replace(/^\[(.+)\]$/, '$1');
819
+ if ((0, node_net_1.isIP)(host) && isPrivateIp(host)) {
820
+ const fallo = Object.assign(new Error(`Delivery to private IP ${host} is not allowed`), { code: 'ESSRFBLOCKED' });
821
+ /* Por el callback cuando lo hay: es como `Agent.createSocket` espera el
822
+ fallo, y lanzar ahí dentro lo convertiría en una excepción sin
823
+ capturar en vez de en un error de la petición. */
824
+ if (callback) {
825
+ callback(fallo);
826
+ return undefined;
827
+ }
828
+ throw fallo;
829
+ }
830
+ return original(opciones, callback);
831
+ });
832
+ return agente;
833
+ }
834
+ let agentes = null;
835
+ /** Los agentes de salida de la entrega. Se crean una vez: llevan pool de
836
+ * conexiones y uno por petición lo tiraría.
837
+ *
838
+ * Llevan las DOS capas: el `lookup` para los nombres y el conector para todo
839
+ * lo demás. Ninguna sobra — ver `conConectorQueRechazaPrivadas`. */
840
+ function agentesDeEntrega() {
841
+ if (!agentes) {
842
+ const opciones = {
843
+ keepAlive: true,
844
+ lookup: lookupQueRechazaPrivadas,
845
+ };
846
+ agentes = {
847
+ httpAgent: conConectorQueRechazaPrivadas(new http.Agent(opciones)),
848
+ httpsAgent: conConectorQueRechazaPrivadas(new https.Agent(opciones)),
849
+ };
850
+ }
851
+ return agentes;
852
+ }
853
+ /**
854
+ * Lo mismo que `agentesDeEntrega`, pero para `fetch`.
855
+ *
856
+ * `agentesDeEntrega` devuelve agentes de `node:http`, que es lo que usa axios.
857
+ * El `fetch` global no los mira: va por undici, que tiene su propio
858
+ * despachador. Sin esto, los sitios escritos con `fetch` se quedaban fuera del
859
+ * guardia — que es exactamente lo que pasaba, y por eso este fichero llevaba
860
+ * meses cableado sólo al procesador de entregas.
861
+ *
862
+ * El `lookup` es EL MISMO que el de los agentes, así que la regla de qué es
863
+ * una dirección de adentro se escribe una sola vez.
864
+ */
865
+ let despachador = null;
866
+ /**
867
+ * El conector que mira CADA conexión, también cuando el host ya es una IP.
868
+ *
869
+ * El `lookup` de arriba sólo se consulta cuando hay un nombre que resolver:
870
+ * si el host ya es una IP literal, Node se salta el DNS entero y el `lookup`
871
+ * no llega a correr. Eso dejaba abierto el salto de una redirección, que es
872
+ * donde más duele, porque la primera URL sí pasa por `assertSafeHttpUrl` pero
873
+ * las siguientes las elige quien conteste:
874
+ *
875
+ * el atacante pide https://loquesea.suyo/x (público de verdad, pasa)
876
+ * su servidor responde 302 Location: http://169.254.169.254/latest/...
877
+ * y ese salto entraba, porque no hay nombre que resolver.
878
+ *
879
+ * El conector, en cambio, corre una vez por conexión y cada salto abre la
880
+ * suya, así que aquí sí se ve. Para los nombres sigue mandando el `lookup`
881
+ * de siempre — misma regla, un solo sitio donde se escribe.
882
+ */
883
+ const conectorQueRechazaPrivadas = (() => {
884
+ let base = null;
885
+ return (opciones, callback) => {
886
+ base ??= (0, undici_1.buildConnector)({ lookup: lookupQueRechazaPrivadas });
887
+ // undici conserva los corchetes de un host IPv6; sin quitarlos la cadena
888
+ // no es una IP literal y el `isIP` de abajo diría que no.
889
+ const host = String(opciones.hostname ?? '').replace(/^\[(.+)\]$/, '$1');
890
+ if ((0, node_net_1.isIP)(host) && isPrivateIp(host)) {
891
+ callback(Object.assign(new Error(`Delivery to private IP ${host} is not allowed`), { code: 'ESSRFBLOCKED' }), null);
892
+ return;
893
+ }
894
+ return base(opciones, callback);
895
+ };
896
+ })();
897
+ function despachadorDeSalida() {
898
+ if (!despachador) {
899
+ despachador = new undici_1.Agent({ connect: conectorQueRechazaPrivadas });
900
+ }
901
+ return despachador;
902
+ }
903
+ /**
904
+ * `fetch` con el guardia puesto: comprueba la URL antes de salir y resuelve
905
+ * por el despachador que rechaza direcciones privadas, así que un DNS que
906
+ * cambie entre la comprobación y el socket tampoco entra.
907
+ *
908
+ * Es el reemplazo de una palabra para cualquier `fetch` que salga a una URL
909
+ * que haya elegido un inquilino.
910
+ */
911
+ async function fetchSeguro(url, init) {
912
+ await assertSafeHttpUrl(url);
913
+ return fetch(url, {
914
+ ...init,
915
+ dispatcher: despachadorDeSalida(),
916
+ });
917
+ }