@retinue/agentkit 0.1.0 → 0.3.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/README.md +59 -277
- package/dist/adapters/audio/openai.d.ts +36 -0
- package/dist/adapters/audio/openai.js +136 -0
- package/dist/adapters/bullmq/lock.d.ts +26 -2
- package/dist/adapters/embeddings/openai.d.ts +45 -0
- package/dist/adapters/embeddings/openai.js +109 -0
- package/dist/adapters/memory/connections.d.ts +10 -0
- package/dist/adapters/memory/connections.js +87 -0
- package/dist/adapters/memory/graph.d.ts +17 -0
- package/dist/adapters/memory/graph.js +267 -0
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.js +3 -0
- package/dist/adapters/memory/rate-limit.d.ts +14 -0
- package/dist/adapters/memory/rate-limit.js +28 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/postgres/connections.d.ts +15 -0
- package/dist/adapters/postgres/connections.js +134 -0
- package/dist/adapters/postgres/graph.d.ts +24 -0
- package/dist/adapters/postgres/graph.js +325 -0
- package/dist/adapters/postgres/index.d.ts +2 -0
- package/dist/adapters/postgres/index.js +2 -0
- package/dist/adapters/postgres/migrations.js +198 -0
- package/dist/adapters/postgres/run-store.js +26 -2
- package/dist/adapters/postgres/schema.d.ts +61 -2
- package/dist/adapters/postgres/schema.js +95 -10
- package/dist/adapters/redis/index.d.ts +1 -0
- package/dist/adapters/redis/index.js +1 -0
- package/dist/adapters/redis/rate-limit.d.ts +46 -0
- package/dist/adapters/redis/rate-limit.js +55 -0
- package/dist/adapters/supabase/index.d.ts +15 -0
- package/dist/adapters/supabase/index.js +15 -0
- package/dist/adapters/supabase/rls.js +33 -0
- package/dist/agents/agent.d.ts +99 -1
- package/dist/agents/agent.js +215 -16
- package/dist/agents/define.js +24 -4
- package/dist/agents/engine.d.ts +48 -0
- package/dist/agents/engine.js +370 -13
- package/dist/audio/index.d.ts +151 -0
- package/dist/audio/index.js +183 -0
- package/dist/capabilities/index.d.ts +5 -1
- package/dist/capabilities/index.js +23 -0
- package/dist/capabilities/runtime.d.ts +8 -0
- package/dist/connections/cipher.d.ts +103 -0
- package/dist/connections/cipher.js +141 -0
- package/dist/connections/index.d.ts +156 -0
- package/dist/connections/index.js +72 -0
- package/dist/connections/oauth/client.d.ts +84 -0
- package/dist/connections/oauth/client.js +141 -0
- package/dist/connections/oauth/index.d.ts +166 -0
- package/dist/connections/oauth/index.js +226 -0
- package/dist/connections/oauth/service.d.ts +90 -0
- package/dist/connections/oauth/service.js +132 -0
- package/dist/connections/pause.d.ts +68 -0
- package/dist/connections/pause.js +97 -0
- package/dist/connections/resolver.d.ts +59 -0
- package/dist/connections/resolver.js +138 -0
- package/dist/connections/resume.d.ts +64 -0
- package/dist/connections/resume.js +99 -0
- package/dist/connections/vault-cipher.d.ts +95 -0
- package/dist/connections/vault-cipher.js +206 -0
- package/dist/core/budget.d.ts +55 -0
- package/dist/core/budget.js +56 -0
- package/dist/core/content-parts.d.ts +26 -2
- package/dist/core/content-parts.js +1 -0
- package/dist/core/context.d.ts +25 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.js +15 -0
- package/dist/core/events.d.ts +110 -2
- package/dist/core/events.js +33 -2
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/validation.js +19 -0
- package/dist/documents/index.d.ts +14 -0
- package/dist/documents/parsers/text.d.ts +16 -0
- package/dist/documents/parsers/text.js +54 -2
- package/dist/entries/connections.d.ts +10 -0
- package/dist/entries/connections.js +10 -0
- package/dist/entries/guardrails.d.ts +14 -0
- package/dist/entries/guardrails.js +14 -0
- package/dist/entries/knowledge.d.ts +17 -0
- package/dist/entries/knowledge.js +16 -0
- package/dist/entries/mcp-server.d.ts +9 -0
- package/dist/entries/mcp-server.js +9 -0
- package/dist/entries/runtime.d.ts +10 -0
- package/dist/entries/runtime.js +8 -0
- package/dist/entries/testing.d.ts +28 -0
- package/dist/entries/testing.js +28 -0
- package/dist/graphql/resolvers.d.ts +7 -1
- package/dist/graphql/resolvers.js +20 -0
- package/dist/graphql/schema.d.ts +1 -1
- package/dist/graphql/schema.js +44 -0
- package/dist/guardrails/index.d.ts +115 -0
- package/dist/guardrails/index.js +108 -0
- package/dist/guardrails/moderation.d.ts +53 -0
- package/dist/guardrails/moderation.js +75 -0
- package/dist/guardrails/pii.d.ts +75 -0
- package/dist/guardrails/pii.js +193 -0
- package/dist/knowledge/communities.d.ts +166 -0
- package/dist/knowledge/communities.js +377 -0
- package/dist/knowledge/graph-global.d.ts +153 -0
- package/dist/knowledge/graph-global.js +246 -0
- package/dist/knowledge/graph-retrieval.d.ts +150 -0
- package/dist/knowledge/graph-retrieval.js +303 -0
- package/dist/knowledge/graph.d.ts +223 -0
- package/dist/knowledge/graph.js +369 -0
- package/dist/knowledge/index.d.ts +25 -0
- package/dist/knowledge/index.js +37 -1
- package/dist/knowledge/navigate.d.ts +89 -0
- package/dist/knowledge/navigate.js +107 -0
- package/dist/knowledge/retrieval.d.ts +98 -5
- package/dist/knowledge/retrieval.js +158 -28
- package/dist/mcp-server/index.d.ts +168 -0
- package/dist/mcp-server/index.js +175 -0
- package/dist/models/extraction.d.ts +73 -0
- package/dist/models/extraction.js +118 -0
- package/dist/models/index.d.ts +40 -1
- package/dist/models/index.js +22 -1
- package/dist/models/pricing.d.ts +2 -0
- package/dist/models/pricing.js +17 -1
- package/dist/models/provider-factory.js +9 -7
- package/dist/models/streaming.d.ts +111 -1
- package/dist/models/streaming.js +184 -3
- package/dist/persistence/index.d.ts +230 -0
- package/dist/persistence/index.js +2 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +12 -0
- package/dist/runtime/worker.d.ts +36 -1
- package/dist/runtime/worker.js +37 -4
- package/dist/security/checklist.js +55 -0
- package/dist/security/findings.js +18 -9
- package/dist/server/bin.d.ts +19 -0
- package/dist/server/bin.js +226 -0
- package/dist/server/boot.d.ts +20 -2
- package/dist/server/boot.js +29 -3
- package/dist/server/cli.d.ts +13 -0
- package/dist/server/cli.js +7 -4
- package/dist/server/doctor.d.ts +74 -0
- package/dist/server/doctor.js +280 -0
- package/dist/skills/catalogue.d.ts +49 -0
- package/dist/skills/catalogue.js +61 -0
- package/dist/skills/context.d.ts +61 -0
- package/dist/skills/context.js +107 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.js +2 -0
- package/dist/telemetry/spans.js +17 -0
- package/dist/testing/conformance/artifact-exports.d.ts +20 -0
- package/dist/testing/conformance/artifact-exports.js +172 -0
- package/dist/testing/conformance/artifacts.d.ts +11 -0
- package/dist/testing/conformance/artifacts.js +316 -0
- package/dist/testing/conformance/audio.d.ts +38 -0
- package/dist/testing/conformance/audio.js +145 -0
- package/dist/testing/conformance/capability.d.ts +23 -0
- package/dist/testing/conformance/capability.js +27 -0
- package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
- package/dist/testing/conformance/checkpoint-store.js +72 -0
- package/dist/testing/conformance/connections.d.ts +14 -0
- package/dist/testing/conformance/connections.js +171 -0
- package/dist/testing/conformance/conversation-store.d.ts +10 -0
- package/dist/testing/conformance/conversation-store.js +60 -0
- package/dist/testing/conformance/evaluation.d.ts +10 -0
- package/dist/testing/conformance/evaluation.js +295 -0
- package/dist/testing/conformance/files.d.ts +19 -0
- package/dist/testing/conformance/files.js +454 -0
- package/dist/testing/conformance/flows.d.ts +16 -0
- package/dist/testing/conformance/flows.js +193 -0
- package/dist/testing/conformance/graph.d.ts +22 -0
- package/dist/testing/conformance/graph.js +500 -0
- package/dist/testing/conformance/hitl.d.ts +25 -0
- package/dist/testing/conformance/hitl.js +523 -0
- package/dist/testing/conformance/index.d.ts +163 -0
- package/dist/testing/conformance/index.js +321 -0
- package/dist/testing/conformance/invariants.d.ts +23 -0
- package/dist/testing/conformance/invariants.js +80 -0
- package/dist/testing/conformance/knowledge.d.ts +41 -0
- package/dist/testing/conformance/knowledge.js +592 -0
- package/dist/testing/conformance/parents.d.ts +50 -0
- package/dist/testing/conformance/parents.js +39 -0
- package/dist/testing/conformance/rate-limit.d.ts +25 -0
- package/dist/testing/conformance/rate-limit.js +71 -0
- package/dist/testing/conformance/records.d.ts +36 -0
- package/dist/testing/conformance/records.js +400 -0
- package/dist/testing/conformance/rollups.d.ts +30 -0
- package/dist/testing/conformance/rollups.js +425 -0
- package/dist/testing/conformance/run-coordinator.d.ts +27 -0
- package/dist/testing/conformance/run-coordinator.js +150 -0
- package/dist/testing/conformance/run-event-log.d.ts +9 -0
- package/dist/testing/conformance/run-event-log.js +138 -0
- package/dist/testing/conformance/run-store.d.ts +12 -0
- package/dist/testing/conformance/run-store.js +294 -0
- package/dist/testing/conformance/session-state.d.ts +38 -0
- package/dist/testing/conformance/session-state.js +198 -0
- package/dist/testing/conformance/usage-limits.d.ts +10 -0
- package/dist/testing/conformance/usage-limits.js +309 -0
- package/dist/testing/conformance.d.ts +8 -0
- package/dist/testing/conformance.js +8 -0
- package/dist/testing/memory-backend.d.ts +39 -0
- package/dist/testing/memory-backend.js +41 -0
- package/dist/testing/pglite.d.ts +55 -0
- package/dist/testing/pglite.js +78 -0
- package/dist/testing/stub-model.d.ts +58 -0
- package/dist/testing/stub-model.js +71 -0
- package/dist/testing/supabase-storage-double.d.ts +35 -0
- package/dist/testing/supabase-storage-double.js +101 -0
- package/dist/toolkit/files.d.ts +125 -0
- package/dist/toolkit/files.js +320 -0
- package/dist/toolkit/http.d.ts +27 -0
- package/dist/toolkit/http.js +48 -0
- package/dist/toolkit/index.d.ts +31 -1
- package/dist/toolkit/index.js +26 -1
- package/dist/toolkit/mime.d.ts +97 -0
- package/dist/toolkit/mime.js +233 -0
- package/dist/toolkit/sandbox.d.ts +119 -0
- package/dist/toolkit/sandbox.js +239 -0
- package/dist/toolkit/ssrf.d.ts +139 -0
- package/dist/toolkit/ssrf.js +349 -0
- package/dist/toolkit/vendor.d.ts +91 -0
- package/dist/toolkit/vendor.js +153 -0
- package/dist/toolkit/web.d.ts +13 -0
- package/dist/toolkit/web.js +7 -1
- package/dist/tools/budget.d.ts +28 -0
- package/dist/tools/budget.js +35 -0
- package/dist/tools/credentials.d.ts +316 -0
- package/dist/tools/credentials.js +369 -0
- package/dist/tools/define.d.ts +45 -0
- package/dist/tools/define.js +26 -0
- package/dist/tools/find.d.ts +109 -0
- package/dist/tools/find.js +210 -0
- package/dist/tools/index.d.ts +35 -2
- package/dist/tools/index.js +11 -0
- package/dist/tools/library/audio.d.ts +63 -0
- package/dist/tools/library/audio.js +148 -0
- package/dist/tools/library/fs.d.ts +24 -0
- package/dist/tools/library/fs.js +102 -0
- package/dist/tools/library/index.d.ts +37 -2
- package/dist/tools/library/index.js +47 -0
- package/dist/tools/library/shell.d.ts +45 -0
- package/dist/tools/library/shell.js +70 -0
- package/dist/tools/meta-tools.js +8 -0
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +250 -4
- package/dist/usage/index.d.ts +21 -0
- package/dist/usage/index.js +1 -0
- package/dist/usage/rate-limit.d.ts +122 -0
- package/dist/usage/rate-limit.js +131 -0
- package/dist/usage/recorder.js +3 -0
- package/package.json +33 -2
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading and writing files, path-scoped — REQ-047 (#206), task #215.
|
|
3
|
+
*
|
|
4
|
+
* In `toolkit/` rather than `tools/` because it performs I/O and boundary rule **R7** forbids that in the tools
|
|
5
|
+
* layer, the same arrangement `http.ts` has with the web tools. The tools in `tools/library/fs.ts` are envelopes
|
|
6
|
+
* over these functions, and every security property lives here — not in the envelope, and not in the schema.
|
|
7
|
+
*
|
|
8
|
+
* ## The one property that matters
|
|
9
|
+
*
|
|
10
|
+
* **A path a model produced must not be able to name a file outside the configured root**, and there are three
|
|
11
|
+
* ways it tries:
|
|
12
|
+
*
|
|
13
|
+
* - `../../etc/passwd` — normalised away by resolving against the root first.
|
|
14
|
+
* - `/etc/passwd` — an absolute path is **refused outright** rather than silently re-rooted. Re-rooting would
|
|
15
|
+
* answer a different question than the one asked, and the model would not know.
|
|
16
|
+
* - A **symlink** inside the root pointing out of it. This is the one that gets missed, because the path is
|
|
17
|
+
* inside the root right up until the filesystem resolves it. So the check is against the *real* path, after
|
|
18
|
+
* symlink resolution, on every call — not against the string.
|
|
19
|
+
*
|
|
20
|
+
* The root itself is resolved once at construction, also through `realpath`: a root that is itself a symlink
|
|
21
|
+
* would otherwise make every real path look like an escape.
|
|
22
|
+
*
|
|
23
|
+
* ## Bytes are bounded while reading
|
|
24
|
+
*
|
|
25
|
+
* A cap applied after `readFile` has already buffered a two-gigabyte file protects nothing. These read into a
|
|
26
|
+
* fixed buffer and report `truncated`, which is the same decision `http.ts` made for the same reason. Truncation
|
|
27
|
+
* rather than refusal, because "the first 200 KB of the log" is usually the answer, and a refusal leaves the
|
|
28
|
+
* model with nothing.
|
|
29
|
+
*/
|
|
30
|
+
import { closeSync, existsSync, openSync, readSync, readdirSync, realpathSync, statSync, writeFileSync, mkdirSync } from "node:fs";
|
|
31
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
32
|
+
/** Bytes returned from one read. Matches the HTTP client's ceiling, for the same reason. */
|
|
33
|
+
export const MAX_FILE_BYTES = 200_000;
|
|
34
|
+
/** Entries returned from one listing. A directory of ten thousand files is not an answer. */
|
|
35
|
+
export const MAX_ENTRIES = 200;
|
|
36
|
+
/** Files examined by one search. Bounded work, so a search cannot become a filesystem crawl. */
|
|
37
|
+
export const MAX_SEARCHED_FILES = 2_000;
|
|
38
|
+
/** Matches returned from one search. */
|
|
39
|
+
export const MAX_MATCHES = 100;
|
|
40
|
+
const forbidden = (path, reason) => ({ ok: false, path, kind: "forbidden", reason });
|
|
41
|
+
/**
|
|
42
|
+
* Is `candidate` inside `root`, both already real paths?
|
|
43
|
+
*
|
|
44
|
+
* `relative` rather than `startsWith`: `/srv/data-secrets` starts with `/srv/data`, and a prefix comparison would
|
|
45
|
+
* accept it. A relative path that begins with `..` or is absolute is outside.
|
|
46
|
+
*/
|
|
47
|
+
export const contains = (root, candidate) => {
|
|
48
|
+
if (candidate === root)
|
|
49
|
+
return true;
|
|
50
|
+
const rel = relative(root, candidate);
|
|
51
|
+
return rel !== "" && !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
|
|
52
|
+
};
|
|
53
|
+
export const createFileReader = (scope) => {
|
|
54
|
+
const maxBytes = scope.maxBytes ?? MAX_FILE_BYTES;
|
|
55
|
+
const maxEntries = scope.maxEntries ?? MAX_ENTRIES;
|
|
56
|
+
const maxMatches = scope.maxMatches ?? MAX_MATCHES;
|
|
57
|
+
const maxSearched = scope.maxSearchedFiles ?? MAX_SEARCHED_FILES;
|
|
58
|
+
/**
|
|
59
|
+
* The root, resolved once through `realpath`.
|
|
60
|
+
*
|
|
61
|
+
* A root that is itself a symlink — `/tmp` on macOS is `/private/tmp` — would otherwise make every resolved
|
|
62
|
+
* path look like an escape, and the tool would refuse everything while appearing to be configured correctly.
|
|
63
|
+
*/
|
|
64
|
+
const realRoot = realpathSync(scope.root);
|
|
65
|
+
const realWritable = scope.writableRoot === undefined ? undefined : realpathSync(scope.writableRoot);
|
|
66
|
+
/** Resolve a model-supplied path inside a root, or say why not. */
|
|
67
|
+
const within = (root, requested, mustExist) => {
|
|
68
|
+
if (isAbsolute(requested))
|
|
69
|
+
return forbidden(requested, "Give a path relative to the configured root. An absolute path is refused rather than re-rooted, so a " +
|
|
70
|
+
"refusal is never mistaken for a different file.");
|
|
71
|
+
const joined = resolve(root, requested);
|
|
72
|
+
/**
|
|
73
|
+
* Lexical containment first, before the filesystem is consulted at all.
|
|
74
|
+
*
|
|
75
|
+
* `../../etc/passwd` used to come back as `not-found`, because `realpath` threw on a path that does not exist
|
|
76
|
+
* and the containment check never ran. Technically safe and a poor answer: the model learns that a path it
|
|
77
|
+
* may not read merely happens to be absent, which is a different fact and an invitation to try another. It
|
|
78
|
+
* also means the refusal depended on the target's existence, which is not a property anybody wants a security
|
|
79
|
+
* boundary to have.
|
|
80
|
+
*/
|
|
81
|
+
if (!contains(root, joined))
|
|
82
|
+
return forbidden(requested, "That path is outside the configured root.");
|
|
83
|
+
if (mustExist) {
|
|
84
|
+
let real;
|
|
85
|
+
try {
|
|
86
|
+
real = realpathSync(joined);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return { ok: false, path: requested, kind: "not-found", reason: `No such path: ${requested}` };
|
|
90
|
+
}
|
|
91
|
+
if (!contains(root, real))
|
|
92
|
+
return forbidden(requested, "That path resolves outside the configured root. Symlinks are followed and then checked.");
|
|
93
|
+
return { ok: true, path: real };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* For a write, the target may not exist yet — nor may its directory.
|
|
97
|
+
*
|
|
98
|
+
* So walk up to the nearest ancestor that *does* exist, resolve **that** through `realpath`, and rebuild the
|
|
99
|
+
* target beneath it. The first version resolved `dirname` only, which refused every write into a directory it
|
|
100
|
+
* was about to create — a correct-looking check that made the tool useless. Resolving the existing prefix is
|
|
101
|
+
* what keeps the symlink guarantee: a link anywhere along the real part of the path is followed and checked.
|
|
102
|
+
*/
|
|
103
|
+
const missing = [];
|
|
104
|
+
let probe = joined;
|
|
105
|
+
while (!existsSync(probe)) {
|
|
106
|
+
const parent = dirname(probe);
|
|
107
|
+
if (parent === probe)
|
|
108
|
+
break;
|
|
109
|
+
missing.unshift(basename(probe));
|
|
110
|
+
probe = parent;
|
|
111
|
+
}
|
|
112
|
+
let realPrefix;
|
|
113
|
+
try {
|
|
114
|
+
realPrefix = realpathSync(probe);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return { ok: false, path: requested, kind: "not-found", reason: `No such path: ${requested}` };
|
|
118
|
+
}
|
|
119
|
+
const candidate = missing.length === 0 ? realPrefix : join(realPrefix, ...missing);
|
|
120
|
+
if (!contains(root, candidate))
|
|
121
|
+
return forbidden(requested, "That path resolves outside the configured root. Symlinks are followed and then checked.");
|
|
122
|
+
return { ok: true, path: candidate };
|
|
123
|
+
};
|
|
124
|
+
const readBounded = (path) => {
|
|
125
|
+
let handle;
|
|
126
|
+
try {
|
|
127
|
+
handle = openSync(path, "r");
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return { ok: false, path, kind: "unreadable", reason: error.message };
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
134
|
+
const read = readSync(handle, buffer, 0, maxBytes + 1, 0);
|
|
135
|
+
const truncated = read > maxBytes;
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
path,
|
|
139
|
+
bytes: truncated ? maxBytes : read,
|
|
140
|
+
truncated,
|
|
141
|
+
content: buffer.subarray(0, truncated ? maxBytes : read).toString("utf8"),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return { ok: false, path, kind: "unreadable", reason: error.message };
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
closeSync(handle);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
/** A deliberately small pattern language: `*.md`, `report*`, `*draft*`. Not a glob engine. */
|
|
152
|
+
const matchesName = (name, pattern) => {
|
|
153
|
+
if (pattern === undefined || pattern === "")
|
|
154
|
+
return true;
|
|
155
|
+
const parts = pattern.toLowerCase().split("*");
|
|
156
|
+
const lower = name.toLowerCase();
|
|
157
|
+
if (parts.length === 1)
|
|
158
|
+
return lower === (parts[0] ?? "");
|
|
159
|
+
let index = 0;
|
|
160
|
+
for (const [position, part] of parts.entries()) {
|
|
161
|
+
if (part === "")
|
|
162
|
+
continue;
|
|
163
|
+
const at = lower.indexOf(part, index);
|
|
164
|
+
if (at === -1)
|
|
165
|
+
return false;
|
|
166
|
+
if (position === 0 && at !== 0)
|
|
167
|
+
return false;
|
|
168
|
+
index = at + part.length;
|
|
169
|
+
}
|
|
170
|
+
const last = parts[parts.length - 1] ?? "";
|
|
171
|
+
return last === "" || lower.endsWith(last);
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
read(requested) {
|
|
175
|
+
const scoped = within(realRoot, requested, true);
|
|
176
|
+
if (!scoped.ok)
|
|
177
|
+
return scoped;
|
|
178
|
+
let stats;
|
|
179
|
+
try {
|
|
180
|
+
stats = statSync(scoped.path);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return { ok: false, path: requested, kind: "not-found", reason: error.message };
|
|
184
|
+
}
|
|
185
|
+
if (stats.isDirectory())
|
|
186
|
+
return { ok: false, path: requested, kind: "not-a-file", reason: `${requested} is a directory — use fs_list.` };
|
|
187
|
+
const outcome = readBounded(scoped.path);
|
|
188
|
+
return outcome.ok ? { ...outcome, path: relative(realRoot, scoped.path) || "." } : { ...outcome, path: requested };
|
|
189
|
+
},
|
|
190
|
+
list(requested = ".") {
|
|
191
|
+
const scoped = within(realRoot, requested, true);
|
|
192
|
+
if (!scoped.ok)
|
|
193
|
+
return scoped;
|
|
194
|
+
let names;
|
|
195
|
+
try {
|
|
196
|
+
names = readdirSync(scoped.path).sort();
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const message = error.code === "ENOTDIR" ? `${requested} is a file — use fs_read.` : error.message;
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
path: requested,
|
|
203
|
+
kind: error.code === "ENOTDIR" ? "not-a-directory" : "unreadable",
|
|
204
|
+
reason: message,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const entries = [];
|
|
208
|
+
for (const name of names.slice(0, maxEntries)) {
|
|
209
|
+
const full = join(scoped.path, name);
|
|
210
|
+
let stats;
|
|
211
|
+
try {
|
|
212
|
+
stats = statSync(full);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// A broken symlink or a file removed mid-listing. Reported as `other` rather than omitted: a name that
|
|
216
|
+
// exists and cannot be described is more useful than a silently shorter list.
|
|
217
|
+
entries.push({ name, path: relative(realRoot, full), kind: "other" });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
entries.push({
|
|
221
|
+
name,
|
|
222
|
+
path: relative(realRoot, full),
|
|
223
|
+
kind: stats.isDirectory() ? "directory" : stats.isFile() ? "file" : "other",
|
|
224
|
+
...(stats.isFile() ? { bytes: stats.size } : {}),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return { ok: true, path: relative(realRoot, scoped.path) || ".", entries, truncated: names.length > maxEntries };
|
|
228
|
+
},
|
|
229
|
+
search({ query, path = ".", namePattern }) {
|
|
230
|
+
if (query.trim() === "")
|
|
231
|
+
return { ok: false, path, kind: "unreadable", reason: "Give something to search for." };
|
|
232
|
+
const scoped = within(realRoot, path, true);
|
|
233
|
+
if (!scoped.ok)
|
|
234
|
+
return scoped;
|
|
235
|
+
const needle = query.toLowerCase();
|
|
236
|
+
const matches = [];
|
|
237
|
+
let searched = 0;
|
|
238
|
+
let truncated = false;
|
|
239
|
+
const walk = (dir) => {
|
|
240
|
+
if (truncated)
|
|
241
|
+
return;
|
|
242
|
+
let names;
|
|
243
|
+
try {
|
|
244
|
+
names = readdirSync(dir).sort();
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
for (const name of names) {
|
|
250
|
+
if (truncated)
|
|
251
|
+
return;
|
|
252
|
+
const full = join(dir, name);
|
|
253
|
+
let stats;
|
|
254
|
+
try {
|
|
255
|
+
stats = statSync(full);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
// The real path is checked here too: a symlinked directory inside the root would otherwise let a
|
|
261
|
+
// search walk out of it, which is the same escape as a symlinked file and easier to miss.
|
|
262
|
+
if (!contains(realRoot, realpathSync(full)))
|
|
263
|
+
continue;
|
|
264
|
+
if (stats.isDirectory()) {
|
|
265
|
+
walk(full);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (!stats.isFile() || !matchesName(name, namePattern))
|
|
269
|
+
continue;
|
|
270
|
+
searched += 1;
|
|
271
|
+
if (searched > maxSearched) {
|
|
272
|
+
truncated = true;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const read = readBounded(full);
|
|
276
|
+
if (!read.ok)
|
|
277
|
+
continue;
|
|
278
|
+
read.content.split("\n").forEach((line, index) => {
|
|
279
|
+
if (truncated || !line.toLowerCase().includes(needle))
|
|
280
|
+
return;
|
|
281
|
+
if (matches.length >= maxMatches) {
|
|
282
|
+
truncated = true;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
matches.push({ path: relative(realRoot, full), line: index + 1, text: line.trim().slice(0, 400) });
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
walk(scoped.path);
|
|
290
|
+
return { ok: true, query, matches, filesSearched: searched, truncated };
|
|
291
|
+
},
|
|
292
|
+
write({ path, content }) {
|
|
293
|
+
if (realWritable === undefined)
|
|
294
|
+
return forbidden(path, "No writable root is configured, so nothing can be written. This is a wiring decision, not a " +
|
|
295
|
+
"permission one — see FileScope.writableRoot.");
|
|
296
|
+
const scoped = within(realWritable, path, false);
|
|
297
|
+
if (!scoped.ok)
|
|
298
|
+
return scoped;
|
|
299
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
300
|
+
if (bytes > maxBytes)
|
|
301
|
+
return { ok: false, path, kind: "too-many", reason: `That is ${bytes} bytes; the ceiling is ${maxBytes}.` };
|
|
302
|
+
let created = true;
|
|
303
|
+
try {
|
|
304
|
+
created = !statSync(scoped.path).isFile();
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
created = true;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
mkdirSync(dirname(scoped.path), { recursive: true });
|
|
311
|
+
writeFileSync(scoped.path, content, "utf8");
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
return { ok: false, path, kind: "unreadable", reason: error.message };
|
|
315
|
+
}
|
|
316
|
+
return { ok: true, path: relative(realWritable, scoped.path), bytes, created };
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
//# sourceMappingURL=files.js.map
|
package/dist/toolkit/http.d.ts
CHANGED
|
@@ -57,6 +57,27 @@ export type HttpFailure = {
|
|
|
57
57
|
readonly kind: "forbidden" | "redirected" | "timeout" | "unreachable" | "http-error" | "unreadable";
|
|
58
58
|
readonly status?: number;
|
|
59
59
|
readonly reason: string;
|
|
60
|
+
/**
|
|
61
|
+
* `Retry-After`, in milliseconds, when the server sent one.
|
|
62
|
+
*
|
|
63
|
+
* Parsed here because it is the one response header a *failure* still needs to carry: a vendor that answers
|
|
64
|
+
* `429` with `Retry-After: 47` has told you exactly how long to wait, and a default backoff that ignores it
|
|
65
|
+
* either hammers the vendor or waits far too long. `PlatformError.retryAfterMs` already exists and the retry
|
|
66
|
+
* path already reads it — this is the missing link between them.
|
|
67
|
+
*
|
|
68
|
+
* Both wire formats: a delay in seconds, and an HTTP date.
|
|
69
|
+
*/
|
|
70
|
+
readonly retryAfterMs?: number;
|
|
71
|
+
/**
|
|
72
|
+
* The vendor's rate-limit headers, and **only** those — see `RATE_LIMIT_HEADERS`.
|
|
73
|
+
*
|
|
74
|
+
* An allowlist rather than every header, for two reasons. A failure travels into error messages and audit
|
|
75
|
+
* records, so carrying `set-cookie` or an auth echo would be a leak with no purpose. And a bounded set says
|
|
76
|
+
* what this field is *for*: `retryAfterMs` answers "how long", and these answer the question X forces —
|
|
77
|
+
* **which** limit was hit. A 15-minute burst limit and a 24-hour cap are both `429`, and treating the second
|
|
78
|
+
* as retryable makes a client sit in backoff until tomorrow.
|
|
79
|
+
*/
|
|
80
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
60
81
|
};
|
|
61
82
|
export type HttpSuccess = {
|
|
62
83
|
readonly ok: true;
|
|
@@ -97,6 +118,12 @@ export type HttpRequest = {
|
|
|
97
118
|
export type HttpClient = {
|
|
98
119
|
request(input: HttpRequest): Promise<HttpOutcome>;
|
|
99
120
|
};
|
|
121
|
+
export declare const rateLimitHeadersOf: (headers: Headers) => {
|
|
122
|
+
headers?: Readonly<Record<string, string>>;
|
|
123
|
+
};
|
|
124
|
+
export declare const retryAfterOf: (headers: Headers) => {
|
|
125
|
+
retryAfterMs?: number;
|
|
126
|
+
};
|
|
100
127
|
export declare const createHttpClient: (config?: HttpClientConfig) => HttpClient;
|
|
101
128
|
/**
|
|
102
129
|
* Read at most `maxBytes`, stopping as they arrive, and abandon the rest.
|
package/dist/toolkit/http.js
CHANGED
|
@@ -56,6 +56,52 @@ export const REQUEST_TIMEOUT_MS = 10_000;
|
|
|
56
56
|
* choose which credential to spend and where to send it. `host` forges the request target past the policy check.
|
|
57
57
|
*/
|
|
58
58
|
const RESERVED_HEADERS = new Set(["authorization", "cookie", "proxy-authorization", "host", "content-length"]);
|
|
59
|
+
/**
|
|
60
|
+
* `Retry-After` as milliseconds, in whichever of its two forms the server used.
|
|
61
|
+
*
|
|
62
|
+
* Returns a spreadable object rather than a number so an absent or unparseable header adds no key at all —
|
|
63
|
+
* `retryAfterMs: undefined` and "no retry hint" are different things to a consumer reading the field.
|
|
64
|
+
*
|
|
65
|
+
* A date in the past yields `0` rather than a negative delay, and an absurd value is capped: a vendor asking
|
|
66
|
+
* for a six-hour wait inside a request path is a vendor to give up on, not to obey.
|
|
67
|
+
*/
|
|
68
|
+
const MAX_RETRY_AFTER_MS = 5 * 60_000;
|
|
69
|
+
/**
|
|
70
|
+
* The response headers a failure carries, by prefix.
|
|
71
|
+
*
|
|
72
|
+
* These are the shapes vendors actually use: `x-ratelimit-*` (GitHub, Reddit), `x-rate-limit-*` (X's
|
|
73
|
+
* per-15-minute window), `x-user-limit-24hour-*` / `x-app-limit-24hour-*` (X's daily caps, which are a
|
|
74
|
+
* different limit with a different remedy), and `x-ms-ratelimit-*` (Azure Resource Manager, whose
|
|
75
|
+
* `remaining-subscription-reads` counts *down* rather than reporting a ceiling — the only one of these that
|
|
76
|
+
* lets a client see a throttle approaching instead of discovering it).
|
|
77
|
+
*/
|
|
78
|
+
const RATE_LIMIT_HEADERS = [
|
|
79
|
+
"retry-after",
|
|
80
|
+
"x-ratelimit-",
|
|
81
|
+
"x-rate-limit-",
|
|
82
|
+
"x-user-limit-",
|
|
83
|
+
"x-app-limit-",
|
|
84
|
+
"x-ms-ratelimit-",
|
|
85
|
+
];
|
|
86
|
+
export const rateLimitHeadersOf = (headers) => {
|
|
87
|
+
const kept = {};
|
|
88
|
+
headers.forEach((value, name) => {
|
|
89
|
+
const lower = name.toLowerCase();
|
|
90
|
+
if (RATE_LIMIT_HEADERS.some((prefix) => lower === prefix || lower.startsWith(prefix)))
|
|
91
|
+
kept[lower] = value;
|
|
92
|
+
});
|
|
93
|
+
return Object.keys(kept).length === 0 ? {} : { headers: kept };
|
|
94
|
+
};
|
|
95
|
+
export const retryAfterOf = (headers) => {
|
|
96
|
+
const raw = headers.get("retry-after");
|
|
97
|
+
if (raw === null || raw.trim() === "")
|
|
98
|
+
return {};
|
|
99
|
+
const seconds = Number(raw.trim());
|
|
100
|
+
const ms = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(raw.trim()) - Date.now();
|
|
101
|
+
if (!Number.isFinite(ms))
|
|
102
|
+
return {};
|
|
103
|
+
return { retryAfterMs: Math.min(Math.max(Math.round(ms), 0), MAX_RETRY_AFTER_MS) };
|
|
104
|
+
};
|
|
59
105
|
export const createHttpClient = (config = {}) => {
|
|
60
106
|
const policy = config.policy ?? DEFAULT_EGRESS_POLICY;
|
|
61
107
|
const doFetch = config.fetchImpl ?? fetch;
|
|
@@ -124,6 +170,8 @@ export const createHttpClient = (config = {}) => {
|
|
|
124
170
|
kind: "http-error",
|
|
125
171
|
status: response.status,
|
|
126
172
|
reason: `That URL returned ${response.status}${text.trim() === "" ? "" : `: ${text.slice(0, 500)}`}`,
|
|
173
|
+
...retryAfterOf(response.headers),
|
|
174
|
+
...rateLimitHeadersOf(response.headers),
|
|
127
175
|
};
|
|
128
176
|
}
|
|
129
177
|
const body = input.fence === false
|
package/dist/toolkit/index.d.ts
CHANGED
|
@@ -10,12 +10,42 @@
|
|
|
10
10
|
* interesting behaviour of an outbound tool — what it refuses, where it stops reading — should be provable
|
|
11
11
|
* without constructing a run.
|
|
12
12
|
*/
|
|
13
|
-
export { DEFAULT_EGRESS_POLICY, MAX_RESPONSE_BYTES, REQUEST_TIMEOUT_MS, createHttpClient, readBounded, } from "./http.js";
|
|
13
|
+
export { DEFAULT_EGRESS_POLICY, MAX_RESPONSE_BYTES, REQUEST_TIMEOUT_MS, createHttpClient, readBounded, rateLimitHeadersOf, retryAfterOf, } from "./http.js";
|
|
14
14
|
export type { HttpClient, HttpClientConfig, HttpFailure, HttpOutcome, HttpRequest, HttpSuccess } from "./http.js";
|
|
15
|
+
export { createVendorTransport } from "./vendor.js";
|
|
16
|
+
/**
|
|
17
|
+
* RFC 5322 message building, shared — REQ-056 (#240), task #241.
|
|
18
|
+
*
|
|
19
|
+
* It began in `tools-google` and moved here when `tools-email` needed the same encoder. The header rules are
|
|
20
|
+
* the kind that fail *silently* — an unencoded non-ASCII subject arrives as mojibake, a header with a line
|
|
21
|
+
* break becomes two headers and the second copies the mail to somebody — so there is one implementation, and
|
|
22
|
+
* `tools-google` re-exports it rather than keeping a copy.
|
|
23
|
+
*/
|
|
24
|
+
export { assertHeaderSafe, boundaryFor, buildMessage, encodeHeader, HeaderInjectionError, headerOf, stripBcc, } from "./mime.js";
|
|
25
|
+
export type { Attachment, OutgoingMessage } from "./mime.js";
|
|
26
|
+
/**
|
|
27
|
+
* The SSRF-hardened fetch, shared — REQ-055 (#237), tasks #238 and #239.
|
|
28
|
+
*
|
|
29
|
+
* It was written for `tools-scrape` and lives here because `tools-browser` needs the *same* implementation
|
|
30
|
+
* rather than a second copy: a browser navigating to `169.254.169.254` is the same hole as a fetch doing it,
|
|
31
|
+
* and two copies of an address classifier is how one of them ends up missing the IPv6-mapped forms.
|
|
32
|
+
*
|
|
33
|
+
* **Not a replacement for `mcp/egress.ts`, which is deliberately stricter.** That one denies *every* IPv6
|
|
34
|
+
* literal unless explicitly allowed, which is right for an MCP endpoint an operator configures once and wrong
|
|
35
|
+
* for scraping the open web, where a great many real sites are v6-only. They differ because the questions
|
|
36
|
+
* differ, and merging them would mean picking one answer for both.
|
|
37
|
+
*/
|
|
38
|
+
export { BlockedError, DEFAULT_USER_AGENT, isPrivateAddress, isPrivateV4, isPrivateV6, nodeTransport, refuseUrl, resolvePublicly, safeFetch, systemResolve, } from "./ssrf.js";
|
|
39
|
+
export type { Refusal, Resolve, SafeFetchOptions, SafeResponse, SafeTransport } from "./ssrf.js";
|
|
40
|
+
export type { VendorClassifier, VendorFailure, VendorTransport, VendorTransportConfig } from "./vendor.js";
|
|
15
41
|
export { DEFAULT_SEARCH_LIMIT, MAX_SNIPPET_CHARS, createFetchJson, createFetchPage, createWebSearch, htmlToText, } from "./web.js";
|
|
16
42
|
export type { JsonResult, PageResult, SearchHit, SearchOutcome, SearchProvider } from "./web.js";
|
|
17
43
|
export { MAX_CELL_CHARS, MAX_CSV_ROWS, MAX_SQL_ROWS, createSqlQuery, createSqlSchema, parseCsv, queryJson, } from "./data.js";
|
|
18
44
|
export type { CsvResult, JsonQueryResult, ReadOnlyQuery, SchemaResult, SqlResult } from "./data.js";
|
|
19
45
|
export { MAX_EXPRESSION_CHARS, calculate, currentTime } from "./compute.js";
|
|
20
46
|
export type { CalculationResult, TimeResult } from "./compute.js";
|
|
47
|
+
export { MAX_ENTRIES, MAX_FILE_BYTES, MAX_MATCHES, MAX_SEARCHED_FILES, contains, createFileReader, } from "./files.js";
|
|
48
|
+
export type { FileEntry, FileFailure, FileList, FileMatch, FileRead, FileReader, FileScope, FileSearch, FileWrite } from "./files.js";
|
|
49
|
+
export { DEFAULT_MEMORY_MB, DEFAULT_TIMEOUT_MS, MAX_OUTPUT_BYTES, createDockerSandbox, createLocalSandbox, dockerArgs, } from "./sandbox.js";
|
|
50
|
+
export type { DockerSandboxConfig, LocalSandboxConfig, Sandbox, SandboxRequest, SandboxResult } from "./sandbox.js";
|
|
21
51
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/toolkit/index.js
CHANGED
|
@@ -10,8 +10,33 @@
|
|
|
10
10
|
* interesting behaviour of an outbound tool — what it refuses, where it stops reading — should be provable
|
|
11
11
|
* without constructing a run.
|
|
12
12
|
*/
|
|
13
|
-
export { DEFAULT_EGRESS_POLICY, MAX_RESPONSE_BYTES, REQUEST_TIMEOUT_MS, createHttpClient, readBounded, } from "./http.js";
|
|
13
|
+
export { DEFAULT_EGRESS_POLICY, MAX_RESPONSE_BYTES, REQUEST_TIMEOUT_MS, createHttpClient, readBounded, rateLimitHeadersOf, retryAfterOf, } from "./http.js";
|
|
14
|
+
export { createVendorTransport } from "./vendor.js";
|
|
15
|
+
/**
|
|
16
|
+
* RFC 5322 message building, shared — REQ-056 (#240), task #241.
|
|
17
|
+
*
|
|
18
|
+
* It began in `tools-google` and moved here when `tools-email` needed the same encoder. The header rules are
|
|
19
|
+
* the kind that fail *silently* — an unencoded non-ASCII subject arrives as mojibake, a header with a line
|
|
20
|
+
* break becomes two headers and the second copies the mail to somebody — so there is one implementation, and
|
|
21
|
+
* `tools-google` re-exports it rather than keeping a copy.
|
|
22
|
+
*/
|
|
23
|
+
export { assertHeaderSafe, boundaryFor, buildMessage, encodeHeader, HeaderInjectionError, headerOf, stripBcc, } from "./mime.js";
|
|
24
|
+
/**
|
|
25
|
+
* The SSRF-hardened fetch, shared — REQ-055 (#237), tasks #238 and #239.
|
|
26
|
+
*
|
|
27
|
+
* It was written for `tools-scrape` and lives here because `tools-browser` needs the *same* implementation
|
|
28
|
+
* rather than a second copy: a browser navigating to `169.254.169.254` is the same hole as a fetch doing it,
|
|
29
|
+
* and two copies of an address classifier is how one of them ends up missing the IPv6-mapped forms.
|
|
30
|
+
*
|
|
31
|
+
* **Not a replacement for `mcp/egress.ts`, which is deliberately stricter.** That one denies *every* IPv6
|
|
32
|
+
* literal unless explicitly allowed, which is right for an MCP endpoint an operator configures once and wrong
|
|
33
|
+
* for scraping the open web, where a great many real sites are v6-only. They differ because the questions
|
|
34
|
+
* differ, and merging them would mean picking one answer for both.
|
|
35
|
+
*/
|
|
36
|
+
export { BlockedError, DEFAULT_USER_AGENT, isPrivateAddress, isPrivateV4, isPrivateV6, nodeTransport, refuseUrl, resolvePublicly, safeFetch, systemResolve, } from "./ssrf.js";
|
|
14
37
|
export { DEFAULT_SEARCH_LIMIT, MAX_SNIPPET_CHARS, createFetchJson, createFetchPage, createWebSearch, htmlToText, } from "./web.js";
|
|
15
38
|
export { MAX_CELL_CHARS, MAX_CSV_ROWS, MAX_SQL_ROWS, createSqlQuery, createSqlSchema, parseCsv, queryJson, } from "./data.js";
|
|
16
39
|
export { MAX_EXPRESSION_CHARS, calculate, currentTime } from "./compute.js";
|
|
40
|
+
export { MAX_ENTRIES, MAX_FILE_BYTES, MAX_MATCHES, MAX_SEARCHED_FILES, contains, createFileReader, } from "./files.js";
|
|
41
|
+
export { DEFAULT_MEMORY_MB, DEFAULT_TIMEOUT_MS, MAX_OUTPUT_BYTES, createDockerSandbox, createLocalSandbox, dockerArgs, } from "./sandbox.js";
|
|
17
42
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 5322 messages, built once for every toolkit that sends mail — REQ-056 (#240), task #241.
|
|
3
|
+
*
|
|
4
|
+
* This started in `tools-google` for Gmail (#234) and moved here when `tools-email` needed the same thing. The
|
|
5
|
+
* reason is the one that moved `ssrf.ts`: the parts that are silent when wrong must have exactly one
|
|
6
|
+
* implementation, because a second copy is the one that ends up not encoding a header.
|
|
7
|
+
*
|
|
8
|
+
* Three things here are wrong *silently*, which is why they are here rather than per package:
|
|
9
|
+
*
|
|
10
|
+
* **A non-ASCII subject.** Headers are ASCII by the spec. Putting an umlaut in one raw does not throw — it
|
|
11
|
+
* arrives as mojibake and nobody tells you.
|
|
12
|
+
*
|
|
13
|
+
* **A header carrying a line break.** A subject of `Update` + CRLF + `Bcc: attacker@example.com` is two
|
|
14
|
+
* headers, and the second silently copies the message to somebody. Reachable from untrusted content: an agent
|
|
15
|
+
* composing a subject from a page it scraped or a mail it read is exactly the path.
|
|
16
|
+
*
|
|
17
|
+
* **Part ordering in `multipart/alternative`.** The spec orders parts least-faithful first, so `text/plain`
|
|
18
|
+
* precedes `text/html`. Reversed, a client showing the *last* part it understands displays the plain-text
|
|
19
|
+
* fallback and the HTML is never seen — mail that looks broken to the recipient and fine to the sender.
|
|
20
|
+
*
|
|
21
|
+
* ## Determinism, and why there is no `Date` or `Message-ID`
|
|
22
|
+
*
|
|
23
|
+
* `email_compose_preview` has to produce **byte-identical** output to what a send transmits, or the rehearsal
|
|
24
|
+
* is of a different message. A `Date` stamped at compose time makes that impossible by construction, and a
|
|
25
|
+
* `Message-ID` generated per call makes it impossible too. Both are added by the sending MTA or the provider's
|
|
26
|
+
* API, which is where they belong — they describe the act of sending, not the message the caller wrote. The
|
|
27
|
+
* multipart boundary is derived from a hash of the content for the same reason: a random boundary would make
|
|
28
|
+
* two composes of the same message differ.
|
|
29
|
+
*/
|
|
30
|
+
/** A header value, RFC 2047 encoded when it needs to be — and left alone when it does not. */
|
|
31
|
+
export declare const encodeHeader: (value: string) => string;
|
|
32
|
+
export declare class HeaderInjectionError extends Error {
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Refuses a header value that would inject another header.
|
|
36
|
+
*
|
|
37
|
+
* Refusing rather than stripping: no legitimate subject or address contains a newline, so nothing correct is
|
|
38
|
+
* lost, and silently removing it would hide an attempt somebody should see.
|
|
39
|
+
*/
|
|
40
|
+
export declare const assertHeaderSafe: (field: string, value: string) => void;
|
|
41
|
+
export type Attachment = {
|
|
42
|
+
readonly filename: string;
|
|
43
|
+
readonly contentType: string;
|
|
44
|
+
/** Base64. The caller encodes, because the bytes may never have been a string. */
|
|
45
|
+
readonly contentBase64: string;
|
|
46
|
+
};
|
|
47
|
+
export type OutgoingMessage = {
|
|
48
|
+
readonly to: readonly string[];
|
|
49
|
+
readonly subject: string;
|
|
50
|
+
readonly cc?: readonly string[];
|
|
51
|
+
readonly bcc?: readonly string[];
|
|
52
|
+
readonly from?: string;
|
|
53
|
+
readonly replyTo?: string;
|
|
54
|
+
/** Plain text. At least one of `text` and `html` is required. */
|
|
55
|
+
readonly text?: string;
|
|
56
|
+
readonly html?: string;
|
|
57
|
+
/**
|
|
58
|
+
* The single-body form, kept because `tools-google` was written against it.
|
|
59
|
+
*
|
|
60
|
+
* Equivalent to `text`. Both exist rather than one being renamed, so moving this module did not change the
|
|
61
|
+
* bytes any existing caller produces — which is the property that made the move safe to make.
|
|
62
|
+
*/
|
|
63
|
+
readonly body?: string;
|
|
64
|
+
readonly attachments?: readonly Attachment[];
|
|
65
|
+
/** The `Message-ID` of the message being replied to. Both threading headers derive from it. */
|
|
66
|
+
readonly inReplyTo?: string;
|
|
67
|
+
/** The original's `References`, so a long thread keeps its whole chain rather than just the last hop. */
|
|
68
|
+
readonly references?: string;
|
|
69
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* A boundary that depends only on the message.
|
|
73
|
+
*
|
|
74
|
+
* Deterministic so `email_compose_preview` and `email_send` produce the same bytes, and content-derived so two
|
|
75
|
+
* different messages do not share one. The loop rules out the remaining case where the chosen boundary appears
|
|
76
|
+
* inside a part — astronomically unlikely with a hash, and cheaper to exclude than to reason about.
|
|
77
|
+
*/
|
|
78
|
+
export declare const boundaryFor: (parts: readonly string[]) => string;
|
|
79
|
+
/**
|
|
80
|
+
* An RFC 5322 message.
|
|
81
|
+
*
|
|
82
|
+
* CRLF line endings, not bare newlines: the spec says CRLF and some servers are strict. Getting it wrong
|
|
83
|
+
* produces a message that works with most providers and is rejected by one, which is the worst kind of bug to
|
|
84
|
+
* find in production.
|
|
85
|
+
*/
|
|
86
|
+
export declare const buildMessage: (message: OutgoingMessage) => string;
|
|
87
|
+
/**
|
|
88
|
+
* Removes the `Bcc` header from a composed message.
|
|
89
|
+
*
|
|
90
|
+
* For SMTP the envelope carries every recipient and the header must not, or each blind recipient can read the
|
|
91
|
+
* whole blind list — the single most embarrassing mail bug there is. The header is kept in the composed form
|
|
92
|
+
* so a preview can show it, and stripped at the transport that would otherwise leak it.
|
|
93
|
+
*/
|
|
94
|
+
export declare const stripBcc: (raw: string) => string;
|
|
95
|
+
/** The value of a header in a composed message, for tests and for reading a provider's echo. */
|
|
96
|
+
export declare const headerOf: (raw: string, name: string) => string | undefined;
|
|
97
|
+
//# sourceMappingURL=mime.d.ts.map
|