cloison-runtime 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +313 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +57 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +27 -0
- package/dist/config/index.js.map +1 -0
- package/dist/credentials/index.d.ts +4 -0
- package/dist/credentials/index.d.ts.map +1 -0
- package/dist/credentials/index.js +3 -0
- package/dist/credentials/index.js.map +1 -0
- package/dist/credentials/proxy.d.ts +3 -0
- package/dist/credentials/proxy.d.ts.map +1 -0
- package/dist/credentials/proxy.js +11 -0
- package/dist/credentials/proxy.js.map +1 -0
- package/dist/credentials/store.d.ts +7 -0
- package/dist/credentials/store.d.ts.map +1 -0
- package/dist/credentials/store.js +115 -0
- package/dist/credentials/store.js.map +1 -0
- package/dist/credentials/types.d.ts +14 -0
- package/dist/credentials/types.d.ts.map +1 -0
- package/dist/credentials/types.js +2 -0
- package/dist/credentials/types.js.map +1 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/runner.d.ts +7 -0
- package/dist/hooks/runner.d.ts.map +1 -0
- package/dist/hooks/runner.js +20 -0
- package/dist/hooks/runner.js.map +1 -0
- package/dist/hooks/types.d.ts +39 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +2 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/infra/env.d.ts +2 -0
- package/dist/infra/env.d.ts.map +1 -0
- package/dist/infra/env.js +6 -0
- package/dist/infra/env.js.map +1 -0
- package/dist/infra/warning-filter.d.ts +8 -0
- package/dist/infra/warning-filter.d.ts.map +1 -0
- package/dist/infra/warning-filter.js +66 -0
- package/dist/infra/warning-filter.js.map +1 -0
- package/dist/logging/subsystem.d.ts +29 -0
- package/dist/logging/subsystem.d.ts.map +1 -0
- package/dist/logging/subsystem.js +322 -0
- package/dist/logging/subsystem.js.map +1 -0
- package/dist/memory/embedding-batch.d.ts +38 -0
- package/dist/memory/embedding-batch.d.ts.map +1 -0
- package/dist/memory/embedding-batch.js +253 -0
- package/dist/memory/embedding-batch.js.map +1 -0
- package/dist/memory/embedding-cache.d.ts +16 -0
- package/dist/memory/embedding-cache.d.ts.map +1 -0
- package/dist/memory/embedding-cache.js +113 -0
- package/dist/memory/embedding-cache.js.map +1 -0
- package/dist/memory/embeddings-debug.d.ts +2 -0
- package/dist/memory/embeddings-debug.d.ts.map +1 -0
- package/dist/memory/embeddings-debug.js +12 -0
- package/dist/memory/embeddings-debug.js.map +1 -0
- package/dist/memory/embeddings.d.ts +17 -0
- package/dist/memory/embeddings.d.ts.map +1 -0
- package/dist/memory/embeddings.js +203 -0
- package/dist/memory/embeddings.js.map +1 -0
- package/dist/memory/file-indexer.d.ts +26 -0
- package/dist/memory/file-indexer.d.ts.map +1 -0
- package/dist/memory/file-indexer.js +260 -0
- package/dist/memory/file-indexer.js.map +1 -0
- package/dist/memory/fs-utils.d.ts +12 -0
- package/dist/memory/fs-utils.d.ts.map +1 -0
- package/dist/memory/fs-utils.js +24 -0
- package/dist/memory/fs-utils.js.map +1 -0
- package/dist/memory/hybrid.d.ts +46 -0
- package/dist/memory/hybrid.d.ts.map +1 -0
- package/dist/memory/hybrid.js +85 -0
- package/dist/memory/hybrid.js.map +1 -0
- package/dist/memory/index.d.ts +17 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +15 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/internal.d.ts +39 -0
- package/dist/memory/internal.d.ts.map +1 -0
- package/dist/memory/internal.js +292 -0
- package/dist/memory/internal.js.map +1 -0
- package/dist/memory/manager-search.d.ts +61 -0
- package/dist/memory/manager-search.d.ts.map +1 -0
- package/dist/memory/manager-search.js +102 -0
- package/dist/memory/manager-search.js.map +1 -0
- package/dist/memory/mmr.d.ts +63 -0
- package/dist/memory/mmr.d.ts.map +1 -0
- package/dist/memory/mmr.js +165 -0
- package/dist/memory/mmr.js.map +1 -0
- package/dist/memory/query-expansion.d.ts +42 -0
- package/dist/memory/query-expansion.d.ts.map +1 -0
- package/dist/memory/query-expansion.js +776 -0
- package/dist/memory/query-expansion.js.map +1 -0
- package/dist/memory/session-indexer.d.ts +41 -0
- package/dist/memory/session-indexer.d.ts.map +1 -0
- package/dist/memory/session-indexer.js +367 -0
- package/dist/memory/session-indexer.js.map +1 -0
- package/dist/memory/simple-manager.d.ts +29 -0
- package/dist/memory/simple-manager.d.ts.map +1 -0
- package/dist/memory/simple-manager.js +216 -0
- package/dist/memory/simple-manager.js.map +1 -0
- package/dist/memory/sqlite.d.ts +2 -0
- package/dist/memory/sqlite.d.ts.map +1 -0
- package/dist/memory/sqlite.js +16 -0
- package/dist/memory/sqlite.js.map +1 -0
- package/dist/memory/ssrf.d.ts +18 -0
- package/dist/memory/ssrf.d.ts.map +1 -0
- package/dist/memory/ssrf.js +396 -0
- package/dist/memory/ssrf.js.map +1 -0
- package/dist/memory/temporal-decay.d.ts +26 -0
- package/dist/memory/temporal-decay.d.ts.map +1 -0
- package/dist/memory/temporal-decay.js +120 -0
- package/dist/memory/temporal-decay.js.map +1 -0
- package/dist/memory/types.d.ts +95 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +2 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/package.json +68 -0
- package/dist/platform/index.d.ts +3 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/index.js +2 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/platform.d.ts +3 -0
- package/dist/platform/platform.d.ts.map +1 -0
- package/dist/platform/platform.js +91 -0
- package/dist/platform/platform.js.map +1 -0
- package/dist/platform/types.d.ts +18 -0
- package/dist/platform/types.d.ts.map +1 -0
- package/dist/platform/types.js +2 -0
- package/dist/platform/types.js.map +1 -0
- package/dist/runtime/agent.d.ts +36 -0
- package/dist/runtime/agent.d.ts.map +1 -0
- package/dist/runtime/agent.js +250 -0
- package/dist/runtime/agent.js.map +1 -0
- package/dist/runtime/api-key-rotation.d.ts +26 -0
- package/dist/runtime/api-key-rotation.d.ts.map +1 -0
- package/dist/runtime/api-key-rotation.js +174 -0
- package/dist/runtime/api-key-rotation.js.map +1 -0
- package/dist/runtime/context-guard.d.ts +32 -0
- package/dist/runtime/context-guard.d.ts.map +1 -0
- package/dist/runtime/context-guard.js +61 -0
- package/dist/runtime/context-guard.js.map +1 -0
- package/dist/runtime/failover-error.d.ts +62 -0
- package/dist/runtime/failover-error.d.ts.map +1 -0
- package/dist/runtime/failover-error.js +733 -0
- package/dist/runtime/failover-error.js.map +1 -0
- package/dist/runtime/failover-policy.d.ts +5 -0
- package/dist/runtime/failover-policy.d.ts.map +1 -0
- package/dist/runtime/failover-policy.js +18 -0
- package/dist/runtime/failover-policy.js.map +1 -0
- package/dist/runtime/index.d.ts +13 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +13 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/memory-flush.d.ts +24 -0
- package/dist/runtime/memory-flush.d.ts.map +1 -0
- package/dist/runtime/memory-flush.js +64 -0
- package/dist/runtime/memory-flush.js.map +1 -0
- package/dist/runtime/memory-tools.d.ts +14 -0
- package/dist/runtime/memory-tools.d.ts.map +1 -0
- package/dist/runtime/memory-tools.js +58 -0
- package/dist/runtime/memory-tools.js.map +1 -0
- package/dist/runtime/model-fallback.d.ts +56 -0
- package/dist/runtime/model-fallback.d.ts.map +1 -0
- package/dist/runtime/model-fallback.js +301 -0
- package/dist/runtime/model-fallback.js.map +1 -0
- package/dist/runtime/model-fallback.types.d.ts +14 -0
- package/dist/runtime/model-fallback.types.d.ts.map +1 -0
- package/dist/runtime/model-fallback.types.js +3 -0
- package/dist/runtime/model-fallback.types.js.map +1 -0
- package/dist/runtime/retry.d.ts +24 -0
- package/dist/runtime/retry.d.ts.map +1 -0
- package/dist/runtime/retry.js +100 -0
- package/dist/runtime/retry.js.map +1 -0
- package/dist/runtime/session-pruning.d.ts +22 -0
- package/dist/runtime/session-pruning.d.ts.map +1 -0
- package/dist/runtime/session-pruning.js +118 -0
- package/dist/runtime/session-pruning.js.map +1 -0
- package/dist/runtime/stream-adapters.d.ts +11 -0
- package/dist/runtime/stream-adapters.d.ts.map +1 -0
- package/dist/runtime/stream-adapters.js +46 -0
- package/dist/runtime/stream-adapters.js.map +1 -0
- package/dist/runtime/subagent.d.ts +83 -0
- package/dist/runtime/subagent.d.ts.map +1 -0
- package/dist/runtime/subagent.js +190 -0
- package/dist/runtime/subagent.js.map +1 -0
- package/dist/runtime/tool-result-truncation.d.ts +25 -0
- package/dist/runtime/tool-result-truncation.d.ts.map +1 -0
- package/dist/runtime/tool-result-truncation.js +115 -0
- package/dist/runtime/tool-result-truncation.js.map +1 -0
- package/dist/sandbox/cgroup.d.ts +20 -0
- package/dist/sandbox/cgroup.d.ts.map +1 -0
- package/dist/sandbox/cgroup.js +82 -0
- package/dist/sandbox/cgroup.js.map +1 -0
- package/dist/sandbox/index.d.ts +12 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +10 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/ipc.d.ts +26 -0
- package/dist/sandbox/ipc.d.ts.map +1 -0
- package/dist/sandbox/ipc.js +154 -0
- package/dist/sandbox/ipc.js.map +1 -0
- package/dist/sandbox/manager.d.ts +4 -0
- package/dist/sandbox/manager.d.ts.map +1 -0
- package/dist/sandbox/manager.js +251 -0
- package/dist/sandbox/manager.js.map +1 -0
- package/dist/sandbox/namespace.d.ts +12 -0
- package/dist/sandbox/namespace.d.ts.map +1 -0
- package/dist/sandbox/namespace.js +119 -0
- package/dist/sandbox/namespace.js.map +1 -0
- package/dist/sandbox/proxy-tools.d.ts +14 -0
- package/dist/sandbox/proxy-tools.d.ts.map +1 -0
- package/dist/sandbox/proxy-tools.js +63 -0
- package/dist/sandbox/proxy-tools.js.map +1 -0
- package/dist/sandbox/rootfs.d.ts +20 -0
- package/dist/sandbox/rootfs.d.ts.map +1 -0
- package/dist/sandbox/rootfs.js +247 -0
- package/dist/sandbox/rootfs.js.map +1 -0
- package/dist/sandbox/seccomp-apply.d.ts +9 -0
- package/dist/sandbox/seccomp-apply.d.ts.map +1 -0
- package/dist/sandbox/seccomp-apply.js +227 -0
- package/dist/sandbox/seccomp-apply.js.map +1 -0
- package/dist/sandbox/seccomp.d.ts +13 -0
- package/dist/sandbox/seccomp.d.ts.map +1 -0
- package/dist/sandbox/seccomp.js +120 -0
- package/dist/sandbox/seccomp.js.map +1 -0
- package/dist/sandbox/types.d.ts +66 -0
- package/dist/sandbox/types.d.ts.map +1 -0
- package/dist/sandbox/types.js +8 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/sandbox/worker.d.ts +15 -0
- package/dist/sandbox/worker.d.ts.map +1 -0
- package/dist/sandbox/worker.js +151 -0
- package/dist/sandbox/worker.js.map +1 -0
- package/dist/sessions/index.d.ts +3 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +3 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/store.d.ts +17 -0
- package/dist/sessions/store.d.ts.map +1 -0
- package/dist/sessions/store.js +70 -0
- package/dist/sessions/store.js.map +1 -0
- package/dist/sessions/transcript-events.d.ts +11 -0
- package/dist/sessions/transcript-events.d.ts.map +1 -0
- package/dist/sessions/transcript-events.js +40 -0
- package/dist/sessions/transcript-events.js.map +1 -0
- package/dist/shared/agent-session.d.ts +10 -0
- package/dist/shared/agent-session.d.ts.map +1 -0
- package/dist/shared/agent-session.js +33 -0
- package/dist/shared/agent-session.js.map +1 -0
- package/dist/shared/constants.d.ts +6 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/constants.js +17 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/fs.d.ts +7 -0
- package/dist/shared/fs.d.ts.map +1 -0
- package/dist/shared/fs.js +14 -0
- package/dist/shared/fs.js.map +1 -0
- package/dist/shared/index.d.ts +4 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +4 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/skills/enablement.d.ts +10 -0
- package/dist/skills/enablement.d.ts.map +1 -0
- package/dist/skills/enablement.js +52 -0
- package/dist/skills/enablement.js.map +1 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +4 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/loader.d.ts +8 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +8 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/registry.d.ts +19 -0
- package/dist/skills/registry.d.ts.map +1 -0
- package/dist/skills/registry.js +106 -0
- package/dist/skills/registry.js.map +1 -0
- package/dist/utils/boolean.d.ts +6 -0
- package/dist/utils/boolean.d.ts.map +1 -0
- package/dist/utils/boolean.js +28 -0
- package/dist/utils/boolean.js.map +1 -0
- package/dist/utils/run-with-concurrency.d.ts +12 -0
- package/dist/utils/run-with-concurrency.d.ts.map +1 -0
- package/dist/utils/run-with-concurrency.js +40 -0
- package/dist/utils/run-with-concurrency.js.map +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +38 -0
- package/dist/utils.js.map +1 -0
- package/dist/workspace/index.d.ts +3 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +2 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/runner.d.ts +19 -0
- package/dist/workspace/runner.d.ts.map +1 -0
- package/dist/workspace/runner.js +491 -0
- package/dist/workspace/runner.js.map +1 -0
- package/dist/workspace/types.d.ts +37 -0
- package/dist/workspace/types.d.ts.map +1 -0
- package/dist/workspace/types.js +2 -0
- package/dist/workspace/types.js.map +1 -0
- package/dist/workspace/workspace.d.ts +12 -0
- package/dist/workspace/workspace.d.ts.map +1 -0
- package/dist/workspace/workspace.js +85 -0
- package/dist/workspace/workspace.js.map +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as crypto from "node:crypto";
|
|
4
|
+
import { requireNodeSqlite } from "./sqlite.js";
|
|
5
|
+
import { cosineSimilarity, parseEmbedding, hashText } from "./internal.js";
|
|
6
|
+
import { extractKeywords } from "./query-expansion.js";
|
|
7
|
+
import { buildFtsQuery, bm25RankToScore, mergeHybridResults } from "./hybrid.js";
|
|
8
|
+
import { debugEmbeddingsLog } from "./embeddings-debug.js";
|
|
9
|
+
import { createEmbeddingCache, hashContent } from "./embedding-cache.js";
|
|
10
|
+
const SNIPPET_MAX_CHARS = 700;
|
|
11
|
+
const MAX_VECTOR_SEARCH_CHUNKS = 10_000;
|
|
12
|
+
function generateId() {
|
|
13
|
+
return `mem_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`;
|
|
14
|
+
}
|
|
15
|
+
export function createSimpleMemoryManager(options) {
|
|
16
|
+
fs.mkdirSync(options.dbDir, { recursive: true });
|
|
17
|
+
const dbPath = path.join(options.dbDir, "memory.db");
|
|
18
|
+
const { DatabaseSync } = requireNodeSqlite();
|
|
19
|
+
const db = new DatabaseSync(dbPath);
|
|
20
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
21
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
22
|
+
// Schema matches OpenClaw's MemoryIndexManager
|
|
23
|
+
db.exec(`
|
|
24
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
25
|
+
id TEXT PRIMARY KEY,
|
|
26
|
+
content TEXT NOT NULL,
|
|
27
|
+
embedding TEXT,
|
|
28
|
+
metadata TEXT,
|
|
29
|
+
source TEXT DEFAULT 'memory',
|
|
30
|
+
path TEXT,
|
|
31
|
+
model TEXT,
|
|
32
|
+
start_line INTEGER DEFAULT 1,
|
|
33
|
+
end_line INTEGER DEFAULT 1,
|
|
34
|
+
hash TEXT,
|
|
35
|
+
created_at TEXT NOT NULL
|
|
36
|
+
)
|
|
37
|
+
`);
|
|
38
|
+
let ftsAvailable = false;
|
|
39
|
+
let stmtFtsInsert = null;
|
|
40
|
+
let stmtFtsDelete = null;
|
|
41
|
+
try {
|
|
42
|
+
db.exec(`
|
|
43
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts
|
|
44
|
+
USING fts5(id, content, source, model)
|
|
45
|
+
`);
|
|
46
|
+
stmtFtsInsert = db.prepare("INSERT INTO chunks_fts(id, content, source, model) VALUES (?, ?, ?, ?)");
|
|
47
|
+
stmtFtsDelete = db.prepare("DELETE FROM chunks_fts WHERE id = ?");
|
|
48
|
+
ftsAvailable = true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// FTS5 not available
|
|
52
|
+
}
|
|
53
|
+
const provider = options.embeddingProvider ?? null;
|
|
54
|
+
const providerModel = provider ? `${provider.id}/${provider.model}` : "none";
|
|
55
|
+
const embeddingCache = (options.enableEmbeddingCache !== false && provider)
|
|
56
|
+
? createEmbeddingCache(db)
|
|
57
|
+
: null;
|
|
58
|
+
const maxCacheEntries = options.maxCacheEntries ?? 50_000;
|
|
59
|
+
const stmtInsert = db.prepare(`
|
|
60
|
+
INSERT OR REPLACE INTO chunks (id, content, embedding, metadata, source, path, model, start_line, end_line, hash, created_at)
|
|
61
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
62
|
+
`);
|
|
63
|
+
const stmtDelete = db.prepare("DELETE FROM chunks WHERE id = ?");
|
|
64
|
+
const stmtListAll = db.prepare("SELECT id, content, metadata, created_at FROM chunks ORDER BY created_at DESC");
|
|
65
|
+
const stmtGet = db.prepare("SELECT * FROM chunks WHERE id = ?");
|
|
66
|
+
return {
|
|
67
|
+
async store(content, metadata) {
|
|
68
|
+
const id = generateId();
|
|
69
|
+
const now = new Date().toISOString();
|
|
70
|
+
const hash = hashText(content);
|
|
71
|
+
let embeddingJson = null;
|
|
72
|
+
if (provider) {
|
|
73
|
+
try {
|
|
74
|
+
const contentHash = hashContent(content);
|
|
75
|
+
const cached = embeddingCache?.get(provider.id, provider.model, contentHash);
|
|
76
|
+
let vec;
|
|
77
|
+
if (cached) {
|
|
78
|
+
vec = cached;
|
|
79
|
+
debugEmbeddingsLog("embedding from cache", { id, dims: vec.length });
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
vec = await provider.embedQuery(content);
|
|
83
|
+
embeddingCache?.set(provider.id, provider.model, "", contentHash, vec, vec.length);
|
|
84
|
+
debugEmbeddingsLog("embedded chunk", { id, dims: vec.length });
|
|
85
|
+
}
|
|
86
|
+
embeddingJson = JSON.stringify(vec);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
debugEmbeddingsLog("embedding failed", { id, error: String(err) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
stmtInsert.run(id, content, embeddingJson, metadata ? JSON.stringify(metadata) : null, "memory", null, providerModel, 1, 1, hash, now);
|
|
93
|
+
if (stmtFtsInsert) {
|
|
94
|
+
try {
|
|
95
|
+
stmtFtsInsert.run(id, content, "memory", providerModel);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// FTS insert failed, keyword search won't find this chunk
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return id;
|
|
102
|
+
},
|
|
103
|
+
async search(query, opts) {
|
|
104
|
+
const maxResults = opts?.maxResults ?? 10;
|
|
105
|
+
const minScore = opts?.minScore ?? 0;
|
|
106
|
+
const candidateLimit = Math.max(1, maxResults * 3);
|
|
107
|
+
// Vector search
|
|
108
|
+
let vectorResults = [];
|
|
109
|
+
if (provider) {
|
|
110
|
+
try {
|
|
111
|
+
const queryVec = await provider.embedQuery(query);
|
|
112
|
+
const rows = db.prepare("SELECT * FROM chunks WHERE embedding IS NOT NULL AND model = ? LIMIT ?").all(providerModel, MAX_VECTOR_SEARCH_CHUNKS);
|
|
113
|
+
const scored = [];
|
|
114
|
+
for (const row of rows) {
|
|
115
|
+
const embedding = parseEmbedding(row.embedding);
|
|
116
|
+
if (embedding.length === 0)
|
|
117
|
+
continue;
|
|
118
|
+
const score = cosineSimilarity(queryVec, embedding);
|
|
119
|
+
if (Number.isFinite(score))
|
|
120
|
+
scored.push({ row, score });
|
|
121
|
+
}
|
|
122
|
+
scored.sort((a, b) => b.score - a.score);
|
|
123
|
+
vectorResults = scored.slice(0, candidateLimit).map(({ row, score }) => ({
|
|
124
|
+
id: row.id,
|
|
125
|
+
path: row.path ?? "",
|
|
126
|
+
startLine: row.start_line ?? 1,
|
|
127
|
+
endLine: row.end_line ?? 1,
|
|
128
|
+
source: row.source ?? "memory",
|
|
129
|
+
snippet: (row.content ?? "").slice(0, SNIPPET_MAX_CHARS),
|
|
130
|
+
vectorScore: score,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Vector search failed
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Keyword search using OpenClaw's query expansion
|
|
138
|
+
let keywordResults = [];
|
|
139
|
+
if (ftsAvailable) {
|
|
140
|
+
const keywords = extractKeywords(query);
|
|
141
|
+
const searchTerms = keywords.length > 0 ? keywords : [query];
|
|
142
|
+
for (const term of searchTerms) {
|
|
143
|
+
const ftsQuery = buildFtsQuery(term);
|
|
144
|
+
if (!ftsQuery)
|
|
145
|
+
continue;
|
|
146
|
+
try {
|
|
147
|
+
const rows = db.prepare(`SELECT id, content, source, bm25(chunks_fts) AS rank FROM chunks_fts WHERE chunks_fts MATCH ? ORDER BY rank ASC LIMIT ?`).all(ftsQuery, candidateLimit);
|
|
148
|
+
for (const row of rows) {
|
|
149
|
+
keywordResults.push({
|
|
150
|
+
id: row.id,
|
|
151
|
+
path: "",
|
|
152
|
+
startLine: 1,
|
|
153
|
+
endLine: 1,
|
|
154
|
+
source: row.source ?? "memory",
|
|
155
|
+
snippet: row.content.slice(0, SNIPPET_MAX_CHARS),
|
|
156
|
+
textScore: bm25RankToScore(row.rank),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// FTS query failed
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Deduplicate keyword results
|
|
165
|
+
const byId = new Map();
|
|
166
|
+
for (const r of keywordResults) {
|
|
167
|
+
const existing = byId.get(r.id);
|
|
168
|
+
if (!existing || r.textScore > existing.textScore)
|
|
169
|
+
byId.set(r.id, r);
|
|
170
|
+
}
|
|
171
|
+
keywordResults = Array.from(byId.values());
|
|
172
|
+
}
|
|
173
|
+
// Merge using OpenClaw's hybrid merge
|
|
174
|
+
const merged = await mergeHybridResults({
|
|
175
|
+
vector: vectorResults,
|
|
176
|
+
keyword: keywordResults,
|
|
177
|
+
vectorWeight: provider ? 0.7 : 0,
|
|
178
|
+
textWeight: provider ? 0.3 : 1,
|
|
179
|
+
});
|
|
180
|
+
return merged
|
|
181
|
+
.filter((r) => r.score >= minScore)
|
|
182
|
+
.slice(0, maxResults);
|
|
183
|
+
},
|
|
184
|
+
async delete(id) {
|
|
185
|
+
const row = stmtGet.get(id);
|
|
186
|
+
if (!row)
|
|
187
|
+
return false;
|
|
188
|
+
stmtDelete.run(id);
|
|
189
|
+
if (stmtFtsDelete) {
|
|
190
|
+
try {
|
|
191
|
+
stmtFtsDelete.run(id);
|
|
192
|
+
}
|
|
193
|
+
catch { }
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
},
|
|
197
|
+
async list() {
|
|
198
|
+
const rows = stmtListAll.all();
|
|
199
|
+
return rows.map((r) => ({
|
|
200
|
+
id: r.id,
|
|
201
|
+
content: r.content,
|
|
202
|
+
metadata: r.metadata ? JSON.parse(r.metadata) : undefined,
|
|
203
|
+
createdAt: r.created_at,
|
|
204
|
+
}));
|
|
205
|
+
},
|
|
206
|
+
async close() {
|
|
207
|
+
if (embeddingCache) {
|
|
208
|
+
embeddingCache.prune(maxCacheEntries);
|
|
209
|
+
}
|
|
210
|
+
db.close();
|
|
211
|
+
},
|
|
212
|
+
get db() { return db; },
|
|
213
|
+
get embeddingCache() { return embeddingCache; },
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=simple-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simple-manager.js","sourceRoot":"","sources":["../../src/memory/simple-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/memory/sqlite.ts"],"names":[],"mappings":"AAKA,wBAAgB,iBAAiB,IAAI,cAAc,aAAa,CAAC,CAahE"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { installProcessWarningFilter } from "../infra/warning-filter.js";
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
export function requireNodeSqlite() {
|
|
5
|
+
installProcessWarningFilter();
|
|
6
|
+
try {
|
|
7
|
+
return require("node:sqlite");
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
11
|
+
// Node distributions can ship without the experimental builtin SQLite module.
|
|
12
|
+
// Surface an actionable error instead of the generic "unknown builtin module".
|
|
13
|
+
throw new Error(`SQLite support is unavailable in this Node runtime (missing node:sqlite). ${message}`, { cause: err });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=sqlite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/memory/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAEzE,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C,MAAM,UAAU,iBAAiB;IAC/B,2BAA2B,EAAE,CAAC;IAC9B,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,aAAa,CAAiC,CAAC;IAChE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,8EAA8E;QAC9E,+EAA+E;QAC/E,MAAM,IAAI,KAAK,CACb,6EAA6E,OAAO,EAAE,EACtF,EAAE,KAAK,EAAE,GAAG,EAAE,CACf,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare class SsrFBlockedError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export type SsrfPolicy = {
|
|
5
|
+
allowPrivateNetwork?: boolean;
|
|
6
|
+
allowedHostnames?: string[];
|
|
7
|
+
hostnameAllowlist?: string[];
|
|
8
|
+
};
|
|
9
|
+
export declare function isBlockedHostname(hostname: string): boolean;
|
|
10
|
+
export declare function isPrivateIpAddress(address: string): boolean;
|
|
11
|
+
export declare function isBlockedHostnameOrIp(hostname: string): boolean;
|
|
12
|
+
export declare function buildBaseUrlPolicy(baseUrl: string): SsrfPolicy | undefined;
|
|
13
|
+
export interface ValidateUrlResult {
|
|
14
|
+
resolvedAddresses: string[];
|
|
15
|
+
}
|
|
16
|
+
export declare function validateUrl(url: string, policy?: SsrfPolicy): Promise<ValidateUrlResult>;
|
|
17
|
+
export declare function fetchWithSsrfGuard(url: string, init: RequestInit, policy?: SsrfPolicy): Promise<Response>;
|
|
18
|
+
//# sourceMappingURL=ssrf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssrf.d.ts","sourceRoot":"","sources":["../../src/memory/ssrf.ts"],"names":[],"mappings":"AAUA,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B,CAAC;AA6BF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI3D;AAyFD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAgB3D;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI/D;AAyBD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAU1E;AAOD,MAAM,WAAW,iBAAiB;IAChC,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA4D9F;AAwGD,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,EACjB,MAAM,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,QAAQ,CAAC,CAwDnB"}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// Ported from OpenClaw src/infra/net/ssrf.ts + fetch-guard.ts
|
|
2
|
+
// DNS validation: we resolve DNS ourselves, validate ALL resolved IPs against
|
|
3
|
+
// the private/special-use blocklist, then pin DNS via undici's Agent to ensure
|
|
4
|
+
// fetch() connects to the exact IPs we validated (eliminates TOCTOU gap).
|
|
5
|
+
// Fail-closed on DNS failure or empty results.
|
|
6
|
+
import * as dns from "node:dns/promises";
|
|
7
|
+
import { lookup as dnsLookupCb } from "node:dns";
|
|
8
|
+
import * as net from "node:net";
|
|
9
|
+
export class SsrFBlockedError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "SsrFBlockedError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// --- Hostname normalization ---
|
|
16
|
+
function normalizeHostname(hostname) {
|
|
17
|
+
const trimmed = hostname.trim().toLowerCase();
|
|
18
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
19
|
+
return trimmed.slice(1, -1);
|
|
20
|
+
}
|
|
21
|
+
return trimmed.replace(/\.+$/, "");
|
|
22
|
+
}
|
|
23
|
+
// --- Blocked hostnames (from OpenClaw) ---
|
|
24
|
+
const BLOCKED_HOSTNAMES = new Set([
|
|
25
|
+
"localhost",
|
|
26
|
+
"localhost.localdomain",
|
|
27
|
+
"metadata.google.internal",
|
|
28
|
+
]);
|
|
29
|
+
function isBlockedHostnameNormalized(normalized) {
|
|
30
|
+
if (BLOCKED_HOSTNAMES.has(normalized))
|
|
31
|
+
return true;
|
|
32
|
+
return (normalized.endsWith(".localhost") ||
|
|
33
|
+
normalized.endsWith(".local") ||
|
|
34
|
+
normalized.endsWith(".internal"));
|
|
35
|
+
}
|
|
36
|
+
export function isBlockedHostname(hostname) {
|
|
37
|
+
const normalized = normalizeHostname(hostname);
|
|
38
|
+
if (!normalized)
|
|
39
|
+
return false;
|
|
40
|
+
return isBlockedHostnameNormalized(normalized);
|
|
41
|
+
}
|
|
42
|
+
// --- IP validation (from OpenClaw) ---
|
|
43
|
+
function isCanonicalDottedDecimalIPv4(address) {
|
|
44
|
+
const parts = address.split(".");
|
|
45
|
+
if (parts.length !== 4)
|
|
46
|
+
return false;
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
if (!/^\d{1,3}$/.test(part))
|
|
49
|
+
return false;
|
|
50
|
+
const num = Number(part);
|
|
51
|
+
if (num < 0 || num > 255)
|
|
52
|
+
return false;
|
|
53
|
+
if (part.length > 1 && part.startsWith("0"))
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
function isLegacyIpv4Literal(address) {
|
|
59
|
+
return /^(?:0x[\da-f]+|\d+)(?:\.(?:0x[\da-f]+|\d+)){0,3}$/i.test(address);
|
|
60
|
+
}
|
|
61
|
+
function looksLikeUnsupportedIpv4Literal(address) {
|
|
62
|
+
const parts = address.split(".");
|
|
63
|
+
if (parts.length === 0 || parts.length > 4)
|
|
64
|
+
return false;
|
|
65
|
+
if (parts.some((part) => part.length === 0))
|
|
66
|
+
return true;
|
|
67
|
+
return parts.every((part) => /^[0-9]+$/.test(part) || /^0x/i.test(part));
|
|
68
|
+
}
|
|
69
|
+
function isBlockedSpecialUseIpv4(ip) {
|
|
70
|
+
if (!net.isIPv4(ip))
|
|
71
|
+
return false;
|
|
72
|
+
const parts = ip.split(".").map(Number);
|
|
73
|
+
const [a, b] = parts;
|
|
74
|
+
if (a === 0)
|
|
75
|
+
return true; // 0.0.0.0/8 "this network"
|
|
76
|
+
if (a === 10)
|
|
77
|
+
return true; // 10.0.0.0/8 RFC 1918
|
|
78
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
79
|
+
return true; // 100.64.0.0/10 CGN
|
|
80
|
+
if (a === 127)
|
|
81
|
+
return true; // 127.0.0.0/8 loopback
|
|
82
|
+
if (a === 169 && b === 254)
|
|
83
|
+
return true; // 169.254.0.0/16 link-local
|
|
84
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
85
|
+
return true; // 172.16.0.0/12 RFC 1918
|
|
86
|
+
if (a === 192 && b === 0 && parts[2] === 0)
|
|
87
|
+
return true; // 192.0.0.0/24 IETF protocol
|
|
88
|
+
if (a === 192 && b === 0 && parts[2] === 2)
|
|
89
|
+
return true; // 192.0.2.0/24 TEST-NET-1
|
|
90
|
+
if (a === 192 && b === 88 && parts[2] === 99)
|
|
91
|
+
return true; // 192.88.99.0/24 6to4 relay
|
|
92
|
+
if (a === 192 && b === 168)
|
|
93
|
+
return true; // 192.168.0.0/16 RFC 1918
|
|
94
|
+
if (a === 198 && (b === 18 || b === 19))
|
|
95
|
+
return true; // 198.18.0.0/15 benchmark
|
|
96
|
+
if (a === 198 && b === 51 && parts[2] === 100)
|
|
97
|
+
return true; // 198.51.100.0/24 TEST-NET-2
|
|
98
|
+
if (a === 203 && b === 0 && parts[2] === 113)
|
|
99
|
+
return true; // 203.0.113.0/24 TEST-NET-3
|
|
100
|
+
if (a >= 224)
|
|
101
|
+
return true; // 224.0.0.0/3 multicast + reserved
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
function extractEmbeddedIpv4(ipv6Lower) {
|
|
105
|
+
// ::ffff:x.x.x.x (IPv4-mapped, RFC 4291)
|
|
106
|
+
if (ipv6Lower.startsWith("::ffff:")) {
|
|
107
|
+
const rest = ipv6Lower.slice(7);
|
|
108
|
+
if (net.isIPv4(rest))
|
|
109
|
+
return rest;
|
|
110
|
+
}
|
|
111
|
+
// ::ffff:0:x.x.x.x (IPv4-translated, RFC 6052)
|
|
112
|
+
if (ipv6Lower.startsWith("::ffff:0:")) {
|
|
113
|
+
const rest = ipv6Lower.slice(9);
|
|
114
|
+
if (net.isIPv4(rest))
|
|
115
|
+
return rest;
|
|
116
|
+
}
|
|
117
|
+
// 64:ff9b::x.x.x.x (NAT64 well-known prefix, RFC 6052)
|
|
118
|
+
if (ipv6Lower.startsWith("64:ff9b::")) {
|
|
119
|
+
const rest = ipv6Lower.slice(9);
|
|
120
|
+
if (net.isIPv4(rest))
|
|
121
|
+
return rest;
|
|
122
|
+
}
|
|
123
|
+
// 64:ff9b:1::x.x.x.x (NAT64 local-use prefix, RFC 8215)
|
|
124
|
+
if (ipv6Lower.startsWith("64:ff9b:1::")) {
|
|
125
|
+
const rest = ipv6Lower.slice(11);
|
|
126
|
+
if (net.isIPv4(rest))
|
|
127
|
+
return rest;
|
|
128
|
+
}
|
|
129
|
+
// ::x.x.x.x (IPv4-compatible, deprecated RFC 4291 §2.5.5.1 but still dangerous)
|
|
130
|
+
if (ipv6Lower.startsWith("::") && !ipv6Lower.startsWith("::ffff")) {
|
|
131
|
+
const rest = ipv6Lower.slice(2);
|
|
132
|
+
if (rest && net.isIPv4(rest))
|
|
133
|
+
return rest;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
function isBlockedSpecialUseIpv6(ip) {
|
|
138
|
+
const lower = ip.toLowerCase();
|
|
139
|
+
if (lower === "::")
|
|
140
|
+
return true; // unspecified
|
|
141
|
+
if (lower === "::1")
|
|
142
|
+
return true; // loopback
|
|
143
|
+
if (lower.startsWith("fe80:"))
|
|
144
|
+
return true; // link-local
|
|
145
|
+
if (lower.startsWith("fc") || lower.startsWith("fd"))
|
|
146
|
+
return true; // ULA
|
|
147
|
+
if (lower.startsWith("ff"))
|
|
148
|
+
return true; // multicast
|
|
149
|
+
const embedded = extractEmbeddedIpv4(lower);
|
|
150
|
+
if (embedded && isBlockedSpecialUseIpv4(embedded))
|
|
151
|
+
return true;
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
export function isPrivateIpAddress(address) {
|
|
155
|
+
let normalized = address.trim().toLowerCase();
|
|
156
|
+
if (normalized.startsWith("[") && normalized.endsWith("]")) {
|
|
157
|
+
normalized = normalized.slice(1, -1);
|
|
158
|
+
}
|
|
159
|
+
if (!normalized)
|
|
160
|
+
return false;
|
|
161
|
+
if (net.isIPv4(normalized))
|
|
162
|
+
return isBlockedSpecialUseIpv4(normalized);
|
|
163
|
+
if (net.isIPv6(normalized))
|
|
164
|
+
return isBlockedSpecialUseIpv6(normalized);
|
|
165
|
+
// Malformed IPv6 literals: fail closed
|
|
166
|
+
if (normalized.includes(":") && !net.isIPv6(normalized))
|
|
167
|
+
return true;
|
|
168
|
+
if (!isCanonicalDottedDecimalIPv4(normalized) && isLegacyIpv4Literal(normalized))
|
|
169
|
+
return true;
|
|
170
|
+
if (looksLikeUnsupportedIpv4Literal(normalized))
|
|
171
|
+
return true;
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
export function isBlockedHostnameOrIp(hostname) {
|
|
175
|
+
const normalized = normalizeHostname(hostname);
|
|
176
|
+
if (!normalized)
|
|
177
|
+
return false;
|
|
178
|
+
return isBlockedHostnameNormalized(normalized) || isPrivateIpAddress(normalized);
|
|
179
|
+
}
|
|
180
|
+
// --- Hostname allowlist ---
|
|
181
|
+
function matchesHostnameAllowlist(hostname, allowlist) {
|
|
182
|
+
if (allowlist.length === 0)
|
|
183
|
+
return true;
|
|
184
|
+
return allowlist.some((pattern) => {
|
|
185
|
+
if (pattern.startsWith("*.")) {
|
|
186
|
+
const suffix = pattern.slice(2);
|
|
187
|
+
if (!suffix || hostname === suffix)
|
|
188
|
+
return false;
|
|
189
|
+
return hostname.endsWith(`.${suffix}`);
|
|
190
|
+
}
|
|
191
|
+
return hostname === pattern;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// --- Policy helpers ---
|
|
195
|
+
function shouldSkipPrivateNetworkChecks(hostname, policy) {
|
|
196
|
+
return (policy?.allowPrivateNetwork === true ||
|
|
197
|
+
new Set(policy?.allowedHostnames?.map(normalizeHostname)).has(hostname));
|
|
198
|
+
}
|
|
199
|
+
export function buildBaseUrlPolicy(baseUrl) {
|
|
200
|
+
const trimmed = baseUrl.trim();
|
|
201
|
+
if (!trimmed)
|
|
202
|
+
return undefined;
|
|
203
|
+
try {
|
|
204
|
+
const parsed = new URL(trimmed);
|
|
205
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
206
|
+
return undefined;
|
|
207
|
+
return { allowedHostnames: [parsed.hostname], hostnameAllowlist: [normalizeHostname(parsed.hostname)] };
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// --- URL validation ---
|
|
214
|
+
const BLOCKED_HOST_OR_IP_MESSAGE = "Blocked hostname or private/internal/special-use IP address";
|
|
215
|
+
const BLOCKED_RESOLVED_IP_MESSAGE = "Blocked: resolves to private/internal/special-use IP address";
|
|
216
|
+
export async function validateUrl(url, policy) {
|
|
217
|
+
const parsed = new URL(url);
|
|
218
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
219
|
+
throw new SsrFBlockedError(`SSRF: blocked non-HTTP protocol: ${parsed.protocol}`);
|
|
220
|
+
}
|
|
221
|
+
const hostname = normalizeHostname(parsed.hostname);
|
|
222
|
+
if (!hostname)
|
|
223
|
+
throw new SsrFBlockedError("SSRF: empty hostname");
|
|
224
|
+
const hostnameAllowlist = (policy?.hostnameAllowlist ?? []).map(normalizeHostname).filter(Boolean);
|
|
225
|
+
if (hostnameAllowlist.length > 0 && !matchesHostnameAllowlist(hostname, hostnameAllowlist)) {
|
|
226
|
+
throw new SsrFBlockedError(`Blocked hostname (not in allowlist): ${parsed.hostname}`);
|
|
227
|
+
}
|
|
228
|
+
if (!shouldSkipPrivateNetworkChecks(hostname, policy)) {
|
|
229
|
+
if (isBlockedHostnameOrIp(hostname)) {
|
|
230
|
+
throw new SsrFBlockedError(BLOCKED_HOST_OR_IP_MESSAGE);
|
|
231
|
+
}
|
|
232
|
+
// Resolve both IPv4 and IPv6 and check ALL results.
|
|
233
|
+
// Fail-closed: if DNS resolution fails entirely, block the request
|
|
234
|
+
// (we cannot verify the target IP is safe).
|
|
235
|
+
const allAddresses = [];
|
|
236
|
+
let v4Error = null;
|
|
237
|
+
let v6Error = null;
|
|
238
|
+
try {
|
|
239
|
+
const v4 = await dns.resolve4(hostname);
|
|
240
|
+
allAddresses.push(...v4);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
if (err instanceof SsrFBlockedError)
|
|
244
|
+
throw err;
|
|
245
|
+
v4Error = err;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const v6 = await dns.resolve6(hostname);
|
|
249
|
+
allAddresses.push(...v6);
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
if (err instanceof SsrFBlockedError)
|
|
253
|
+
throw err;
|
|
254
|
+
v6Error = err;
|
|
255
|
+
}
|
|
256
|
+
// Fail-closed: if we got zero resolved addresses (whether DNS errored
|
|
257
|
+
// or returned empty results) and the hostname is not a literal IP,
|
|
258
|
+
// block the request — we cannot verify the target IP is safe.
|
|
259
|
+
if (allAddresses.length === 0 && !net.isIP(hostname)) {
|
|
260
|
+
throw new SsrFBlockedError(`SSRF: unable to resolve hostname "${hostname}" — blocking (fail-closed)`);
|
|
261
|
+
}
|
|
262
|
+
for (const addr of allAddresses) {
|
|
263
|
+
if (isBlockedHostnameOrIp(addr)) {
|
|
264
|
+
throw new SsrFBlockedError(BLOCKED_RESOLVED_IP_MESSAGE);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return { resolvedAddresses: allAddresses };
|
|
268
|
+
}
|
|
269
|
+
return { resolvedAddresses: [] };
|
|
270
|
+
}
|
|
271
|
+
function createPinnedLookupCb(targetHostname, addresses) {
|
|
272
|
+
const normalizedTarget = normalizeHostname(targetHostname);
|
|
273
|
+
let index = 0;
|
|
274
|
+
const records = addresses.map((addr) => ({
|
|
275
|
+
address: addr,
|
|
276
|
+
family: addr.includes(":") ? 6 : 4,
|
|
277
|
+
}));
|
|
278
|
+
return ((hostname, optionsOrCb, maybeCb) => {
|
|
279
|
+
const cb = (typeof optionsOrCb === "function" ? optionsOrCb : maybeCb);
|
|
280
|
+
if (!cb)
|
|
281
|
+
return;
|
|
282
|
+
const normalized = normalizeHostname(hostname);
|
|
283
|
+
if (normalized !== normalizedTarget || records.length === 0) {
|
|
284
|
+
if (typeof optionsOrCb === "function") {
|
|
285
|
+
return dnsLookupCb(hostname, cb);
|
|
286
|
+
}
|
|
287
|
+
return dnsLookupCb(hostname, optionsOrCb, cb);
|
|
288
|
+
}
|
|
289
|
+
const opts = typeof optionsOrCb === "object" && optionsOrCb !== null
|
|
290
|
+
? optionsOrCb
|
|
291
|
+
: {};
|
|
292
|
+
const requestedFamily = typeof optionsOrCb === "number"
|
|
293
|
+
? optionsOrCb
|
|
294
|
+
: (opts.family ?? 0);
|
|
295
|
+
const candidates = requestedFamily === 4 || requestedFamily === 6
|
|
296
|
+
? records.filter((r) => r.family === requestedFamily)
|
|
297
|
+
: records;
|
|
298
|
+
const usable = candidates.length > 0 ? candidates : records;
|
|
299
|
+
if (opts.all) {
|
|
300
|
+
cb(null, usable);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const chosen = usable[index % usable.length];
|
|
304
|
+
index = (index + 1) % usable.length;
|
|
305
|
+
cb(null, chosen.address, chosen.family);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
async function buildPinnedFetchInit(hostname, resolvedAddresses, init) {
|
|
309
|
+
if (resolvedAddresses.length === 0) {
|
|
310
|
+
return { init, cleanup: async () => { } };
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
// Node.js 22+ bundles undici; use its Agent for DNS pinning
|
|
314
|
+
// @ts-expect-error -- undici is bundled in Node.js 22+ but may lack type declarations
|
|
315
|
+
const { Agent } = await import("undici");
|
|
316
|
+
const lookup = createPinnedLookupCb(hostname, resolvedAddresses);
|
|
317
|
+
const agent = new Agent({ connect: { lookup } });
|
|
318
|
+
return {
|
|
319
|
+
init: { ...init, dispatcher: agent },
|
|
320
|
+
cleanup: async () => { try {
|
|
321
|
+
await agent.close();
|
|
322
|
+
}
|
|
323
|
+
catch { } },
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return { init, cleanup: async () => { } };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// --- Guarded fetch with redirect handling (from fetch-guard.ts) ---
|
|
331
|
+
const DEFAULT_MAX_REDIRECTS = 3;
|
|
332
|
+
function retainSafeHeadersForCrossOriginRedirect(headers) {
|
|
333
|
+
if (!headers)
|
|
334
|
+
return undefined;
|
|
335
|
+
const safe = {};
|
|
336
|
+
const headerObj = new Headers(headers);
|
|
337
|
+
const SAFE_HEADERS = new Set(["accept", "accept-language", "content-language", "content-type"]);
|
|
338
|
+
headerObj.forEach((value, key) => {
|
|
339
|
+
if (SAFE_HEADERS.has(key.toLowerCase())) {
|
|
340
|
+
safe[key] = value;
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
return Object.keys(safe).length > 0 ? safe : undefined;
|
|
344
|
+
}
|
|
345
|
+
function isRedirectStatus(status) {
|
|
346
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
347
|
+
}
|
|
348
|
+
export async function fetchWithSsrfGuard(url, init, policy) {
|
|
349
|
+
const maxRedirects = DEFAULT_MAX_REDIRECTS;
|
|
350
|
+
const visited = new Set();
|
|
351
|
+
let currentUrl = url;
|
|
352
|
+
let currentInit = init ? { ...init } : undefined;
|
|
353
|
+
let redirectCount = 0;
|
|
354
|
+
while (true) {
|
|
355
|
+
let parsedUrl;
|
|
356
|
+
try {
|
|
357
|
+
parsedUrl = new URL(currentUrl);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
throw new Error("Invalid URL: must be http or https");
|
|
361
|
+
}
|
|
362
|
+
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
363
|
+
throw new Error("Invalid URL: must be http or https");
|
|
364
|
+
}
|
|
365
|
+
const validationResult = await validateUrl(currentUrl, policy);
|
|
366
|
+
const pinned = await buildPinnedFetchInit(parsedUrl.hostname, validationResult.resolvedAddresses, { ...(currentInit ?? {}), redirect: "manual" });
|
|
367
|
+
let response;
|
|
368
|
+
try {
|
|
369
|
+
response = await fetch(parsedUrl.toString(), pinned.init);
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
await pinned.cleanup();
|
|
373
|
+
}
|
|
374
|
+
if (isRedirectStatus(response.status)) {
|
|
375
|
+
const location = response.headers.get("location");
|
|
376
|
+
if (!location)
|
|
377
|
+
throw new Error(`Redirect missing location header (${response.status})`);
|
|
378
|
+
redirectCount += 1;
|
|
379
|
+
if (redirectCount > maxRedirects)
|
|
380
|
+
throw new Error(`Too many redirects (limit: ${maxRedirects})`);
|
|
381
|
+
const nextParsedUrl = new URL(location, parsedUrl);
|
|
382
|
+
const nextUrl = nextParsedUrl.toString();
|
|
383
|
+
if (visited.has(nextUrl))
|
|
384
|
+
throw new Error("Redirect loop detected");
|
|
385
|
+
if (nextParsedUrl.origin !== parsedUrl.origin && currentInit?.headers) {
|
|
386
|
+
const safeHeaders = retainSafeHeadersForCrossOriginRedirect(currentInit.headers);
|
|
387
|
+
currentInit = { ...currentInit, headers: safeHeaders };
|
|
388
|
+
}
|
|
389
|
+
visited.add(nextUrl);
|
|
390
|
+
currentUrl = nextUrl;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
return response;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
//# sourceMappingURL=ssrf.js.map
|