nemoris 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +49 -49
- package/LICENSE +21 -21
- package/README.md +209 -209
- package/SECURITY.md +59 -119
- package/bin/nemoris +46 -46
- package/config/agents/agent.toml.example +28 -28
- package/config/agents/content.toml +23 -0
- package/config/agents/default.toml +22 -22
- package/config/agents/heartbeat.toml +35 -0
- package/config/agents/iris.toml +23 -0
- package/config/agents/lab.toml +23 -0
- package/config/agents/main.toml +45 -0
- package/config/agents/nemo.toml +21 -0
- package/config/agents/ops.toml +38 -0
- package/config/agents/orchestrator.toml +18 -18
- package/config/agents/revenue.toml +23 -0
- package/config/agents/testyboo.toml +19 -0
- package/config/delivery.toml +73 -73
- package/config/embeddings.toml +5 -5
- package/config/identity/content-purpose.md +11 -0
- package/config/identity/content-soul.md +45 -0
- package/config/identity/default-purpose.md +1 -1
- package/config/identity/default-soul.md +3 -3
- package/config/identity/heartbeat-purpose.md +9 -0
- package/config/identity/heartbeat-soul.md +16 -0
- package/config/identity/iris-purpose.md +17 -0
- package/config/identity/iris-soul.md +68 -0
- package/config/identity/lab-purpose.md +10 -0
- package/config/identity/lab-soul.md +38 -0
- package/config/identity/main-purpose.md +17 -0
- package/config/identity/main-soul.md +66 -0
- package/config/identity/main-user.md +22 -0
- package/config/identity/ops-purpose.md +9 -0
- package/config/identity/ops-soul.md +16 -0
- package/config/identity/orchestrator-purpose.md +1 -1
- package/config/identity/orchestrator-soul.md +1 -1
- package/config/identity/revenue-purpose.md +9 -0
- package/config/identity/revenue-soul.md +41 -0
- package/config/identity/testyboo-purpose.md +13 -0
- package/config/identity/testyboo-soul.md +20 -0
- package/config/improvement-targets.toml +15 -15
- package/config/jobs/heartbeat-check.toml +30 -30
- package/config/jobs/memory-rollup.toml +46 -46
- package/config/jobs/workspace-health.toml +63 -63
- package/config/mcp.toml +16 -16
- package/config/output-contracts.toml +17 -17
- package/config/peers.toml +32 -32
- package/config/peers.toml.example +32 -32
- package/config/policies/memory-default.toml +10 -10
- package/config/policies/memory-heartbeat.toml +5 -5
- package/config/policies/memory-ops.toml +10 -10
- package/config/policies/tools-heartbeat-minimal.toml +8 -8
- package/config/policies/tools-interactive-safe.toml +8 -8
- package/config/policies/tools-ops-bounded.toml +8 -8
- package/config/policies/tools-orchestrator.toml +7 -7
- package/config/providers/anthropic.toml +15 -15
- package/config/providers/ollama.toml +5 -5
- package/config/providers/openai-codex.toml +9 -9
- package/config/providers/openrouter.toml +5 -5
- package/config/router.toml +22 -22
- package/config/runtime.toml +114 -114
- package/config/skills/self-improvement.toml +15 -15
- package/config/skills/telegram-onboarding-spec.md +240 -240
- package/config/skills/workspace-monitor.toml +15 -15
- package/config/task-router.toml +42 -42
- package/install.sh +50 -50
- package/package.json +91 -90
- package/src/auth/auth-profiles.js +169 -169
- package/src/auth/openai-codex-oauth.js +285 -285
- package/src/battle.js +449 -449
- package/src/cli/help.js +265 -265
- package/src/cli/output-filter.js +49 -49
- package/src/cli/runtime-control.js +704 -704
- package/src/cli-main.js +2763 -2763
- package/src/cli.js +78 -78
- package/src/config/loader.js +332 -332
- package/src/config/schema-validator.js +214 -214
- package/src/config/toml-lite.js +8 -8
- package/src/daemon/action-handlers.js +71 -71
- package/src/daemon/healing-tick.js +87 -87
- package/src/daemon/health-probes.js +90 -90
- package/src/daemon/notifier.js +57 -57
- package/src/daemon/nurse.js +218 -218
- package/src/daemon/repair-log.js +106 -106
- package/src/daemon/rule-staging.js +90 -90
- package/src/daemon/rules.js +29 -29
- package/src/daemon/telegram-commands.js +54 -54
- package/src/daemon/updater.js +85 -85
- package/src/jobs/job-runner.js +78 -78
- package/src/mcp/consumer.js +129 -129
- package/src/memory/active-recall.js +171 -171
- package/src/memory/backend-manager.js +97 -97
- package/src/memory/backends/file-backend.js +38 -38
- package/src/memory/backends/qmd-backend.js +219 -219
- package/src/memory/embedding-guards.js +24 -24
- package/src/memory/embedding-index.js +118 -118
- package/src/memory/embedding-service.js +179 -179
- package/src/memory/file-index.js +177 -177
- package/src/memory/memory-signature.js +5 -5
- package/src/memory/memory-store.js +648 -648
- package/src/memory/retrieval-planner.js +66 -66
- package/src/memory/scoring.js +145 -145
- package/src/memory/simhash.js +78 -78
- package/src/memory/sqlite-active-store.js +824 -824
- package/src/memory/write-policy.js +36 -36
- package/src/onboarding/aliases.js +33 -33
- package/src/onboarding/auth/api-key.js +224 -224
- package/src/onboarding/auth/ollama-detect.js +42 -42
- package/src/onboarding/clack-prompter.js +77 -77
- package/src/onboarding/doctor.js +530 -530
- package/src/onboarding/lock.js +42 -42
- package/src/onboarding/model-catalog.js +344 -344
- package/src/onboarding/phases/auth.js +576 -589
- package/src/onboarding/phases/build.js +130 -130
- package/src/onboarding/phases/choose.js +82 -82
- package/src/onboarding/phases/detect.js +98 -98
- package/src/onboarding/phases/hatch.js +216 -216
- package/src/onboarding/phases/identity.js +79 -79
- package/src/onboarding/phases/ollama.js +345 -345
- package/src/onboarding/phases/scaffold.js +99 -99
- package/src/onboarding/phases/telegram.js +377 -377
- package/src/onboarding/phases/validate.js +204 -204
- package/src/onboarding/phases/verify.js +206 -206
- package/src/onboarding/platform.js +482 -482
- package/src/onboarding/status-bar.js +95 -95
- package/src/onboarding/templates.js +794 -794
- package/src/onboarding/toml-writer.js +38 -38
- package/src/onboarding/tui.js +250 -250
- package/src/onboarding/uninstall.js +153 -153
- package/src/onboarding/wizard.js +516 -499
- package/src/providers/anthropic.js +168 -168
- package/src/providers/base.js +247 -247
- package/src/providers/circuit-breaker.js +136 -136
- package/src/providers/ollama.js +163 -163
- package/src/providers/openai-codex.js +149 -149
- package/src/providers/openrouter.js +136 -136
- package/src/providers/registry.js +36 -36
- package/src/providers/router.js +16 -16
- package/src/runtime/bootstrap-cache.js +47 -47
- package/src/runtime/capabilities-prompt.js +25 -25
- package/src/runtime/completion-ping.js +99 -99
- package/src/runtime/config-validator.js +121 -121
- package/src/runtime/context-ledger.js +360 -360
- package/src/runtime/cutover-readiness.js +42 -42
- package/src/runtime/daemon.js +729 -729
- package/src/runtime/delivery-ack.js +195 -195
- package/src/runtime/delivery-adapters/local-file.js +41 -41
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
- package/src/runtime/delivery-adapters/shadow.js +13 -13
- package/src/runtime/delivery-adapters/standalone-http.js +98 -98
- package/src/runtime/delivery-adapters/telegram.js +104 -104
- package/src/runtime/delivery-adapters/tui.js +128 -128
- package/src/runtime/delivery-manager.js +807 -807
- package/src/runtime/delivery-store.js +168 -168
- package/src/runtime/dependency-health.js +118 -118
- package/src/runtime/envelope.js +114 -114
- package/src/runtime/evaluation.js +1089 -1089
- package/src/runtime/exec-approvals.js +216 -216
- package/src/runtime/executor.js +500 -500
- package/src/runtime/failure-ping.js +67 -67
- package/src/runtime/flows.js +83 -83
- package/src/runtime/guards.js +45 -45
- package/src/runtime/handoff.js +51 -51
- package/src/runtime/identity-cache.js +28 -28
- package/src/runtime/improvement-engine.js +109 -109
- package/src/runtime/improvement-harness.js +581 -581
- package/src/runtime/input-sanitiser.js +72 -72
- package/src/runtime/interaction-contract.js +347 -347
- package/src/runtime/lane-readiness.js +226 -226
- package/src/runtime/migration.js +323 -323
- package/src/runtime/model-resolution.js +78 -78
- package/src/runtime/network.js +64 -64
- package/src/runtime/notification-store.js +97 -97
- package/src/runtime/notifier.js +256 -256
- package/src/runtime/orchestrator.js +53 -53
- package/src/runtime/orphan-reaper.js +41 -41
- package/src/runtime/output-contract-schema.js +139 -139
- package/src/runtime/output-contract-validator.js +439 -439
- package/src/runtime/peer-readiness.js +69 -69
- package/src/runtime/peer-registry.js +133 -133
- package/src/runtime/pilot-status.js +108 -108
- package/src/runtime/prompt-builder.js +261 -261
- package/src/runtime/provider-attempt.js +582 -582
- package/src/runtime/report-fallback.js +71 -71
- package/src/runtime/result-normalizer.js +183 -183
- package/src/runtime/retention.js +74 -74
- package/src/runtime/review.js +244 -244
- package/src/runtime/route-job.js +15 -15
- package/src/runtime/run-store.js +38 -38
- package/src/runtime/schedule.js +88 -88
- package/src/runtime/scheduler-state.js +434 -434
- package/src/runtime/scheduler.js +656 -656
- package/src/runtime/session-compactor.js +182 -182
- package/src/runtime/session-search.js +155 -155
- package/src/runtime/slack-inbound.js +249 -249
- package/src/runtime/ssrf.js +102 -102
- package/src/runtime/status-aggregator.js +330 -330
- package/src/runtime/task-contract.js +140 -140
- package/src/runtime/task-packet.js +107 -107
- package/src/runtime/task-router.js +140 -140
- package/src/runtime/telegram-inbound.js +1565 -1565
- package/src/runtime/token-counter.js +134 -134
- package/src/runtime/token-estimator.js +59 -59
- package/src/runtime/tool-loop.js +200 -200
- package/src/runtime/transport-server.js +311 -311
- package/src/runtime/tui-server.js +411 -411
- package/src/runtime/ulid.js +44 -44
- package/src/security/ssrf-check.js +197 -197
- package/src/setup.js +369 -369
- package/src/shadow/bridge.js +303 -303
- package/src/skills/loader.js +84 -84
- package/src/tools/catalog.json +49 -49
- package/src/tools/cli-delegate.js +44 -44
- package/src/tools/mcp-client.js +106 -106
- package/src/tools/micro/cancel-task.js +6 -6
- package/src/tools/micro/complete-task.js +6 -6
- package/src/tools/micro/fail-task.js +6 -6
- package/src/tools/micro/http-fetch.js +74 -74
- package/src/tools/micro/index.js +36 -36
- package/src/tools/micro/lcm-recall.js +60 -60
- package/src/tools/micro/list-dir.js +17 -17
- package/src/tools/micro/list-skills.js +46 -46
- package/src/tools/micro/load-skill.js +38 -38
- package/src/tools/micro/memory-search.js +45 -45
- package/src/tools/micro/read-file.js +11 -11
- package/src/tools/micro/session-search.js +54 -54
- package/src/tools/micro/shell-exec.js +43 -43
- package/src/tools/micro/trigger-job.js +79 -79
- package/src/tools/micro/web-search.js +58 -58
- package/src/tools/micro/workspace-paths.js +39 -39
- package/src/tools/micro/write-file.js +14 -14
- package/src/tools/micro/write-memory.js +41 -41
- package/src/tools/registry.js +348 -348
- package/src/tools/tool-result-contract.js +36 -36
- package/src/tui/chat.js +835 -835
- package/src/tui/renderer.js +175 -175
- package/src/tui/socket-client.js +217 -217
- package/src/utils/canonical-json.js +29 -29
- package/src/utils/compaction.js +30 -30
- package/src/utils/env-loader.js +5 -5
- package/src/utils/errors.js +80 -80
- package/src/utils/fs.js +101 -101
- package/src/utils/ids.js +5 -5
- package/src/utils/model-context-limits.js +30 -30
- package/src/utils/token-budget.js +74 -74
- package/src/utils/usage-cost.js +25 -25
- package/src/utils/usage-metrics.js +14 -14
package/src/runtime/ulid.js
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
4
|
-
|
|
5
|
-
let lastTime = 0;
|
|
6
|
-
let lastRandom = new Uint8Array(10);
|
|
7
|
-
|
|
8
|
-
export function ulid() {
|
|
9
|
-
let now = Date.now();
|
|
10
|
-
if (now === lastTime) {
|
|
11
|
-
// Increment random component for monotonicity within same millisecond
|
|
12
|
-
for (let i = 9; i >= 0; i--) {
|
|
13
|
-
if (lastRandom[i] < 255) { lastRandom[i]++; break; }
|
|
14
|
-
lastRandom[i] = 0;
|
|
15
|
-
}
|
|
16
|
-
} else {
|
|
17
|
-
lastTime = now;
|
|
18
|
-
randomBytes(10).copy(lastRandom);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
let str = "";
|
|
22
|
-
// Encode 48-bit timestamp → 10 Crockford Base32 chars
|
|
23
|
-
for (let i = 9; i >= 0; i--) {
|
|
24
|
-
str = ENCODING[now & 31] + str;
|
|
25
|
-
now = Math.floor(now / 32);
|
|
26
|
-
}
|
|
27
|
-
// Encode 80-bit random → 16 Crockford Base32 chars
|
|
28
|
-
// Process as two 40-bit halves, each → 8 chars
|
|
29
|
-
const rand = lastRandom;
|
|
30
|
-
for (let half = 0; half < 2; half++) {
|
|
31
|
-
const off = half * 5;
|
|
32
|
-
const hi = (rand[off] << 12) | (rand[off + 1] << 4) | (rand[off + 2] >> 4);
|
|
33
|
-
const lo = ((rand[off + 2] & 0xF) << 16) | (rand[off + 3] << 8) | rand[off + 4];
|
|
34
|
-
str += ENCODING[(hi >> 15) & 31];
|
|
35
|
-
str += ENCODING[(hi >> 10) & 31];
|
|
36
|
-
str += ENCODING[(hi >> 5) & 31];
|
|
37
|
-
str += ENCODING[hi & 31];
|
|
38
|
-
str += ENCODING[(lo >> 15) & 31];
|
|
39
|
-
str += ENCODING[(lo >> 10) & 31];
|
|
40
|
-
str += ENCODING[(lo >> 5) & 31];
|
|
41
|
-
str += ENCODING[lo & 31];
|
|
42
|
-
}
|
|
43
|
-
return str;
|
|
44
|
-
}
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
4
|
+
|
|
5
|
+
let lastTime = 0;
|
|
6
|
+
let lastRandom = new Uint8Array(10);
|
|
7
|
+
|
|
8
|
+
export function ulid() {
|
|
9
|
+
let now = Date.now();
|
|
10
|
+
if (now === lastTime) {
|
|
11
|
+
// Increment random component for monotonicity within same millisecond
|
|
12
|
+
for (let i = 9; i >= 0; i--) {
|
|
13
|
+
if (lastRandom[i] < 255) { lastRandom[i]++; break; }
|
|
14
|
+
lastRandom[i] = 0;
|
|
15
|
+
}
|
|
16
|
+
} else {
|
|
17
|
+
lastTime = now;
|
|
18
|
+
randomBytes(10).copy(lastRandom);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let str = "";
|
|
22
|
+
// Encode 48-bit timestamp → 10 Crockford Base32 chars
|
|
23
|
+
for (let i = 9; i >= 0; i--) {
|
|
24
|
+
str = ENCODING[now & 31] + str;
|
|
25
|
+
now = Math.floor(now / 32);
|
|
26
|
+
}
|
|
27
|
+
// Encode 80-bit random → 16 Crockford Base32 chars
|
|
28
|
+
// Process as two 40-bit halves, each → 8 chars
|
|
29
|
+
const rand = lastRandom;
|
|
30
|
+
for (let half = 0; half < 2; half++) {
|
|
31
|
+
const off = half * 5;
|
|
32
|
+
const hi = (rand[off] << 12) | (rand[off + 1] << 4) | (rand[off + 2] >> 4);
|
|
33
|
+
const lo = ((rand[off + 2] & 0xF) << 16) | (rand[off + 3] << 8) | rand[off + 4];
|
|
34
|
+
str += ENCODING[(hi >> 15) & 31];
|
|
35
|
+
str += ENCODING[(hi >> 10) & 31];
|
|
36
|
+
str += ENCODING[(hi >> 5) & 31];
|
|
37
|
+
str += ENCODING[hi & 31];
|
|
38
|
+
str += ENCODING[(lo >> 15) & 31];
|
|
39
|
+
str += ENCODING[(lo >> 10) & 31];
|
|
40
|
+
str += ENCODING[(lo >> 5) & 31];
|
|
41
|
+
str += ENCODING[lo & 31];
|
|
42
|
+
}
|
|
43
|
+
return str;
|
|
44
|
+
}
|
|
@@ -1,197 +1,197 @@
|
|
|
1
|
-
import net from "node:net";
|
|
2
|
-
import { lookup } from "node:dns/promises";
|
|
3
|
-
|
|
4
|
-
const IPV4_BITS = 32;
|
|
5
|
-
const IPV6_BITS = 128;
|
|
6
|
-
const BLOCKED_HOSTNAME_RE = /^(localhost|localhost\.localdomain|local|internal)$/i;
|
|
7
|
-
const LOOPBACK_HOSTNAME_RE = /^(localhost|localhost\.localdomain)$/i;
|
|
8
|
-
|
|
9
|
-
const BLOCKED_IPV4_RANGES = [
|
|
10
|
-
rangeFromPrefix("0.0.0.0", 8, IPV4_BITS),
|
|
11
|
-
rangeFromPrefix("10.0.0.0", 8, IPV4_BITS),
|
|
12
|
-
rangeFromPrefix("100.64.0.0", 10, IPV4_BITS),
|
|
13
|
-
rangeFromPrefix("127.0.0.0", 8, IPV4_BITS),
|
|
14
|
-
rangeFromPrefix("169.254.0.0", 16, IPV4_BITS),
|
|
15
|
-
rangeFromPrefix("172.16.0.0", 12, IPV4_BITS),
|
|
16
|
-
rangeFromPrefix("192.168.0.0", 16, IPV4_BITS),
|
|
17
|
-
rangeFromPrefix("198.18.0.0", 15, IPV4_BITS),
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
const LOOPBACK_IPV4_RANGES = [
|
|
21
|
-
rangeFromPrefix("127.0.0.0", 8, IPV4_BITS),
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
const BLOCKED_IPV6_RANGES = [
|
|
25
|
-
rangeFromPrefix("::", 128, IPV6_BITS),
|
|
26
|
-
rangeFromPrefix("::1", 128, IPV6_BITS),
|
|
27
|
-
rangeFromPrefix("fc00::", 7, IPV6_BITS),
|
|
28
|
-
rangeFromPrefix("fe80::", 10, IPV6_BITS),
|
|
29
|
-
rangeFromPrefix("ff00::", 8, IPV6_BITS),
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
const LOOPBACK_IPV6_RANGES = [
|
|
33
|
-
rangeFromPrefix("::1", 128, IPV6_BITS),
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
export const OUTBOUND_ADDRESS_POLICY = {
|
|
37
|
-
BLOCK_PRIVATE: "block-private",
|
|
38
|
-
REQUIRE_LOOPBACK: "require-loopback",
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
function normaliseAddress(address) {
|
|
42
|
-
if (typeof address !== "string") return "";
|
|
43
|
-
if (address.startsWith("[") && address.endsWith("]")) {
|
|
44
|
-
return address.slice(1, -1);
|
|
45
|
-
}
|
|
46
|
-
return address;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function ipv4ToBigInt(address) {
|
|
50
|
-
return normaliseAddress(address)
|
|
51
|
-
.split(".")
|
|
52
|
-
.reduce((value, octet) => (value << 8n) + BigInt(Number(octet)), 0n);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function expandEmbeddedIpv4(address) {
|
|
56
|
-
if (!address.includes(".")) return address;
|
|
57
|
-
const lastColon = address.lastIndexOf(":");
|
|
58
|
-
const ipv4Value = ipv4ToBigInt(address.slice(lastColon + 1));
|
|
59
|
-
const upper = Number((ipv4Value >> 16n) & 0xffffn).toString(16);
|
|
60
|
-
const lower = Number(ipv4Value & 0xffffn).toString(16);
|
|
61
|
-
return `${address.slice(0, lastColon)}:${upper}:${lower}`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function ipv6ToBigInt(address) {
|
|
65
|
-
const expanded = expandEmbeddedIpv4(normaliseAddress(address).toLowerCase());
|
|
66
|
-
const hasCompression = expanded.includes("::");
|
|
67
|
-
const [head, tail = ""] = expanded.split("::");
|
|
68
|
-
const headParts = head ? head.split(":").filter(Boolean) : [];
|
|
69
|
-
const tailParts = tail ? tail.split(":").filter(Boolean) : [];
|
|
70
|
-
const missingParts = hasCompression ? 8 - (headParts.length + tailParts.length) : 0;
|
|
71
|
-
const parts = hasCompression
|
|
72
|
-
? [...headParts, ...Array(missingParts).fill("0"), ...tailParts]
|
|
73
|
-
: expanded.split(":");
|
|
74
|
-
|
|
75
|
-
if (parts.length !== 8) {
|
|
76
|
-
throw new Error(`Invalid IPv6 address: ${address}`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return parts.reduce((value, part) => (value << 16n) + BigInt(parseInt(part || "0", 16)), 0n);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function rangeFromPrefix(address, prefixLength, totalBits) {
|
|
83
|
-
const rawValue = totalBits === IPV4_BITS ? ipv4ToBigInt(address) : ipv6ToBigInt(address);
|
|
84
|
-
const shift = BigInt(totalBits - prefixLength);
|
|
85
|
-
const maxValue = (1n << BigInt(totalBits)) - 1n;
|
|
86
|
-
const mask = shift === 0n ? maxValue : maxValue ^ ((1n << shift) - 1n);
|
|
87
|
-
const start = rawValue & mask;
|
|
88
|
-
const end = start | (maxValue ^ mask);
|
|
89
|
-
return { start, end };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function isAddressInRanges(address, ranges) {
|
|
93
|
-
const normalised = normaliseAddress(address);
|
|
94
|
-
const family = net.isIP(normalised);
|
|
95
|
-
if (family === 4) {
|
|
96
|
-
const value = ipv4ToBigInt(normalised);
|
|
97
|
-
return ranges.some((range) => value >= range.start && value <= range.end);
|
|
98
|
-
}
|
|
99
|
-
if (family === 6) {
|
|
100
|
-
const value = ipv6ToBigInt(normalised);
|
|
101
|
-
return ranges.some((range) => value >= range.start && value <= range.end);
|
|
102
|
-
}
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function isBlockedIpAddress(address) {
|
|
107
|
-
return isAddressInRanges(address, BLOCKED_IPV4_RANGES.concat(BLOCKED_IPV6_RANGES));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function isLoopbackIpAddress(address) {
|
|
111
|
-
return isAddressInRanges(address, LOOPBACK_IPV4_RANGES.concat(LOOPBACK_IPV6_RANGES));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function policyRejectsHostname(hostname, { addressPolicy, allowLoopbackAddresses }) {
|
|
115
|
-
if (addressPolicy === OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK) {
|
|
116
|
-
return !LOOPBACK_HOSTNAME_RE.test(hostname);
|
|
117
|
-
}
|
|
118
|
-
if (allowLoopbackAddresses && LOOPBACK_HOSTNAME_RE.test(hostname)) {
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
return BLOCKED_HOSTNAME_RE.test(hostname);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function policyRejectsAddress(address, { addressPolicy, allowLoopbackAddresses }) {
|
|
125
|
-
if (addressPolicy === OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK) {
|
|
126
|
-
return !isLoopbackIpAddress(address);
|
|
127
|
-
}
|
|
128
|
-
if (allowLoopbackAddresses && isLoopbackIpAddress(address)) {
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
return isBlockedIpAddress(address);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export async function inspectOutboundUrl(rawUrl, {
|
|
135
|
-
lookupImpl = lookup,
|
|
136
|
-
allowedProtocols = ["http:", "https:"],
|
|
137
|
-
invalidUrlMessage = "Invalid URL.",
|
|
138
|
-
invalidProtocolMessage = "Only http:// and https:// URLs are allowed.",
|
|
139
|
-
privateAddressMessage = "Target resolves to a private/reserved IP address.",
|
|
140
|
-
loopbackOnlyMessage = "Target must resolve to a loopback address.",
|
|
141
|
-
addressPolicy = OUTBOUND_ADDRESS_POLICY.BLOCK_PRIVATE,
|
|
142
|
-
allowLoopbackAddresses = false,
|
|
143
|
-
} = {}) {
|
|
144
|
-
let parsedUrl;
|
|
145
|
-
try {
|
|
146
|
-
parsedUrl = rawUrl instanceof URL ? rawUrl : new URL(rawUrl);
|
|
147
|
-
} catch {
|
|
148
|
-
return { ok: false, reason: invalidUrlMessage };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (!allowedProtocols.includes(parsedUrl.protocol)) {
|
|
152
|
-
return { ok: false, reason: invalidProtocolMessage, parsedUrl };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const hostname = parsedUrl.hostname;
|
|
156
|
-
if (!hostname) {
|
|
157
|
-
return {
|
|
158
|
-
ok: false,
|
|
159
|
-
reason: addressPolicy === OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK ? loopbackOnlyMessage : privateAddressMessage,
|
|
160
|
-
parsedUrl
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (policyRejectsHostname(hostname, { addressPolicy, allowLoopbackAddresses })) {
|
|
165
|
-
return {
|
|
166
|
-
ok: false,
|
|
167
|
-
reason: addressPolicy === OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK ? loopbackOnlyMessage : privateAddressMessage,
|
|
168
|
-
parsedUrl
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (net.isIP(normaliseAddress(hostname)) && policyRejectsAddress(hostname, { addressPolicy, allowLoopbackAddresses })) {
|
|
173
|
-
return {
|
|
174
|
-
ok: false,
|
|
175
|
-
reason: addressPolicy === OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK ? loopbackOnlyMessage : privateAddressMessage,
|
|
176
|
-
parsedUrl
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
const addresses = await lookupImpl(normaliseAddress(hostname), { all: true, verbatim: true });
|
|
182
|
-
if (addressPolicy === OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK) {
|
|
183
|
-
if (!addresses.length || addresses.some(({ address }) => !isLoopbackIpAddress(address))) {
|
|
184
|
-
return { ok: false, reason: loopbackOnlyMessage, parsedUrl };
|
|
185
|
-
}
|
|
186
|
-
} else if (addresses.some(({ address }) => policyRejectsAddress(address, { addressPolicy, allowLoopbackAddresses }))) {
|
|
187
|
-
return { ok: false, reason: privateAddressMessage, parsedUrl };
|
|
188
|
-
}
|
|
189
|
-
} catch {
|
|
190
|
-
if (addressPolicy === OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK) {
|
|
191
|
-
return { ok: false, reason: loopbackOnlyMessage, parsedUrl };
|
|
192
|
-
}
|
|
193
|
-
// Allow downstream fetch errors to surface if DNS resolution fails here.
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return { ok: true, parsedUrl };
|
|
197
|
-
}
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { lookup } from "node:dns/promises";
|
|
3
|
+
|
|
4
|
+
const IPV4_BITS = 32;
|
|
5
|
+
const IPV6_BITS = 128;
|
|
6
|
+
const BLOCKED_HOSTNAME_RE = /^(localhost|localhost\.localdomain|local|internal)$/i;
|
|
7
|
+
const LOOPBACK_HOSTNAME_RE = /^(localhost|localhost\.localdomain)$/i;
|
|
8
|
+
|
|
9
|
+
const BLOCKED_IPV4_RANGES = [
|
|
10
|
+
rangeFromPrefix("0.0.0.0", 8, IPV4_BITS),
|
|
11
|
+
rangeFromPrefix("10.0.0.0", 8, IPV4_BITS),
|
|
12
|
+
rangeFromPrefix("100.64.0.0", 10, IPV4_BITS),
|
|
13
|
+
rangeFromPrefix("127.0.0.0", 8, IPV4_BITS),
|
|
14
|
+
rangeFromPrefix("169.254.0.0", 16, IPV4_BITS),
|
|
15
|
+
rangeFromPrefix("172.16.0.0", 12, IPV4_BITS),
|
|
16
|
+
rangeFromPrefix("192.168.0.0", 16, IPV4_BITS),
|
|
17
|
+
rangeFromPrefix("198.18.0.0", 15, IPV4_BITS),
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const LOOPBACK_IPV4_RANGES = [
|
|
21
|
+
rangeFromPrefix("127.0.0.0", 8, IPV4_BITS),
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const BLOCKED_IPV6_RANGES = [
|
|
25
|
+
rangeFromPrefix("::", 128, IPV6_BITS),
|
|
26
|
+
rangeFromPrefix("::1", 128, IPV6_BITS),
|
|
27
|
+
rangeFromPrefix("fc00::", 7, IPV6_BITS),
|
|
28
|
+
rangeFromPrefix("fe80::", 10, IPV6_BITS),
|
|
29
|
+
rangeFromPrefix("ff00::", 8, IPV6_BITS),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const LOOPBACK_IPV6_RANGES = [
|
|
33
|
+
rangeFromPrefix("::1", 128, IPV6_BITS),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const OUTBOUND_ADDRESS_POLICY = {
|
|
37
|
+
BLOCK_PRIVATE: "block-private",
|
|
38
|
+
REQUIRE_LOOPBACK: "require-loopback",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function normaliseAddress(address) {
|
|
42
|
+
if (typeof address !== "string") return "";
|
|
43
|
+
if (address.startsWith("[") && address.endsWith("]")) {
|
|
44
|
+
return address.slice(1, -1);
|
|
45
|
+
}
|
|
46
|
+
return address;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ipv4ToBigInt(address) {
|
|
50
|
+
return normaliseAddress(address)
|
|
51
|
+
.split(".")
|
|
52
|
+
.reduce((value, octet) => (value << 8n) + BigInt(Number(octet)), 0n);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function expandEmbeddedIpv4(address) {
|
|
56
|
+
if (!address.includes(".")) return address;
|
|
57
|
+
const lastColon = address.lastIndexOf(":");
|
|
58
|
+
const ipv4Value = ipv4ToBigInt(address.slice(lastColon + 1));
|
|
59
|
+
const upper = Number((ipv4Value >> 16n) & 0xffffn).toString(16);
|
|
60
|
+
const lower = Number(ipv4Value & 0xffffn).toString(16);
|
|
61
|
+
return `${address.slice(0, lastColon)}:${upper}:${lower}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ipv6ToBigInt(address) {
|
|
65
|
+
const expanded = expandEmbeddedIpv4(normaliseAddress(address).toLowerCase());
|
|
66
|
+
const hasCompression = expanded.includes("::");
|
|
67
|
+
const [head, tail = ""] = expanded.split("::");
|
|
68
|
+
const headParts = head ? head.split(":").filter(Boolean) : [];
|
|
69
|
+
const tailParts = tail ? tail.split(":").filter(Boolean) : [];
|
|
70
|
+
const missingParts = hasCompression ? 8 - (headParts.length + tailParts.length) : 0;
|
|
71
|
+
const parts = hasCompression
|
|
72
|
+
? [...headParts, ...Array(missingParts).fill("0"), ...tailParts]
|
|
73
|
+
: expanded.split(":");
|
|
74
|
+
|
|
75
|
+
if (parts.length !== 8) {
|
|
76
|
+
throw new Error(`Invalid IPv6 address: ${address}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return parts.reduce((value, part) => (value << 16n) + BigInt(parseInt(part || "0", 16)), 0n);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function rangeFromPrefix(address, prefixLength, totalBits) {
|
|
83
|
+
const rawValue = totalBits === IPV4_BITS ? ipv4ToBigInt(address) : ipv6ToBigInt(address);
|
|
84
|
+
const shift = BigInt(totalBits - prefixLength);
|
|
85
|
+
const maxValue = (1n << BigInt(totalBits)) - 1n;
|
|
86
|
+
const mask = shift === 0n ? maxValue : maxValue ^ ((1n << shift) - 1n);
|
|
87
|
+
const start = rawValue & mask;
|
|
88
|
+
const end = start | (maxValue ^ mask);
|
|
89
|
+
return { start, end };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isAddressInRanges(address, ranges) {
|
|
93
|
+
const normalised = normaliseAddress(address);
|
|
94
|
+
const family = net.isIP(normalised);
|
|
95
|
+
if (family === 4) {
|
|
96
|
+
const value = ipv4ToBigInt(normalised);
|
|
97
|
+
return ranges.some((range) => value >= range.start && value <= range.end);
|
|
98
|
+
}
|
|
99
|
+
if (family === 6) {
|
|
100
|
+
const value = ipv6ToBigInt(normalised);
|
|
101
|
+
return ranges.some((range) => value >= range.start && value <= range.end);
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isBlockedIpAddress(address) {
|
|
107
|
+
return isAddressInRanges(address, BLOCKED_IPV4_RANGES.concat(BLOCKED_IPV6_RANGES));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isLoopbackIpAddress(address) {
|
|
111
|
+
return isAddressInRanges(address, LOOPBACK_IPV4_RANGES.concat(LOOPBACK_IPV6_RANGES));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function policyRejectsHostname(hostname, { addressPolicy, allowLoopbackAddresses }) {
|
|
115
|
+
if (addressPolicy === OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK) {
|
|
116
|
+
return !LOOPBACK_HOSTNAME_RE.test(hostname);
|
|
117
|
+
}
|
|
118
|
+
if (allowLoopbackAddresses && LOOPBACK_HOSTNAME_RE.test(hostname)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return BLOCKED_HOSTNAME_RE.test(hostname);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function policyRejectsAddress(address, { addressPolicy, allowLoopbackAddresses }) {
|
|
125
|
+
if (addressPolicy === OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK) {
|
|
126
|
+
return !isLoopbackIpAddress(address);
|
|
127
|
+
}
|
|
128
|
+
if (allowLoopbackAddresses && isLoopbackIpAddress(address)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
return isBlockedIpAddress(address);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function inspectOutboundUrl(rawUrl, {
|
|
135
|
+
lookupImpl = lookup,
|
|
136
|
+
allowedProtocols = ["http:", "https:"],
|
|
137
|
+
invalidUrlMessage = "Invalid URL.",
|
|
138
|
+
invalidProtocolMessage = "Only http:// and https:// URLs are allowed.",
|
|
139
|
+
privateAddressMessage = "Target resolves to a private/reserved IP address.",
|
|
140
|
+
loopbackOnlyMessage = "Target must resolve to a loopback address.",
|
|
141
|
+
addressPolicy = OUTBOUND_ADDRESS_POLICY.BLOCK_PRIVATE,
|
|
142
|
+
allowLoopbackAddresses = false,
|
|
143
|
+
} = {}) {
|
|
144
|
+
let parsedUrl;
|
|
145
|
+
try {
|
|
146
|
+
parsedUrl = rawUrl instanceof URL ? rawUrl : new URL(rawUrl);
|
|
147
|
+
} catch {
|
|
148
|
+
return { ok: false, reason: invalidUrlMessage };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!allowedProtocols.includes(parsedUrl.protocol)) {
|
|
152
|
+
return { ok: false, reason: invalidProtocolMessage, parsedUrl };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const hostname = parsedUrl.hostname;
|
|
156
|
+
if (!hostname) {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
reason: addressPolicy === OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK ? loopbackOnlyMessage : privateAddressMessage,
|
|
160
|
+
parsedUrl
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (policyRejectsHostname(hostname, { addressPolicy, allowLoopbackAddresses })) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
reason: addressPolicy === OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK ? loopbackOnlyMessage : privateAddressMessage,
|
|
168
|
+
parsedUrl
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (net.isIP(normaliseAddress(hostname)) && policyRejectsAddress(hostname, { addressPolicy, allowLoopbackAddresses })) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
reason: addressPolicy === OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK ? loopbackOnlyMessage : privateAddressMessage,
|
|
176
|
+
parsedUrl
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const addresses = await lookupImpl(normaliseAddress(hostname), { all: true, verbatim: true });
|
|
182
|
+
if (addressPolicy === OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK) {
|
|
183
|
+
if (!addresses.length || addresses.some(({ address }) => !isLoopbackIpAddress(address))) {
|
|
184
|
+
return { ok: false, reason: loopbackOnlyMessage, parsedUrl };
|
|
185
|
+
}
|
|
186
|
+
} else if (addresses.some(({ address }) => policyRejectsAddress(address, { addressPolicy, allowLoopbackAddresses }))) {
|
|
187
|
+
return { ok: false, reason: privateAddressMessage, parsedUrl };
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
if (addressPolicy === OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK) {
|
|
191
|
+
return { ok: false, reason: loopbackOnlyMessage, parsedUrl };
|
|
192
|
+
}
|
|
193
|
+
// Allow downstream fetch errors to surface if DNS resolution fails here.
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { ok: true, parsedUrl };
|
|
197
|
+
}
|