functionalscript 0.34.0 → 0.35.1

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 CHANGED
@@ -28,22 +28,39 @@ Install FunctionalScript via npm:
28
28
  npm install -g functionalscript
29
29
  ```
30
30
 
31
- The FunctionalScript compiler command (`fjs compile`) currently supports:
31
+ The `fjs` CLI provides several commands:
32
32
 
33
- * `import` statements
34
- * `const` declarations
33
+ | Command | Description |
34
+ |------------------|-----------------------------------------------------------|
35
+ | `fjs test` / `t` | Run the FunctionalScript test suite |
36
+ | `fjs compile` / `c` | Compile a `.f.ts` module to JavaScript |
37
+ | `fjs run` / `r` | Run a FunctionalScript module as a Node program |
38
+ | `fjs cas` / `s` | Content-addressable storage (`add`, `get`, `list`) |
39
+ | `fjs mcp` / `m` | Run an MCP server over stdio exposing the CAS as tools |
40
+ | `fjs ci` / `i` | Generate the GitHub Actions CI workflow |
35
41
 
36
- It does **not** yet support functions or complex expressions.
42
+ ### Content-Addressable Storage (CAS)
37
43
 
38
- Example usage with `fjs`:
44
+ FunctionalScript ships a built-in CAS for storing and retrieving blobs by their cryptographic hash:
39
45
 
40
46
  ```bash
