@ozzylabs/feedradar 0.1.3 → 0.1.5

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 (144) hide show
  1. package/README.ja.md +31 -6
  2. package/README.md +31 -6
  3. package/dist/agents/claude-code.d.ts +12 -1
  4. package/dist/agents/claude-code.d.ts.map +1 -1
  5. package/dist/agents/claude-code.js +9 -5
  6. package/dist/agents/claude-code.js.map +1 -1
  7. package/dist/agents/codex-cli.d.ts +7 -1
  8. package/dist/agents/codex-cli.d.ts.map +1 -1
  9. package/dist/agents/codex-cli.js +9 -5
  10. package/dist/agents/codex-cli.js.map +1 -1
  11. package/dist/agents/copilot.d.ts +7 -1
  12. package/dist/agents/copilot.d.ts.map +1 -1
  13. package/dist/agents/copilot.js +9 -5
  14. package/dist/agents/copilot.js.map +1 -1
  15. package/dist/agents/gemini-cli.d.ts +7 -1
  16. package/dist/agents/gemini-cli.d.ts.map +1 -1
  17. package/dist/agents/gemini-cli.js +9 -5
  18. package/dist/agents/gemini-cli.js.map +1 -1
  19. package/dist/agents/index.d.ts +1 -1
  20. package/dist/agents/index.d.ts.map +1 -1
  21. package/dist/agents/types.d.ts +33 -0
  22. package/dist/agents/types.d.ts.map +1 -1
  23. package/dist/cli/_progress.d.ts +138 -0
  24. package/dist/cli/_progress.d.ts.map +1 -0
  25. package/dist/cli/_progress.js +176 -0
  26. package/dist/cli/_progress.js.map +1 -0
  27. package/dist/cli/doctor.d.ts +20 -0
  28. package/dist/cli/doctor.d.ts.map +1 -1
  29. package/dist/cli/doctor.js +291 -2
  30. package/dist/cli/doctor.js.map +1 -1
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +2 -0
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/research.d.ts +18 -20
  35. package/dist/cli/research.d.ts.map +1 -1
  36. package/dist/cli/research.js +318 -203
  37. package/dist/cli/research.js.map +1 -1
  38. package/dist/cli/respawn.d.ts +53 -0
  39. package/dist/cli/respawn.d.ts.map +1 -0
  40. package/dist/cli/respawn.js +120 -0
  41. package/dist/cli/respawn.js.map +1 -0
  42. package/dist/cli/review.d.ts +7 -0
  43. package/dist/cli/review.d.ts.map +1 -1
  44. package/dist/cli/review.js +46 -1
  45. package/dist/cli/review.js.map +1 -1
  46. package/dist/cli/source.d.ts +23 -2
  47. package/dist/cli/source.d.ts.map +1 -1
  48. package/dist/cli/source.js +425 -7
  49. package/dist/cli/source.js.map +1 -1
  50. package/dist/cli/update.d.ts +7 -0
  51. package/dist/cli/update.d.ts.map +1 -1
  52. package/dist/cli/update.js +41 -1
  53. package/dist/cli/update.js.map +1 -1
  54. package/dist/cli/watch.d.ts.map +1 -1
  55. package/dist/cli/watch.js +65 -3
  56. package/dist/cli/watch.js.map +1 -1
  57. package/dist/cli/workflow/generate-combined.d.ts +100 -0
  58. package/dist/cli/workflow/generate-combined.d.ts.map +1 -0
  59. package/dist/cli/workflow/generate-combined.js +387 -0
  60. package/dist/cli/workflow/generate-combined.js.map +1 -0
  61. package/dist/cli/workflow/generate-watch.d.ts +142 -0
  62. package/dist/cli/workflow/generate-watch.d.ts.map +1 -0
  63. package/dist/cli/workflow/generate-watch.js +338 -0
  64. package/dist/cli/workflow/generate-watch.js.map +1 -0
  65. package/dist/cli/workflow.d.ts +29 -0
  66. package/dist/cli/workflow.d.ts.map +1 -0
  67. package/dist/cli/workflow.js +66 -0
  68. package/dist/cli/workflow.js.map +1 -0
  69. package/dist/core/feeds/_fetch.d.ts +103 -0
  70. package/dist/core/feeds/_fetch.d.ts.map +1 -0
  71. package/dist/core/feeds/_fetch.js +364 -0
  72. package/dist/core/feeds/_fetch.js.map +1 -0
  73. package/dist/core/feeds/_jsonpath.d.ts +57 -0
  74. package/dist/core/feeds/_jsonpath.d.ts.map +1 -0
  75. package/dist/core/feeds/_jsonpath.js +207 -0
  76. package/dist/core/feeds/_jsonpath.js.map +1 -0
  77. package/dist/core/feeds/github-api.d.ts.map +1 -1
  78. package/dist/core/feeds/github-api.js +2 -1
  79. package/dist/core/feeds/github-api.js.map +1 -1
  80. package/dist/core/feeds/html-js.d.ts +29 -0
  81. package/dist/core/feeds/html-js.d.ts.map +1 -1
  82. package/dist/core/feeds/html-js.js +86 -2
  83. package/dist/core/feeds/html-js.js.map +1 -1
  84. package/dist/core/feeds/html.d.ts.map +1 -1
  85. package/dist/core/feeds/html.js +2 -1
  86. package/dist/core/feeds/html.js.map +1 -1
  87. package/dist/core/feeds/index.d.ts +1 -1
  88. package/dist/core/feeds/index.d.ts.map +1 -1
  89. package/dist/core/feeds/index.js +4 -0
  90. package/dist/core/feeds/index.js.map +1 -1
  91. package/dist/core/feeds/json-api.d.ts +3 -0
  92. package/dist/core/feeds/json-api.d.ts.map +1 -0
  93. package/dist/core/feeds/json-api.js +723 -0
  94. package/dist/core/feeds/json-api.js.map +1 -0
  95. package/dist/core/feeds/json-feed.d.ts +11 -0
  96. package/dist/core/feeds/json-feed.d.ts.map +1 -0
  97. package/dist/core/feeds/json-feed.js +242 -0
  98. package/dist/core/feeds/json-feed.js.map +1 -0
  99. package/dist/core/feeds/npm-registry.d.ts.map +1 -1
  100. package/dist/core/feeds/npm-registry.js +2 -1
  101. package/dist/core/feeds/npm-registry.js.map +1 -1
  102. package/dist/core/feeds/rss.d.ts.map +1 -1
  103. package/dist/core/feeds/rss.js +2 -1
  104. package/dist/core/feeds/rss.js.map +1 -1
  105. package/dist/core/feeds/types.d.ts +123 -0
  106. package/dist/core/feeds/types.d.ts.map +1 -1
  107. package/dist/core/progress.d.ts +101 -0
  108. package/dist/core/progress.d.ts.map +1 -0
  109. package/dist/core/progress.js +212 -0
  110. package/dist/core/progress.js.map +1 -0
  111. package/dist/core/proxy.d.ts +87 -0
  112. package/dist/core/proxy.d.ts.map +1 -0
  113. package/dist/core/proxy.js +146 -0
  114. package/dist/core/proxy.js.map +1 -0
  115. package/dist/core/recipes.d.ts +138 -0
  116. package/dist/core/recipes.d.ts.map +1 -0
  117. package/dist/core/recipes.js +238 -0
  118. package/dist/core/recipes.js.map +1 -0
  119. package/dist/core/watcher.d.ts +61 -1
  120. package/dist/core/watcher.d.ts.map +1 -1
  121. package/dist/core/watcher.js +99 -2
  122. package/dist/core/watcher.js.map +1 -1
  123. package/dist/index.js +17 -4
  124. package/dist/index.js.map +1 -1
  125. package/dist/recipes/aws-whats-new.yaml +61 -0
  126. package/dist/recipes/dev-to.yaml +40 -0
  127. package/dist/schemas/index.d.ts +1 -0
  128. package/dist/schemas/index.d.ts.map +1 -1
  129. package/dist/schemas/index.js +1 -0
  130. package/dist/schemas/index.js.map +1 -1
  131. package/dist/schemas/recipe.d.ts +115 -0
  132. package/dist/schemas/recipe.d.ts.map +1 -0
  133. package/dist/schemas/recipe.js +54 -0
  134. package/dist/schemas/recipe.js.map +1 -0
  135. package/dist/schemas/source.d.ts +130 -0
  136. package/dist/schemas/source.d.ts.map +1 -1
  137. package/dist/schemas/source.js +130 -0
  138. package/dist/schemas/source.js.map +1 -1
  139. package/dist/templates/agents/AGENTS.md +31 -3
  140. package/dist/templates/feedradar.md +23 -8
  141. package/dist/templates/workflows/combined.template.yaml.tmpl +110 -0
  142. package/dist/templates/workflows/watch.template.yaml.tmpl +103 -0
  143. package/dist/templates/workflows/watch.yaml +5 -1
  144. package/package.json +2 -3
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Shared HTTP fetch wrapper for every feed adapter (`rss` / `html` /
3
+ * `npm-registry` / `github-releases`).
4
+ *
5
+ * Why a wrapper instead of having each adapter call `fetch` directly:
6
+ * - Proxy environments and flaky public RSS endpoints regularly hang or 5xx,
7
+ * so a default 30s **timeout** and a small **retry loop** for transient
8
+ * errors are needed everywhere — not just one adapter (issue #165).
9
+ * - 4xx responses (404 / 401 / 403) are permanent: retrying them just burns
10
+ * the user's GitHub rate-limit / wall time. We only retry transient signals.
11
+ * - Each adapter still passes its own `headers` and reads its own response
12
+ * body, so the wrapper stays a thin "fetch + timeout + retry" layer rather
13
+ * than a request-builder. The `options.fetch` test seam keeps working
14
+ * because the wrapper takes a `FetchLike` from the caller; mocks injected
15
+ * by tests flow through unchanged.
16
+ *
17
+ * Default tuning (see `resolveFetchConfig`):
18
+ * - Timeout: 30s per attempt.
19
+ * - Retries: 2 (so up to 3 attempts total).
20
+ * - Backoff: 200ms → 800ms (×4 exponential).
21
+ *
22
+ * Both timeout and retries can be overridden via env vars `RADAR_FETCH_TIMEOUT_MS`
23
+ * and `RADAR_FETCH_RETRIES`. We resolve them at call time (not module load)
24
+ * so tests can mutate `process.env` between runs without import cache games.
25
+ */
26
+ /** Default per-attempt timeout in milliseconds. */
27
+ export const DEFAULT_FETCH_TIMEOUT_MS = 30_000;
28
+ /** Default number of retries after the initial attempt. */
29
+ export const DEFAULT_FETCH_RETRIES = 2;
30
+ /** Initial backoff in ms; doubled-and-quadrupled each retry (200 → 800 → ...). */
31
+ export const DEFAULT_FETCH_BACKOFF_BASE_MS = 200;
32
+ /** Multiplier between successive backoff attempts. */
33
+ export const DEFAULT_FETCH_BACKOFF_FACTOR = 4;
34
+ /** Parse a positive integer env var, falling back to `fallback` on missing / invalid. */
35
+ function parsePositiveInt(value, fallback) {
36
+ if (value == null)
37
+ return fallback;
38
+ const n = Number.parseInt(value, 10);
39
+ if (!Number.isFinite(n) || n < 0)
40
+ return fallback;
41
+ return n;
42
+ }
43
+ /**
44
+ * SSRF host-blocklist for the shared fetch wrapper (ADR-0009 §D5b /
45
+ * ADR-0012 §D5b). Every adapter that funnels through `fetchWithRetry`
46
+ * inherits this check, including pagination follow-up URLs and recipe-
47
+ * supplied URLs in `kind: json-api` where a malicious recipe / compromised
48
+ * upstream could otherwise point us at:
49
+ *
50
+ * - cloud instance metadata endpoints (`169.254.169.254`, etc.) — IAM
51
+ * credential theft
52
+ * - LAN / VPC internal services (`10.0.0.0/8`, `192.168.0.0/16`,
53
+ * `172.16.0.0/12`) — internal admin panels, unauthenticated dev servers
54
+ * - loopback (`127.0.0.0/8` / `::1`) — user's localhost dev / debug services
55
+ * - link-local (`169.254.0.0/16` / `fe80::/10`)
56
+ * - IPv6 ULA (`fc00::/7`)
57
+ * - non-HTTP schemes (`file://`, `data:`, `gopher://`, `ftp://`, …) —
58
+ * filesystem reads / SMTP smuggling / arbitrary protocol abuse
59
+ *
60
+ * Scope: this is the minimum useful defense — we inspect the URL hostname
61
+ * literal only. We do not resolve DNS; an attacker can still get past this
62
+ * by pointing a public DNS record at `127.0.0.1` (DNS rebinding). That
63
+ * deeper defense is tracked separately (see ADR-0009 §D5b note on DNS
64
+ * rebinding); the literal check still cuts off the common SSRF recipes
65
+ * (metadata IPs, `file://`, `localhost`) for negligible cost.
66
+ *
67
+ * Override via `RADAR_FETCH_HOST_ALLOWLIST` (comma-separated host literals).
68
+ * Intended for testing (e2e smoke tests that spin up a local HTTP fixture
69
+ * on `127.0.0.1`) and explicit opt-in to private-network targets. The
70
+ * allowlist is matched against the URL hostname after lowercasing and
71
+ * trimming IPv6 brackets; entries are exact-match (no glob / CIDR).
72
+ */
73
+ /** IPv4 octet sequence — used as a heuristic to detect numeric-only hosts. */
74
+ const IPV4_PATTERN = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
75
+ /** Hostnames that always resolve to a loopback / unspecified address. */
76
+ const BLOCKED_HOSTNAMES = new Set(["localhost", "ip6-localhost", "ip6-loopback"]);
77
+ /**
78
+ * Parse the `RADAR_FETCH_HOST_ALLOWLIST` env into a normalized Set. We
79
+ * lowercase and strip IPv6 brackets so users can write either `[::1]` or
80
+ * `::1` and have the override apply.
81
+ */
82
+ function parseHostAllowlist(value) {
83
+ if (!value)
84
+ return new Set();
85
+ const entries = value
86
+ .split(",")
87
+ .map((s) => s.trim())
88
+ .filter((s) => s.length > 0)
89
+ .map((s) => normalizeHost(s));
90
+ return new Set(entries);
91
+ }
92
+ /** Lowercase a host and strip wrapping `[` / `]` from IPv6 literals. */
93
+ function normalizeHost(host) {
94
+ const lower = host.toLowerCase();
95
+ if (lower.startsWith("[") && lower.endsWith("]")) {
96
+ return lower.slice(1, -1);
97
+ }
98
+ return lower;
99
+ }
100
+ /** Whether a host string is an IPv4 address in one of the private ranges. */
101
+ function isPrivateIPv4(host) {
102
+ const match = host.match(IPV4_PATTERN);
103
+ if (!match)
104
+ return false;
105
+ const [, a, b, c, d] = match;
106
+ const o1 = Number.parseInt(a ?? "", 10);
107
+ const o2 = Number.parseInt(b ?? "", 10);
108
+ const o3 = Number.parseInt(c ?? "", 10);
109
+ const o4 = Number.parseInt(d ?? "", 10);
110
+ // Reject malformed octets (>255). The URL parser already rejects most of
111
+ // these but we keep the bound check for defense in depth.
112
+ if (![o1, o2, o3, o4].every((n) => Number.isFinite(n) && n >= 0 && n <= 255)) {
113
+ return true; // malformed = unsafe; treat as blocked.
114
+ }
115
+ // 10.0.0.0/8
116
+ if (o1 === 10)
117
+ return true;
118
+ // 172.16.0.0/12 (172.16.x.x – 172.31.x.x)
119
+ if (o1 === 172 && o2 >= 16 && o2 <= 31)
120
+ return true;
121
+ // 192.168.0.0/16
122
+ if (o1 === 192 && o2 === 168)
123
+ return true;
124
+ // 127.0.0.0/8 (loopback)
125
+ if (o1 === 127)
126
+ return true;
127
+ // 169.254.0.0/16 (link-local / AWS metadata)
128
+ if (o1 === 169 && o2 === 254)
129
+ return true;
130
+ // 0.0.0.0/8 (unspecified — listens-everywhere on Linux)
131
+ if (o1 === 0)
132
+ return true;
133
+ return false;
134
+ }
135
+ /**
136
+ * Whether an IPv6 literal (already lowercase, brackets stripped) is in a
137
+ * blocked range. We do not implement full address arithmetic — instead we
138
+ * pattern-match the textual prefix, which is enough for the well-known
139
+ * ranges we want to block (loopback, link-local, ULA, unspecified).
140
+ *
141
+ * Edge: `::ffff:a.b.c.d` IPv4-mapped form is normalized by Node's URL
142
+ * parser to bracketed IPv6, so we also rerun the IPv4 check on the trailing
143
+ * dotted-quad to catch the mapped-loopback case (`::ffff:127.0.0.1`).
144
+ */
145
+ function isPrivateIPv6(host) {
146
+ if (host === "::" || host === "::1")
147
+ return true;
148
+ // Link-local: fe80::/10 — first 10 bits are 1111 1110 10, which in hex
149
+ // means `fe80::` through `febf::`. Match the first two chars after fe.
150
+ if (/^fe[89ab][0-9a-f]?:/.test(host))
151
+ return true;
152
+ // ULA: fc00::/7 — first 7 bits are 1111 110, i.e. fc00–fdff.
153
+ if (/^f[cd][0-9a-f]{2}:/.test(host))
154
+ return true;
155
+ // IPv4-mapped IPv6, dotted-quad form: ::ffff:127.0.0.1, ::ffff:10.0.0.1
156
+ const v4MappedDotted = host.match(/^::ffff:([\d.]+)$/);
157
+ if (v4MappedDotted?.[1] && isPrivateIPv4(v4MappedDotted[1]))
158
+ return true;
159
+ // IPv4-mapped IPv6, hex form: Node's URL parser normalizes
160
+ // `::ffff:127.0.0.1` → `[::ffff:7f00:1]`, so we also accept the two
161
+ // 16-bit hex groups after `::ffff:` and reconstruct the dotted quad.
162
+ const v4MappedHex = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
163
+ if (v4MappedHex?.[1] && v4MappedHex[2]) {
164
+ const high = Number.parseInt(v4MappedHex[1], 16);
165
+ const low = Number.parseInt(v4MappedHex[2], 16);
166
+ if (Number.isFinite(high) && Number.isFinite(low)) {
167
+ const dotted = `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
168
+ if (isPrivateIPv4(dotted))
169
+ return true;
170
+ }
171
+ }
172
+ return false;
173
+ }
174
+ /**
175
+ * Validate a fetch URL against the SSRF host blocklist (ADR-0009 §D5b).
176
+ *
177
+ * Throws a `TypeError` with a user-friendly message when the URL is
178
+ * rejected. Returns nothing on success.
179
+ *
180
+ * Exported so adapters / tests can trigger the check independently of
181
+ * `fetchWithRetry` (e.g. validate a recipe URL at config-load time).
182
+ */
183
+ export function validateFetchUrl(url, env = process.env) {
184
+ let parsed;
185
+ try {
186
+ parsed = url instanceof URL ? url : new URL(url);
187
+ }
188
+ catch {
189
+ throw new TypeError(`refused to fetch invalid URL: ${String(url)}`);
190
+ }
191
+ const allowlist = parseHostAllowlist(env.RADAR_FETCH_HOST_ALLOWLIST);
192
+ const host = normalizeHost(parsed.hostname);
193
+ // Allowlist short-circuits both scheme and host checks so users testing
194
+ // a local fixture over `http://127.0.0.1:PORT/` do not need to fight
195
+ // the blocklist for every adapter test.
196
+ if (allowlist.has(host))
197
+ return;
198
+ // Reject non-HTTP(S) schemes outright. `data:` / `file:` / `gopher:` /
199
+ // `ftp:` / `javascript:` all have no business being fetched through an
200
+ // adapter; even when they would not technically SSRF, they break the
201
+ // wrapper's invariant ("HTTP request, retry on transient errors").
202
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
203
+ throw new TypeError(`refused to fetch URL with non-HTTP scheme "${parsed.protocol}" (${String(url)}). ` +
204
+ `Only http:// and https:// are allowed.`);
205
+ }
206
+ // Blocked literal hostnames (`localhost` etc.).
207
+ if (BLOCKED_HOSTNAMES.has(host)) {
208
+ throw new TypeError(`refused to fetch loopback hostname "${host}" (${String(url)}). ` +
209
+ `Set RADAR_FETCH_HOST_ALLOWLIST=${host} to override (testing only).`);
210
+ }
211
+ // Numeric IPv4 / IPv6 ranges.
212
+ if (isPrivateIPv4(host)) {
213
+ throw new TypeError(`refused to fetch private / loopback IPv4 address "${host}" (${String(url)}). ` +
214
+ `Set RADAR_FETCH_HOST_ALLOWLIST=${host} to override (testing only).`);
215
+ }
216
+ if (host.includes(":") && isPrivateIPv6(host)) {
217
+ throw new TypeError(`refused to fetch private / loopback IPv6 address "${host}" (${String(url)}). ` +
218
+ `Set RADAR_FETCH_HOST_ALLOWLIST=${host} to override (testing only).`);
219
+ }
220
+ }
221
+ /**
222
+ * Build a `FetchConfig` from env vars + explicit overrides.
223
+ *
224
+ * Precedence: explicit `options` → env vars → defaults. Negative / non-numeric
225
+ * env values fall through to defaults so a typo never breaks fetching.
226
+ */
227
+ export function resolveFetchConfig(options = {}) {
228
+ const env = options.env ?? process.env;
229
+ const timeoutMs = options.timeoutMs ?? parsePositiveInt(env.RADAR_FETCH_TIMEOUT_MS, DEFAULT_FETCH_TIMEOUT_MS);
230
+ const retries = options.retries ?? parsePositiveInt(env.RADAR_FETCH_RETRIES, DEFAULT_FETCH_RETRIES);
231
+ return {
232
+ timeoutMs,
233
+ retries,
234
+ backoffBaseMs: options.backoffBaseMs ?? DEFAULT_FETCH_BACKOFF_BASE_MS,
235
+ backoffFactor: options.backoffFactor ?? DEFAULT_FETCH_BACKOFF_FACTOR,
236
+ };
237
+ }
238
+ /**
239
+ * Node-style error code we want to retry on. Matches the codes the runtime
240
+ * uses when a connection drops mid-flight or the OS routing layer rejects.
241
+ */
242
+ const TRANSIENT_ERROR_CODES = new Set(["ECONNRESET", "ETIMEDOUT", "ENETUNREACH", "EAI_AGAIN"]);
243
+ /** Whether a thrown error from `fetch` looks like a transient network failure. */
244
+ function isTransientNetworkError(err) {
245
+ if (err == null || typeof err !== "object")
246
+ return false;
247
+ const e = err;
248
+ // Direct Node-style errors (undici occasionally throws `{ code: 'ECONNRESET' }`).
249
+ if (typeof e.code === "string" && TRANSIENT_ERROR_CODES.has(e.code))
250
+ return true;
251
+ // AbortError from our per-attempt timeout — treat as transient so the retry
252
+ // loop gets a chance. Caller-initiated aborts surface differently (their
253
+ // signal already aborted before our `AbortSignal.any` collapsed).
254
+ if (e.name === "TimeoutError" || e.name === "AbortError")
255
+ return true;
256
+ // `fetch` wraps the underlying cause in `TypeError("fetch failed", { cause })`.
257
+ if (e.cause != null)
258
+ return isTransientNetworkError(e.cause);
259
+ return false;
260
+ }
261
+ /** Whether an HTTP status code should trigger a retry (5xx is transient). */
262
+ function isTransientStatus(status) {
263
+ return status >= 500 && status <= 599;
264
+ }
265
+ /** Default sleep used between retries; injectable for tests via options.sleep. */
266
+ function defaultSleep(ms) {
267
+ return new Promise((resolve) => setTimeout(resolve, ms));
268
+ }
269
+ /**
270
+ * Compose two signals: returns a signal that aborts when either input aborts.
271
+ * Uses `AbortSignal.any` (Node 20.3+ / 22+). Falls back to a manual controller
272
+ * for the legacy path (kept defensive — package engines is `>=22` but we still
273
+ * guard so the wrapper does not hard-depend on the static method).
274
+ */
275
+ function composeSignals(...signals) {
276
+ const present = signals.filter((s) => s != null);
277
+ if (present.length === 0)
278
+ return undefined;
279
+ if (present.length === 1)
280
+ return present[0];
281
+ if (typeof AbortSignal.any === "function")
282
+ return AbortSignal.any(present);
283
+ const controller = new AbortController();
284
+ for (const s of present) {
285
+ if (s.aborted) {
286
+ controller.abort(s.reason);
287
+ break;
288
+ }
289
+ s.addEventListener("abort", () => controller.abort(s.reason), { once: true });
290
+ }
291
+ return controller.signal;
292
+ }
293
+ /**
294
+ * Issue an HTTP request via `fetchImpl` with timeout + retry semantics.
295
+ *
296
+ * The caller still owns the request shape (headers / method) and the response
297
+ * body — we only retry the network round-trip. 5xx responses are returned to
298
+ * the retry loop, but on the **final** attempt the response is returned to
299
+ * the caller as-is (so the caller's adapter-specific error message wins).
300
+ *
301
+ * Retries kick in for:
302
+ * - Thrown errors that look transient (ECONNRESET / ETIMEDOUT / ENETUNREACH
303
+ * / EAI_AGAIN, or our own timeout AbortError).
304
+ * - HTTP 5xx responses.
305
+ *
306
+ * Retries do NOT happen for:
307
+ * - HTTP 4xx (permanent — fail fast so users see real config issues).
308
+ * - HTTP 2xx / 3xx (already success / handled by the adapter).
309
+ * - Caller-initiated aborts (we re-throw to surface the cancellation).
310
+ */
311
+ export async function fetchWithRetry(fetchImpl, url, init = {}, options = {}) {
312
+ const config = resolveFetchConfig(options);
313
+ const sleep = options.sleep ?? defaultSleep;
314
+ const callerSignal = options.signal ?? init.signal;
315
+ // SSRF host blocklist (ADR-0009 / ADR-0012 §D5b). Runs *before* the retry
316
+ // loop so a blocked URL fails fast with a single clear error rather than
317
+ // looking like a transient network failure across multiple attempts.
318
+ validateFetchUrl(url, options.env ?? process.env);
319
+ let lastError;
320
+ let lastResponse;
321
+ // attempts = retries + 1; e.g. retries=2 → 3 total attempts.
322
+ const totalAttempts = config.retries + 1;
323
+ for (let attempt = 0; attempt < totalAttempts; attempt++) {
324
+ // Per-attempt timeout. We rebuild the signal each iteration so a slow
325
+ // attempt does not poison the budget for the next one.
326
+ const timeoutSignal = AbortSignal.timeout(config.timeoutMs);
327
+ const signal = composeSignals(callerSignal, timeoutSignal);
328
+ try {
329
+ const response = await fetchImpl(url, { ...init, signal });
330
+ // Retry transient 5xx; permanent 4xx / 2xx / 3xx fall through to the caller.
331
+ if (isTransientStatus(response.status) && attempt < totalAttempts - 1) {
332
+ lastResponse = response;
333
+ await sleep(backoffDelay(attempt, config));
334
+ continue;
335
+ }
336
+ return response;
337
+ }
338
+ catch (err) {
339
+ // Caller-initiated abort short-circuits the retry loop — re-throw so the
340
+ // adapter / consumer can see the cancellation.
341
+ if (callerSignal?.aborted)
342
+ throw err;
343
+ if (isTransientNetworkError(err) && attempt < totalAttempts - 1) {
344
+ lastError = err;
345
+ await sleep(backoffDelay(attempt, config));
346
+ continue;
347
+ }
348
+ throw err;
349
+ }
350
+ }
351
+ // We only reach here if every attempt was a retryable 5xx (no exception
352
+ // was thrown). Surface the last response so the adapter's HTTP-status error
353
+ // message wins (the wrapper does not know each adapter's preferred phrasing).
354
+ if (lastResponse)
355
+ return lastResponse;
356
+ // Defensive — should be unreachable, but if every attempt threw and we
357
+ // exited the loop without returning, re-throw the last error.
358
+ throw lastError ?? new Error("fetchWithRetry: exhausted retries with no response");
359
+ }
360
+ /** Compute the backoff delay for the given attempt index (0-based). */
361
+ function backoffDelay(attemptIndex, config) {
362
+ return config.backoffBaseMs * config.backoffFactor ** attemptIndex;
363
+ }
364
+ //# sourceMappingURL=_fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_fetch.js","sourceRoot":"","sources":["../../../src/core/feeds/_fetch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * JSONPath-lite — minimal expression evaluator for the `kind: json-api` adapter
3
+ * (ADR-0012 §D2).
4
+ *
5
+ * Supports the smallest subset of JSONPath that covers the page-based JSON
6
+ * APIs FeedRadar targets (AWS What's New, dev.to, Anthropic news, …) without
7
+ * bringing in a 30 KB dep:
8
+ *
9
+ * $ — root
10
+ * .field — property access (dotted form)
11
+ * ['field'] — property access (bracket form, single key)
12
+ * [N] — array index (non-negative integer)
13
+ * [*] — array wildcard (returns all elements)
14
+ * .* — object wildcard (returns all values)
15
+ * $.a.b.c — chained property access
16
+ * $.array[*].field — chain after wildcard
17
+ *
18
+ * Out of scope (throws `JsonPathError` at compile time):
19
+ *
20
+ * ..field — recursive descent
21
+ * [?(...)] — filter expressions
22
+ * [N:M] — slicing
23
+ * ['a','b'] — multi-key
24
+ * length() / min() / ... — functions
25
+ *
26
+ * Two evaluation modes:
27
+ *
28
+ * - `selectOne(path, root)`: returns a single value (the first match), or
29
+ * `undefined` when nothing matches. Used for scalar selectors like
30
+ * `selectors.title` / `selectors.url`.
31
+ *
32
+ * - `selectAll(path, root)`: returns an array of all matches. Used for
33
+ * `selectors.items` which is expected to yield the per-item list.
34
+ *
35
+ * Errors:
36
+ *
37
+ * - `JsonPathError` (compile time): syntactically invalid or unsupported.
38
+ * - At runtime, missing properties / out-of-range indices simply return
39
+ * `undefined` / `[]` so recipe authors can selectors-and-shrug their way
40
+ * through optional fields like `summary` / `publishedAt`.
41
+ */
42
+ /** Error thrown when a path expression is syntactically invalid or uses an unsupported feature. */
43
+ export declare class JsonPathError extends Error {
44
+ constructor(message: string);
45
+ }
46
+ /**
47
+ * Return the first match for `path` against `root`, or `undefined` when the
48
+ * path does not match anything. Used by scalar selectors.
49
+ */
50
+ export declare function selectOne(path: string, root: unknown): unknown;
51
+ /**
52
+ * Return all matches for `path` against `root`. Used by `selectors.items` to
53
+ * pick the per-item list. Always returns an array (never `undefined`) so the
54
+ * adapter can iterate without a null check.
55
+ */
56
+ export declare function selectAll(path: string, root: unknown): unknown[];
57
+ //# sourceMappingURL=_jsonpath.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_jsonpath.d.ts","sourceRoot":"","sources":["../../../src/core/feeds/_jsonpath.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,mGAAmG;AACnG,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAI5B;AA6JD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAG9D;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,EAAE,CAEhE"}
@@ -0,0 +1,207 @@
1
+ /**
2
+ * JSONPath-lite — minimal expression evaluator for the `kind: json-api` adapter
3
+ * (ADR-0012 §D2).
4
+ *
5
+ * Supports the smallest subset of JSONPath that covers the page-based JSON
6
+ * APIs FeedRadar targets (AWS What's New, dev.to, Anthropic news, …) without
7
+ * bringing in a 30 KB dep:
8
+ *
9
+ * $ — root
10
+ * .field — property access (dotted form)
11
+ * ['field'] — property access (bracket form, single key)
12
+ * [N] — array index (non-negative integer)
13
+ * [*] — array wildcard (returns all elements)
14
+ * .* — object wildcard (returns all values)
15
+ * $.a.b.c — chained property access
16
+ * $.array[*].field — chain after wildcard
17
+ *
18
+ * Out of scope (throws `JsonPathError` at compile time):
19
+ *
20
+ * ..field — recursive descent
21
+ * [?(...)] — filter expressions
22
+ * [N:M] — slicing
23
+ * ['a','b'] — multi-key
24
+ * length() / min() / ... — functions
25
+ *
26
+ * Two evaluation modes:
27
+ *
28
+ * - `selectOne(path, root)`: returns a single value (the first match), or
29
+ * `undefined` when nothing matches. Used for scalar selectors like
30
+ * `selectors.title` / `selectors.url`.
31
+ *
32
+ * - `selectAll(path, root)`: returns an array of all matches. Used for
33
+ * `selectors.items` which is expected to yield the per-item list.
34
+ *
35
+ * Errors:
36
+ *
37
+ * - `JsonPathError` (compile time): syntactically invalid or unsupported.
38
+ * - At runtime, missing properties / out-of-range indices simply return
39
+ * `undefined` / `[]` so recipe authors can selectors-and-shrug their way
40
+ * through optional fields like `summary` / `publishedAt`.
41
+ */
42
+ /** Error thrown when a path expression is syntactically invalid or uses an unsupported feature. */
43
+ export class JsonPathError extends Error {
44
+ constructor(message) {
45
+ super(message);
46
+ this.name = "JsonPathError";
47
+ }
48
+ }
49
+ /** Tokens that mark an unsupported (out-of-scope) construct. Detected pre-parse. */
50
+ const UNSUPPORTED_PATTERNS = [
51
+ { pattern: /\.\./, message: "recursive descent ('..') is not supported" },
52
+ { pattern: /\[\?/, message: "filter expression ('[?(...)]') is not supported" },
53
+ { pattern: /\[\s*\d+\s*:/, message: "slice ('[N:M]') is not supported" },
54
+ { pattern: /\[\s*['"][^'"]+['"]\s*,/, message: "multi-key ('[a,b]') is not supported" },
55
+ ];
56
+ /**
57
+ * Compile a path expression into an array of steps.
58
+ *
59
+ * The expression must start with `$`. Trailing whitespace is tolerated; any
60
+ * other deviation throws so misconfigured recipes surface at parse time
61
+ * (rather than silently returning empty selectors on every fetch).
62
+ */
63
+ function compile(path) {
64
+ const trimmed = path.trim();
65
+ if (trimmed.length === 0) {
66
+ throw new JsonPathError("path is empty");
67
+ }
68
+ for (const { pattern, message } of UNSUPPORTED_PATTERNS) {
69
+ if (pattern.test(trimmed)) {
70
+ throw new JsonPathError(`jsonpath-lite: ${message} (path: '${path}')`);
71
+ }
72
+ }
73
+ if (trimmed[0] !== "$") {
74
+ throw new JsonPathError(`jsonpath-lite: path must start with '$' (path: '${path}')`);
75
+ }
76
+ const steps = [];
77
+ let i = 1; // past the '$'
78
+ while (i < trimmed.length) {
79
+ const ch = trimmed[i];
80
+ if (ch === ".") {
81
+ i++;
82
+ // Object wildcard: `.*`
83
+ if (trimmed[i] === "*") {
84
+ steps.push({ kind: "wildcard-object" });
85
+ i++;
86
+ continue;
87
+ }
88
+ // Property: `.field` — consume identifier-ish chars.
89
+ const start = i;
90
+ while (i < trimmed.length && /[A-Za-z0-9_-]/.test(trimmed[i] ?? "")) {
91
+ i++;
92
+ }
93
+ if (start === i) {
94
+ throw new JsonPathError(`jsonpath-lite: expected property name after '.' at position ${i} (path: '${path}')`);
95
+ }
96
+ steps.push({ kind: "prop", name: trimmed.slice(start, i) });
97
+ continue;
98
+ }
99
+ if (ch === "[") {
100
+ // Find the matching close bracket.
101
+ const close = trimmed.indexOf("]", i);
102
+ if (close === -1) {
103
+ throw new JsonPathError(`jsonpath-lite: unclosed '[' at position ${i} (path: '${path}')`);
104
+ }
105
+ const inner = trimmed.slice(i + 1, close).trim();
106
+ if (inner === "*") {
107
+ steps.push({ kind: "wildcard-array" });
108
+ }
109
+ else if (/^-?\d+$/.test(inner)) {
110
+ const idx = Number.parseInt(inner, 10);
111
+ if (idx < 0) {
112
+ throw new JsonPathError(`jsonpath-lite: negative index not supported (path: '${path}')`);
113
+ }
114
+ steps.push({ kind: "index", index: idx });
115
+ }
116
+ else if ((inner.startsWith("'") && inner.endsWith("'")) ||
117
+ (inner.startsWith('"') && inner.endsWith('"'))) {
118
+ // Single-key bracket form: ['field'] / ["field"]
119
+ const name = inner.slice(1, -1);
120
+ if (name.length === 0) {
121
+ throw new JsonPathError(`jsonpath-lite: empty bracket key not supported (path: '${path}')`);
122
+ }
123
+ steps.push({ kind: "prop", name });
124
+ }
125
+ else {
126
+ throw new JsonPathError(`jsonpath-lite: unsupported bracket expression '[${inner}]' (path: '${path}')`);
127
+ }
128
+ i = close + 1;
129
+ continue;
130
+ }
131
+ throw new JsonPathError(`jsonpath-lite: unexpected character '${ch}' at position ${i} (path: '${path}')`);
132
+ }
133
+ return steps;
134
+ }
135
+ /** Apply one step to a single value, producing zero or more values. */
136
+ function applyStep(step, value) {
137
+ if (value == null)
138
+ return [];
139
+ switch (step.kind) {
140
+ case "prop": {
141
+ if (typeof value !== "object")
142
+ return [];
143
+ // Arrays do not expose arbitrary string properties for JSON purposes;
144
+ // we restrict to plain objects so `$.items.title` does not accidentally
145
+ // match `Array.prototype.title`.
146
+ if (Array.isArray(value))
147
+ return [];
148
+ const obj = value;
149
+ // `hasOwnProperty` avoids picking up prototype-chain pollution.
150
+ if (!Object.hasOwn(obj, step.name))
151
+ return [];
152
+ return [obj[step.name]];
153
+ }
154
+ case "index": {
155
+ if (!Array.isArray(value))
156
+ return [];
157
+ if (step.index >= value.length)
158
+ return [];
159
+ return [value[step.index]];
160
+ }
161
+ case "wildcard-array": {
162
+ if (!Array.isArray(value))
163
+ return [];
164
+ return value.slice();
165
+ }
166
+ case "wildcard-object": {
167
+ if (typeof value !== "object" || Array.isArray(value))
168
+ return [];
169
+ return Object.values(value);
170
+ }
171
+ }
172
+ }
173
+ /**
174
+ * Walk every compiled step, fanning out at wildcards. Returns the full set
175
+ * of matches in document order.
176
+ */
177
+ function evaluate(path, root) {
178
+ const steps = compile(path);
179
+ let frontier = [root];
180
+ for (const step of steps) {
181
+ const next = [];
182
+ for (const v of frontier) {
183
+ next.push(...applyStep(step, v));
184
+ }
185
+ frontier = next;
186
+ if (frontier.length === 0)
187
+ return [];
188
+ }
189
+ return frontier;
190
+ }
191
+ /**
192
+ * Return the first match for `path` against `root`, or `undefined` when the
193
+ * path does not match anything. Used by scalar selectors.
194
+ */
195
+ export function selectOne(path, root) {
196
+ const matches = evaluate(path, root);
197
+ return matches.length > 0 ? matches[0] : undefined;
198
+ }
199
+ /**
200
+ * Return all matches for `path` against `root`. Used by `selectors.items` to
201
+ * pick the per-item list. Always returns an array (never `undefined`) so the
202
+ * adapter can iterate without a null check.
203
+ */
204
+ export function selectAll(path, root) {
205
+ return evaluate(path, root);
206
+ }
207
+ //# sourceMappingURL=_jsonpath.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_jsonpath.js","sourceRoot":"","sources":["../../../src/core/feeds/_jsonpath.ts"],"names":[],"mappings":"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"}