content,dns: resolve proxy fake IP addresses safely
Use DNS over HTTPS only when a host has only proxy fake IP addresses. Keep private address checks and pinned article connections. Test DNS failures, redirects, and interruption.
This commit is contained in:
1 parent
ce87e56053
commit
3751fd00d3
7 files changed
+526
-6
No files matched your search
@@ -24,3 +24,7 @@ AI_MODELS='[{"id":"gemini-flash","name":"Gemini Flash","provider":"google","mode
|
|||||||
# Set the RSS worker limits.
|
# Set the RSS worker limits.
|
||||||
RSS_POLL_SECONDS=900
|
RSS_POLL_SECONDS=900
|
||||||
RSS_CONCURRENCY=2
|
RSS_CONCURRENCY=2
|
||||||
|
|
||||||
|
# Set the DNS over HTTPS fallback for proxy fake IP addresses.
|
||||||
|
# Set this value to off to disable the fallback.
|
||||||
|
DNS_OVER_HTTPS_URL=https://cloudflare-dns.com/dns-query
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"@hugeicons/core-free-icons": "^4.3.5",
|
"@hugeicons/core-free-icons": "^4.3.5",
|
||||||
"@hugeicons/react": "^1.1.10",
|
"@hugeicons/react": "^1.1.10",
|
||||||
"@mozilla/readability": "^0.6.0",
|
"@mozilla/readability": "^0.6.0",
|
||||||
|
"@multiformats/dns": "^1.0.15",
|
||||||
"@tailwindcss/vite": "^4",
|
"@tailwindcss/vite": "^4",
|
||||||
"@tanstack/react-router": "latest",
|
"@tanstack/react-router": "latest",
|
||||||
"@tanstack/react-start": "latest",
|
"@tanstack/react-start": "latest",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from "node:assert/strict"
|
import assert from "node:assert/strict"
|
||||||
import test from "node:test"
|
import dns from "node:dns/promises"
|
||||||
import { Effect } from "effect"
|
import test, { type TestContext } from "node:test"
|
||||||
|
import { ConfigProvider, Deferred, Effect, Fiber } from "effect"
|
||||||
import { Agent, MockAgent } from "undici"
|
import { Agent, MockAgent } from "undici"
|
||||||
import {
|
import {
|
||||||
canonicalUrl,
|
canonicalUrl,
|
||||||
@@ -10,6 +11,42 @@ import {
|
|||||||
validatePublicUrl,
|
validatePublicUrl,
|
||||||
} from "./content"
|
} from "./content"
|
||||||
|
|
||||||
|
const testUrl = "https://www.eflx.top/blog/ui-composition-first-principle"
|
||||||
|
const dnsEndpoint = "https://cloudflare-dns.com/dns-query"
|
||||||
|
|
||||||
|
function checkUrl(value = testUrl, endpoint = dnsEndpoint) {
|
||||||
|
return validatePublicUrl(value).pipe(
|
||||||
|
Effect.provideService(
|
||||||
|
ConfigProvider.ConfigProvider,
|
||||||
|
ConfigProvider.fromUnknown({ DNS_OVER_HTTPS_URL: endpoint }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function fakeDns(context: TestContext) {
|
||||||
|
return context.mock.method(dns, "lookup", async () => [
|
||||||
|
{ address: "198.18.0.109", family: 4 },
|
||||||
|
{ address: "198.19.255.254", family: 4 },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function dnsAnswer(type: "A" | "AAAA", addresses: string[] = []) {
|
||||||
|
return Response.json({
|
||||||
|
Status: 0,
|
||||||
|
TC: false,
|
||||||
|
Question: [{ name: "www.eflx.top", type: type === "A" ? 1 : 28 }],
|
||||||
|
Answer: [
|
||||||
|
{ name: "www.eflx.top", type: 5, TTL: 300, data: "cname.example.com" },
|
||||||
|
...addresses.map((address) => ({
|
||||||
|
name: "cname.example.com",
|
||||||
|
type: type === "A" ? 1 : 28,
|
||||||
|
TTL: 300,
|
||||||
|
data: address,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
test("article URLs reject local, private, mapped, and reserved addresses", async () => {
|
test("article URLs reject local, private, mapped, and reserved addresses", async () => {
|
||||||
for (const address of [
|
for (const address of [
|
||||||
"0.0.0.0",
|
"0.0.0.0",
|
||||||
@@ -21,6 +58,8 @@ test("article URLs reject local, private, mapped, and reserved addresses", async
|
|||||||
"100.64.0.1",
|
"100.64.0.1",
|
||||||
"224.0.0.1",
|
"224.0.0.1",
|
||||||
"192.0.2.1",
|
"192.0.2.1",
|
||||||
|
"198.18.0.109",
|
||||||
|
"198.19.255.254",
|
||||||
"198.51.100.1",
|
"198.51.100.1",
|
||||||
"203.0.113.1",
|
"203.0.113.1",
|
||||||
"::",
|
"::",
|
||||||
@@ -68,6 +107,260 @@ test("article URLs accept public IPv4 and IPv6 addresses", async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("public DNS answers retain the native route without an HTTPS lookup", async (context) => {
|
||||||
|
const addresses = [
|
||||||
|
{ address: "66.33.60.129", family: 4 },
|
||||||
|
{ address: "2606:4700:4700::1111", family: 6 },
|
||||||
|
]
|
||||||
|
context.mock.method(dns, "lookup", async () => addresses)
|
||||||
|
const remote = context.mock.method(globalThis, "fetch", async () => {
|
||||||
|
throw new Error("A public native answer must not use the fallback.")
|
||||||
|
})
|
||||||
|
const result = await Effect.runPromise(checkUrl(testUrl, "invalid URL"))
|
||||||
|
assert.deepEqual(result.addresses, addresses)
|
||||||
|
assert.equal(remote.mock.callCount(), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("fake DNS uses public HTTPS answers when one address family has no records", async (context) => {
|
||||||
|
fakeDns(context)
|
||||||
|
const questions: string[] = []
|
||||||
|
context.mock.method(
|
||||||
|
globalThis,
|
||||||
|
"fetch",
|
||||||
|
async (...[input, options]: Parameters<typeof globalThis.fetch>) => {
|
||||||
|
const url = new URL(String(input))
|
||||||
|
assert.equal(`${url.origin}${url.pathname}`, dnsEndpoint)
|
||||||
|
assert.equal(url.searchParams.get("name"), "www.eflx.top")
|
||||||
|
assert.ok(options?.signal instanceof AbortSignal)
|
||||||
|
assert.equal(
|
||||||
|
new Headers(options.headers).get("accept"),
|
||||||
|
"application/dns-json",
|
||||||
|
)
|
||||||
|
const type = url.searchParams.get("type")
|
||||||
|
assert.ok(type === "A" || type === "AAAA")
|
||||||
|
questions.push(type)
|
||||||
|
return dnsAnswer(
|
||||||
|
type,
|
||||||
|
type === "A" ? ["66.33.60.129", "66.33.60.130"] : [],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const result = await Effect.runPromise(checkUrl())
|
||||||
|
assert.equal(result.url.toString(), testUrl)
|
||||||
|
assert.deepEqual(questions.sort(), ["A", "AAAA"])
|
||||||
|
assert.deepEqual(result.addresses, [
|
||||||
|
{ address: "66.33.60.129", family: 4 },
|
||||||
|
{ address: "66.33.60.130", family: 4 },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("fake DNS retains both public address families from the HTTPS resolver", async (context) => {
|
||||||
|
fakeDns(context)
|
||||||
|
context.mock.method(
|
||||||
|
globalThis,
|
||||||
|
"fetch",
|
||||||
|
async (...[input]: Parameters<typeof globalThis.fetch>) => {
|
||||||
|
const type = new URL(String(input)).searchParams.get("type")
|
||||||
|
return type === "A"
|
||||||
|
? dnsAnswer("A", ["66.33.60.129"])
|
||||||
|
: dnsAnswer("AAAA", ["2606:4700:4700::1111"])
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const result = await Effect.runPromise(checkUrl())
|
||||||
|
assert.deepEqual(result.addresses, [
|
||||||
|
{ address: "66.33.60.129", family: 4 },
|
||||||
|
{ address: "2606:4700:4700::1111", family: 6 },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("private hosts, IP literals, and mixed DNS answers cannot use the fallback", async (context) => {
|
||||||
|
let addresses = [{ address: "198.18.0.109", family: 4 }]
|
||||||
|
context.mock.method(dns, "lookup", async () => addresses)
|
||||||
|
const remote = context.mock.method(globalThis, "fetch", async () => {
|
||||||
|
throw new Error("An unsafe host must not use the fallback.")
|
||||||
|
})
|
||||||
|
for (const host of [
|
||||||
|
"198.18.0.109",
|
||||||
|
"198.19.255.254",
|
||||||
|
"[::ffff:198.18.0.109]",
|
||||||
|
"localhost",
|
||||||
|
"feed.local",
|
||||||
|
"feed.internal",
|
||||||
|
]) {
|
||||||
|
await assert.rejects(Effect.runPromise(checkUrl(`https://${host}/`)))
|
||||||
|
}
|
||||||
|
for (const values of [
|
||||||
|
["127.0.0.1"],
|
||||||
|
["10.1.2.3"],
|
||||||
|
["198.18.0.109", "10.1.2.3"],
|
||||||
|
["198.18.0.109", "66.33.60.129"],
|
||||||
|
["198.18.0.109", "198.51.100.1"],
|
||||||
|
["66.33.60.129", "127.0.0.1"],
|
||||||
|
]) {
|
||||||
|
addresses = values.map((address) => ({ address, family: 4 }))
|
||||||
|
await assert.rejects(Effect.runPromise(checkUrl()))
|
||||||
|
}
|
||||||
|
assert.equal(remote.mock.callCount(), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("native lookup failures cannot bypass the DNS policy", async (context) => {
|
||||||
|
context.mock.method(dns, "lookup", async () => {
|
||||||
|
throw Object.assign(new Error("No DNS entry"), { code: "ENOTFOUND" })
|
||||||
|
})
|
||||||
|
const remote = context.mock.method(globalThis, "fetch", async () => {
|
||||||
|
throw new Error("A lookup failure must not use the fallback.")
|
||||||
|
})
|
||||||
|
await assert.rejects(Effect.runPromise(checkUrl()), /host could not be found/)
|
||||||
|
assert.equal(remote.mock.callCount(), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("HTTPS resolution rejects failures, incomplete answers, and private addresses", async (context) => {
|
||||||
|
fakeDns(context)
|
||||||
|
const remote = context.mock.method(globalThis, "fetch", async () =>
|
||||||
|
dnsAnswer("A", ["66.33.60.129"]),
|
||||||
|
)
|
||||||
|
const invalidResponses = [
|
||||||
|
() => new Response("Unavailable", { status: 503 }),
|
||||||
|
() => Response.json({ Status: 3, TC: false, Answer: [] }),
|
||||||
|
() => Response.json({ Status: 0, TC: true, Answer: [] }),
|
||||||
|
() => dnsAnswer("A"),
|
||||||
|
() => dnsAnswer("A", ["66.33.60.129", "127.0.0.1"]),
|
||||||
|
() => dnsAnswer("A", ["198.18.0.109"]),
|
||||||
|
() => dnsAnswer("AAAA", ["::ffff:127.0.0.1"]),
|
||||||
|
() => dnsAnswer("A", ["invalid address"]),
|
||||||
|
() => new Response("invalid JSON", { status: 200 }),
|
||||||
|
]
|
||||||
|
for (const response of invalidResponses) {
|
||||||
|
remote.mock.mockImplementation(async () => response())
|
||||||
|
await assert.rejects(Effect.runPromise(checkUrl()))
|
||||||
|
}
|
||||||
|
remote.mock.mockImplementation(async (input) => {
|
||||||
|
const type = new URL(String(input)).searchParams.get("type")
|
||||||
|
return type === "A"
|
||||||
|
? dnsAnswer("A", ["66.33.60.129"])
|
||||||
|
: dnsAnswer("AAAA", ["::1"])
|
||||||
|
})
|
||||||
|
await assert.rejects(Effect.runPromise(checkUrl()))
|
||||||
|
remote.mock.mockImplementation(async () => {
|
||||||
|
throw new Error("The resolver connection failed.")
|
||||||
|
})
|
||||||
|
await assert.rejects(Effect.runPromise(checkUrl()))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("HTTPS resolution can be disabled and requires a valid trusted endpoint", async (context) => {
|
||||||
|
fakeDns(context)
|
||||||
|
const remote = context.mock.method(globalThis, "fetch", async () => {
|
||||||
|
throw new Error("An invalid endpoint must not receive a query.")
|
||||||
|
})
|
||||||
|
for (const endpoint of [
|
||||||
|
"off",
|
||||||
|
"invalid URL",
|
||||||
|
"http://cloudflare-dns.com/dns-query",
|
||||||
|
"https://user:secret@cloudflare-dns.com/dns-query",
|
||||||
|
"https://cloudflare-dns.com/dns-query?name=other.example",
|
||||||
|
"https://cloudflare-dns.com/dns-query#fragment",
|
||||||
|
]) {
|
||||||
|
await assert.rejects(Effect.runPromise(checkUrl(testUrl, endpoint)))
|
||||||
|
}
|
||||||
|
assert.equal(remote.mock.callCount(), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("interrupting HTTPS resolution cancels both address queries", {
|
||||||
|
timeout: 3_000,
|
||||||
|
}, async (context) => {
|
||||||
|
fakeDns(context)
|
||||||
|
const started = Deferred.makeUnsafe<void>()
|
||||||
|
let starts = 0
|
||||||
|
let cancellations = 0
|
||||||
|
context.mock.method(
|
||||||
|
globalThis,
|
||||||
|
"fetch",
|
||||||
|
(...[_input, options]: Parameters<typeof globalThis.fetch>) => {
|
||||||
|
const signal = options?.signal
|
||||||
|
assert.ok(signal instanceof AbortSignal)
|
||||||
|
return new Promise<Response>((_resolve, reject) => {
|
||||||
|
signal.addEventListener(
|
||||||
|
"abort",
|
||||||
|
() => {
|
||||||
|
cancellations++
|
||||||
|
reject(signal.reason)
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
)
|
||||||
|
if (++starts === 2) Effect.runSync(Deferred.succeed(started, undefined))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const fiber = Effect.runFork(checkUrl())
|
||||||
|
context.after(() => Effect.runPromise(Fiber.interrupt(fiber)))
|
||||||
|
await Effect.runPromise(Deferred.await(started))
|
||||||
|
await Effect.runPromise(Fiber.interrupt(fiber))
|
||||||
|
assert.equal(cancellations, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("an HTTPS resolver failure cancels the other address query", {
|
||||||
|
timeout: 3_000,
|
||||||
|
}, async (context) => {
|
||||||
|
fakeDns(context)
|
||||||
|
let starts = 0
|
||||||
|
let failQuery: (() => void) | undefined
|
||||||
|
let cancelled = false
|
||||||
|
context.mock.method(
|
||||||
|
globalThis,
|
||||||
|
"fetch",
|
||||||
|
(...[input, options]: Parameters<typeof globalThis.fetch>) => {
|
||||||
|
const type = new URL(String(input)).searchParams.get("type")
|
||||||
|
const signal = options?.signal
|
||||||
|
assert.ok(signal instanceof AbortSignal)
|
||||||
|
return new Promise<Response>((resolve, reject) => {
|
||||||
|
if (type === "A")
|
||||||
|
failQuery = () =>
|
||||||
|
resolve(new Response("Unavailable", { status: 503 }))
|
||||||
|
signal.addEventListener(
|
||||||
|
"abort",
|
||||||
|
() => {
|
||||||
|
if (type === "AAAA") cancelled = true
|
||||||
|
reject(signal.reason)
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
)
|
||||||
|
if (++starts === 2) failQuery?.()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await assert.rejects(Effect.runPromise(checkUrl()))
|
||||||
|
assert.equal(cancelled, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("a public HTTPS DNS answer cannot permit a redirect to a private host", async (context) => {
|
||||||
|
fakeDns(context)
|
||||||
|
const resolver = context.mock.method(
|
||||||
|
globalThis,
|
||||||
|
"fetch",
|
||||||
|
async (...[input]: Parameters<typeof globalThis.fetch>) => {
|
||||||
|
const type = new URL(String(input)).searchParams.get("type")
|
||||||
|
return type === "A" ? dnsAnswer("A", ["66.33.60.129"]) : dnsAnswer("AAAA")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const mock = new MockAgent()
|
||||||
|
mock.disableNetConnect()
|
||||||
|
context.mock.method(
|
||||||
|
Agent.prototype,
|
||||||
|
"dispatch",
|
||||||
|
(...[options, handler]: Parameters<Agent["dispatch"]>) =>
|
||||||
|
mock.get(String(options.origin)).dispatch(options, handler),
|
||||||
|
)
|
||||||
|
context.after(() => mock.close())
|
||||||
|
mock
|
||||||
|
.get("https://www.eflx.top")
|
||||||
|
.intercept({ path: "/blog/ui-composition-first-principle", method: "GET" })
|
||||||
|
.reply(302, "", {
|
||||||
|
headers: { location: "http://169.254.169.254/latest/meta-data/" },
|
||||||
|
})
|
||||||
|
await assert.rejects(Effect.runPromise(fetchDocument(testUrl)), /public host/)
|
||||||
|
assert.equal(resolver.mock.callCount(), 2)
|
||||||
|
})
|
||||||
|
|
||||||
test("URL normalization retains article parameters and removes tracking", () => {
|
test("URL normalization retains article parameters and removes tracking", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
canonicalUrl(
|
canonicalUrl(
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { lookup } from "node:dns/promises"
|
import dns from "node:dns/promises"
|
||||||
import { isIP } from "node:net"
|
import { isIP } from "node:net"
|
||||||
import { Readability } from "@mozilla/readability"
|
import { Readability } from "@mozilla/readability"
|
||||||
import { Effect } from "effect"
|
import { RecordType } from "@multiformats/dns"
|
||||||
|
import { dnsJsonOverHttps } from "@multiformats/dns/resolvers"
|
||||||
|
import { Config, Effect } from "effect"
|
||||||
import ipaddr from "ipaddr.js"
|
import ipaddr from "ipaddr.js"
|
||||||
import { parseHTML } from "linkedom"
|
import { parseHTML } from "linkedom"
|
||||||
import { Agent, fetch } from "undici"
|
import { Agent, fetch } from "undici"
|
||||||
@@ -10,6 +12,7 @@ import { AppError } from "./errors"
|
|||||||
const MAX_BYTES = 2_000_000
|
const MAX_BYTES = 2_000_000
|
||||||
const MAX_TEXT = 60_000
|
const MAX_TEXT = 60_000
|
||||||
const NETWORK_TIMEOUT = 20_000
|
const NETWORK_TIMEOUT = 20_000
|
||||||
|
const FAKE_IP_RANGE = ipaddr.IPv4.parseCIDR("198.18.0.0/15")
|
||||||
const redirects = new Set([301, 302, 303, 307, 308])
|
const redirects = new Set([301, 302, 303, 307, 308])
|
||||||
|
|
||||||
export function isPublicAddress(address: string): boolean {
|
export function isPublicAddress(address: string): boolean {
|
||||||
@@ -51,6 +54,81 @@ export function canonicalUrl(value: string): string {
|
|||||||
return url.toString()
|
return url.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolveProxyHostname = Effect.fn("content.resolveProxyHostname")(
|
||||||
|
function* (hostname: string) {
|
||||||
|
const endpoint = yield* Config.String("DNS_OVER_HTTPS_URL").pipe(
|
||||||
|
Config.withDefault("https://cloudflare-dns.com/dns-query"),
|
||||||
|
)
|
||||||
|
if (endpoint === "off") return []
|
||||||
|
const url = yield* Effect.try({
|
||||||
|
try: () => {
|
||||||
|
const url = new URL(endpoint)
|
||||||
|
if (
|
||||||
|
url.protocol !== "https:" ||
|
||||||
|
url.username ||
|
||||||
|
url.password ||
|
||||||
|
url.search ||
|
||||||
|
url.hash
|
||||||
|
)
|
||||||
|
throw new Error(
|
||||||
|
"Use an HTTPS DNS endpoint without credentials or parameters.",
|
||||||
|
)
|
||||||
|
return url
|
||||||
|
},
|
||||||
|
catch: (cause) =>
|
||||||
|
new AppError({
|
||||||
|
message: "Set DNS_OVER_HTTPS_URL to a valid HTTPS DNS endpoint.",
|
||||||
|
status: 500,
|
||||||
|
cause,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const resolve = dnsJsonOverHttps(url.toString())
|
||||||
|
const responses = yield* Effect.forEach(
|
||||||
|
[RecordType.A, RecordType.AAAA],
|
||||||
|
(type) =>
|
||||||
|
Effect.tryPromise({
|
||||||
|
try: (signal) => resolve(hostname, { types: type, signal }),
|
||||||
|
catch: (cause) =>
|
||||||
|
new AppError({
|
||||||
|
message: "The public DNS lookup failed. Try again.",
|
||||||
|
status: 502,
|
||||||
|
cause,
|
||||||
|
}),
|
||||||
|
}).pipe(
|
||||||
|
Effect.filterOrFail(
|
||||||
|
(response) => response.Status === 0 && !response.TC,
|
||||||
|
() =>
|
||||||
|
new AppError({
|
||||||
|
message: "The public DNS lookup failed. Try again.",
|
||||||
|
status: 502,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
{ concurrency: 2 },
|
||||||
|
).pipe(
|
||||||
|
Effect.timeout("10 seconds"),
|
||||||
|
Effect.mapError((cause) =>
|
||||||
|
cause instanceof AppError
|
||||||
|
? cause
|
||||||
|
: new AppError({
|
||||||
|
message: "The public DNS lookup timed out. Try again.",
|
||||||
|
status: 502,
|
||||||
|
cause,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return responses.flatMap((response) =>
|
||||||
|
response.Answer.flatMap((answer) => {
|
||||||
|
// The resolver normalizes numeric DNS record types to their names.
|
||||||
|
const type = String(answer.type)
|
||||||
|
return ["A", "AAAA", "1", "28"].includes(type)
|
||||||
|
? [{ address: answer.data, family: isIP(answer.data) }]
|
||||||
|
: []
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export const validatePublicUrl = Effect.fn("content.validatePublicUrl")(
|
export const validatePublicUrl = Effect.fn("content.validatePublicUrl")(
|
||||||
function* (value: string) {
|
function* (value: string) {
|
||||||
const url = yield* Effect.try({
|
const url = yield* Effect.try({
|
||||||
@@ -73,11 +151,11 @@ export const validatePublicUrl = Effect.fn("content.validatePublicUrl")(
|
|||||||
status: 400,
|
status: 400,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const addresses = yield* Effect.tryPromise({
|
let addresses = yield* Effect.tryPromise({
|
||||||
try: () =>
|
try: () =>
|
||||||
isIP(hostname)
|
isIP(hostname)
|
||||||
? Promise.resolve([{ address: hostname, family: isIP(hostname) }])
|
? Promise.resolve([{ address: hostname, family: isIP(hostname) }])
|
||||||
: lookup(hostname, { all: true, verbatim: true }),
|
: dns.lookup(hostname, { all: true, verbatim: true }),
|
||||||
catch: (cause) =>
|
catch: (cause) =>
|
||||||
new AppError({
|
new AppError({
|
||||||
message: "The URL host could not be found.",
|
message: "The URL host could not be found.",
|
||||||
@@ -96,6 +174,17 @@ export const validatePublicUrl = Effect.fn("content.validatePublicUrl")(
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
!isIP(hostname) &&
|
||||||
|
addresses.length > 0 &&
|
||||||
|
addresses.every(
|
||||||
|
({ address }) =>
|
||||||
|
isIP(address) === 4 &&
|
||||||
|
ipaddr.IPv4.parse(address).match(FAKE_IP_RANGE),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
addresses = yield* resolveProxyHostname(hostname)
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!addresses.length ||
|
!addresses.length ||
|
||||||
addresses.some(({ address }) => !isPublicAddress(address))
|
addresses.some(({ address }) => !isPublicAddress(address))
|
||||||
|
|||||||
@@ -67,6 +67,20 @@ Provider keys remain on the server.
|
|||||||
Change these values to control RSS request volume.
|
Change these values to control RSS request volume.
|
||||||
The worker uses each user's stored preferences to select articles.
|
The worker uses each user's stored preferences to select articles.
|
||||||
|
|
||||||
|
## DNS lookup
|
||||||
|
|
||||||
|
The application uses the host's DNS settings for normal lookups.
|
||||||
|
Some network proxies return addresses in `198.18.0.0/15`.
|
||||||
|
These are called fake IP addresses.
|
||||||
|
Article and RSS connections do not use these addresses.
|
||||||
|
For a host with only these addresses, Stellar uses a DNS over HTTPS (DoH) resolver.
|
||||||
|
|
||||||
|
The default resolver is `https://cloudflare-dns.com/dns-query`.
|
||||||
|
Set `DNS_OVER_HTTPS_URL` to use another HTTPS DNS JSON endpoint.
|
||||||
|
Set `DNS_OVER_HTTPS_URL=off` to disable this fallback.
|
||||||
|
Stellar checks IPv4 and IPv6 answers and rejects private addresses.
|
||||||
|
It uses only checked addresses for article and RSS connections.
|
||||||
|
|
||||||
## Direct server values
|
## Direct server values
|
||||||
|
|
||||||
Compose supplies these values to the application and worker.
|
Compose supplies these values to the application and worker.
|
||||||
|
|||||||
Generated
+118
@@ -53,6 +53,9 @@ importers:
|
|||||||
'@mozilla/readability':
|
'@mozilla/readability':
|
||||||
specifier: ^0.6.0
|
specifier: ^0.6.0
|
||||||
version: 0.6.0
|
version: 0.6.0
|
||||||
|
'@multiformats/dns':
|
||||||
|
specifier: ^1.0.15
|
||||||
|
version: 1.0.15
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4
|
specifier: ^4
|
||||||
version: 4.3.0(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15))
|
version: 4.3.0(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15))
|
||||||
@@ -570,9 +573,16 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@chainsafe/is-ip@2.1.0':
|
||||||
|
resolution: {integrity: sha512-KIjt+6IfysQ4GCv66xihEitBjvhU/bixbbbFxdJ1sqCp4uJ0wuZiYBPhksZoy4lfaF0k9cwNzY5upEW/VWdw3w==}
|
||||||
|
|
||||||
'@date-fns/tz@1.5.0':
|
'@date-fns/tz@1.5.0':
|
||||||
resolution: {integrity: sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==}
|
resolution: {integrity: sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==}
|
||||||
|
|
||||||
|
'@dnsquery/dns-packet@6.1.1':
|
||||||
|
resolution: {integrity: sha512-WXTuFvL3G+74SchFAtz3FgIYVOe196ycvGsMgvSH/8Goptb1qpIQtIuM4SOK9G9lhMWYpHxnXyy544ZhluFOew==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
'@dotenvx/dotenvx@1.75.1':
|
'@dotenvx/dotenvx@1.75.1':
|
||||||
resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==}
|
resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -1437,6 +1447,12 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
|
'@leichtgewicht/ip-codec@2.0.5':
|
||||||
|
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
|
||||||
|
|
||||||
|
'@libp2p/interface@3.3.0':
|
||||||
|
resolution: {integrity: sha512-SXahM/4IgpiFKTtocbXYSTA1wEUVjktmT1yBCzBbc2Vgsu1VBCg6SvsFmXo2Hbeyev3D8EU+y3uaCKDB43C/Vg==}
|
||||||
|
|
||||||
'@modelcontextprotocol/sdk@1.32.0':
|
'@modelcontextprotocol/sdk@1.32.0':
|
||||||
resolution: {integrity: sha512-8BviX/hK4Gd2eL1KdTwm0gfl4d3wdoErW0Jd00h6nomUAtk6IJk5vwQJc6kfWtJTzN5OEV/lgIiJKI8fiDGAlA==}
|
resolution: {integrity: sha512-8BviX/hK4Gd2eL1KdTwm0gfl4d3wdoErW0Jd00h6nomUAtk6IJk5vwQJc6kfWtJTzN5OEV/lgIiJKI8fiDGAlA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1451,6 +1467,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==}
|
resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
'@multiformats/dns@1.0.15':
|
||||||
|
resolution: {integrity: sha512-W0zAMABtAn+3chgFcPGvllKND7M6GblMAAFcJQTy+iMmGiyFErZYPzAh2b50Y3SFf340iFV7+ckVgZkGfUyxzA==}
|
||||||
|
|
||||||
|
'@multiformats/multiaddr@13.0.3':
|
||||||
|
resolution: {integrity: sha512-mEqqJ4r3a/uuFMTpRkU316wGNIDQNhuVWpm+ebKTQeYsfv9jXbPONWM6VVnj3KGUrwfsX7GZOyp4TFqEA2SPCw==}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.1.5':
|
'@napi-rs/wasm-runtime@1.1.5':
|
||||||
resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==}
|
resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3360,6 +3382,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
hashlru@2.3.0:
|
||||||
|
resolution: {integrity: sha512-0cMsjjIC8I+D3M44pOQdsy0OHXGLVz6Z0beRuufhKa0KfaD2wGwAev6jILzXsd3/vpnNQJmWyZtIILqM1N+n5A==}
|
||||||
|
|
||||||
hasown@2.0.4:
|
hasown@2.0.4:
|
||||||
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
|
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3786,6 +3811,9 @@ packages:
|
|||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
|
main-event@1.0.5:
|
||||||
|
resolution: {integrity: sha512-4l9z8r7Q446mhhVTwdHmfPksOYBwN2xa7ewEW2yxPVNQapIaAf57ORFLMB91SpMEMa4wXowect7fq3uMLjDnGA==}
|
||||||
|
|
||||||
markdown-table@3.0.4:
|
markdown-table@3.0.4:
|
||||||
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||||
|
|
||||||
@@ -3974,6 +4002,9 @@ packages:
|
|||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
multiformats@14.0.5:
|
||||||
|
resolution: {integrity: sha512-vbIm83F2yZ1pWJGS0yl0ysracIvv56LtbrIyiIQHoLdYDJOMoLfVFsXhh9DUH4SFdkdkFhucyWniihsNzVEjkQ==}
|
||||||
|
|
||||||
mute-stream@2.0.0:
|
mute-stream@2.0.0:
|
||||||
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
|
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
@@ -4065,6 +4096,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
|
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
p-queue@9.3.3:
|
||||||
|
resolution: {integrity: sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
p-timeout@7.0.2:
|
||||||
|
resolution: {integrity: sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
p-try@2.2.0:
|
p-try@2.2.0:
|
||||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -4163,6 +4202,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==}
|
resolution: {integrity: sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
progress-events@1.1.0:
|
||||||
|
resolution: {integrity: sha512-82DVc5tI36neVB3IjdXR11ztwGuoBc98em9ijzubeZKxI47OlV2Znq6mlPqE5xPDzO2Uw98GHiQSjj2favBCRQ==}
|
||||||
|
|
||||||
prompts@2.4.2:
|
prompts@2.4.2:
|
||||||
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -4637,6 +4679,15 @@ packages:
|
|||||||
uhyphen@0.2.0:
|
uhyphen@0.2.0:
|
||||||
resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==}
|
resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==}
|
||||||
|
|
||||||
|
uint8-varint@3.0.0:
|
||||||
|
resolution: {integrity: sha512-S4DdpXBaLwKcFo7f0bWzWfHjbZ/i3QhM842qn+ZvHjxqFCfUcEB9SQNcmI69S+zMlcmIcKxsk9Iyw77S2Kxv6Q==}
|
||||||
|
|
||||||
|
uint8arraylist@3.0.2:
|
||||||
|
resolution: {integrity: sha512-LDVoq9BQaGJzGDUovEnoX6rpKCvnY/Jbtws4ikwnBzjRbq5qBAFpBZevUEbSmMM87aO0Sp+wOZy2ZXf5yODmXQ==}
|
||||||
|
|
||||||
|
uint8arrays@6.1.1:
|
||||||
|
resolution: {integrity: sha512-iz7JN0XCSZYA111lhFG2Ui9EhFvTNekqSRHw3lvMHq+dzwWy1OQftxFQREEh4rffU0oSoXdQHsk2TiHKVm4fsA==}
|
||||||
|
|
||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
@@ -4751,6 +4802,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
|
utf8-codec@1.0.0:
|
||||||
|
resolution: {integrity: sha512-S/QSLezp3qvG4ld5PUfXiH7mCFxLKjSVZRFkB3DOjgwHuJPFDkInAXc/anf7BAbHt/D38ozDzL+QMZ6/7gsI6w==}
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
@@ -5285,8 +5339,15 @@ snapshots:
|
|||||||
'@biomejs/cli-win32-x64@2.5.15':
|
'@biomejs/cli-win32-x64@2.5.15':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@chainsafe/is-ip@2.1.0': {}
|
||||||
|
|
||||||
'@date-fns/tz@1.5.0': {}
|
'@date-fns/tz@1.5.0': {}
|
||||||
|
|
||||||
|
'@dnsquery/dns-packet@6.1.1':
|
||||||
|
dependencies:
|
||||||
|
'@leichtgewicht/ip-codec': 2.0.5
|
||||||
|
utf8-codec: 1.0.0
|
||||||
|
|
||||||
'@dotenvx/dotenvx@1.75.1':
|
'@dotenvx/dotenvx@1.75.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@dotenvx/primitives': 0.8.0
|
'@dotenvx/primitives': 0.8.0
|
||||||
@@ -5861,6 +5922,17 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@leichtgewicht/ip-codec@2.0.5': {}
|
||||||
|
|
||||||
|
'@libp2p/interface@3.3.0':
|
||||||
|
dependencies:
|
||||||
|
'@multiformats/dns': 1.0.15
|
||||||
|
'@multiformats/multiaddr': 13.0.3
|
||||||
|
main-event: 1.0.5
|
||||||
|
multiformats: 14.0.5
|
||||||
|
progress-events: 1.1.0
|
||||||
|
uint8arraylist: 3.0.2
|
||||||
|
|
||||||
'@modelcontextprotocol/sdk@1.32.0(supports-color@7.2.0)(zod@3.25.76)':
|
'@modelcontextprotocol/sdk@1.32.0(supports-color@7.2.0)(zod@3.25.76)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@hono/node-server': 2.1.3(hono@4.13.12)
|
'@hono/node-server': 2.1.3(hono@4.13.12)
|
||||||
@@ -5885,6 +5957,22 @@ snapshots:
|
|||||||
|
|
||||||
'@mozilla/readability@0.6.0': {}
|
'@mozilla/readability@0.6.0': {}
|
||||||
|
|
||||||
|
'@multiformats/dns@1.0.15':
|
||||||
|
dependencies:
|
||||||
|
'@dnsquery/dns-packet': 6.1.1
|
||||||
|
'@libp2p/interface': 3.3.0
|
||||||
|
hashlru: 2.3.0
|
||||||
|
p-queue: 9.3.3
|
||||||
|
progress-events: 1.1.0
|
||||||
|
uint8arrays: 6.1.1
|
||||||
|
|
||||||
|
'@multiformats/multiaddr@13.0.3':
|
||||||
|
dependencies:
|
||||||
|
'@chainsafe/is-ip': 2.1.0
|
||||||
|
multiformats: 14.0.5
|
||||||
|
uint8-varint: 3.0.0
|
||||||
|
uint8arrays: 6.1.1
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
|
'@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/core': 1.10.0
|
'@emnapi/core': 1.10.0
|
||||||
@@ -7572,6 +7660,8 @@ snapshots:
|
|||||||
|
|
||||||
has-symbols@1.1.0: {}
|
has-symbols@1.1.0: {}
|
||||||
|
|
||||||
|
hashlru@2.3.0: {}
|
||||||
|
|
||||||
hasown@2.0.4:
|
hasown@2.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
@@ -7892,6 +7982,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
main-event@1.0.5: {}
|
||||||
|
|
||||||
markdown-table@3.0.4: {}
|
markdown-table@3.0.4: {}
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
@@ -8281,6 +8373,8 @@ snapshots:
|
|||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
multiformats@14.0.5: {}
|
||||||
|
|
||||||
mute-stream@2.0.0: {}
|
mute-stream@2.0.0: {}
|
||||||
|
|
||||||
nanoid@3.3.19: {}
|
nanoid@3.3.19: {}
|
||||||
@@ -8383,6 +8477,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-limit: 2.3.0
|
p-limit: 2.3.0
|
||||||
|
|
||||||
|
p-queue@9.3.3:
|
||||||
|
dependencies:
|
||||||
|
eventemitter3: 5.0.4
|
||||||
|
p-timeout: 7.0.2
|
||||||
|
|
||||||
|
p-timeout@7.0.2: {}
|
||||||
|
|
||||||
p-try@2.2.0: {}
|
p-try@2.2.0: {}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
@@ -8461,6 +8562,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
parse-ms: 4.0.0
|
parse-ms: 4.0.0
|
||||||
|
|
||||||
|
progress-events@1.1.0: {}
|
||||||
|
|
||||||
prompts@2.4.2:
|
prompts@2.4.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
kleur: 3.0.3
|
kleur: 3.0.3
|
||||||
@@ -9055,6 +9158,19 @@ snapshots:
|
|||||||
|
|
||||||
uhyphen@0.2.0: {}
|
uhyphen@0.2.0: {}
|
||||||
|
|
||||||
|
uint8-varint@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
uint8arraylist: 3.0.2
|
||||||
|
uint8arrays: 6.1.1
|
||||||
|
|
||||||
|
uint8arraylist@3.0.2:
|
||||||
|
dependencies:
|
||||||
|
uint8arrays: 6.1.1
|
||||||
|
|
||||||
|
uint8arrays@6.1.1:
|
||||||
|
dependencies:
|
||||||
|
multiformats: 14.0.5
|
||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
undici@7.30.0: {}
|
undici@7.30.0: {}
|
||||||
@@ -9141,6 +9257,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.3.0
|
react: 19.3.0
|
||||||
|
|
||||||
|
utf8-codec@1.0.0: {}
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
validate-npm-package-name@7.0.2: {}
|
validate-npm-package-name@7.0.2: {}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"ANTHROPIC_API_KEY",
|
"ANTHROPIC_API_KEY",
|
||||||
"RSS_POLL_SECONDS",
|
"RSS_POLL_SECONDS",
|
||||||
"RSS_CONCURRENCY",
|
"RSS_CONCURRENCY",
|
||||||
|
"DNS_OVER_HTTPS_URL",
|
||||||
"TEST_APP_ORIGIN",
|
"TEST_APP_ORIGIN",
|
||||||
"TEST_BASE_URL",
|
"TEST_BASE_URL",
|
||||||
"TEST_ADMIN_PASSWORD",
|
"TEST_ADMIN_PASSWORD",
|
||||||
|
|||||||
Reference in new issue
Block a user