@mulmoclaude/core 1.0.1 → 1.2.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.
- package/assets/helps/collection-skills.md +28 -0
- package/dist/artifacts/paths.cjs +68 -0
- package/dist/artifacts/paths.cjs.map +1 -0
- package/dist/artifacts/paths.d.ts +45 -0
- package/dist/artifacts/paths.js +62 -0
- package/dist/artifacts/paths.js.map +1 -0
- package/dist/atomic-C_7YpMiM.cjs +102 -0
- package/dist/atomic-C_7YpMiM.cjs.map +1 -0
- package/dist/atomic-DPpdrJzO.js +89 -0
- package/dist/atomic-DPpdrJzO.js.map +1 -0
- package/dist/collection/core/completion.d.ts +21 -0
- package/dist/collection/core/ids.d.ts +5 -0
- package/dist/collection/core/linkTargets.d.ts +14 -0
- package/dist/collection/core/ownProp.d.ts +9 -0
- package/dist/collection/core/recordKeys.d.ts +14 -0
- package/dist/collection/core/schemaRules.d.ts +167 -0
- package/dist/collection/core/schemaZ.d.ts +489 -9
- package/dist/collection/core/sortValueOf.d.ts +21 -0
- package/dist/collection/core/textSearch.d.ts +12 -0
- package/dist/collection/index.cjs +109 -29
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.d.ts +4 -0
- package/dist/collection/index.js +98 -29
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/registry/server/index.cjs +14 -42
- package/dist/collection/registry/server/index.cjs.map +1 -1
- package/dist/collection/registry/server/index.js +6 -34
- package/dist/collection/registry/server/index.js.map +1 -1
- package/dist/collection/server/discovery.d.ts +7 -0
- package/dist/collection/server/host.d.ts +3 -8
- package/dist/collection/server/index.cjs +10 -9
- package/dist/collection/server/index.d.ts +1 -0
- package/dist/collection/server/index.js +4 -4
- package/dist/collection/server/io.d.ts +0 -88
- package/dist/collection/server/skillAssets.d.ts +90 -0
- package/dist/collection-watchers/config.d.ts +2 -4
- package/dist/collection-watchers/index.cjs +5 -5
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.js +5 -5
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/{discovery-BYIsLqag.js → discovery-DYlEOa9a.js} +426 -392
- package/dist/discovery-DYlEOa9a.js.map +1 -0
- package/dist/{discovery-Dpz6SWwe.cjs → discovery-UxkvBxV-.cjs} +444 -434
- package/dist/discovery-UxkvBxV-.cjs.map +1 -0
- package/dist/dist-Cwk0e12G.js +50 -0
- package/dist/dist-Cwk0e12G.js.map +1 -0
- package/dist/dist-pWpC-b04.cjs +61 -0
- package/dist/dist-pWpC-b04.cjs.map +1 -0
- package/dist/feeds/index.cjs +2 -2
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/server/host.d.ts +5 -8
- package/dist/feeds/server/index.cjs +143 -61
- package/dist/feeds/server/index.cjs.map +1 -1
- package/dist/feeds/server/index.js +141 -59
- package/dist/feeds/server/index.js.map +1 -1
- package/dist/files/atomic.d.ts +34 -0
- package/dist/files/index.cjs +47 -0
- package/dist/files/index.cjs.map +1 -0
- package/dist/files/index.d.ts +3 -0
- package/dist/files/index.js +40 -0
- package/dist/files/index.js.map +1 -0
- package/dist/files/json.d.ts +4 -0
- package/dist/files/safe.d.ts +9 -0
- package/dist/google/auth.d.ts +1 -1
- package/dist/google/collectionDateTime.d.ts +5 -0
- package/dist/google/collectionSync.d.ts +2 -2
- package/dist/google/fsJson.d.ts +3 -2
- package/dist/google/host.d.ts +3 -6
- package/dist/google/index.cjs +64 -91
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.d.ts +1 -0
- package/dist/google/index.js +57 -85
- package/dist/google/index.js.map +1 -1
- package/dist/host/hostSlot.d.ts +27 -0
- package/dist/{ids-DWmHjm17.cjs → ids-BJ7rdrDN.cjs} +17 -1
- package/dist/{ids-DWmHjm17.cjs.map → ids-BJ7rdrDN.cjs.map} +1 -1
- package/dist/{ids-D1M1T6KJ.js → ids-Bv6AOW9Z.js} +12 -2
- package/dist/{ids-D1M1T6KJ.js.map → ids-Bv6AOW9Z.js.map} +1 -1
- package/dist/{ingestTypes-DbuQNuK6.cjs → ingestTypes-CkbM-zrO.cjs} +2 -2
- package/dist/{ingestTypes-DbuQNuK6.cjs.map → ingestTypes-CkbM-zrO.cjs.map} +1 -1
- package/dist/{ingestTypes-Ci4eOgv-.js → ingestTypes-DuCiZqye.js} +2 -2
- package/dist/{ingestTypes-Ci4eOgv-.js.map → ingestTypes-DuCiZqye.js.map} +1 -1
- package/dist/notifier/engine.d.ts +2 -4
- package/dist/notifier/index.cjs +1 -1
- package/dist/notifier/index.js +1 -1
- package/dist/{notifier-ChpY0XrY.js → notifier-BdA5qzhe.js} +14 -16
- package/dist/notifier-BdA5qzhe.js.map +1 -0
- package/dist/{notifier-bS8IEeLA.cjs → notifier-tMsAXyXp.cjs} +14 -16
- package/dist/notifier-tMsAXyXp.cjs.map +1 -0
- package/dist/plugin-vue/fileWatch.d.ts +5 -0
- package/dist/plugin-vue/i18n.cjs +45 -0
- package/dist/plugin-vue/i18n.cjs.map +1 -0
- package/dist/plugin-vue/i18n.js +44 -0
- package/dist/plugin-vue/i18n.js.map +1 -0
- package/dist/plugin-vue/index.cjs +98 -0
- package/dist/plugin-vue/index.cjs.map +1 -0
- package/dist/plugin-vue/index.d.ts +5 -0
- package/dist/plugin-vue/index.js +91 -0
- package/dist/plugin-vue/index.js.map +1 -0
- package/dist/plugin-vue/markdownDoc.d.ts +10 -0
- package/dist/plugin-vue/pluginI18n.d.ts +22 -0
- package/dist/plugin-vue/useClipboardCopy.d.ts +6 -0
- package/dist/plugin-vue/useFileWatch.d.ts +8 -0
- package/dist/plugin-vue/useMarkdownDoc.d.ts +3 -0
- package/dist/{promptSafety-Bugq2kqL.js → promptSafety-BNelBKhh.js} +79 -7
- package/dist/promptSafety-BNelBKhh.js.map +1 -0
- package/dist/{promptSafety-DbE6eZmP.cjs → promptSafety-QAtADXUF.cjs} +108 -6
- package/dist/promptSafety-QAtADXUF.cjs.map +1 -0
- package/dist/remote-host/server/hostRunner.d.ts +2 -1
- package/dist/remote-host/server/index.cjs +6 -5
- package/dist/remote-host/server/index.cjs.map +1 -1
- package/dist/remote-host/server/index.js +3 -2
- package/dist/remote-host/server/index.js.map +1 -1
- package/dist/scheduler/index.cjs +3 -3
- package/dist/scheduler/index.cjs.map +1 -1
- package/dist/scheduler/index.js +2 -2
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/task-manager.d.ts +2 -5
- package/dist/{server-CtCBLYgZ.js → server-CnwgECW-.js} +209 -41
- package/dist/server-CnwgECW-.js.map +1 -0
- package/dist/{server-BjWrcc5T.cjs → server-DFo3zFYB.cjs} +250 -46
- package/dist/server-DFo3zFYB.cjs.map +1 -0
- package/dist/skill-bridge/index.cjs +1 -1
- package/dist/skill-bridge/index.js +1 -1
- package/dist/translation/client.cjs +16 -0
- package/dist/translation/client.cjs.map +1 -1
- package/dist/translation/client.d.ts +6 -0
- package/dist/translation/client.js +16 -1
- package/dist/translation/client.js.map +1 -1
- package/dist/utils/errors.d.ts +2 -1
- package/dist/utils/fetch.cjs +36 -0
- package/dist/utils/fetch.cjs.map +1 -0
- package/dist/{collection/registry/server → utils}/fetch.d.ts +5 -3
- package/dist/utils/fetch.js +34 -0
- package/dist/utils/fetch.js.map +1 -0
- package/dist/utils/index.cjs +11 -3
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.js +9 -1
- package/dist/utils/index.js.map +1 -0
- package/dist/whisper/index.cjs +9 -9
- package/dist/whisper/index.cjs.map +1 -1
- package/dist/whisper/index.js +3 -3
- package/dist/wiki/index.cjs +3 -16
- package/dist/wiki/index.cjs.map +1 -1
- package/dist/wiki/index.js +1 -14
- package/dist/wiki/index.js.map +1 -1
- package/dist/wiki/render.d.ts +2 -4
- package/dist/wiki/server/frontmatter.d.ts +2 -6
- package/dist/wiki/server/index.cjs +4 -40
- package/dist/wiki/server/index.cjs.map +1 -1
- package/dist/wiki/server/index.js +2 -38
- package/dist/wiki/server/index.js.map +1 -1
- package/dist/workspace-setup/index.js +22 -6
- package/dist/workspace-setup/index.js.map +1 -1
- package/package.json +50 -8
- package/dist/collection/server/atomic.d.ts +0 -1
- package/dist/discovery-BYIsLqag.js.map +0 -1
- package/dist/discovery-Dpz6SWwe.cjs.map +0 -1
- package/dist/errors-7P5eMOSX.cjs +0 -30
- package/dist/errors-7P5eMOSX.cjs.map +0 -1
- package/dist/errors-eid6Mes3.js +0 -19
- package/dist/errors-eid6Mes3.js.map +0 -1
- package/dist/google/fetch.d.ts +0 -8
- package/dist/notifier-ChpY0XrY.js.map +0 -1
- package/dist/notifier-bS8IEeLA.cjs.map +0 -1
- package/dist/promptSafety-Bugq2kqL.js.map +0 -1
- package/dist/promptSafety-DbE6eZmP.cjs.map +0 -1
- package/dist/server-BjWrcc5T.cjs.map +0 -1
- package/dist/server-CtCBLYgZ.js.map +0 -1
|
@@ -1,40 +1,34 @@
|
|
|
1
|
-
import { c as
|
|
2
|
-
import {
|
|
3
|
-
import "../../server-
|
|
4
|
-
import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../../ingestTypes-
|
|
1
|
+
import { c as FEED_SCHEDULES, l as INGEST_KINDS } from "../../ids-Bv6AOW9Z.js";
|
|
2
|
+
import { N as listItems, V as resolveDataDir, W as safeSlugName, d as storeFor, et as createForwardingLogger, n as discoverCollections, tt as createHostSlot } from "../../discovery-DYlEOa9a.js";
|
|
3
|
+
import { A as buildCollectionActionSeedPrompt, P as readSkillTemplate, j as promptPathsFor } from "../../server-CnwgECW-.js";
|
|
4
|
+
import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../../ingestTypes-DuCiZqye.js";
|
|
5
5
|
import { FEEDS_DIR, feedDir, feedStatePath, feedsRoot, ingestStateDir, ingestStatePath } from "../paths.js";
|
|
6
|
-
import { d as publish, n as clear } from "../../notifier-
|
|
6
|
+
import { d as publish, n as clear } from "../../notifier-BdA5qzhe.js";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
9
8
|
import { createHash } from "node:crypto";
|
|
9
|
+
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
10
10
|
import { lookup } from "node:dns/promises";
|
|
11
11
|
import { isIP } from "node:net";
|
|
12
12
|
import { XMLParser } from "fast-xml-parser";
|
|
13
13
|
import { MISSED_RUN_POLICIES, SCHEDULE_TYPES } from "@receptron/task-scheduler";
|
|
14
14
|
//#region src/feeds/server/host.ts
|
|
15
|
-
var
|
|
15
|
+
var hostSlot = createHostSlot("@mulmoclaude/core/feeds: configureFeedsHost()");
|
|
16
16
|
/** Wire the feeds engine to a host. Call once at startup, before any refresh. */
|
|
17
17
|
function configureFeedsHost(host) {
|
|
18
|
-
|
|
19
|
-
current = host;
|
|
18
|
+
hostSlot.set(host);
|
|
20
19
|
}
|
|
21
20
|
/** The configured host, or throw if `configureFeedsHost` was never called. */
|
|
22
21
|
function requireFeedsHost() {
|
|
23
|
-
|
|
24
|
-
return current;
|
|
22
|
+
return hostSlot.get();
|
|
25
23
|
}
|
|
26
24
|
/** Test-only: clear the configured host. */
|
|
27
25
|
function resetFeedsHostForTesting() {
|
|
28
|
-
|
|
26
|
+
hostSlot.reset();
|
|
29
27
|
}
|
|
30
28
|
/** Forwarding logger so engine modules can `import { log }` without each
|
|
31
|
-
* reaching for `requireFeedsHost().log`.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
warn: (prefix, msg, data) => requireFeedsHost().log.warn(prefix, msg, data),
|
|
35
|
-
info: (prefix, msg, data) => requireFeedsHost().log.info(prefix, msg, data),
|
|
36
|
-
debug: (prefix, msg, data) => requireFeedsHost().log.debug(prefix, msg, data)
|
|
37
|
-
};
|
|
29
|
+
* reaching for `requireFeedsHost().log`. Non-critical: calls before the host
|
|
30
|
+
* is wired are dropped (not thrown), matching the collection/google seams. */
|
|
31
|
+
var log = createForwardingLogger(() => hostSlot.peek()?.log ?? null);
|
|
38
32
|
//#endregion
|
|
39
33
|
//#region src/feeds/server/registry.ts
|
|
40
34
|
/** Every registered feed, as a discovered collection (carrying its
|
|
@@ -84,69 +78,156 @@ function getRetriever(kind) {
|
|
|
84
78
|
return registry.get(kind);
|
|
85
79
|
}
|
|
86
80
|
//#endregion
|
|
87
|
-
//#region
|
|
88
|
-
var
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
var
|
|
95
|
-
|
|
81
|
+
//#region ../common/dist/ssrf.js
|
|
82
|
+
var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
83
|
+
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
84
|
+
"localhost",
|
|
85
|
+
"ip6-localhost",
|
|
86
|
+
"ip6-loopback"
|
|
87
|
+
]);
|
|
88
|
+
var BLOCKED_HOSTNAME_SUFFIXES = [
|
|
89
|
+
".localhost",
|
|
90
|
+
".local",
|
|
91
|
+
".internal"
|
|
92
|
+
];
|
|
93
|
+
/** IPv4 ranges that must never be fetched, as [firstAddress, prefixLength].
|
|
94
|
+
* The union of the pre-#2459 per-guard tables — removing an entry weakens
|
|
95
|
+
* every guard in the repo at once, so treat edits as security changes. */
|
|
96
|
+
var BLOCKED_IPV4_RANGES = [
|
|
96
97
|
["0.0.0.0", 8],
|
|
97
98
|
["10.0.0.0", 8],
|
|
98
99
|
["100.64.0.0", 10],
|
|
99
100
|
["127.0.0.0", 8],
|
|
100
101
|
["169.254.0.0", 16],
|
|
101
102
|
["172.16.0.0", 12],
|
|
102
|
-
["192.
|
|
103
|
+
["192.0.0.0", 24],
|
|
104
|
+
["192.168.0.0", 16],
|
|
105
|
+
["198.18.0.0", 15],
|
|
106
|
+
["224.0.0.0", 4],
|
|
107
|
+
["240.0.0.0", 4]
|
|
103
108
|
];
|
|
109
|
+
/** Dotted-decimal IPv4 → unsigned 32-bit value; null for anything else
|
|
110
|
+
* (hex / octal octets, whitespace, wrong part count). */
|
|
104
111
|
function ipv4ToInt(address) {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
|
|
112
|
+
const parts = address.split(".");
|
|
113
|
+
if (parts.length !== 4) return null;
|
|
114
|
+
const octets = parts.map((part) => /^\d{1,3}$/.test(part) ? Number(part) : NaN);
|
|
115
|
+
if (octets.some((octet) => Number.isNaN(octet) || octet > 255)) return null;
|
|
116
|
+
return octets.reduce((value, octet) => value * 256 + octet, 0);
|
|
117
|
+
}
|
|
118
|
+
function isBlockedIpv4Value(value) {
|
|
119
|
+
return BLOCKED_IPV4_RANGES.some(([base, prefix]) => {
|
|
120
|
+
const baseValue = ipv4ToInt(base);
|
|
121
|
+
if (baseValue === null) return false;
|
|
122
|
+
const mask = prefix === 0 ? 0 : -1 << 32 - prefix >>> 0;
|
|
123
|
+
return (value & mask) === (baseValue & mask);
|
|
124
|
+
});
|
|
108
125
|
}
|
|
126
|
+
/** True when this parses as IPv4 AND falls inside a blocked range. Non-IPv4
|
|
127
|
+
* input is false — "not a blocked literal", not "safe"; a caller that must
|
|
128
|
+
* reject non-IP input entirely does so at its own boundary. */
|
|
109
129
|
function isBlockedIpv4(address) {
|
|
110
130
|
const value = ipv4ToInt(address);
|
|
111
|
-
if (value === null) return
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
if (value === null) return false;
|
|
132
|
+
return isBlockedIpv4Value(value);
|
|
133
|
+
}
|
|
134
|
+
/** `URL.hostname` keeps the brackets on an IPv6 literal (`[::1]`); strip them
|
|
135
|
+
* so the address checks see the bare address. */
|
|
136
|
+
function stripIpv6Brackets(hostname) {
|
|
137
|
+
return hostname.replace(/^\[|\]$/g, "");
|
|
138
|
+
}
|
|
139
|
+
/** The 32-bit value inside an IPv4-mapped IPv6 literal, or null when the
|
|
140
|
+
* address is not v4-mapped. The hex spelling matters: WHATWG URL serializes
|
|
141
|
+
* `[::ffff:127.0.0.1]` as `::ffff:7f00:1`, so matching only the dotted form
|
|
142
|
+
* would wave the mapped loopback through any URL-sourced check. */
|
|
143
|
+
function mappedIpv4Value(lower) {
|
|
144
|
+
const dotted = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower);
|
|
145
|
+
if (dotted) return ipv4ToInt(dotted[1]);
|
|
146
|
+
const hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(lower);
|
|
147
|
+
if (hex) return Number.parseInt(hex[1], 16) * 65536 + Number.parseInt(hex[2], 16);
|
|
148
|
+
return null;
|
|
117
149
|
}
|
|
150
|
+
/** Blocks loopback/unspecified, fc00::/7 unique-local, fe80::/10 link-local,
|
|
151
|
+
* and IPv4-mapped forms of blocked v4 addresses. Mask-based on the leading
|
|
152
|
+
* group — prefix string matching would miss e.g. fe9a:: (inside fe80::/10). */
|
|
118
153
|
function isBlockedIpv6(address) {
|
|
119
|
-
const lower = address.toLowerCase();
|
|
154
|
+
const lower = stripIpv6Brackets(address.toLowerCase());
|
|
120
155
|
if (lower === "::1" || lower === "::") return true;
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
156
|
+
const mapped = mappedIpv4Value(lower);
|
|
157
|
+
if (mapped !== null) return isBlockedIpv4Value(mapped);
|
|
158
|
+
const [head] = lower.split(":");
|
|
159
|
+
if (head.length === 0) return false;
|
|
160
|
+
const leading = Number.parseInt(head, 16);
|
|
161
|
+
if (Number.isNaN(leading)) return false;
|
|
162
|
+
if ((leading & 65024) === 64512) return true;
|
|
163
|
+
if ((leading & 65472) === 65152) return true;
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
/** True when this literal address must never be fetched. Non-IP input is
|
|
167
|
+
* false — see `isBlockedIpv4`. */
|
|
127
168
|
function isBlockedIp(address) {
|
|
169
|
+
return address.includes(":") ? isBlockedIpv6(address) : isBlockedIpv4(address);
|
|
170
|
+
}
|
|
171
|
+
/** Names that are internal by convention (localhost, *.local, *.internal…),
|
|
172
|
+
* rejected without waiting for DNS to prove it. */
|
|
173
|
+
function isBlockedHostname(hostname) {
|
|
174
|
+
const lower = hostname.toLowerCase().replace(/\.$/, "");
|
|
175
|
+
if (BLOCKED_HOSTNAMES.has(lower)) return true;
|
|
176
|
+
return BLOCKED_HOSTNAME_SUFFIXES.some((suffix) => lower.endsWith(suffix));
|
|
177
|
+
}
|
|
178
|
+
function parseUrl(raw) {
|
|
179
|
+
try {
|
|
180
|
+
return new URL(raw);
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Parse and apply every check that doesn't need the network: shape, scheme,
|
|
186
|
+
* obviously-internal hostname, and literal addresses. Returns null on reject.
|
|
187
|
+
* Callers must still vet what non-literal hostnames resolve to. */
|
|
188
|
+
function parseSafeUrlShape(raw) {
|
|
189
|
+
const url = parseUrl(raw);
|
|
190
|
+
if (url === null) return null;
|
|
191
|
+
if (!ALLOWED_PROTOCOLS.has(url.protocol)) return null;
|
|
192
|
+
const hostname = stripIpv6Brackets(url.hostname);
|
|
193
|
+
if (hostname.length === 0) return null;
|
|
194
|
+
if (isBlockedHostname(hostname)) return null;
|
|
195
|
+
if (isBlockedIp(hostname)) return null;
|
|
196
|
+
return url;
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/feeds/server/fetch/httpClient.ts
|
|
200
|
+
var ONE_SECOND_MS = 1e3;
|
|
201
|
+
/** Identifies the bot to site operators. */
|
|
202
|
+
var FEED_USER_AGENT = "MulmoClaude-FeedBot/1.0 (+https://github.com/receptron/mulmoclaude)";
|
|
203
|
+
/** Per-request wall-clock cap so a hung server can't wedge a refresh. */
|
|
204
|
+
var DEFAULT_FEED_TIMEOUT_MS = 30 * ONE_SECOND_MS;
|
|
205
|
+
/** Cap on redirect hops followed (each re-checked for SSRF). */
|
|
206
|
+
var MAX_REDIRECTS = 5;
|
|
207
|
+
/** DNS answers should be IP literals; anything else is blocked outright. */
|
|
208
|
+
function isBlockedResolvedIp(address) {
|
|
128
209
|
const kind = isIP(address);
|
|
129
210
|
if (kind === 4) return isBlockedIpv4(address);
|
|
130
211
|
if (kind === 6) return isBlockedIpv6(address);
|
|
131
212
|
return true;
|
|
132
213
|
}
|
|
133
|
-
|
|
134
|
-
* address (SSRF guard). Throws with a clear reason; returns void on pass. */
|
|
135
|
-
async function assertFetchableUrl(rawUrl) {
|
|
136
|
-
if (!/^https?:\/\//i.test(rawUrl)) throw new Error(`refusing non-http(s) URL: ${rawUrl}`);
|
|
137
|
-
const { hostname } = new URL(rawUrl);
|
|
138
|
-
const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
139
|
-
if (isIP(host)) {
|
|
140
|
-
if (isBlockedIp(host)) throw new Error(`refusing to fetch a private/loopback address: ${host}`);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
let addresses;
|
|
214
|
+
async function resolveAddresses(host) {
|
|
144
215
|
try {
|
|
145
|
-
|
|
216
|
+
return await lookup(host, { all: true });
|
|
146
217
|
} catch (err) {
|
|
147
218
|
throw new Error(`could not resolve host '${host}': ${String(err)}`);
|
|
148
219
|
}
|
|
149
|
-
|
|
220
|
+
}
|
|
221
|
+
/** Reject non-http(s) URLs, internal-by-convention hostnames, and URLs whose
|
|
222
|
+
* host is / resolves to a private/loopback address (SSRF guard). Throws with
|
|
223
|
+
* a clear reason; returns void on pass. */
|
|
224
|
+
async function assertFetchableUrl(rawUrl) {
|
|
225
|
+
if (!/^https?:\/\//i.test(rawUrl)) throw new Error(`refusing non-http(s) URL: ${rawUrl}`);
|
|
226
|
+
const url = parseSafeUrlShape(rawUrl);
|
|
227
|
+
if (url === null) throw new Error(`refusing to fetch a private/loopback/internal address: ${rawUrl}`);
|
|
228
|
+
const host = stripIpv6Brackets(url.hostname);
|
|
229
|
+
if (isIP(host)) return;
|
|
230
|
+
const blocked = (await resolveAddresses(host)).find((entry) => isBlockedResolvedIp(entry.address));
|
|
150
231
|
if (blocked) throw new Error(`refusing to fetch '${host}' — resolves to a private/loopback address (${blocked.address})`);
|
|
151
232
|
}
|
|
152
233
|
async function fetchOnce(url, timeoutMs) {
|
|
@@ -305,7 +386,8 @@ function step(current, token) {
|
|
|
305
386
|
if (current === null || current === void 0) return void 0;
|
|
306
387
|
if (token.kind === "key") {
|
|
307
388
|
if (typeof current !== "object" || Array.isArray(current)) return void 0;
|
|
308
|
-
|
|
389
|
+
const record = current;
|
|
390
|
+
return Object.hasOwn(record, token.key) ? record[token.key] : void 0;
|
|
309
391
|
}
|
|
310
392
|
return Array.isArray(current) ? current[token.index] : void 0;
|
|
311
393
|
}
|