41
- fjs compile example.f.js output.json
42
- # or
43
- fjs compile example.f.js output.f.js
47
+ fjs cas add myfile.txt # store a file, print its cBase32 hash
48
+ fjs cas get <hash> # restore a blob by hash
49
+ fjs cas list # list all stored hashes
44
50
  ```
45
51
 
46
- FunctionalScript code can be compiled directly into either JSON or JavaScript without imports.
52
+ Blobs are stored under `~/.cas/` and addressed by their SHA-256 hash encoded in cBase32.
53
+
54
+ ### MCP Server
55
+
56
+ The CAS is also exposed as an [MCP](https://modelcontextprotocol.io/) server so LLM agents can read and write blobs without a shell:
57
+
58
+ ```bash
59
+ # register with Claude CLI
60
+ claude mcp add cas -- npx functionalscript m
61
+ ```
62
+
63
+ See [`fs/cas/mcp/README.md`](fs/cas/mcp/README.md) for details on the `cas_add`, `cas_get`, and `cas_list` tools.
47
64
 
48
65
  ## Vision
49
66
 
@@ -67,10 +84,6 @@ In FunctionalScript:
67
84
  - A module can depend only on another FunctionalScript module.
68
85
  - It also has no standard library. Only a safe subset of standard JavaScript API can be used without referencing other modules.
69
86
 
70
- ## Our Next Step
71
-
72
- [Re-architecture of NaNVM](https://medium.com/@sergeyshandar/nanvm-re-architecture-8097f766ec1c?sk=d14ec1daf73ac5442f12ce20b2bc037a).
73
-
74
87
  ## Sponsors
75
88
 
76
89
  - [KirillOsenkov](https://github.com/KirillOsenkov),
@@ -8,4 +8,6 @@ export declare const proof: {
8
8
  invalidInput: () => void;
9
9
  validPadding: () => void;
10
10
  knownVectors: () => void;
11
+ decodeOverflow: () => void;
12
+ encodeLargeVecIsSlow: () => void;
11
13
  };
@@ -1,14 +1,9 @@
1
- import { empty, vec } from "../types/bit_vec/module.f.js";
1
+ import { assertEq } from "../asserts/module.f.js";
2
+ import { empty, vec, repeat, vec8 } from "../types/bit_vec/module.f.js";
2
3
  import { encode, decode } from "./module.f.js";
3
4
  const check = (s, v) => {
4
- const sr = encode(v);
5
- if (sr !== s) {
6
- throw ['encode', sr, s];
7
- }
8
- const vr = decode(s);
9
- if (vr !== v) {
10
- throw ['decode', vr, v];
11
- }
5
+ assertEq(encode(v), s);
6
+ assertEq(decode(s), v);
12
7
  };
13
8
  export const proof = {
14
9
  empty: () => {
@@ -46,62 +41,29 @@ export const proof = {
46
41
  },
47
42
  nonOctet: () => {
48
43
  // encode rejects bit vectors whose length is not a multiple of 8
49
- if (encode(vec(1n)(0n)) !== null) {
50
- throw '1-bit input should return null';
51
- }
52
- if (encode(vec(6n)(0n)) !== null) {
53
- throw '6-bit input should return null';
54
- }
55
- if (encode(vec(12n)(0n)) !== null) {
56
- throw '12-bit input should return null';
57
- }
44
+ assertEq(encode(vec(1n)(0n)), null);
45
+ assertEq(encode(vec(6n)(0n)), null);
46
+ assertEq(encode(vec(12n)(0n)), null);
58
47
  },
59
48
  invalidInput: () => {
60
- if (decode('!') !== null) {
61
- throw 'invalid char should return null';
62
- }
63
- if (decode('A!AA') !== null) {
64
- throw 'invalid char mid-string should return null';
65
- }
66
- if (decode('A===') !== null) {
67
- throw 'three equals signs should return null';
68
- }
69
- if (decode('A') !== null) {
70
- throw 'length 1 (not multiple of 4) should return null';
71
- }
72
- if (decode('AA') !== null) {
73
- throw 'length 2 (no padding) should return null';
74
- }
75
- if (decode('AAA') !== null) {
76
- throw 'length 3 (no padding) should return null';
77
- }
78
- if (decode('=AAA') !== null) {
79
- throw 'padding not at end should return null';
80
- }
49
+ assertEq(decode('!'), null);
50
+ assertEq(decode('A!AA'), null);
51
+ assertEq(decode('A==='), null);
52
+ assertEq(decode('A'), null);
53
+ assertEq(decode('AA'), null);
54
+ assertEq(decode('AAA'), null);
55
+ assertEq(decode('=AAA'), null);
81
56
  // Non-zero padding bits must be rejected (RFC 4648 §3.5)
82
- if (decode('AB==') !== null) {
83
- throw 'non-zero 4-pad-bit should return null';
84
- }
85
- if (decode('AAB=') !== null) {
86
- throw 'non-zero 2-pad-bit should return null';
87
- }
57
+ assertEq(decode('AB=='), null);
58
+ assertEq(decode('AAB='), null);
88
59
  },
89
60
  validPadding: () => {
90
61
  // "==" padding
91
- const v2 = decode('AA==');
92
- if (v2 !== vec(8n)(0n)) {
93
- throw ['AA== should decode to 8-bit zero', v2];
94
- }
62
+ assertEq(decode('AA=='), vec(8n)(0n));
95
63
  // "=" padding
96
- const v1 = decode('AAA=');
97
- if (v1 !== vec(16n)(0n)) {
98
- throw ['AAA= should decode to 16-bit zero', v1];
99
- }
64
+ assertEq(decode('AAA='), vec(16n)(0n));
100
65
  // No padding
101
- const v0 = decode('AAAA');
102
- if (v0 !== vec(24n)(0n)) {
103
- throw ['AAAA should decode to 24-bit zero', v0];
104
- }
66
+ assertEq(decode('AAAA'), vec(24n)(0n));
105
67
  },
106
68
  knownVectors: () => {
107
69
  // RFC 4648 §10 test vectors (byte-aligned, treating bytes as 8-bit MSB vectors)
@@ -114,4 +76,35 @@ export const proof = {
114
76
  // 0x66 0x6f 0x6f ('foo') → Zm9v
115
77
  check('Zm9v', vec(24n)(0x666f6fn));
116
78
  },
79
+ decodeOverflow: () => {
80
+ // 174_764 base64 chars decode to 1_048_584 bits, 8 over `maxLength`;
81
+ // `decode` should return `null` instead of throwing on (or hanging
82
+ // while building) an oversized `bigint`.
83
+ const oversized = 'A'.repeat(174_764);
84
+ assertEq(decode(oversized), null);
85
+ },
86
+ // Regression guard: `encode` used to be quadratic in the input size, not
87
+ // linear. `baseN`'s `vecToString` (`fs/base_n/module.f.ts`) popped one
88
+ // 6-bit chunk off the front at a time via `popFront`, and each `popFront`
89
+ // re-masked the entire remaining bigint through `vec()`'s `m & ui` —
90
+ // O(remaining length) — so encoding a vector of `n` bits cost O(n²). A
91
+ // 90,000-byte input took ~5.6s on Node and ~18.4s on Bun (Bun's `bigint`
92
+ // has a much higher per-operation constant, see PR #1190), reliably
93
+ // blowing `bun test`'s 5s per-test timeout — the same class of failure
94
+ // that hit `cas_get content:true` on a comparably-sized binary blob in CI
95
+ // (PR #1201).
96
+ //
97
+ // Fixed by rewriting `vecToString` as a balanced recursive split
98
+ // (`unpackToString`), mirroring the `chunkList`/`unpackChunkList`
99
+ // divide-and-conquer already used by `u8List` and by concatenation
100
+ // (`msb.concat` / `unpackListToVec`, PR #1192) — true O(n log n) now,
101
+ // since each half is masked to its own length before recursing. The same
102
+ // call now takes ~50ms on Node and ~225ms on Bun. No timing assertion
103
+ // (duration varies by engine/machine) — this just exercises the call and
104
+ // relies on the test runner's own per-test timing to catch a regression.
105
+ encodeLargeVecIsSlow: () => {
106
+ const big = repeat(90000n)(vec8(0xffn));
107
+ const result = encode(big);
108
+ assertEq(result?.length, 120_000);
109
+ },
117
110
  };
@@ -18,9 +18,10 @@ import type { Nullable } from '../types/nullable/module.f.ts';
18
18
  */
19
19
  export type BaseN = {
20
20
  /**
21
- * Encodes a bit vector by repeatedly popping `bits`-wide MSB chunks and
22
- * indexing them into the alphabet. A trailing partial chunk shorter than
23
- * `bits` is left-padded with zeros (the underlying `popFront` semantics).
21
+ * Encodes a bit vector by splitting it into `bits`-wide MSB chunks
22
+ * (`chunkList`, a balanced divide-and-conquer split, not a linear scan)
23
+ * and indexing each into the alphabet. A trailing partial chunk shorter
24
+ * than `bits` is left-padded with zeros.
24
25
  */
25
26
  readonly vecToString: (v: Vec) => string;
26
27
  /**
@@ -11,8 +11,14 @@
11
11
  *
12
12
  * @module
13
13
  */
14
- import { msb, length, vec, empty } from "../types/bit_vec/module.f.js";
15
- const { popFront, concat } = msb;
14
+ import { msb, lsb, vec, chunkList, unpack } from "../types/bit_vec/module.f.js";
15
+ import { fold } from "../types/list/module.f.js";
16
+ import { compose } from "../types/function/module.f.js";
17
+ const { unpackSplit } = msb;
18
+ const { tryListToVec: reversedListToVec } = lsb;
19
+ // `chunkList(msb)` doesn't depend on `bits` or `v` — shared across every
20
+ // `baseN(...)` codec (base64, cbase32, ...).
21
+ const chunkListMsb = chunkList(msb);
16
22
  /**
17
23
  * Builds a {@link BaseN} codec for a fixed chunk width and alphabet.
18
24
  *
@@ -24,31 +30,43 @@ const { popFront, concat } = msb;
24
30
  * `i`/`l`→`1`, `o`→`0`.
25
31
  */
26
32
  export const baseN = (bits, alphabet, normalize) => {
27
- const popFrontN = popFront(bits);
28
33
  const vecN = vec(bits);
29
34
  const toIndex = normalize === undefined
30
35
  ? (c) => alphabet.indexOf(c)
31
36
  : (c) => alphabet.indexOf(normalize(c));
37
+ const unpackSplitBits = unpackSplit(bits);
38
+ // Converts one `<= bits`-wide chunk (as yielded by `chunkList`, already
39
+ // masked to its own length) to its alphabet index. A trailing partial
40
+ // chunk shorter than `bits` is left-padded with zeros: `unpackSplit`'s
41
+ // shift amount goes negative, which per spec becomes a left shift.
42
+ const chunkToIndex = (chunk) => {
43
+ const u = unpack(chunk);
44
+ return Number(u.length < bits ? unpackSplitBits(u)[0] : u.uint);
45
+ };
46
+ // Folds directly over `chunkList`'s lazy list in one pass — faster than
47
+ // `map` into a second lazy list before joining, since there's no second
48
+ // list to allocate/traverse.
49
+ const chunkToString = (chunk) => (acc) => acc + alphabet[chunkToIndex(chunk)];
32
50
  return {
33
- vecToString: v => {
34
- let result = '';
35
- while (length(v) > 0n) {
36
- const [r, rest] = popFrontN(v);
37
- result += alphabet[Number(r)];
38
- v = rest;
39
- }
40
- return result;
41
- },
51
+ // `chunkListMsb(bits)` then `fold(chunkToString)('')` — neither half
52
+ // depends on `v`, so `compose` builds (and this closure captures)
53
+ // the composed function once per `baseN(...)` codec; no need to name
54
+ // the halves separately just to get that one-time build.
55
+ vecToString: compose(chunkListMsb(bits))(fold(chunkToString)('')),
42
56
  stringToVec: s => {
43
- let result = empty;
57
+ // Build a reversed chunk list, bailing out at the first invalid
58
+ // character so malformed input is rejected in O(prefix) time and
59
+ // `normalize` is never run past it. `listToVec` then concatenates in
60
+ // O(n log n).
61
+ let chunks = null;
44
62
  for (const c of s) {
45
63
  const index = toIndex(c);
46
64
  if (index < 0) {
47
65
  return null;
48
66
  }
49
- result = concat(result)(vecN(BigInt(index)));
67
+ chunks = { first: vecN(BigInt(index)), tail: chunks };
50
68
  }
51
- return result;
69
+ return reversedListToVec(chunks);
52
70
  },
53
71
  };
54
72
  };
@@ -7,4 +7,5 @@ export declare const proof: {
7
7
  decodeInvalid: () => void;
8
8
  normalizeHit: () => void;
9
9
  normalizeMiss: () => void;
10
+ big: () => void;
10
11
  };
@@ -1,4 +1,5 @@
1
- import { empty, vec } from "../types/bit_vec/module.f.js";
1
+ import { assertEq } from "../asserts/module.f.js";
2
+ import { empty, maxLength, vec, length } from "../types/bit_vec/module.f.js";
2
3
  import { baseN } from "./module.f.js";
3
4
  const hex = baseN(4n, '0123456789abcdef');
4
5
  const cb32 = baseN(5n, '0123456789abcdefghjkmnpqrstvwxyz', c => {
@@ -18,6 +19,9 @@ const cb32 = baseN(5n, '0123456789abcdefghjkmnpqrstvwxyz', c => {
18
19
  }
19
20
  }
20
21
  });
22
+ // Sample input for the `big` proof below: 262 144 `f` characters decode into a
23
+ // 1 Mibit (`maxLength`) vector.
24
+ const bigSampleHex = `f`.repeat(Number(maxLength >> 2n));
21
25
  export const proof = {
22
26
  encodeEmpty: () => {
23
27
  const s = hex.vecToString(empty);
@@ -83,4 +87,11 @@ export const proof = {
83
87
  throw 'unknown char should return null';
84
88
  }
85
89
  },
90
+ // Decodes a 1 Mibit hex string. With the O(n log n) `listToVec` builder this
91
+ // runs in well under a second (was ~13 s node / ~43 s bun under the old
92
+ // per-chunk `concat`).
93
+ big: () => {
94
+ const x = hex.stringToVec(bigSampleHex);
95
+ assertEq(length(x), maxLength);
96
+ }
86
97
  };
@@ -47,11 +47,14 @@
47
47
  * only metadata was asked for; streaming detection returns correct
48
48
  * `{ length, mime_type, type[, url] }` regardless of size.
49
49
  *
50
- * **`content: true`** materializes the bytes (via `collectRead`, bounded by
51
- * `maxLength`), classifies them with the same machine via `fs/mime` `detectVec`,
52
- * then encodes the inline payload by `type` a `fs/text/utf8` `fromVec` string for
53
- * `text`, base64 for `base64`. A blob larger than `maxLength` is unsupported here
54
- * and should be fetched via `url`.
50
+ * **`content: true`** first derives `{ length, mime_type, type }` with the same
51
+ * size-independent `fs/mime` `detectStream` machine, then materializes the bytes
52
+ * (via `collectRead`, bounded by `maxLength`) and encodes the inline payload by
53
+ * `type` — a `fs/text/utf8` `fromVec` string for `text`, base64 for `base64`. A
54
+ * blob larger than `maxLength` (128 KiB) cannot be buffered into one `Vec`, so it
55
+ * is rejected here with a descriptive *"too large"* error (carrying the byte size
56
+ * and `url`) rather than being misreported as absent; it should be fetched via
57
+ * `url` or inspected with metadata-only `cas_get`.
55
58
  *
56
59
  * ## Encoding split: hashes vs. content
57
60
  *
@@ -69,6 +72,8 @@
69
72
  * - `type: 'base64'` with malformed content (base64 `decode` → `null`) → `isError`
70
73
  * - malformed `hash` (`cBase32ToVec` → `null`) → `isError`
71
74
  * - `cas_get` on an absent hash (`c.read` → `undefined`) → `isError`
75
+ * - `cas_get` with `content: true` on a blob larger than `maxLength` → `isError`
76
+ * (distinct "too large" message, not "no such hash")
72
77
  * - unknown tool `name` → `isError`
73
78
  *
74
79
  * @module
@@ -79,9 +84,9 @@ import { pure } from "../../effects/module.f.js";
79
84
  import { create } from "../../effects/memory/module.f.js";
80
85
  import { cBase32ToVec, vecToCBase32 } from "../../cbase32/module.f.js";
81
86
  import { decode as base64Decode, encode as base64Encode } from "../../base64/module.f.js";
82
- import { utf8 } from "../../text/module.f.js";
83
- import { detectVec, detectStream } from "../../mime/module.f.js";
84
- import { empty, length as bitVecLength, maxLength, msb } from "../../types/bit_vec/module.f.js";
87
+ import { tryUtf8 } from "../../text/module.f.js";
88
+ import { detectStream } from "../../mime/module.f.js";
89
+ import { empty, length as bitVecLength, maxLength, maxLengthBytes, msb } from "../../types/bit_vec/module.f.js";
85
90
  import { ok, error } from "../../types/result/module.f.js";
86
91
  import { rm } from "../../effects/node/module.f.js";
87
92
  import { stdioTransport } from "../../mcp/stdio/module.f.js";
@@ -139,7 +144,7 @@ const casToolRegistry = (home) => {
139
144
  const c = fileCas(sha256)(home);
140
145
  const casUploadDir = `${home}/cas_upload`;
141
146
  return [
142
- toolEntry('cas_add', 'Store content and return its hash (cBase32). Pass type:"base64" for binary; type:"url" to stream a file from $HOME/cas_upload/ (no size limit); omit or pass type:"text" for UTF-8 text (default).', casAddArgs, ({ type, content }) => {
147
+ toolEntry('cas_add', 'Store content and return its hash (cBase32). Pass type:"base64" for binary; omit or pass type:"text" for UTF-8 text (default). Inline content (text/base64) is capped at 128 KiB (131072 bytes) — larger content is rejected. For larger content use type:"url" to stream a file from $HOME/cas_upload/ (no size limit).', casAddArgs, ({ type, content }) => {
143
148
  // type:'url' — stream the file into cas, then delete the source on success
144
149
  if (type === 'url') {
145
150
  if (!content.startsWith(`${casUploadDir}/`) || content.includes('..')) {
@@ -150,66 +155,56 @@ const casToolRegistry = (home) => {
150
155
  : rm(content).step(() => pure(okResult(vecToCBase32(v)))));
151
156
  }
152
157
  // type:'text' or 'base64' — resolve content to Vec, store via c.write()
153
- let x;
154
- switch (type) {
155
- case 'base64':
156
- const value = base64Decode(content);
157
- x = pure(value === null ? `invalid base64 content: ${content}` : value);
158
- break;
159
- default:
160
- x = pure(utf8(content));
161
- break;
162
- }
163
- return x.step(value => typeof value === 'string'
164
- ? pure(errorResult(value))
158
+ let x = type === 'base64'
159
+ ? base64Decode(content)
160
+ : tryUtf8(content);
161
+ return x === null
162
+ ? pure(errorResult('too large or malformed use type:"url" for large content'))
165
163
  // The resolved content fits in one chunk; feed it as a single-item stream.
166
- : c.write(nonEmpty(ok(value), elEmpty())).step(hashResult => pure(hashResult[0] === 'error'
164
+ : c.write(nonEmpty(ok(x), elEmpty())).step(([tag, hash]) => pure(tag === 'error'
167
165
  ? errorResult('write')
168
- : okResult(vecToCBase32(hashResult[1])))));
166
+ : okResult(vecToCBase32(hash))));
169
167
  }),
170
- toolEntry('cas_get', 'Inspect a blob by hash. Always returns JSON {length,mime_type,type[,url]} where type is "text" or "base64". Pass content:true to also include the inline content string.', casGetArgs, r => {
168
+ toolEntry('cas_get', 'Inspect a blob by hash. Always returns JSON {length,mime_type,type[,url]} where type is "text" or "base64". Pass content:true to also include the inline content string, but content is capped at 128 KiB (131072 bytes) — a larger blob is rejected with an error. To download a blob, prefer the url field returned in the result instead of requesting inline content.', casGetArgs, r => {
171
169
  const key = cBase32ToVec(r.hash);
172
170
  if (key === null) {
173
171
  return pure(errorResult(`invalid cBase32 hash: ${r.hash}`));
174
172
  }
175
173
  const url = c.url(key);
176
- // Metadata-only (the default): derive `{ length, mime_type, type }` by
177
- // streaming the blob through the detector state machine. Never buffers
178
- // the blob, so this is size-independent — large blobs that overflow a
179
- // single `Vec` still return correct metadata instead of an error.
180
- if (r.content !== true) {
181
- return detectStream(c.read(key)).step(result => {
182
- if (result[0] === 'error') {
183
- return pure(errorResult(`no such hash: ${r.hash}`));
184
- }
185
- const { length, mime_type, type } = result[1];
186
- const meta = { length: Number(length), mime_type, type, url };
187
- return pure(okResult(toJson(meta)));
188
- });
189
- }
190
- // content:true — collect the bytes (bounded by `maxLength`) so they can
191
- // be encoded inline. Classification uses the *same* `detectVec` state
192
- // machine as the metadata-only path; `type` then selects the encoding —
193
- // a UTF-8 string for `text`, base64 for `base64`.
194
- return collectRead(c.read(key)).step(result => {
195
- if (result[0] === 'error') {
174
+ return detectStream(c.read(key)).step(([tag, detected]) => {
175
+ if (tag === 'error') {
196
176
  return pure(errorResult(`no such hash: ${r.hash}`));
197
177
  }
198
- const value = result[1];
199
- const { length, mime_type, type } = detectVec(value);
178
+ const { length, mime_type, type } = detected;
200
179
  const meta = { length: Number(length), mime_type, type, url };
201
- if (type === 'text') {
202
- // `type: 'text'` means the detector validated `value` as UTF-8, so
203
- // `fromVec` is non-null here; guard defensively regardless.
204
- const str = fromVec(value);
205
- return pure(str === null
206
- ? errorResult(`content is not byte-aligned: ${r.hash}`)
207
- : okResult(toJson({ ...meta, content: str })));
180
+ if (r.content !== true) {
181
+ // content:true path continues below; this is just the metadata step.
182
+ return pure(okResult(toJson(meta)));
183
+ }
184
+ // A single `Vec` caps at `maxLength` bits (`maxLengthBytes` bytes), so
185
+ // a larger blob cannot be buffered for inline transfer. Report the
186
+ // size and point at the size-independent alternatives instead of
187
+ // misreporting an existing blob as `no such hash`.
188
+ if (length > maxLengthBytes) {
189
+ return pure(errorResult(`blob too large to fetch inline (${length} bytes, limit ${maxLengthBytes} bytes); use the url field (${url}) or omit content for metadata`));
208
190
  }
209
- const blob = base64Encode(value);
210
- return pure(blob === null
211
- ? errorResult(`content is not byte-aligned: ${r.hash}`)
212
- : okResult(toJson({ ...meta, content: blob })));
191
+ return collectRead(c.read(key)).step(([collectTag, value]) => {
192
+ if (collectTag === 'error') {
193
+ return pure(errorResult(`no such hash: ${r.hash}`));
194
+ }
195
+ if (type === 'text') {
196
+ // `type: 'text'` means the detector validated `value` as UTF-8,
197
+ // so `fromVec` is non-null here; guard defensively regardless.
198
+ const str = fromVec(value);
199
+ return pure(str === null
200
+ ? errorResult(`content is not byte-aligned: ${r.hash}`)
201
+ : okResult(toJson({ ...meta, content: str })));
202
+ }
203
+ const blob = base64Encode(value);
204
+ return pure(blob === null
205
+ ? errorResult(`content is not byte-aligned: ${r.hash}`)
206
+ : okResult(toJson({ ...meta, content: blob })));
207
+ });
213
208
  });
214
209
  }),
215
210
  toolEntry('cas_list', 'List all stored content hashes (cBase32), one per line.', casListArgs, () => c.list().step(hashes => pure(okResult(hashes.map(vecToCBase32).join('\n'))))),
@@ -2,6 +2,11 @@ export declare const proof: {
2
2
  getMetaLargeMultiChunkBlobNoError: () => void;
3
3
  getMetaLargeMultiChunkUtf8Symbols: () => void;
4
4
  getMetaLargeMultiChunkTextThenBinary: () => void;
5
+ getContentLargeBlobTooLargeError: () => void;
6
+ getContentMissingHashIsError: () => void;
7
+ getContentBase64InflationOverflowWritesInternalError: () => void;
8
+ getContentBase64NearBoundarySucceeds: () => void;
9
+ getContentDoubleEscapedOverflowWritesInternalError: () => void;
5
10
  toolsListAdvertisesThreeTools: () => void;
6
11
  addReturnsHash: () => void;
7
12
  textAddGetMetaRoundTrips: () => void;
@@ -14,6 +19,10 @@ export declare const proof: {
14
19
  listEnumeratesStoredHashes: () => void;
15
20
  addInvalidContentIsError: () => void;
16
21
  addBadLengthContentIsError: () => void;
22
+ addBase64OverLimitIsError: () => void;
23
+ addTextOverLimitIsError: () => void;
24
+ addTextAtLimitSucceeds: () => void;
25
+ addBase64AtLimitIsError: () => void;
17
26
  getUnterminatedHashIsError: () => void;
18
27
  addMissingContentIsError: () => void;
19
28
  getMissingHashArgumentIsError: () => void;