@mulmoclaude/core 1.0.1 → 1.2.1

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 (177) hide show
  1. package/assets/helps/collection-skills.md +28 -0
  2. package/dist/artifacts/paths.cjs +68 -0
  3. package/dist/artifacts/paths.cjs.map +1 -0
  4. package/dist/artifacts/paths.d.ts +45 -0
  5. package/dist/artifacts/paths.js +62 -0
  6. package/dist/artifacts/paths.js.map +1 -0
  7. package/dist/atomic-C_7YpMiM.cjs +102 -0
  8. package/dist/atomic-C_7YpMiM.cjs.map +1 -0
  9. package/dist/atomic-DPpdrJzO.js +89 -0
  10. package/dist/atomic-DPpdrJzO.js.map +1 -0
  11. package/dist/collection/core/chatSeed.d.ts +4 -0
  12. package/dist/collection/core/completion.d.ts +53 -0
  13. package/dist/collection/core/ids.d.ts +10 -0
  14. package/dist/collection/core/linkTargets.d.ts +14 -0
  15. package/dist/collection/core/ownProp.d.ts +9 -0
  16. package/dist/collection/core/recordKeys.d.ts +21 -0
  17. package/dist/collection/core/schemaRules.d.ts +167 -0
  18. package/dist/collection/core/schemaZ.d.ts +489 -9
  19. package/dist/collection/core/sortValueOf.d.ts +21 -0
  20. package/dist/collection/core/textSearch.d.ts +12 -0
  21. package/dist/collection/index.cjs +132 -29
  22. package/dist/collection/index.cjs.map +1 -1
  23. package/dist/collection/index.d.ts +5 -0
  24. package/dist/collection/index.js +115 -29
  25. package/dist/collection/index.js.map +1 -1
  26. package/dist/collection/registry/server/index.cjs +14 -42
  27. package/dist/collection/registry/server/index.cjs.map +1 -1
  28. package/dist/collection/registry/server/index.js +6 -34
  29. package/dist/collection/registry/server/index.js.map +1 -1
  30. package/dist/collection/server/discovery.d.ts +7 -0
  31. package/dist/collection/server/host.d.ts +3 -8
  32. package/dist/collection/server/index.cjs +10 -9
  33. package/dist/collection/server/index.d.ts +1 -0
  34. package/dist/collection/server/index.js +4 -4
  35. package/dist/collection/server/io.d.ts +0 -88
  36. package/dist/collection/server/skillAssets.d.ts +90 -0
  37. package/dist/collection-watchers/config.d.ts +2 -4
  38. package/dist/collection-watchers/index.cjs +5 -5
  39. package/dist/collection-watchers/index.cjs.map +1 -1
  40. package/dist/collection-watchers/index.js +5 -5
  41. package/dist/collection-watchers/index.js.map +1 -1
  42. package/dist/{discovery-Dpz6SWwe.cjs → discovery--kRUWed0.cjs} +444 -434
  43. package/dist/discovery--kRUWed0.cjs.map +1 -0
  44. package/dist/{discovery-BYIsLqag.js → discovery-CKR4TOX3.js} +426 -392
  45. package/dist/discovery-CKR4TOX3.js.map +1 -0
  46. package/dist/dist-Cwk0e12G.js +50 -0
  47. package/dist/dist-Cwk0e12G.js.map +1 -0
  48. package/dist/dist-pWpC-b04.cjs +61 -0
  49. package/dist/dist-pWpC-b04.cjs.map +1 -0
  50. package/dist/feeds/index.cjs +2 -2
  51. package/dist/feeds/index.js +2 -2
  52. package/dist/feeds/server/host.d.ts +5 -8
  53. package/dist/feeds/server/index.cjs +143 -61
  54. package/dist/feeds/server/index.cjs.map +1 -1
  55. package/dist/feeds/server/index.js +141 -59
  56. package/dist/feeds/server/index.js.map +1 -1
  57. package/dist/files/atomic.d.ts +34 -0
  58. package/dist/files/index.cjs +47 -0
  59. package/dist/files/index.cjs.map +1 -0
  60. package/dist/files/index.d.ts +3 -0
  61. package/dist/files/index.js +40 -0
  62. package/dist/files/index.js.map +1 -0
  63. package/dist/files/json.d.ts +4 -0
  64. package/dist/files/safe.d.ts +9 -0
  65. package/dist/google/auth.d.ts +1 -1
  66. package/dist/google/collectionDateTime.d.ts +5 -0
  67. package/dist/google/collectionSync.d.ts +2 -2
  68. package/dist/google/fsJson.d.ts +3 -2
  69. package/dist/google/host.d.ts +3 -6
  70. package/dist/google/index.cjs +64 -91
  71. package/dist/google/index.cjs.map +1 -1
  72. package/dist/google/index.d.ts +1 -0
  73. package/dist/google/index.js +57 -85
  74. package/dist/google/index.js.map +1 -1
  75. package/dist/{graph-o5FZZbXf.js → graph-BcwqiSpR.js} +2 -14
  76. package/dist/graph-BcwqiSpR.js.map +1 -0
  77. package/dist/{graph-2R4HbQAg.cjs → graph-DjE1Az1C.cjs} +2 -14
  78. package/dist/graph-DjE1Az1C.cjs.map +1 -0
  79. package/dist/host/hostSlot.d.ts +27 -0
  80. package/dist/{ids-DWmHjm17.cjs → ids-BnTh3Hl0.cjs} +29 -1
  81. package/dist/{ids-DWmHjm17.cjs.map → ids-BnTh3Hl0.cjs.map} +1 -1
  82. package/dist/{ids-D1M1T6KJ.js → ids-s4GfmE93.js} +18 -2
  83. package/dist/{ids-D1M1T6KJ.js.map → ids-s4GfmE93.js.map} +1 -1
  84. package/dist/{ingestTypes-Ci4eOgv-.js → ingestTypes-L59cIGgX.js} +2 -2
  85. package/dist/{ingestTypes-Ci4eOgv-.js.map → ingestTypes-L59cIGgX.js.map} +1 -1
  86. package/dist/{ingestTypes-DbuQNuK6.cjs → ingestTypes-xX8VpQqh.cjs} +2 -2
  87. package/dist/{ingestTypes-DbuQNuK6.cjs.map → ingestTypes-xX8VpQqh.cjs.map} +1 -1
  88. package/dist/notifier/engine.d.ts +2 -4
  89. package/dist/notifier/index.cjs +1 -1
  90. package/dist/notifier/index.js +1 -1
  91. package/dist/{notifier-ChpY0XrY.js → notifier-BdA5qzhe.js} +14 -16
  92. package/dist/notifier-BdA5qzhe.js.map +1 -0
  93. package/dist/{notifier-bS8IEeLA.cjs → notifier-tMsAXyXp.cjs} +14 -16
  94. package/dist/notifier-tMsAXyXp.cjs.map +1 -0
  95. package/dist/plugin-vue/fileWatch.d.ts +5 -0
  96. package/dist/plugin-vue/i18n.cjs +45 -0
  97. package/dist/plugin-vue/i18n.cjs.map +1 -0
  98. package/dist/plugin-vue/i18n.js +44 -0
  99. package/dist/plugin-vue/i18n.js.map +1 -0
  100. package/dist/plugin-vue/index.cjs +98 -0
  101. package/dist/plugin-vue/index.cjs.map +1 -0
  102. package/dist/plugin-vue/index.d.ts +5 -0
  103. package/dist/plugin-vue/index.js +91 -0
  104. package/dist/plugin-vue/index.js.map +1 -0
  105. package/dist/plugin-vue/markdownDoc.d.ts +10 -0
  106. package/dist/plugin-vue/pluginI18n.d.ts +22 -0
  107. package/dist/plugin-vue/useClipboardCopy.d.ts +6 -0
  108. package/dist/plugin-vue/useFileWatch.d.ts +8 -0
  109. package/dist/plugin-vue/useMarkdownDoc.d.ts +3 -0
  110. package/dist/{promptSafety-Bugq2kqL.js → promptSafety-CQ5Un4wi.js} +104 -7
  111. package/dist/promptSafety-CQ5Un4wi.js.map +1 -0
  112. package/dist/{promptSafety-DbE6eZmP.cjs → promptSafety-DfZYNtjK.cjs} +151 -6
  113. package/dist/promptSafety-DfZYNtjK.cjs.map +1 -0
  114. package/dist/remote-host/server/hostRunner.d.ts +5 -1
  115. package/dist/remote-host/server/index.cjs +75 -21
  116. package/dist/remote-host/server/index.cjs.map +1 -1
  117. package/dist/remote-host/server/index.js +72 -18
  118. package/dist/remote-host/server/index.js.map +1 -1
  119. package/dist/scheduler/index.cjs +3 -3
  120. package/dist/scheduler/index.cjs.map +1 -1
  121. package/dist/scheduler/index.js +2 -2
  122. package/dist/scheduler/index.js.map +1 -1
  123. package/dist/scheduler/task-manager.d.ts +2 -5
  124. package/dist/{server-CtCBLYgZ.js → server-BZJCw2HU.js} +209 -41
  125. package/dist/server-BZJCw2HU.js.map +1 -0
  126. package/dist/{server-BjWrcc5T.cjs → server-w5ZZRLju.cjs} +250 -46
  127. package/dist/server-w5ZZRLju.cjs.map +1 -0
  128. package/dist/skill-bridge/index.cjs +1 -1
  129. package/dist/skill-bridge/index.js +1 -1
  130. package/dist/translation/client.cjs +16 -0
  131. package/dist/translation/client.cjs.map +1 -1
  132. package/dist/translation/client.d.ts +6 -0
  133. package/dist/translation/client.js +16 -1
  134. package/dist/translation/client.js.map +1 -1
  135. package/dist/utils/errors.d.ts +2 -1
  136. package/dist/utils/fetch.cjs +36 -0
  137. package/dist/utils/fetch.cjs.map +1 -0
  138. package/dist/{collection/registry/server → utils}/fetch.d.ts +5 -3
  139. package/dist/utils/fetch.js +34 -0
  140. package/dist/utils/fetch.js.map +1 -0
  141. package/dist/utils/index.cjs +11 -3
  142. package/dist/utils/index.cjs.map +1 -0
  143. package/dist/utils/index.js +9 -1
  144. package/dist/utils/index.js.map +1 -0
  145. package/dist/whisper/index.cjs +9 -9
  146. package/dist/whisper/index.cjs.map +1 -1
  147. package/dist/whisper/index.js +3 -3
  148. package/dist/wiki/index.cjs +23 -28
  149. package/dist/wiki/index.cjs.map +1 -1
  150. package/dist/wiki/index.js +21 -26
  151. package/dist/wiki/index.js.map +1 -1
  152. package/dist/wiki/link.d.ts +7 -1
  153. package/dist/wiki/render.d.ts +5 -7
  154. package/dist/wiki/server/frontmatter.d.ts +2 -6
  155. package/dist/wiki/server/index.cjs +5 -41
  156. package/dist/wiki/server/index.cjs.map +1 -1
  157. package/dist/wiki/server/index.js +3 -39
  158. package/dist/wiki/server/index.js.map +1 -1
  159. package/dist/workspace-setup/index.js +22 -6
  160. package/dist/workspace-setup/index.js.map +1 -1
  161. package/package.json +54 -11
  162. package/dist/collection/server/atomic.d.ts +0 -1
  163. package/dist/discovery-BYIsLqag.js.map +0 -1
  164. package/dist/discovery-Dpz6SWwe.cjs.map +0 -1
  165. package/dist/errors-7P5eMOSX.cjs +0 -30
  166. package/dist/errors-7P5eMOSX.cjs.map +0 -1
  167. package/dist/errors-eid6Mes3.js +0 -19
  168. package/dist/errors-eid6Mes3.js.map +0 -1
  169. package/dist/google/fetch.d.ts +0 -8
  170. package/dist/graph-2R4HbQAg.cjs.map +0 -1
  171. package/dist/graph-o5FZZbXf.js.map +0 -1
  172. package/dist/notifier-ChpY0XrY.js.map +0 -1
  173. package/dist/notifier-bS8IEeLA.cjs.map +0 -1
  174. package/dist/promptSafety-Bugq2kqL.js.map +0 -1
  175. package/dist/promptSafety-DbE6eZmP.cjs.map +0 -1
  176. package/dist/server-BjWrcc5T.cjs.map +0 -1
  177. package/dist/server-CtCBLYgZ.js.map +0 -1
