functionalscript 0.35.1 → 0.36.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/fs/base64/module.f.js +6 -5
- package/fs/base64/proof.f.d.ts +1 -0
- package/fs/base64/proof.f.js +10 -1
- package/fs/cas/mcp/module.f.d.ts +8 -4
- package/fs/cas/mcp/module.f.js +33 -21
- package/fs/cas/mcp/proof.f.d.ts +0 -7
- package/fs/cas/mcp/proof.f.js +47 -139
- package/fs/cas/module.f.js +3 -10
- package/fs/ci/config/module.f.d.ts +7 -7
- package/fs/ci/config/module.f.js +8 -8
- package/fs/crypto/sha2/module.f.js +20 -19
- package/fs/crypto/sha2/proof.f.d.ts +1 -0
- package/fs/crypto/sha2/proof.f.js +18 -0
- package/fs/djs/parser/proof.f.js +12 -0
- package/fs/effects/module.f.d.ts +15 -0
- package/fs/effects/module.f.js +14 -0
- package/fs/effects/node/module.f.js +5 -15
- package/fs/effects/node/module.js +3 -1
- package/fs/effects/node/proof.f.d.ts +3 -0
- package/fs/effects/node/proof.f.js +19 -1
- package/fs/effects/node/virtual/module.f.js +1 -2
- package/fs/effects/proof.f.d.ts +5 -0
- package/fs/effects/proof.f.js +22 -1
- package/fs/fjs/proof.f.d.ts +1 -0
- package/fs/fjs/proof.f.js +6 -0
- package/fs/fsc/module.f.d.ts +1 -0
- package/fs/fsc/module.f.js +5 -1
- package/fs/js/tokenizer/module.f.d.ts +1 -1
- package/fs/js/tokenizer/module.f.js +7 -1
- package/fs/js/tokenizer/proof.f.js +59 -0
- package/fs/json/parser/proof.f.js +18 -0
- package/fs/path/module.f.d.ts +6 -0
- package/fs/path/module.f.js +6 -0
- package/fs/path/proof.f.d.ts +1 -0
- package/fs/path/proof.f.js +40 -2
- package/fs/text/utf8/module.f.d.ts +6 -0
- package/fs/text/utf8/module.f.js +15 -2
- package/fs/text/utf8/proof.f.js +53 -0
- package/fs/types/result/module.f.d.ts +4 -0
- package/fs/types/result/module.f.js +4 -0
- package/fs/types/result/proof.f.d.ts +1 -0
- package/fs/types/result/proof.f.js +12 -2
- package/package.json +1 -1
package/fs/base64/module.f.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { msb, length, vec, empty } from "../types/bit_vec/module.f.js";
|
|
7
7
|
import { baseN } from "../base_n/module.f.js";
|
|
8
8
|
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
9
|
-
const { popFront
|
|
9
|
+
const { popFront } = msb;
|
|
10
10
|
const { vecToString, stringToVec } = baseN(6n, alphabet);
|
|
11
11
|
export const encode = (input) => {
|
|
12
12
|
const len = length(input);
|
|
@@ -14,10 +14,11 @@ export const encode = (input) => {
|
|
|
14
14
|
if (len % 8n !== 0n) {
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
// `vecToString` (via `baseN`'s `chunkList`) already left-pads a trailing
|
|
18
|
+
// partial 6-bit chunk with zeros, so `input` needs no explicit padding —
|
|
19
|
+
// building one would risk pushing an intermediate `Vec` past `maxLength`
|
|
20
|
+
// for input already at or near that limit, for no benefit.
|
|
21
|
+
let result = vecToString(input);
|
|
21
22
|
// Append `=` padding to make total length a multiple of 4.
|
|
22
23
|
while (result.length % 4 !== 0) {
|
|
23
24
|
result += '=';
|
package/fs/base64/proof.f.d.ts
CHANGED
package/fs/base64/proof.f.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertEq } from "../asserts/module.f.js";
|
|
2
|
-
import { empty, vec, repeat, vec8 } from "../types/bit_vec/module.f.js";
|
|
2
|
+
import { empty, vec, repeat, vec8, maxLength } from "../types/bit_vec/module.f.js";
|
|
3
3
|
import { encode, decode } from "./module.f.js";
|
|
4
4
|
const check = (s, v) => {
|
|
5
5
|
assertEq(encode(v), s);
|
|
@@ -83,6 +83,15 @@ export const proof = {
|
|
|
83
83
|
const oversized = 'A'.repeat(174_764);
|
|
84
84
|
assertEq(decode(oversized), null);
|
|
85
85
|
},
|
|
86
|
+
encodeAtMaxLengthSucceeds: () => {
|
|
87
|
+
// A `maxLength`-sized vector's trailing partial 6-bit chunk is
|
|
88
|
+
// left-padded by `vecToString` itself (see `baseN`), so `encode`
|
|
89
|
+
// never needs to build an over-`maxLength` intermediate and must not
|
|
90
|
+
// reject this boundary input. (`decode` of the resulting string
|
|
91
|
+
// still fails on this exact boundary — a separate, still-open bug;
|
|
92
|
+
// see `fs/base64/todo/decode-rejects-max-size-input.md`.)
|
|
93
|
+
assertEq(encode(vec(maxLength)(0n)), 'A'.repeat(174_763) + '=');
|
|
94
|
+
},
|
|
86
95
|
// Regression guard: `encode` used to be quadratic in the input size, not
|
|
87
96
|
// linear. `baseN`'s `vecToString` (`fs/base_n/module.f.ts`) popped one
|
|
88
97
|
// 6-bit chunk off the front at a time via `popFront`, and each `popFront`
|
package/fs/cas/mcp/module.f.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { type Effect } from '../../effects/module.f.ts';
|
|
2
2
|
import { type MemOp } from '../../effects/memory/module.f.ts';
|
|
3
|
-
import { type Read, type
|
|
3
|
+
import { type Read, type Write } from '../../effects/node/module.f.ts';
|
|
4
4
|
import { type McpConfig, type McpHandlers } from '../../mcp/module.f.ts';
|
|
5
5
|
import { type FileCasOperation } from '../module.f.ts';
|
|
6
6
|
/** Arguments for `cas_add`: content to store, with optional encoding type. */
|
|
7
7
|
export declare const casAddArgs: {
|
|
8
8
|
readonly content: import("../../types/rtti/module.f.ts").String;
|
|
9
|
-
readonly type: import("../../types/rtti/module.f.ts").Or<["text", "base64",
|
|
9
|
+
readonly type: import("../../types/rtti/module.f.ts").Or<["text", "base64", undefined]>;
|
|
10
10
|
};
|
|
11
11
|
/** Arguments for `cas_get`: the cBase32 hash to look up; optionally request inline content. */
|
|
12
12
|
export declare const casGetArgs: {
|
|
@@ -18,7 +18,7 @@ export declare const casListArgs: {};
|
|
|
18
18
|
/**
|
|
19
19
|
* MCP handlers for `FileCas`.
|
|
20
20
|
*/
|
|
21
|
-
export declare const casMcpHandlers: (home: string) => McpHandlers<FileCasOperation
|
|
21
|
+
export declare const casMcpHandlers: (home: string) => McpHandlers<FileCasOperation>;
|
|
22
22
|
/**
|
|
23
23
|
* Static MCP configuration for the CAS server: advertises the `tools`
|
|
24
24
|
* capability, identifies the server, and pins the protocol version.
|
|
@@ -29,4 +29,8 @@ export declare const casConfig: McpConfig;
|
|
|
29
29
|
* the `mcpStep` for `c`, and drives the read → parse → dispatch → write loop
|
|
30
30
|
* until stdin EOF.
|
|
31
31
|
*/
|
|
32
|
-
export declare const casMcpServer: (home: string) => Effect<Read | Write | MemOp | FileCasOperation
|
|
32
|
+
export declare const casMcpServer: (home: string) => Effect<Read | Write | MemOp | FileCasOperation, void>;
|
|
33
|
+
export declare const proof: {
|
|
34
|
+
casMcpServer: () => void;
|
|
35
|
+
collectReadOverflow: () => void;
|
|
36
|
+
};
|
package/fs/cas/mcp/module.f.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* | Tool | args | action | result |
|
|
13
13
|
* |------------|-------------------------------|------------------|-------------------------------------|
|
|
14
|
-
* | `cas_add` | `{ content, type? }` | write
|
|
14
|
+
* | `cas_add` | `{ content, type? }` | `c.write(...)` | hash (cBase32) |
|
|
15
15
|
* | `cas_get` | `{ hash, content?: boolean }` | `c.read(key)` | JSON `{length,mime_type,type[,content]}` |
|
|
16
16
|
* | `cas_list` | `{}` | `c.list()` | hashes, one per line |
|
|
17
17
|
*
|
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
* without any encoding step.
|
|
24
24
|
* - `'base64'`: `content` is RFC 4648 base64, decoded to bytes before storage.
|
|
25
25
|
* Use this for pre-encoded binary payloads.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
*
|
|
27
|
+
* Inline content (either encoding) is capped at 128 KiB (`maxLength`). There is
|
|
28
|
+
* no MCP route for larger content — the server never opens a local path named by
|
|
29
|
+
* the client (see the invariant in `fs/cas/mcp/README.md`); large files go
|
|
30
|
+
* through the `cas` CLI (`cas add <path>`) instead, run directly by the user.
|
|
29
31
|
*
|
|
30
32
|
* ## `cas_get` output
|
|
31
33
|
*
|
|
@@ -80,18 +82,18 @@
|
|
|
80
82
|
*/
|
|
81
83
|
import { string, option, or, boolean } from "../../types/rtti/module.f.js";
|
|
82
84
|
import { stringify } from "../../json/module.f.js";
|
|
83
|
-
import { pure } from "../../effects/module.f.js";
|
|
85
|
+
import { pure, decode } from "../../effects/module.f.js";
|
|
84
86
|
import { create } from "../../effects/memory/module.f.js";
|
|
85
87
|
import { cBase32ToVec, vecToCBase32 } from "../../cbase32/module.f.js";
|
|
86
88
|
import { decode as base64Decode, encode as base64Encode } from "../../base64/module.f.js";
|
|
87
89
|
import { tryUtf8 } from "../../text/module.f.js";
|
|
88
90
|
import { detectStream } from "../../mime/module.f.js";
|
|
89
|
-
import { empty, length as bitVecLength, maxLength, maxLengthBytes, msb } from "../../types/bit_vec/module.f.js";
|
|
91
|
+
import { empty, length as bitVecLength, maxLength, maxLengthBytes, msb, vec } from "../../types/bit_vec/module.f.js";
|
|
90
92
|
import { ok, error } from "../../types/result/module.f.js";
|
|
91
|
-
import {
|
|
93
|
+
import {} from "../../effects/node/module.f.js";
|
|
92
94
|
import { stdioTransport } from "../../mcp/stdio/module.f.js";
|
|
93
95
|
import { mcpStep, uninitializedState, toolEntry, fromRegistry, errorResult, } from "../../mcp/module.f.js";
|
|
94
|
-
import {
|
|
96
|
+
import { fileCas } from "../module.f.js";
|
|
95
97
|
import { fromVec } from "../../text/utf8/module.f.js";
|
|
96
98
|
import { identity } from "../../types/function/module.f.js";
|
|
97
99
|
import { sha256 } from "../../crypto/sha2/module.f.js";
|
|
@@ -100,7 +102,7 @@ import { nonEmpty, empty as elEmpty } from "../../effects/list/module.f.js";
|
|
|
100
102
|
/** Arguments for `cas_add`: content to store, with optional encoding type. */
|
|
101
103
|
export const casAddArgs = {
|
|
102
104
|
content: string,
|
|
103
|
-
type: or('text', 'base64',
|
|
105
|
+
type: or('text', 'base64', undefined)
|
|
104
106
|
};
|
|
105
107
|
/** Arguments for `cas_get`: the cBase32 hash to look up; optionally request inline content. */
|
|
106
108
|
export const casGetArgs = {
|
|
@@ -142,24 +144,14 @@ const toJson = stringify(identity);
|
|
|
142
144
|
/** Registry of all CAS tools. */
|
|
143
145
|
const casToolRegistry = (home) => {
|
|
144
146
|
const c = fileCas(sha256)(home);
|
|
145
|
-
const casUploadDir = `${home}/cas_upload`;
|
|
146
147
|
return [
|
|
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
|
|
148
|
-
// type:'url' — stream the file into cas, then delete the source on success
|
|
149
|
-
if (type === 'url') {
|
|
150
|
-
if (!content.startsWith(`${casUploadDir}/`) || content.includes('..')) {
|
|
151
|
-
return pure(errorResult(`cas_add type:url paths must be within ${casUploadDir}/ — got: ${content}`));
|
|
152
|
-
}
|
|
153
|
-
return casAddFile(c)(content).step(([t, v]) => t === 'error'
|
|
154
|
-
? pure(errorResult(`upload failed: ${v}`))
|
|
155
|
-
: rm(content).step(() => pure(okResult(vecToCBase32(v)))));
|
|
156
|
-
}
|
|
148
|
+
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 is capped at 128 KiB (131072 bytes) — larger content is rejected. For larger content, store the file with the `cas` CLI instead: run `npx functionalscript cas add <path>` yourself if you have shell access, or give the user that exact command to run — it prints the resulting hash on stdout.', casAddArgs, ({ type, content }) => {
|
|
157
149
|
// type:'text' or 'base64' — resolve content to Vec, store via c.write()
|
|
158
150
|
let x = type === 'base64'
|
|
159
151
|
? base64Decode(content)
|
|
160
152
|
: tryUtf8(content);
|
|
161
153
|
return x === null
|
|
162
|
-
? pure(errorResult('too large or malformed —
|
|
154
|
+
? pure(errorResult('too large or malformed — for large content, run `npx functionalscript cas add <path>` (or have the user run it) instead'))
|
|
163
155
|
// The resolved content fits in one chunk; feed it as a single-item stream.
|
|
164
156
|
: c.write(nonEmpty(ok(x), elEmpty())).step(([tag, hash]) => pure(tag === 'error'
|
|
165
157
|
? errorResult('write')
|
|
@@ -235,3 +227,23 @@ export const casConfig = {
|
|
|
235
227
|
* until stdin EOF.
|
|
236
228
|
*/
|
|
237
229
|
export const casMcpServer = (home) => create(uninitializedState).step(key => stdioTransport(mcpStep(casConfig)(casMcpHandlers(home))(key)));
|
|
230
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
231
|
+
export const proof = {
|
|
232
|
+
// casMcpServer is never called in integration tests because it drives a
|
|
233
|
+
// real stdio server; call it here to cover its Effect-building body.
|
|
234
|
+
casMcpServer: () => { casMcpServer('/'); },
|
|
235
|
+
// The overflow guard in collectRead (lines 125-126) is only reached when
|
|
236
|
+
// the running total of two stream chunks would exceed maxLength. Feed a
|
|
237
|
+
// pure stream whose second chunk pushes it just over the limit so the
|
|
238
|
+
// error branch executes without any real I/O.
|
|
239
|
+
collectReadOverflow: () => {
|
|
240
|
+
const half = maxLength / 2n;
|
|
241
|
+
const v1 = vec(half)(0n);
|
|
242
|
+
const v2 = vec(half + 1n)(0n);
|
|
243
|
+
const stream = nonEmpty(ok(v1), nonEmpty(ok(v2), elEmpty()));
|
|
244
|
+
const d = decode(collectRead(stream));
|
|
245
|
+
if (!d.done || d.result[0] !== 'error') {
|
|
246
|
+
throw 'expected overflow error';
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
};
|
package/fs/cas/mcp/proof.f.d.ts
CHANGED
|
@@ -30,18 +30,11 @@ export declare const proof: {
|
|
|
30
30
|
getMissingHashIsError: () => void;
|
|
31
31
|
unknownToolIsError: () => void;
|
|
32
32
|
toolErrorIsNotJsonRpcError: () => void;
|
|
33
|
-
addUrlStoresFileAndReturnsHash: () => void;
|
|
34
|
-
addUrlRoundTrips: () => void;
|
|
35
|
-
addUrlMissingFileIsError: () => void;
|
|
36
33
|
getMetaReturnsLengthAndMimeType: () => void;
|
|
37
34
|
getMetaBinaryBlob: () => void;
|
|
38
35
|
getMetaOctetStreamForUnknownBinary: () => void;
|
|
39
36
|
getMetaOctetStreamForNulBlob: () => void;
|
|
40
37
|
getMetaMissingHashIsError: () => void;
|
|
41
38
|
getMetaInvalidHashIsError: () => void;
|
|
42
|
-
addUrlFromSubdirectorySucceeds: () => void;
|
|
43
|
-
addUrlFromApprovedDirectorySucceeds: () => void;
|
|
44
|
-
addUrlFromRandomDirectoryIsRejected: () => void;
|
|
45
|
-
addUrlWithPathTraversalIsRejected: () => void;
|
|
46
39
|
getOctetStreamWithContentIncludesBase64: () => void;
|
|
47
40
|
};
|
package/fs/cas/mcp/proof.f.js
CHANGED
|
@@ -6,7 +6,9 @@ import { msb, u8ListToVec, vec8, repeat, length, maxLengthBytes } from "../../ty
|
|
|
6
6
|
import { vecToCBase32 } from "../../cbase32/module.f.js";
|
|
7
7
|
import { encode as base64Encode } from "../../base64/module.f.js";
|
|
8
8
|
import { utf8 } from "../../text/module.f.js";
|
|
9
|
-
import {} from "../module.f.js";
|
|
9
|
+
import { fileCas } from "../module.f.js";
|
|
10
|
+
import { sha256 } from "../../crypto/sha2/module.f.js";
|
|
11
|
+
import { nonEmpty, empty as elEmpty } from "../../effects/list/module.f.js";
|
|
10
12
|
import { mcpStep, uninitializedState, } from "../../mcp/module.f.js";
|
|
11
13
|
import { emptyState, virtual } from "../../effects/node/virtual/module.f.js";
|
|
12
14
|
import { casConfig, casMcpHandlers } from "./module.f.js";
|
|
@@ -59,6 +61,19 @@ const runSessionVirtual = (root, home = '/home/user') => (msgs) => {
|
|
|
59
61
|
});
|
|
60
62
|
return virtual({ ...emptyState, root })(effect)[1];
|
|
61
63
|
};
|
|
64
|
+
// Seeds a blob directly into the virtual store via `c.write`, bypassing the
|
|
65
|
+
// MCP `cas_add` tool entirely — used by tests that need a stored blob larger
|
|
66
|
+
// than `cas_add`'s inline limit, now that the removed `type:'url'` path is no
|
|
67
|
+
// longer available to stage one through MCP. Returns the resulting root
|
|
68
|
+
// `Dir` (with the new shard written) and the blob's cBase32 hash so a
|
|
69
|
+
// subsequent `runSessionVirtual` call can read it back via `cas_get`.
|
|
70
|
+
const seedBlob = (root, home = '/home/user') => (chunks) => {
|
|
71
|
+
const c = fileCas(sha256)(home);
|
|
72
|
+
const stream = chunks.reduceRight((tail, chunk) => nonEmpty(resultOk(chunk), tail), elEmpty());
|
|
73
|
+
const [state, result] = virtual({ ...emptyState, root })(c.write(stream));
|
|
74
|
+
assert(result[0] === 'ok', result);
|
|
75
|
+
return [state.root, vecToCBase32(result[1])];
|
|
76
|
+
};
|
|
62
77
|
// ── Messages ────────────────────────────────────────────────────────────────────
|
|
63
78
|
const init = { jsonrpc: '2.0', method: 'initialize', id: 1,
|
|
64
79
|
params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'c', version: '0' } } };
|
|
@@ -95,8 +110,9 @@ const binarySample = base64Encode(vec8(0x2an));
|
|
|
95
110
|
// `cas_get` detects its type and returns base64 with mime_type image/png.
|
|
96
111
|
const pngSample = base64Encode(u8ListToVec(msb)([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x01]));
|
|
97
112
|
// Returns the RFC 4648 base64 encoding of `n` zero bytes, computed directly
|
|
98
|
-
// without bigint arithmetic —
|
|
99
|
-
//
|
|
113
|
+
// without bigint arithmetic — independent of `base64.encode` so these tests
|
|
114
|
+
// don't rely on the correctness of the function they're indirectly exercising
|
|
115
|
+
// (`cas_add` decodes the string via `base64.decode`). Handles every n%3
|
|
100
116
|
// residue: rem=0 → no padding, rem=1 → 'AA==', rem=2 → 'AAA='.
|
|
101
117
|
const base64OfA = (n) => {
|
|
102
118
|
const groups = Number(n / 3n);
|
|
@@ -117,17 +133,10 @@ const base64OfA = (n) => {
|
|
|
117
133
|
// exactly `maxLengthBytes` (the largest single `Vec` the runtime allows), so the
|
|
118
134
|
// two-chunk blob spans two read chunks without any one `Vec` exceeding the cap.
|
|
119
135
|
const largeMultiChunkBlobMeta = (chunk0, chunk1, expectedType, expectedMime) => () => {
|
|
120
|
-
const root = {
|
|
121
|
-
const [
|
|
122
|
-
init, initialized,
|
|
123
|
-
call(2, 'cas_add', { content: '/home/user/cas_upload/big', type: 'url' }),
|
|
124
|
-
]).slice(2);
|
|
125
|
-
assert(!resultOf(addResp).isError);
|
|
126
|
-
const hash = textOf(addResp);
|
|
127
|
-
const [, metaResp] = runSessionVirtual(root)([
|
|
136
|
+
const [root, hash] = seedBlob({})([chunk0, chunk1]);
|
|
137
|
+
const [metaResp] = runSessionVirtual(root)([
|
|
128
138
|
init, initialized,
|
|
129
|
-
call(2, '
|
|
130
|
-
call(3, 'cas_get', { hash }),
|
|
139
|
+
call(2, 'cas_get', { hash }),
|
|
131
140
|
]).slice(2);
|
|
132
141
|
assert(!resultOf(metaResp).isError);
|
|
133
142
|
const meta = JSON.parse(textOf(metaResp));
|
|
@@ -145,8 +154,7 @@ const binaryChunk = repeat(maxLengthBytes)(vec8(0xffn));
|
|
|
145
154
|
// 100,000 raw bytes of 0xFF: comfortably under `maxLengthBytes` (so `cas_get`'s
|
|
146
155
|
// own `length > maxLengthBytes` guard does not fire), but base64 inflates it to
|
|
147
156
|
// ceil(100_000/3)*4 = 133,336 chars — already past `maxLength` (131,072 bytes)
|
|
148
|
-
// before the JSON-RPC envelope adds anything.
|
|
149
|
-
// separate `base64Encode`-near-`maxLength` padding bug (see `base64OfA` above).
|
|
157
|
+
// before the JSON-RPC envelope adds anything.
|
|
150
158
|
const oversizedBase64Chunk = repeat(100000n)(vec8(0xffn));
|
|
151
159
|
// 90,000 raw bytes: base64 inflates to 120,000 chars, leaving ~11 KiB of margin
|
|
152
160
|
// under `maxLength` once the envelope is added — the paired "still succeeds" case.
|
|
@@ -170,16 +178,10 @@ export const proof = {
|
|
|
170
178
|
// "too large" error, not "no such hash" — the blob exists, it just can't be
|
|
171
179
|
// buffered inline. The metadata-only path (above) still returns its size/type.
|
|
172
180
|
getContentLargeBlobTooLargeError: () => {
|
|
173
|
-
const root = {
|
|
174
|
-
const [
|
|
175
|
-
init, initialized,
|
|
176
|
-
call(2, 'cas_add', { content: '/home/user/cas_upload/big', type: 'url' }),
|
|
177
|
-
]).slice(2);
|
|
178
|
-
const hash = textOf(addResp);
|
|
179
|
-
const [, getResp] = runSessionVirtual(root)([
|
|
181
|
+
const [root, hash] = seedBlob({})([asciiChunk, asciiChunk]);
|
|
182
|
+
const [getResp] = runSessionVirtual(root)([
|
|
180
183
|
init, initialized,
|
|
181
|
-
call(2, '
|
|
182
|
-
call(3, 'cas_get', { hash, content: true }),
|
|
184
|
+
call(2, 'cas_get', { hash, content: true }),
|
|
183
185
|
]).slice(2);
|
|
184
186
|
assertEq(resultOf(getResp).isError, true);
|
|
185
187
|
const text = textOf(getResp);
|
|
@@ -208,31 +210,21 @@ export const proof = {
|
|
|
208
210
|
// `fs/base64/proof.f.ts` `encodeLargeVecIsSlow`). Now well under budget on
|
|
209
211
|
// both engines.
|
|
210
212
|
getContentBase64InflationOverflowWritesInternalError: () => {
|
|
211
|
-
const root = {
|
|
212
|
-
const [
|
|
213
|
-
call(2, '
|
|
214
|
-
]);
|
|
215
|
-
const hash = textOf(addResp);
|
|
216
|
-
const [, getResp] = runStdio(root)([
|
|
217
|
-
call(2, 'cas_add', { content: '/home/user/cas_upload/big', type: 'url' }),
|
|
218
|
-
call(3, 'cas_get', { hash, content: true }),
|
|
213
|
+
const [root, hash] = seedBlob({})([oversizedBase64Chunk]);
|
|
214
|
+
const [getResp] = runStdio(root)([
|
|
215
|
+
call(2, 'cas_get', { hash, content: true }),
|
|
219
216
|
]);
|
|
220
217
|
const err = getResp;
|
|
221
218
|
assertEq(err.error?.code, -32603);
|
|
222
|
-
assertEq(err.id,
|
|
219
|
+
assertEq(err.id, 2);
|
|
223
220
|
},
|
|
224
221
|
// The paired boundary case: a blob whose base64 inflation leaves enough
|
|
225
222
|
// margin under `maxLength` for the envelope — confirms the fallback above
|
|
226
223
|
// only triggers on genuine overflow, not on every `content:true` binary read.
|
|
227
224
|
getContentBase64NearBoundarySucceeds: () => {
|
|
228
|
-
const root = {
|
|
229
|
-
const [
|
|
230
|
-
call(2, '
|
|
231
|
-
]);
|
|
232
|
-
const hash = textOf(addResp);
|
|
233
|
-
const [, getResp] = runStdio(root)([
|
|
234
|
-
call(2, 'cas_add', { content: '/home/user/cas_upload/big', type: 'url' }),
|
|
235
|
-
call(3, 'cas_get', { hash, content: true }),
|
|
225
|
+
const [root, hash] = seedBlob({})([boundaryBase64Chunk]);
|
|
226
|
+
const [getResp] = runStdio(root)([
|
|
227
|
+
call(2, 'cas_get', { hash, content: true }),
|
|
236
228
|
]);
|
|
237
229
|
assert(!resultOf(getResp).isError);
|
|
238
230
|
const result = JSON.parse(textOf(getResp));
|
|
@@ -243,25 +235,21 @@ export const proof = {
|
|
|
243
235
|
// (33,000 bytes) well under `maxLengthBytes`, but each quote costs 2 bytes on
|
|
244
236
|
// the inner CAS-JSON escape and 4 bytes once the outer JSON-RPC envelope
|
|
245
237
|
// re-escapes that backslash-quote pair — no `cas_get`-level size guard could
|
|
246
|
-
// see this coming, only the transport's real encode attempt can.
|
|
247
|
-
//
|
|
248
|
-
// `cas_get`'s response-side escaping without
|
|
249
|
-
// inline `content` argument on the request
|
|
238
|
+
// see this coming, only the transport's real encode attempt can. Seeded
|
|
239
|
+
// directly into the store via `seedBlob` (not an inlined `cas_add` JSON
|
|
240
|
+
// string) so the test exercises `cas_get`'s response-side escaping without
|
|
241
|
+
// also paying for parsing a huge inline `content` argument on the request
|
|
242
|
+
// side. Confirms `writeResponse`'s
|
|
250
243
|
// `tryUtf8` fallback (not a crash) handles double-escaping overflow, again
|
|
251
244
|
// preserving the request's `id`.
|
|
252
245
|
getContentDoubleEscapedOverflowWritesInternalError: () => {
|
|
253
|
-
const root = {
|
|
254
|
-
const [
|
|
255
|
-
call(2, '
|
|
256
|
-
]);
|
|
257
|
-
const hash = textOf(addResp);
|
|
258
|
-
const [, getResp] = runStdio(root)([
|
|
259
|
-
call(2, 'cas_add', { content: '/home/user/cas_upload/q', type: 'url' }),
|
|
260
|
-
call(3, 'cas_get', { hash, content: true }),
|
|
246
|
+
const [root, hash] = seedBlob({})([quotesChunk]);
|
|
247
|
+
const [getResp] = runStdio(root)([
|
|
248
|
+
call(2, 'cas_get', { hash, content: true }),
|
|
261
249
|
]);
|
|
262
250
|
const err = getResp;
|
|
263
251
|
assertEq(err.error?.code, -32603);
|
|
264
|
-
assertEq(err.id,
|
|
252
|
+
assertEq(err.id, 2);
|
|
265
253
|
},
|
|
266
254
|
toolsListAdvertisesThreeTools: () => {
|
|
267
255
|
const [resp] = runSessionVirtual({})([init, initialized, list(2)]).slice(2);
|
|
@@ -370,6 +358,7 @@ export const proof = {
|
|
|
370
358
|
const [resp] = session(call(2, 'cas_add', { content: 'A'.repeat(174_764), type: 'base64' }));
|
|
371
359
|
assertEq(resultOf(resp).isError, true);
|
|
372
360
|
assert(textOf(resp).includes('too large or malformed'));
|
|
361
|
+
assert(textOf(resp).includes('cas add'));
|
|
373
362
|
},
|
|
374
363
|
// One ASCII byte past maxLengthBytes — tryUtf8 returns null and cas_add
|
|
375
364
|
// surfaces an error.
|
|
@@ -377,6 +366,7 @@ export const proof = {
|
|
|
377
366
|
const [resp] = session(call(2, 'cas_add', { content: 'a'.repeat(Number(maxLengthBytes) + 1) }));
|
|
378
367
|
assertEq(resultOf(resp).isError, true);
|
|
379
368
|
assert(textOf(resp).includes('too large or malformed'));
|
|
369
|
+
assert(textOf(resp).includes('cas add'));
|
|
380
370
|
},
|
|
381
371
|
// Exactly maxLengthBytes (131,072) ASCII bytes — tryUtf8 returns a non-null
|
|
382
372
|
// Vec (len === maxLength, not over), so cas_add stores cleanly.
|
|
@@ -394,6 +384,7 @@ export const proof = {
|
|
|
394
384
|
const [resp] = session(call(2, 'cas_add', { content: base64OfA(maxLengthBytes), type: 'base64' }));
|
|
395
385
|
assertEq(resultOf(resp).isError, true);
|
|
396
386
|
assert(textOf(resp).includes('too large or malformed'));
|
|
387
|
+
assert(textOf(resp).includes('cas add'));
|
|
397
388
|
},
|
|
398
389
|
getUnterminatedHashIsError: () => {
|
|
399
390
|
const [resp] = session(call(2, 'cas_get', { hash: '0' }));
|
|
@@ -425,60 +416,11 @@ export const proof = {
|
|
|
425
416
|
assert(!('error' in resp));
|
|
426
417
|
assert('result' in resp);
|
|
427
418
|
},
|
|
428
|
-
// cas_add with type:'url' streams a file from /home/user/cas_upload/ into CAS.
|
|
429
|
-
addUrlStoresFileAndReturnsHash: () => {
|
|
430
|
-
const fileContent = utf8('hello from file');
|
|
431
|
-
const root = { 'home': { 'user': { 'cas_upload': { 'hello.txt': [fileContent] } } } };
|
|
432
|
-
const [addUrlResp] = runSessionVirtual(root)([
|
|
433
|
-
init, initialized,
|
|
434
|
-
call(2, 'cas_add', { content: '/home/user/cas_upload/hello.txt', type: 'url' }),
|
|
435
|
-
]).slice(2);
|
|
436
|
-
assert(!resultOf(addUrlResp).isError);
|
|
437
|
-
assert(textOf(addUrlResp).length > 0);
|
|
438
|
-
},
|
|
439
|
-
addUrlRoundTrips: () => {
|
|
440
|
-
const fileContent = utf8('round-trip content');
|
|
441
|
-
const root = { 'home': { 'user': { 'cas_upload': { 'rt.txt': [fileContent] } } } };
|
|
442
|
-
// First pass: add to get the hash (deterministic for same content).
|
|
443
|
-
const [addResp] = runSessionVirtual(root)([
|
|
444
|
-
init, initialized,
|
|
445
|
-
call(2, 'cas_add', { content: '/home/user/cas_upload/rt.txt', type: 'url' }),
|
|
446
|
-
]).slice(2);
|
|
447
|
-
const hash = textOf(addResp);
|
|
448
|
-
// Second pass: add again + get in one session (file re-present in fresh virtual state).
|
|
449
|
-
const [, getResp] = runSessionVirtual(root)([
|
|
450
|
-
init, initialized,
|
|
451
|
-
call(2, 'cas_add', { content: '/home/user/cas_upload/rt.txt', type: 'url' }),
|
|
452
|
-
call(3, 'cas_get', { hash, content: true }),
|
|
453
|
-
]).slice(2);
|
|
454
|
-
assert(!resultOf(getResp).isError);
|
|
455
|
-
const result = JSON.parse(textOf(getResp));
|
|
456
|
-
assertEq(result.type, 'text');
|
|
457
|
-
assertEq(result.content, 'round-trip content');
|
|
458
|
-
},
|
|
459
|
-
addUrlMissingFileIsError: () => {
|
|
460
|
-
const [resp] = runSessionVirtual({})([
|
|
461
|
-
init, initialized,
|
|
462
|
-
call(2, 'cas_add', { content: '/home/user/cas_upload/nonexistent.txt', type: 'url' }),
|
|
463
|
-
]).slice(2);
|
|
464
|
-
assertEq(resultOf(resp).isError, true);
|
|
465
|
-
},
|
|
466
419
|
// cas_get without content:true returns only metadata.
|
|
467
420
|
getMetaReturnsLengthAndMimeType: () => {
|
|
468
|
-
const
|
|
469
|
-
const root = { 'home': { 'user': { 'cas_upload': { 'f': [fileContent] } } } };
|
|
470
|
-
// First pass: add to get the hash.
|
|
471
|
-
const [addResp] = runSessionVirtual(root)([
|
|
472
|
-
init, initialized,
|
|
473
|
-
call(2, 'cas_add', { content: '/home/user/cas_upload/f', type: 'url' }),
|
|
474
|
-
]).slice(2);
|
|
421
|
+
const [addResp] = session(call(2, 'cas_add', { content: 'text content' }));
|
|
475
422
|
const hash = textOf(addResp);
|
|
476
|
-
|
|
477
|
-
const [, metaResp] = runSessionVirtual(root)([
|
|
478
|
-
init, initialized,
|
|
479
|
-
call(2, 'cas_add', { content: '/home/user/cas_upload/f', type: 'url' }),
|
|
480
|
-
call(3, 'cas_get', { hash }),
|
|
481
|
-
]).slice(2);
|
|
423
|
+
const [, metaResp] = session(call(2, 'cas_add', { content: 'text content' }), call(3, 'cas_get', { hash }));
|
|
482
424
|
assert(!resultOf(metaResp).isError);
|
|
483
425
|
const meta = JSON.parse(textOf(metaResp));
|
|
484
426
|
assertEq(meta.mime_type, 'text/plain');
|
|
@@ -531,40 +473,6 @@ export const proof = {
|
|
|
531
473
|
const [resp] = session(call(2, 'cas_get', { hash: 'bad!' }));
|
|
532
474
|
assertEq(resultOf(resp).isError, true);
|
|
533
475
|
},
|
|
534
|
-
// cas_add type:'url' with a subdirectory path flattens slashes to '-' in staging.
|
|
535
|
-
addUrlFromSubdirectorySucceeds: () => {
|
|
536
|
-
const fileContent = utf8('nested file content');
|
|
537
|
-
const root = { 'home': { 'user': { 'cas_upload': { 'subdir': { 'file.txt': [fileContent] } } } } };
|
|
538
|
-
const [resp] = runSessionVirtual(root)([
|
|
539
|
-
init, initialized,
|
|
540
|
-
call(2, 'cas_add', { content: '/home/user/cas_upload/subdir/file.txt', type: 'url' }),
|
|
541
|
-
]).slice(2);
|
|
542
|
-
assert(!resultOf(resp).isError);
|
|
543
|
-
assert(textOf(resp).length > 0);
|
|
544
|
-
},
|
|
545
|
-
// cas_add with type:'url' accepts paths within /home/user/cas_upload/
|
|
546
|
-
addUrlFromApprovedDirectorySucceeds: () => {
|
|
547
|
-
const fileContent = utf8('approved file');
|
|
548
|
-
const root = { 'home': { 'user': { 'cas_upload': { 'test.txt': [fileContent] } } } };
|
|
549
|
-
const [resp] = runSessionVirtual(root)([
|
|
550
|
-
init, initialized,
|
|
551
|
-
call(2, 'cas_add', { content: '/home/user/cas_upload/test.txt', type: 'url' }),
|
|
552
|
-
]).slice(2);
|
|
553
|
-
assert(!resultOf(resp).isError);
|
|
554
|
-
assert(textOf(resp).length > 0);
|
|
555
|
-
},
|
|
556
|
-
// cas_add with type:'url' rejects paths outside /home/user/cas_upload/
|
|
557
|
-
addUrlFromRandomDirectoryIsRejected: () => {
|
|
558
|
-
const [resp] = session(call(2, 'cas_add', { content: '/tmp/secret.txt', type: 'url' }));
|
|
559
|
-
assert(resultOf(resp).isError === true);
|
|
560
|
-
assert(textOf(resp).includes('/home/user/cas_upload/'));
|
|
561
|
-
},
|
|
562
|
-
// cas_add with type:'url' rejects path traversal attempts with ..
|
|
563
|
-
addUrlWithPathTraversalIsRejected: () => {
|
|
564
|
-
const [resp] = session(call(2, 'cas_add', { content: '/home/user/cas_upload/../../etc/passwd', type: 'url' }));
|
|
565
|
-
assert(resultOf(resp).isError === true);
|
|
566
|
-
assert(textOf(resp).includes('/home/user/cas_upload/'));
|
|
567
|
-
},
|
|
568
476
|
// cas_get with content:true on octet-stream (no magic bytes, not UTF-8) returns inline base64.
|
|
569
477
|
getOctetStreamWithContentIncludesBase64: () => {
|
|
570
478
|
const binaryContent = u8ListToVec(msb)([0xFF, 0xFE, 0x00, 0x01]);
|
package/fs/cas/module.f.js
CHANGED
|
@@ -7,7 +7,7 @@ import { sha256 } from "../crypto/sha2/module.f.js";
|
|
|
7
7
|
import { join, normalize, parse } from "../path/module.f.js";
|
|
8
8
|
import { empty, length, maxLengthBytes, msb, vec } from "../types/bit_vec/module.f.js";
|
|
9
9
|
import { cBase32ToVec, vecToCBase32 } from "../cbase32/module.f.js";
|
|
10
|
-
import { foldStep, forEachStep, pure } from "../effects/module.f.js";
|
|
10
|
+
import { foldStep, forEachStep, okStep, pure } from "../effects/module.f.js";
|
|
11
11
|
import { access, createExclusive, isNotFound, mkdir, now, randomInt, readBytes, readdir, rename, rm, stat, writeBytes, } from "../effects/node/module.f.js";
|
|
12
12
|
import { toOption } from "../types/nullable/module.f.js";
|
|
13
13
|
import { error, ok, unwrap } from "../types/result/module.f.js";
|
|
@@ -141,9 +141,7 @@ export const fileCas = (sha2) => (path) => {
|
|
|
141
141
|
.step(t0 => {
|
|
142
142
|
const path0 = join(stageDir, stageName(t0 + leaseDelta, rndStr));
|
|
143
143
|
return createExclusive(path0)
|
|
144
|
-
.step((
|
|
145
|
-
? pure(error(e))
|
|
146
|
-
: loop(sha2.init, 0, path0)(payload));
|
|
144
|
+
.step(okStep(() => loop(sha2.init, 0, path0)(payload)));
|
|
147
145
|
}));
|
|
148
146
|
}));
|
|
149
147
|
},
|
|
@@ -200,10 +198,5 @@ cas.write(streamFile(path));
|
|
|
200
198
|
export const casUpload = (home) => (fileName) => {
|
|
201
199
|
const src = join(home, 'cas_upload', fileName);
|
|
202
200
|
const c = fileCas(sha256)(home);
|
|
203
|
-
return casAddFile(c)(src).step((
|
|
204
|
-
if (result[0] === 'error') {
|
|
205
|
-
return pure(result);
|
|
206
|
-
}
|
|
207
|
-
return rm(src).step(() => pure(result));
|
|
208
|
-
});
|
|
201
|
+
return casAddFile(c)(src).step(okStep(v => rm(src).step(() => pure(ok(v)))));
|
|
209
202
|
};
|
|
@@ -19,9 +19,9 @@ export declare const images: {
|
|
|
19
19
|
readonly arm: "windows-11-arm";
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
|
-
export declare const functionalscript: "0.
|
|
22
|
+
export declare const functionalscript: "0.35.2";
|
|
23
23
|
export declare const bun = "1.3.14";
|
|
24
|
-
export declare const deno = "2.9.
|
|
24
|
+
export declare const deno = "2.9.1";
|
|
25
25
|
export declare const playwright = "1.61.1";
|
|
26
26
|
export declare const node: {
|
|
27
27
|
readonly default: "26.4.0";
|
|
@@ -29,15 +29,15 @@ export declare const node: {
|
|
|
29
29
|
readonly node24: "24.18.0";
|
|
30
30
|
};
|
|
31
31
|
export declare const wasmtime = "46.0.1";
|
|
32
|
-
export declare const wasmer = "7.
|
|
33
|
-
export declare const tsgo = "7.0.0-dev.
|
|
32
|
+
export declare const wasmer = "7.2.0";
|
|
33
|
+
export declare const tsgo = "7.0.0-dev.20260707.2";
|
|
34
34
|
export declare const actions: {
|
|
35
35
|
readonly 'actions/checkout': "v7";
|
|
36
36
|
readonly 'actions/setup-node': "v6.4.0";
|
|
37
37
|
readonly 'actions/cache': "v6.1.0";
|
|
38
|
-
readonly 'denoland/setup-deno': "v2.0.
|
|
38
|
+
readonly 'denoland/setup-deno': "v2.0.5";
|
|
39
39
|
readonly 'oven-sh/setup-bun': "v2.2.0";
|
|
40
|
-
readonly 'bytecodealliance/actions/wasmtime/setup': "v1";
|
|
40
|
+
readonly 'bytecodealliance/actions/wasmtime/setup': "v1.1.3";
|
|
41
41
|
readonly 'wasmerio/setup-wasmer': "v3.1";
|
|
42
|
-
readonly 'dtolnay/rust-toolchain': "1.96.
|
|
42
|
+
readonly 'dtolnay/rust-toolchain': "1.96.1";
|
|
43
43
|
};
|
package/fs/ci/config/module.f.js
CHANGED
|
@@ -22,13 +22,13 @@ export const images = {
|
|
|
22
22
|
};
|
|
23
23
|
// Bootstrap package version used by generated smoke tests. Keep this on a
|
|
24
24
|
// published FunctionalScript release; do not tie it to package.json's current
|
|
25
|
-
// in-repo version.
|
|
25
|
+
// in-repo version.
|
|
26
26
|
// https://www.npmjs.com/package/functionalscript
|
|
27
|
-
export const functionalscript = '0.
|
|
27
|
+
export const functionalscript = '0.35.2';
|
|
28
28
|
// https://bun.sh/
|
|
29
29
|
export const bun = '1.3.14';
|
|
30
30
|
// https://deno.com/
|
|
31
|
-
export const deno = '2.9.
|
|
31
|
+
export const deno = '2.9.1';
|
|
32
32
|
// https://www.npmjs.com/package/playwright
|
|
33
33
|
export const playwright = '1.61.1';
|
|
34
34
|
// https://nodejs.org/en/download
|
|
@@ -40,9 +40,9 @@ export const node = {
|
|
|
40
40
|
// https://github.com/bytecodealliance/wasmtime/releases
|
|
41
41
|
export const wasmtime = '46.0.1';
|
|
42
42
|
// https://github.com/wasmerio/wasmer/releases
|
|
43
|
-
export const wasmer = '7.
|
|
43
|
+
export const wasmer = '7.2.0';
|
|
44
44
|
// https://www.npmjs.com/package/@typescript/native-preview?activeTab=versions
|
|
45
|
-
export const tsgo = '7.0.0-dev.
|
|
45
|
+
export const tsgo = '7.0.0-dev.20260707.2';
|
|
46
46
|
// GitHub Action versions used by CI step builders. The key is the action
|
|
47
47
|
// `owner/name`; call sites compose the full ref as
|
|
48
48
|
// `` `${name}@${actions[name]}` ``.
|
|
@@ -55,13 +55,13 @@ export const actions = {
|
|
|
55
55
|
// https://github.com/marketplace/actions/cache
|
|
56
56
|
'actions/cache': 'v6.1.0',
|
|
57
57
|
// https://github.com/marketplace/actions/setup-deno
|
|
58
|
-
'denoland/setup-deno': 'v2.0.
|
|
58
|
+
'denoland/setup-deno': 'v2.0.5',
|
|
59
59
|
// https://github.com/marketplace/actions/setup-bun
|
|
60
60
|
'oven-sh/setup-bun': 'v2.2.0',
|
|
61
61
|
// https://github.com/bytecodealliance/actions
|
|
62
|
-
'bytecodealliance/actions/wasmtime/setup': 'v1',
|
|
62
|
+
'bytecodealliance/actions/wasmtime/setup': 'v1.1.3',
|
|
63
63
|
// https://github.com/wasmerio/setup-wasmer
|
|
64
64
|
'wasmerio/setup-wasmer': 'v3.1',
|
|
65
65
|
// https://rust-lang.org/ - value is Rust version, not action version
|
|
66
|
-
'dtolnay/rust-toolchain': '1.96.
|
|
66
|
+
'dtolnay/rust-toolchain': '1.96.1',
|
|
67
67
|
};
|