@@ -1,40 +1,34 @@
1
- import { c as INGEST_KINDS, s as FEED_SCHEDULES } from "../../ids-D1M1T6KJ.js";
2
- import { A as buildCollectionActionSeedPrompt, F as promptPathsFor, P as listItems, X as safeSlugName, n as discoverCollections, q as resolveDataDir, u as storeFor, z as readSkillTemplate } from "../../discovery-BYIsLqag.js";
3
- import "../../server-CtCBLYgZ.js";
4
- import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../../ingestTypes-Ci4eOgv-.js";
1
+ import { l as FEED_SCHEDULES, u as INGEST_KINDS } from "../../ids-s4GfmE93.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-CKR4TOX3.js";
3
+ import { A as buildCollectionActionSeedPrompt, P as readSkillTemplate, j as promptPathsFor } from "../../server-BZJCw2HU.js";
4
+ import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../../ingestTypes-L59cIGgX.js";
5
5
  import { FEEDS_DIR, feedDir, feedStatePath, feedsRoot, ingestStateDir, ingestStatePath } from "../paths.js";
6
- import { d as publish, n as clear } from "../../notifier-ChpY0XrY.js";
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 current = null;
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
- if (current && current !== host) throw new Error("@mulmoclaude/core/feeds: configureFeedsHost() was already called with a different host");
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
- if (!current) throw new Error("@mulmoclaude/core/feeds: configureFeedsHost() was not called by the host");
24
- return current;
22
+ return hostSlot.get();
25
23
  }
26
24
  /** Test-only: clear the configured host. */
27
25
  function resetFeedsHostForTesting() {
28
- current = null;
26
+ hostSlot.reset();
29
27
  }
30
28
  /** Forwarding logger so engine modules can `import { log }` without each
31
- * reaching for `requireFeedsHost().log`. */
32
- var log = {
33
- error: (prefix, msg, data) => requireFeedsHost().log.error(prefix, msg, data),
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 src/feeds/server/fetch/httpClient.ts
88
- var ONE_SECOND_MS = 1e3;
89
- /** Identifies the bot to site operators. */
90
- var FEED_USER_AGENT = "MulmoClaude-FeedBot/1.0 (+https://github.com/receptron/mulmoclaude)";
91
- /** Per-request wall-clock cap so a hung server can't wedge a refresh. */
92
- var DEFAULT_FEED_TIMEOUT_MS = 30 * ONE_SECOND_MS;
93
- /** Cap on redirect hops followed (each re-checked for SSRF). */
94
- var MAX_REDIRECTS = 5;
95
- var BLOCKED_V4_CIDRS = [
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.168.0.0", 16]
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 octets = address.split(".").map(Number);
106
- if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null;
107
- return (octets[0] << 24 | octets[1] << 16 | octets[2] << 8 | octets[3]) >>> 0;
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 true;
112
- return BLOCKED_V4_CIDRS.some(([base, bits]) => {
113
- const baseInt = ipv4ToInt(base) ?? 0;
114
- const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
115
- return (value & mask) === (baseInt & mask);
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
- if (lower.startsWith("fe80")) return true;
122
- if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
123
- const mapped = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(lower);
124
- return mapped ? isBlockedIpv4(mapped[1]) : false;
125
- }
126
- /** True for any address we must not fetch (also blocks non-IP input). */
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
- /** Reject non-http(s) URLs and URLs that resolve to a private/loopback
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
- addresses = await lookup(host, { all: true });
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
- const blocked = addresses.find((entry) => isBlockedIp(entry.address));
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
- return current[token.key];
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
  }