@bash0816/claude-code 2.1.245 → 2.1.248
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 +1 -1
- package/bin/claude +10 -0
- package/config/claude-native-audited-versions.json +23 -1
- package/config/claude-termux-release-manifest.json +3 -3
- package/lib/bunfs-esm-loader.mjs +104 -11
- package/lib/bunfs-esm-loader.test.js +173 -14
- package/lib/native-validators.js +90 -0
- package/lib/native-validators.test.js +160 -0
- package/lib/prepare-native.js +5 -71
- package/lib/termux-run-claude-native.sh +42 -20
- package/package.json +2 -2
package/README.md
CHANGED
package/bin/claude
CHANGED
|
@@ -16,6 +16,16 @@ NODE="${MAGI_NODE:-node}"
|
|
|
16
16
|
if [ -n "${MAGI_NODE:-}" ]; then
|
|
17
17
|
export PATH="$(dirname -- "$NODE"):$PATH"
|
|
18
18
|
fi
|
|
19
|
+
|
|
20
|
+
if ! "$NODE" -e '
|
|
21
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
22
|
+
const ok = major > 23 || (major === 23 && minor >= 5) || (major === 22 && minor >= 15);
|
|
23
|
+
process.exit(ok ? 0 : 1);
|
|
24
|
+
'; then
|
|
25
|
+
echo "Error: claude-code requires Node.js >=22.15.0 <23.0.0 or >=23.5.0 (module.registerHooks() API). Detected: $("$NODE" --version)" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
19
29
|
CONFIG_FILE="${PACKAGE_DIR}/config/claude-native-audited-versions.json"
|
|
20
30
|
PACKAGE_VERSION=$("$NODE" -e 'const fs=require("fs"); const path=require("path"); const p=JSON.parse(fs.readFileSync(path.join(process.argv[1],"package.json"),"utf8")); process.stdout.write(p.version)' "${PACKAGE_DIR}")
|
|
21
31
|
CLAUDE_VERSION="${CLAUDE_TERMUX_CLAUDE_VERSION:-${PACKAGE_VERSION}}"
|
|
@@ -777,7 +777,29 @@
|
|
|
777
777
|
"entry_format": "esm-chunked",
|
|
778
778
|
"tarball_integrity": "sha512-Qbn5HnZbYeW4GdifVkDGfcVKqj3/f3U9sfVd3LEaUZh08CcS8D/ptJ76zuBfQUGHJHfToAdNVfak86uJ84b1Dg==",
|
|
779
779
|
"tarball_sha256": "668662e7b5d91a93cff6c75736e60f3d5d3bed4cbe077ae4feefc63ad1253f4d",
|
|
780
|
-
"
|
|
780
|
+
"cycle_hoists": [],
|
|
781
|
+
"status": "termux_verified"
|
|
782
|
+
},
|
|
783
|
+
"2.1.248": {
|
|
784
|
+
"wrapper_spec": "@anthropic-ai/claude-code@2.1.248",
|
|
785
|
+
"native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.248",
|
|
786
|
+
"entry_format": "esm-chunked",
|
|
787
|
+
"tarball_integrity": "sha512-2qwjgGuzlVfkJC7UWUjl86hiv5Ok5vtVxAZ/5fpdhzQKuBLlxrJJu3z5tVI+VrO/x/hcqx5kJuy9/O9K4eT+Kg==",
|
|
788
|
+
"tarball_sha256": "bd2e2b6ebace34039b073b0f586d521ec09c895fa2a39de8d1c86423ae5884ca",
|
|
789
|
+
"status": "offset_discovered",
|
|
790
|
+
"num_modules": 1946,
|
|
791
|
+
"byte_count": 136124175,
|
|
792
|
+
"cycle_hoists": [
|
|
793
|
+
{ "file": "chunk-vmw9kxhv.js", "targetModule": "chunk-y0jj307t.js", "expectedOccurrences": 1, "assertProperties": [] },
|
|
794
|
+
{ "file": "chunk-9n7t2tbt.js", "targetModule": "chunk-ppd18hq1.js", "expectedOccurrences": 1, "assertProperties": ["MonitorTool"] },
|
|
795
|
+
{ "file": "chunk-9n7t2tbt.js", "targetModule": "chunk-830s9480.js", "expectedOccurrences": 1, "assertProperties": ["EndConversationTool"] },
|
|
796
|
+
{ "file": "chunk-9n7t2tbt.js", "targetModule": "chunk-m4hx5z9g.js", "expectedOccurrences": 1, "assertProperties": ["ArtifactTool"] },
|
|
797
|
+
{ "file": "chunk-9n7t2tbt.js", "targetModule": "chunk-eg121wbt.js", "expectedOccurrences": 1, "assertProperties": ["WorkflowTool"] },
|
|
798
|
+
{ "file": "chunk-9n7t2tbt.js", "targetModule": "chunk-prrm838s.js", "expectedOccurrences": 1, "assertProperties": [] },
|
|
799
|
+
{ "file": "chunk-9n7t2tbt.js", "targetModule": "chunk-exvwsc2a.js", "expectedOccurrences": 2, "assertProperties": [] },
|
|
800
|
+
{ "file": "chunk-9n7t2tbt.js", "targetModule": "chunk-s61yn21a.js", "expectedOccurrences": 1, "assertProperties": ["default"] },
|
|
801
|
+
{ "file": "chunk-9n7t2tbt.js", "targetModule": "chunk-040m9a9s.js", "expectedOccurrences": 3, "assertProperties": ["getWorkflowCommands", "invalidateWorkflowCache", "warmWorkflows"] }
|
|
802
|
+
]
|
|
781
803
|
}
|
|
782
804
|
}
|
|
783
805
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 1,
|
|
3
3
|
"package_name": "@bash0816/claude-code",
|
|
4
|
-
"latest_audited_version": "2.1.
|
|
5
|
-
"latest_candidate_version": "2.1.
|
|
6
|
-
"previous_stable_version": "2.1.
|
|
4
|
+
"latest_audited_version": "2.1.245",
|
|
5
|
+
"latest_candidate_version": "2.1.248",
|
|
6
|
+
"previous_stable_version": "2.1.241",
|
|
7
7
|
"stable_pinned_version": "2.1.220-2",
|
|
8
8
|
"manifest_url": "https://raw.githubusercontent.com/bash0816/ClaudeCode-Termux/main/config/claude-termux-release-manifest.json"
|
|
9
9
|
}
|
package/lib/bunfs-esm-loader.mjs
CHANGED
|
@@ -9,12 +9,50 @@ let CHILD_PROCESS_GUARD_PATH = null;
|
|
|
9
9
|
let VM_GUARD_PATH = null;
|
|
10
10
|
let WS_STUB_PATH = null;
|
|
11
11
|
|
|
12
|
+
let CYCLE_HOISTS = [];
|
|
13
|
+
|
|
12
14
|
export function initialize(data) {
|
|
13
15
|
PROCESS_OWNED_DIR = data.processOwnedDir;
|
|
14
16
|
SOURCE_BIN = data.sourceBin;
|
|
15
17
|
CHILD_PROCESS_GUARD_PATH = data.childProcessGuardPath;
|
|
16
18
|
VM_GUARD_PATH = data.vmGuardPath;
|
|
17
19
|
WS_STUB_PATH = data.wsStubPath;
|
|
20
|
+
CYCLE_HOISTS = Array.isArray(data.cycleHoists) ? data.cycleHoists : [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function tryHoistCycleBreakingImports(filePath, source) {
|
|
24
|
+
const rel = path.relative(PROCESS_OWNED_DIR, filePath);
|
|
25
|
+
const records = CYCLE_HOISTS.filter((r) => r.file === rel);
|
|
26
|
+
if (records.length === 0) return null;
|
|
27
|
+
|
|
28
|
+
let hoistedImportLines = '';
|
|
29
|
+
let newSource = source;
|
|
30
|
+
let varIndex = 0;
|
|
31
|
+
const targetToVar = new Map();
|
|
32
|
+
|
|
33
|
+
for (const record of records) {
|
|
34
|
+
const literal = `import.meta.require("/$bunfs/root/${record.targetModule}")`;
|
|
35
|
+
const occurrences = newSource.split(literal).length - 1;
|
|
36
|
+
if (occurrences !== record.expectedOccurrences) continue;
|
|
37
|
+
|
|
38
|
+
const real = path.resolve(PROCESS_OWNED_DIR, record.targetModule);
|
|
39
|
+
if (path.relative(PROCESS_OWNED_DIR, real).startsWith('..')) continue;
|
|
40
|
+
if (!existsSync(real)) continue;
|
|
41
|
+
|
|
42
|
+
let varName = targetToVar.get(record.targetModule);
|
|
43
|
+
if (!varName) {
|
|
44
|
+
varName = `__bunfsHoisted_${varIndex++}`;
|
|
45
|
+
targetToVar.set(record.targetModule, varName);
|
|
46
|
+
hoistedImportLines += `import * as ${varName} from ${JSON.stringify(pathToFileURL(real).href)};\n`;
|
|
47
|
+
if (record.assertProperties && record.assertProperties.length > 0) {
|
|
48
|
+
hoistedImportLines += `__bunfsAssertHoistedProps(${varName}, ${JSON.stringify(record.targetModule)}, ${JSON.stringify(record.assertProperties)});\n`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
newSource = newSource.replaceAll(literal, varName);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!targetToVar.size) return null;
|
|
55
|
+
return { hoistedImportLine: hoistedImportLines, source: newSource };
|
|
18
56
|
}
|
|
19
57
|
|
|
20
58
|
function buildImportMetaRequirePolyfillPrelude(anchorUrl) {
|
|
@@ -22,24 +60,68 @@ function buildImportMetaRequirePolyfillPrelude(anchorUrl) {
|
|
|
22
60
|
`import __bunfsGuardedChildProcess from ${JSON.stringify(pathToFileURL(CHILD_PROCESS_GUARD_PATH).href)};\n` +
|
|
23
61
|
`import __bunfsGuardedVm from ${JSON.stringify(pathToFileURL(VM_GUARD_PATH).href)};\n` +
|
|
24
62
|
`import { createRequire as __bunfsCreateRequire } from "node:module";\n` +
|
|
63
|
+
`import __bunfsMetaRequirePath from "node:path";\n` +
|
|
64
|
+
`import { existsSync as __bunfsMetaRequireExistsSync } from "node:fs";\n` +
|
|
65
|
+
`import { readFileSync as __bunfsMetaRequireReadFileSync } from "node:fs";\n` +
|
|
66
|
+
`function __bunfsAssertHoistedProps(ns, targetModule, propNames) {\n` +
|
|
67
|
+
` for (const p of propNames) {\n` +
|
|
68
|
+
` let v;\n` +
|
|
69
|
+
` try {\n` +
|
|
70
|
+
` v = ns[p];\n` +
|
|
71
|
+
` } catch (e) {\n` +
|
|
72
|
+
` if (e instanceof ReferenceError) {\n` +
|
|
73
|
+
` throw new Error("bunfs cycle-hoist: accessing \\"" + p + "\\" on " + targetModule + " threw ReferenceError (TDZ) - evaluation-order regression");\n` +
|
|
74
|
+
` }\n` +
|
|
75
|
+
` throw e;\n` +
|
|
76
|
+
` }\n` +
|
|
77
|
+
` if (v === undefined) {\n` +
|
|
78
|
+
` throw new Error("bunfs cycle-hoist: \\"" + p + "\\" is undefined after hoisting from " + targetModule + " (evaluation-order regression?)");\n` +
|
|
79
|
+
` }\n` +
|
|
80
|
+
` }\n` +
|
|
81
|
+
`}\n` +
|
|
25
82
|
`const __bunfsRealRequire = __bunfsCreateRequire(${JSON.stringify(anchorUrl)});\n` +
|
|
83
|
+
`const __bunfsOwnedDir = ${JSON.stringify(PROCESS_OWNED_DIR)};\n` +
|
|
26
84
|
`const __bunfsMetaRequire = (id) => {\n` +
|
|
27
85
|
` if (id === "child_process" || id === "node:child_process") return __bunfsGuardedChildProcess;\n` +
|
|
28
86
|
` if (id === "vm" || id === "node:vm") return __bunfsGuardedVm;\n` +
|
|
87
|
+
` if (typeof id === "string" && id.startsWith("/$bunfs/root/")) {\n` +
|
|
88
|
+
` const rel = id.slice("/$bunfs/root/".length);\n` +
|
|
89
|
+
` if (rel.includes("..") || __bunfsMetaRequirePath.isAbsolute(rel)) {\n` +
|
|
90
|
+
` throw new Error("bunfs meta-require: rejected specifier " + id);\n` +
|
|
91
|
+
` }\n` +
|
|
92
|
+
` const real = __bunfsMetaRequirePath.resolve(__bunfsOwnedDir, rel);\n` +
|
|
93
|
+
` if (__bunfsMetaRequirePath.relative(__bunfsOwnedDir, real).startsWith("..")) {\n` +
|
|
94
|
+
` throw new Error("bunfs meta-require: path escapes process-owned dir: " + id);\n` +
|
|
95
|
+
` }\n` +
|
|
96
|
+
` if (!__bunfsMetaRequireExistsSync(real)) {\n` +
|
|
97
|
+
` throw new Error("bunfs meta-require: missing extracted module " + id + " -> " + real);\n` +
|
|
98
|
+
` }\n` +
|
|
99
|
+
` const ext = __bunfsMetaRequirePath.extname(real);\n` +
|
|
100
|
+
` if (ext === ".md" || ext === ".txt") {\n` +
|
|
101
|
+
` return __bunfsMetaRequireReadFileSync(real, "utf8");\n` +
|
|
102
|
+
` }\n` +
|
|
103
|
+
` return __bunfsRealRequire(real);\n` +
|
|
104
|
+
` }\n` +
|
|
29
105
|
` return __bunfsRealRequire(id);\n` +
|
|
30
106
|
`};\n`
|
|
31
107
|
);
|
|
32
108
|
}
|
|
33
109
|
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
110
|
+
export function resolve(specifier, context, nextResolve) {
|
|
111
|
+
const parentURL = context && context.parentURL;
|
|
112
|
+
const ownedPrefix = pathToFileURL(PROCESS_OWNED_DIR + path.sep).href;
|
|
113
|
+
const fromChunk = typeof parentURL === 'string' && parentURL.startsWith(ownedPrefix);
|
|
114
|
+
|
|
115
|
+
if (fromChunk) {
|
|
116
|
+
if (specifier === 'child_process' || specifier === 'node:child_process') {
|
|
117
|
+
return { url: pathToFileURL(CHILD_PROCESS_GUARD_PATH).href, shortCircuit: true, format: 'module' };
|
|
118
|
+
}
|
|
119
|
+
if (specifier === 'vm' || specifier === 'node:vm') {
|
|
120
|
+
return { url: pathToFileURL(VM_GUARD_PATH).href, shortCircuit: true, format: 'module' };
|
|
121
|
+
}
|
|
122
|
+
if (specifier === 'ws') {
|
|
123
|
+
return { url: pathToFileURL(WS_STUB_PATH).href, shortCircuit: true, format: 'module' };
|
|
124
|
+
}
|
|
43
125
|
}
|
|
44
126
|
if (specifier.startsWith('/$bunfs/root/')) {
|
|
45
127
|
const rel = specifier.slice('/$bunfs/root/'.length);
|
|
@@ -58,17 +140,28 @@ export async function resolve(specifier, context, nextResolve) {
|
|
|
58
140
|
return nextResolve(specifier, context);
|
|
59
141
|
}
|
|
60
142
|
|
|
61
|
-
export
|
|
143
|
+
export function load(url, context, nextLoad) {
|
|
62
144
|
const ownedPrefix = pathToFileURL(PROCESS_OWNED_DIR + path.sep).href;
|
|
63
145
|
if (!url.startsWith(ownedPrefix)) {
|
|
64
146
|
return nextLoad(url, context);
|
|
65
147
|
}
|
|
66
148
|
const filePath = fileURLToPath(url);
|
|
67
149
|
let source = readFileSync(filePath, 'utf8');
|
|
150
|
+
|
|
151
|
+
let hoistedImportLine = '';
|
|
152
|
+
const hoistResult = tryHoistCycleBreakingImports(filePath, source);
|
|
153
|
+
if (hoistResult) {
|
|
154
|
+
hoistedImportLine = hoistResult.hoistedImportLine;
|
|
155
|
+
source = hoistResult.source;
|
|
156
|
+
}
|
|
157
|
+
|
|
68
158
|
if (source.includes('import.meta.require')) {
|
|
69
159
|
const anchorUrl = pathToFileURL(SOURCE_BIN).href;
|
|
70
|
-
source =
|
|
160
|
+
source = hoistedImportLine +
|
|
161
|
+
buildImportMetaRequirePolyfillPrelude(anchorUrl) +
|
|
71
162
|
source.replaceAll('import.meta.require', '__bunfsMetaRequire');
|
|
163
|
+
} else if (hoistedImportLine) {
|
|
164
|
+
source = hoistedImportLine + source;
|
|
72
165
|
}
|
|
73
166
|
return { format: 'module', source, shortCircuit: true };
|
|
74
167
|
}
|
|
@@ -33,15 +33,15 @@ test('resolve() handles child_process and node:child_process specifiers', async
|
|
|
33
33
|
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
const nextResolve =
|
|
36
|
+
const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
|
|
37
37
|
|
|
38
38
|
// child_process should resolve to childProcessGuardPath
|
|
39
|
-
const result1 =
|
|
39
|
+
const result1 = loader.resolve('child_process', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
|
|
40
40
|
assert.ok(result1.url.includes(guardPath));
|
|
41
41
|
assert.equal(result1.shortCircuit, true);
|
|
42
42
|
|
|
43
43
|
// node:child_process should also resolve to childProcessGuardPath
|
|
44
|
-
const result2 =
|
|
44
|
+
const result2 = loader.resolve('node:child_process', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
|
|
45
45
|
assert.ok(result2.url.includes(guardPath));
|
|
46
46
|
assert.equal(result2.shortCircuit, true);
|
|
47
47
|
} finally {
|
|
@@ -65,15 +65,15 @@ test('resolve() handles vm and node:vm specifiers', async () => {
|
|
|
65
65
|
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
const nextResolve =
|
|
68
|
+
const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
|
|
69
69
|
|
|
70
70
|
// vm should resolve to vmGuardPath
|
|
71
|
-
const result1 =
|
|
71
|
+
const result1 = loader.resolve('vm', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
|
|
72
72
|
assert.ok(result1.url.includes(vmGuardPath));
|
|
73
73
|
assert.equal(result1.shortCircuit, true);
|
|
74
74
|
|
|
75
75
|
// node:vm should also resolve to vmGuardPath
|
|
76
|
-
const result2 =
|
|
76
|
+
const result2 = loader.resolve('node:vm', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
|
|
77
77
|
assert.ok(result2.url.includes(vmGuardPath));
|
|
78
78
|
assert.equal(result2.shortCircuit, true);
|
|
79
79
|
} finally {
|
|
@@ -97,9 +97,9 @@ test('resolve() handles ws specifier', async () => {
|
|
|
97
97
|
wsStubPath: wsStubPath,
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
const nextResolve =
|
|
100
|
+
const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
|
|
101
101
|
|
|
102
|
-
const result =
|
|
102
|
+
const result = loader.resolve('ws', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
|
|
103
103
|
assert.ok(result.url.includes(wsStubPath));
|
|
104
104
|
assert.equal(result.shortCircuit, true);
|
|
105
105
|
} finally {
|
|
@@ -150,9 +150,9 @@ test('resolve() rejects path traversal with ..', async () => {
|
|
|
150
150
|
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
-
const nextResolve =
|
|
153
|
+
const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
assert.throws(
|
|
156
156
|
() => loader.resolve('/$bunfs/root/../../etc/passwd', {}, nextResolve),
|
|
157
157
|
/rejected specifier|escapes/,
|
|
158
158
|
);
|
|
@@ -175,9 +175,9 @@ test('resolve() rejects absolute paths', async () => {
|
|
|
175
175
|
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
const nextResolve =
|
|
178
|
+
const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
assert.throws(
|
|
181
181
|
() => loader.resolve('/$bunfs/root//etc/passwd', {}, nextResolve),
|
|
182
182
|
/rejected specifier|escapes/,
|
|
183
183
|
);
|
|
@@ -200,9 +200,9 @@ test('resolve() throws error for missing extracted module', async () => {
|
|
|
200
200
|
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
-
const nextResolve =
|
|
203
|
+
const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
assert.throws(
|
|
206
206
|
() => loader.resolve('/$bunfs/root/nonexistent.js', {}, nextResolve),
|
|
207
207
|
/missing extracted module/,
|
|
208
208
|
);
|
|
@@ -327,3 +327,162 @@ test('load() calls nextLoad for URLs outside processOwnedDir', async () => {
|
|
|
327
327
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
328
328
|
}
|
|
329
329
|
});
|
|
330
|
+
|
|
331
|
+
test('load() hoists the cycle-breaking import.meta.require call in chunk-vmw9kxhv.js', async () => {
|
|
332
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
333
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-hoist-test-${process.pid}-${Date.now()}`);
|
|
334
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
335
|
+
|
|
336
|
+
fs.writeFileSync(path.join(tempDir, 'chunk-y0jj307t.js'), 'export const daemonColdStartGbDefault = () => "fixture";\n');
|
|
337
|
+
const targetFile = path.join(tempDir, 'chunk-vmw9kxhv.js');
|
|
338
|
+
const sourceCode = 'var O9=import.meta.require("/$bunfs/root/chunk-y0jj307t.js");\nexport const value = O9.daemonColdStartGbDefault();\n';
|
|
339
|
+
fs.writeFileSync(targetFile, sourceCode);
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
loader.initialize({
|
|
343
|
+
processOwnedDir: tempDir,
|
|
344
|
+
sourceBin: '/dummy/bin',
|
|
345
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
346
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
347
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
348
|
+
cycleHoists: [{ file: 'chunk-vmw9kxhv.js', targetModule: 'chunk-y0jj307t.js', expectedOccurrences: 1, assertProperties: [] }],
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const fileUrl = pathToFileURL(targetFile).href;
|
|
352
|
+
const result = await loader.load(fileUrl, {}, async () => ({ source: 'fallback' }));
|
|
353
|
+
|
|
354
|
+
assert.equal(result.format, 'module');
|
|
355
|
+
assert.ok(result.source.includes('import * as __bunfsHoisted_0 from'));
|
|
356
|
+
assert.ok(!result.source.includes('var O9=import.meta.require('));
|
|
357
|
+
assert.ok(result.source.includes('var O9=__bunfsHoisted_0'));
|
|
358
|
+
assert.equal(result.shortCircuit, true);
|
|
359
|
+
} finally {
|
|
360
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('import.meta.require resolves /$bunfs/root/ specifiers via loader integration', async () => {
|
|
365
|
+
const { registerHooks } = await import('node:module');
|
|
366
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
367
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-integration-${process.pid}-${Date.now()}`);
|
|
368
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
// Create guard files
|
|
372
|
+
fs.writeFileSync(path.join(tempDir, 'guard.mjs'), 'export default {};');
|
|
373
|
+
fs.writeFileSync(path.join(tempDir, 'vm-guard.mjs'), 'export default {};');
|
|
374
|
+
fs.writeFileSync(path.join(tempDir, 'ws-stub.mjs'), 'export default {};');
|
|
375
|
+
|
|
376
|
+
// Create ESM fixture that will be required via import.meta.require (normal case).
|
|
377
|
+
// PROCESS_OWNED_DIR only ever contains genuine ESM chunk files extracted from the
|
|
378
|
+
// Bun esm-chunked bundle (verified against a real 2.1.248 extraction: 0 of 1768
|
|
379
|
+
// chunk files are CommonJS), so load() always returns format: 'module' for this dir.
|
|
380
|
+
fs.writeFileSync(path.join(tempDir, 'foo.js'), 'export const value = 42;');
|
|
381
|
+
|
|
382
|
+
// Create ESM files for each test scenario
|
|
383
|
+
const okCallerPath = path.join(tempDir, 'ok-caller.mjs');
|
|
384
|
+
fs.writeFileSync(okCallerPath, 'export const result = import.meta.require("/$bunfs/root/foo.js").value;\n');
|
|
385
|
+
|
|
386
|
+
const traversalCallerPath = path.join(tempDir, 'traversal-caller.mjs');
|
|
387
|
+
fs.writeFileSync(traversalCallerPath, 'import.meta.require("/$bunfs/root/../../etc/passwd");\n');
|
|
388
|
+
|
|
389
|
+
const missingCallerPath = path.join(tempDir, 'missing-caller.mjs');
|
|
390
|
+
fs.writeFileSync(missingCallerPath, 'import.meta.require("/$bunfs/root/nonexistent.js");\n');
|
|
391
|
+
|
|
392
|
+
// Register loader (only once) with data
|
|
393
|
+
const sourceBin = path.join(tempDir, 'dummy-bin');
|
|
394
|
+
fs.writeFileSync(sourceBin, '#!/bin/false');
|
|
395
|
+
|
|
396
|
+
loader.initialize({
|
|
397
|
+
processOwnedDir: tempDir,
|
|
398
|
+
sourceBin: sourceBin,
|
|
399
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
400
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
401
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
402
|
+
cycleHoists: [
|
|
403
|
+
{ file: 'chunk-vmw9kxhv.js', targetModule: 'chunk-y0jj307t.js', expectedOccurrences: 1, assertProperties: [] },
|
|
404
|
+
],
|
|
405
|
+
});
|
|
406
|
+
registerHooks({ resolve: loader.resolve, load: loader.load });
|
|
407
|
+
|
|
408
|
+
// Test 1: Normal case - should load and resolve correctly
|
|
409
|
+
const okModule = await import(pathToFileURL(okCallerPath).href);
|
|
410
|
+
assert.equal(okModule.result, 42);
|
|
411
|
+
|
|
412
|
+
// Test 2: Path traversal rejection - should throw error
|
|
413
|
+
await assert.rejects(
|
|
414
|
+
() => import(pathToFileURL(traversalCallerPath).href),
|
|
415
|
+
/rejected specifier|escapes/,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// Test 3: Missing module rejection - should throw error
|
|
419
|
+
await assert.rejects(
|
|
420
|
+
() => import(pathToFileURL(missingCallerPath).href),
|
|
421
|
+
/missing extracted module/,
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// Cycle regression proof: a generic sync-require-into-in-flight-static-import cycle
|
|
425
|
+
// must throw ERR_REQUIRE_CYCLE_MODULE when NOT hoisted (proves our understanding of
|
|
426
|
+
// the bug mechanism is correct, independent of the real chunk-y0jj307t.js file).
|
|
427
|
+
fs.writeFileSync(
|
|
428
|
+
path.join(tempDir, 'chunk-cycle-demo-target.js'),
|
|
429
|
+
'import "/$bunfs/root/chunk-vmw9kxhv-a.js";\nexport const daemonColdStartGbDefault = () => "fixture";\n',
|
|
430
|
+
);
|
|
431
|
+
fs.writeFileSync(
|
|
432
|
+
path.join(tempDir, 'chunk-vmw9kxhv-a.js'),
|
|
433
|
+
'var O9X=import.meta.require("/$bunfs/root/chunk-cycle-demo-target.js");\nexport const value = O9X;\n',
|
|
434
|
+
);
|
|
435
|
+
await assert.rejects(
|
|
436
|
+
() => import(pathToFileURL(path.join(tempDir, 'chunk-vmw9kxhv-a.js')).href),
|
|
437
|
+
(err) => {
|
|
438
|
+
assert.equal(err.code, 'ERR_REQUIRE_CYCLE_MODULE');
|
|
439
|
+
return true;
|
|
440
|
+
},
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
// Cycle fix proof: the real chunk-vmw9kxhv.js / chunk-y0jj307t.js pair (exact filenames
|
|
444
|
+
// and declaration text that tryHoistCycleBreakingImport() targets) must resolve cleanly
|
|
445
|
+
// once hoisting is applied, and the hoisted namespace's property access must work.
|
|
446
|
+
fs.writeFileSync(
|
|
447
|
+
path.join(tempDir, 'chunk-y0jj307t.js'),
|
|
448
|
+
'import "/$bunfs/root/chunk-vmw9kxhv.js";\nexport const daemonColdStartGbDefault = () => "fixture";\n',
|
|
449
|
+
);
|
|
450
|
+
fs.writeFileSync(
|
|
451
|
+
path.join(tempDir, 'chunk-vmw9kxhv.js'),
|
|
452
|
+
'var O9=import.meta.require("/$bunfs/root/chunk-y0jj307t.js");\nexport const value = O9.daemonColdStartGbDefault();\n',
|
|
453
|
+
);
|
|
454
|
+
const hoistedModule = await import(pathToFileURL(path.join(tempDir, 'chunk-vmw9kxhv.js')).href);
|
|
455
|
+
assert.equal(hoistedModule.value, 'fixture');
|
|
456
|
+
|
|
457
|
+
fs.writeFileSync(path.join(tempDir, 'doc.md'), '# Hello\nSome markdown text.\n');
|
|
458
|
+
fs.writeFileSync(
|
|
459
|
+
path.join(tempDir, 'md-caller.mjs'),
|
|
460
|
+
'export const result = import.meta.require("/$bunfs/root/doc.md");\n',
|
|
461
|
+
);
|
|
462
|
+
const mdModule = await import(pathToFileURL(path.join(tempDir, 'md-caller.mjs')).href);
|
|
463
|
+
assert.equal(typeof mdModule.result, 'string');
|
|
464
|
+
assert.equal(mdModule.result, '# Hello\nSome markdown text.\n');
|
|
465
|
+
|
|
466
|
+
fs.writeFileSync(path.join(tempDir, 'note.txt'), 'plain text content');
|
|
467
|
+
fs.writeFileSync(
|
|
468
|
+
path.join(tempDir, 'txt-caller.mjs'),
|
|
469
|
+
'export const result = import.meta.require("/$bunfs/root/note.txt");\n',
|
|
470
|
+
);
|
|
471
|
+
const txtModule = await import(pathToFileURL(path.join(tempDir, 'txt-caller.mjs')).href);
|
|
472
|
+
assert.equal(typeof txtModule.result, 'string');
|
|
473
|
+
assert.equal(txtModule.result, 'plain text content');
|
|
474
|
+
|
|
475
|
+
fs.writeFileSync(
|
|
476
|
+
path.join(tempDir, 'chunk-alias.js'),
|
|
477
|
+
'export const ee = import.meta.require;\n',
|
|
478
|
+
);
|
|
479
|
+
fs.writeFileSync(
|
|
480
|
+
path.join(tempDir, 'alias-caller.mjs'),
|
|
481
|
+
'import { ee } from "/$bunfs/root/chunk-alias.js";\nexport const result = ee("/$bunfs/root/doc.md");\n',
|
|
482
|
+
);
|
|
483
|
+
const aliasModule = await import(pathToFileURL(path.join(tempDir, 'alias-caller.mjs')).href);
|
|
484
|
+
assert.equal(aliasModule.result, '# Hello\nSome markdown text.\n');
|
|
485
|
+
} finally {
|
|
486
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
487
|
+
}
|
|
488
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const packageDir = path.resolve(__dirname, '..');
|
|
8
|
+
|
|
9
|
+
function verifyTarball(file, audited, version) {
|
|
10
|
+
const buf = fs.readFileSync(file);
|
|
11
|
+
if (audited.tarball_sha256) {
|
|
12
|
+
const sha256 = crypto.createHash('sha256').update(buf).digest('hex');
|
|
13
|
+
if (sha256 !== audited.tarball_sha256) {
|
|
14
|
+
throw new Error(`tarball sha256 mismatch for ${version}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (audited.tarball_integrity) {
|
|
18
|
+
const sha512 = `sha512-${crypto.createHash('sha512').update(buf).digest('base64')}`;
|
|
19
|
+
if (sha512 !== audited.tarball_integrity) {
|
|
20
|
+
throw new Error(`tarball integrity mismatch for ${version}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (audited.tarball_size !== undefined && Number(audited.tarball_size) !== buf.length) {
|
|
24
|
+
throw new Error(`tarball size mismatch for ${version}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateOffsets(file, audited, version) {
|
|
29
|
+
if (audited.entry_format === 'esm-chunked') {
|
|
30
|
+
validateEsmChunkedOffsets(file, audited, version);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
validateLegacyCjsOffsets(file, audited, version);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function validateLegacyCjsOffsets(file, audited, version) {
|
|
37
|
+
const buf = fs.readFileSync(file);
|
|
38
|
+
const start = Number(audited.entry_js_offset);
|
|
39
|
+
const end = Number(audited.entry_end_offset);
|
|
40
|
+
const startMarker = Buffer.from('function(exports, require, module, __filename, __dirname) {// Claude Code is a Beta product');
|
|
41
|
+
const endMarker = Buffer.from('/$bunfs/root/image-processor.js');
|
|
42
|
+
|
|
43
|
+
if (!(start > 0 && end > start && end <= buf.length)) {
|
|
44
|
+
throw new Error(`invalid audited offsets for ${version}`);
|
|
45
|
+
}
|
|
46
|
+
if (!buf.subarray(start, start + startMarker.length).equals(startMarker)) {
|
|
47
|
+
throw new Error(`audited start offset validation failed for ${version}`);
|
|
48
|
+
}
|
|
49
|
+
if (!buf.subarray(end, end + endMarker.length).equals(endMarker)) {
|
|
50
|
+
throw new Error(`audited end offset validation failed for ${version}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateEsmChunkedOffsets(file, audited, version) {
|
|
55
|
+
// 371MB超のバイナリ全体をreadFileSyncしない(実機でOOM確認済み)。
|
|
56
|
+
// discoverModuleGraphは範囲readSyncのみでトレイラー・モジュールテーブルを検証する。
|
|
57
|
+
const { discoverModuleGraph, readEntryContentPrefix } = require(path.join(packageDir, 'lib', 'bunfs-extract.js'));
|
|
58
|
+
const graph = discoverModuleGraph(file);
|
|
59
|
+
try {
|
|
60
|
+
if (!(graph.numModules > 0)) {
|
|
61
|
+
throw new Error(`esm-chunked module graph is empty for ${version}`);
|
|
62
|
+
}
|
|
63
|
+
if (graph.entryName !== '/$bunfs/root/cli') {
|
|
64
|
+
throw new Error(`esm-chunked entry module name mismatch for ${version}: ${graph.entryName}`);
|
|
65
|
+
}
|
|
66
|
+
if (!Object.prototype.hasOwnProperty.call(audited, 'cycle_hoists')) {
|
|
67
|
+
throw new Error(`esm-chunked audited metadata for ${version} is missing cycle_hoists field`);
|
|
68
|
+
}
|
|
69
|
+
if (audited.num_modules !== undefined && graph.numModules !== audited.num_modules) {
|
|
70
|
+
throw new Error(`esm-chunked num_modules mismatch for ${version}: expected ${audited.num_modules}, got ${graph.numModules}`);
|
|
71
|
+
}
|
|
72
|
+
if (audited.byte_count !== undefined && graph.byteCount !== audited.byte_count) {
|
|
73
|
+
throw new Error(`esm-chunked byte_count mismatch for ${version}: expected ${audited.byte_count}, got ${graph.byteCount}`);
|
|
74
|
+
}
|
|
75
|
+
const prefix = readEntryContentPrefix(graph.fd, graph.entryModule, 256).toString('utf8');
|
|
76
|
+
const codeStart = prefix.replace(/^(\s*\/\/[^\n]*\n)+/, '').replace(/^\(/, '');
|
|
77
|
+
if (codeStart.startsWith('function(exports, require, module, __filename, __dirname) {')) {
|
|
78
|
+
throw new Error(`entry module for ${version} is legacy-cjs wrapped, but audited entry_format is esm-chunked`);
|
|
79
|
+
}
|
|
80
|
+
} finally {
|
|
81
|
+
fs.closeSync(graph.fd);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
validateEsmChunkedOffsets,
|
|
87
|
+
validateLegacyCjsOffsets,
|
|
88
|
+
validateOffsets,
|
|
89
|
+
verifyTarball,
|
|
90
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
validateEsmChunkedOffsets,
|
|
11
|
+
} = require('./native-validators.js');
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
discoverModuleGraph,
|
|
15
|
+
} = require('./bunfs-extract.js');
|
|
16
|
+
|
|
17
|
+
const TRAILER = '\n---- Bun! ----\n';
|
|
18
|
+
|
|
19
|
+
// StandaloneModuleGraphの最小合成バイナリを構築する。
|
|
20
|
+
// レイアウト: [preamble padding][module contents][module table][Offsets(32byte)][trailer]
|
|
21
|
+
function buildSyntheticBinary({ modules, entryPointId, corruptTrailer = false, preamblePadding = 64 }) {
|
|
22
|
+
const nameBuffers = modules.map((m) => Buffer.from(m.name, 'utf8'));
|
|
23
|
+
const contentBuffers = modules.map((m) => Buffer.from(m.content ?? '', 'utf8'));
|
|
24
|
+
|
|
25
|
+
const dataParts = [];
|
|
26
|
+
const nameOffsets = [];
|
|
27
|
+
const contOffsets = [];
|
|
28
|
+
let cursor = 0;
|
|
29
|
+
for (let i = 0; i < modules.length; i += 1) {
|
|
30
|
+
nameOffsets.push(cursor);
|
|
31
|
+
dataParts.push(nameBuffers[i]);
|
|
32
|
+
cursor += nameBuffers[i].length;
|
|
33
|
+
}
|
|
34
|
+
for (let i = 0; i < modules.length; i += 1) {
|
|
35
|
+
contOffsets.push(cursor);
|
|
36
|
+
dataParts.push(contentBuffers[i]);
|
|
37
|
+
cursor += contentBuffers[i].length;
|
|
38
|
+
}
|
|
39
|
+
const byteCountBeforeTable = cursor;
|
|
40
|
+
|
|
41
|
+
const MODULE_TABLE_ENTRY_SIZE = 52;
|
|
42
|
+
const modTable = Buffer.alloc(MODULE_TABLE_ENTRY_SIZE * modules.length);
|
|
43
|
+
for (let i = 0; i < modules.length; i += 1) {
|
|
44
|
+
const base = i * MODULE_TABLE_ENTRY_SIZE;
|
|
45
|
+
modTable.writeUInt32LE(nameOffsets[i], base);
|
|
46
|
+
modTable.writeUInt32LE(nameBuffers[i].length, base + 4);
|
|
47
|
+
modTable.writeUInt32LE(contOffsets[i], base + 8);
|
|
48
|
+
modTable.writeUInt32LE(contentBuffers[i].length, base + 12);
|
|
49
|
+
modTable[base + 49] = modules[i].loader ?? 1; // 1 = js
|
|
50
|
+
}
|
|
51
|
+
const modulesOffset = byteCountBeforeTable;
|
|
52
|
+
const modulesLength = modTable.length;
|
|
53
|
+
const byteCount = byteCountBeforeTable + modulesLength;
|
|
54
|
+
|
|
55
|
+
const offsetsBuf = Buffer.alloc(32);
|
|
56
|
+
offsetsBuf.writeBigUInt64LE(BigInt(byteCount), 0);
|
|
57
|
+
offsetsBuf.writeUInt32LE(modulesOffset, 8);
|
|
58
|
+
offsetsBuf.writeUInt32LE(modulesLength, 12);
|
|
59
|
+
offsetsBuf.writeUInt32LE(entryPointId, 16);
|
|
60
|
+
|
|
61
|
+
const trailerBuf = Buffer.from(corruptTrailer ? '\n---- NOT BUN ----\n' : TRAILER, 'utf8');
|
|
62
|
+
|
|
63
|
+
return Buffer.concat([
|
|
64
|
+
Buffer.alloc(preamblePadding),
|
|
65
|
+
...dataParts,
|
|
66
|
+
modTable,
|
|
67
|
+
offsetsBuf,
|
|
68
|
+
trailerBuf,
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function writeTempBinary(buf) {
|
|
73
|
+
const file = path.join(os.tmpdir(), `native-validators-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.bin`);
|
|
74
|
+
fs.writeFileSync(file, buf);
|
|
75
|
+
return file;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test('validateEsmChunkedOffsets accepts matching num_modules and byte_count', () => {
|
|
79
|
+
const buf = buildSyntheticBinary({
|
|
80
|
+
modules: [
|
|
81
|
+
{ name: '/$bunfs/root/cli', content: 'export default 1;' },
|
|
82
|
+
],
|
|
83
|
+
entryPointId: 0,
|
|
84
|
+
});
|
|
85
|
+
const file = writeTempBinary(buf);
|
|
86
|
+
try {
|
|
87
|
+
const graph = discoverModuleGraph(file);
|
|
88
|
+
try {
|
|
89
|
+
const audited = {
|
|
90
|
+
num_modules: graph.numModules,
|
|
91
|
+
byte_count: graph.byteCount,
|
|
92
|
+
entry_format: 'esm-chunked',
|
|
93
|
+
cycle_hoists: [],
|
|
94
|
+
};
|
|
95
|
+
assert.doesNotThrow(() => validateEsmChunkedOffsets(file, audited, '9.9.9'));
|
|
96
|
+
} finally {
|
|
97
|
+
fs.closeSync(graph.fd);
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
fs.rmSync(file, { force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('validateEsmChunkedOffsets rejects mismatched num_modules', () => {
|
|
105
|
+
const buf = buildSyntheticBinary({
|
|
106
|
+
modules: [
|
|
107
|
+
{ name: '/$bunfs/root/cli', content: 'export default 1;' },
|
|
108
|
+
],
|
|
109
|
+
entryPointId: 0,
|
|
110
|
+
});
|
|
111
|
+
const file = writeTempBinary(buf);
|
|
112
|
+
try {
|
|
113
|
+
const graph = discoverModuleGraph(file);
|
|
114
|
+
try {
|
|
115
|
+
const audited = {
|
|
116
|
+
num_modules: graph.numModules + 1, // intentionally wrong
|
|
117
|
+
byte_count: graph.byteCount,
|
|
118
|
+
entry_format: 'esm-chunked',
|
|
119
|
+
cycle_hoists: [],
|
|
120
|
+
};
|
|
121
|
+
assert.throws(
|
|
122
|
+
() => validateEsmChunkedOffsets(file, audited, '9.9.9'),
|
|
123
|
+
/num_modules mismatch/,
|
|
124
|
+
);
|
|
125
|
+
} finally {
|
|
126
|
+
fs.closeSync(graph.fd);
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
fs.rmSync(file, { force: true });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('validateEsmChunkedOffsets rejects mismatched byte_count', () => {
|
|
134
|
+
const buf = buildSyntheticBinary({
|
|
135
|
+
modules: [
|
|
136
|
+
{ name: '/$bunfs/root/cli', content: 'export default 1;' },
|
|
137
|
+
],
|
|
138
|
+
entryPointId: 0,
|
|
139
|
+
});
|
|
140
|
+
const file = writeTempBinary(buf);
|
|
141
|
+
try {
|
|
142
|
+
const graph = discoverModuleGraph(file);
|
|
143
|
+
try {
|
|
144
|
+
const audited = {
|
|
145
|
+
num_modules: graph.numModules,
|
|
146
|
+
byte_count: graph.byteCount + 1, // intentionally wrong
|
|
147
|
+
entry_format: 'esm-chunked',
|
|
148
|
+
cycle_hoists: [],
|
|
149
|
+
};
|
|
150
|
+
assert.throws(
|
|
151
|
+
() => validateEsmChunkedOffsets(file, audited, '9.9.9'),
|
|
152
|
+
/byte_count mismatch/,
|
|
153
|
+
);
|
|
154
|
+
} finally {
|
|
155
|
+
fs.closeSync(graph.fd);
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
fs.rmSync(file, { force: true });
|
|
159
|
+
}
|
|
160
|
+
});
|
package/lib/prepare-native.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const cp = require('child_process');
|
|
5
|
-
const crypto = require('crypto');
|
|
6
5
|
const fs = require('fs');
|
|
7
6
|
const os = require('os');
|
|
8
7
|
const path = require('path');
|
|
@@ -16,6 +15,8 @@ const curlRetries = process.env.CLAUDE_TERMUX_FETCH_RETRIES || '4';
|
|
|
16
15
|
const curlConnectTimeout = process.env.CLAUDE_TERMUX_FETCH_CONNECT_TIMEOUT || '20';
|
|
17
16
|
const curlMaxTime = process.env.CLAUDE_TERMUX_FETCH_MAX_TIME || '300';
|
|
18
17
|
|
|
18
|
+
const { validateOffsets, verifyTarball } = require(path.join(packageDir, 'lib', 'native-validators.js'));
|
|
19
|
+
|
|
19
20
|
if (!item) {
|
|
20
21
|
console.error(`Unsupported audited Claude Code version: ${version}`);
|
|
21
22
|
process.exit(1);
|
|
@@ -27,7 +28,7 @@ const nativeDest = path.join(versionDir, 'app', 'node_modules', '@anthropic-ai',
|
|
|
27
28
|
const sourceBin = path.join(nativeDest, 'claude');
|
|
28
29
|
|
|
29
30
|
if (fs.existsSync(sourceBin)) {
|
|
30
|
-
validateOffsets(sourceBin, item);
|
|
31
|
+
validateOffsets(sourceBin, item, version);
|
|
31
32
|
process.exit(0);
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -36,7 +37,7 @@ const packDir = fs.mkdtempSync(path.join(os.tmpdir(), `claude-code-${version}-`)
|
|
|
36
37
|
|
|
37
38
|
try {
|
|
38
39
|
const tgzPath = fetchNativeTarball(item.native_spec, packDir);
|
|
39
|
-
verifyTarball(tgzPath, item);
|
|
40
|
+
verifyTarball(tgzPath, item, version);
|
|
40
41
|
|
|
41
42
|
const extractDir = path.join(packDir, 'native');
|
|
42
43
|
fs.mkdirSync(extractDir, { recursive: true });
|
|
@@ -45,7 +46,7 @@ try {
|
|
|
45
46
|
fs.rmSync(nativeDest, { recursive: true, force: true });
|
|
46
47
|
fs.mkdirSync(path.dirname(nativeDest), { recursive: true });
|
|
47
48
|
fs.cpSync(path.join(extractDir, 'package'), nativeDest, { recursive: true });
|
|
48
|
-
validateOffsets(sourceBin, item);
|
|
49
|
+
validateOffsets(sourceBin, item, version);
|
|
49
50
|
} finally {
|
|
50
51
|
fs.rmSync(packDir, { recursive: true, force: true });
|
|
51
52
|
}
|
|
@@ -107,70 +108,3 @@ function fetchNativeTarball(spec, packDir) {
|
|
|
107
108
|
}
|
|
108
109
|
return path.join(packDir, packEntry.filename);
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
-
function verifyTarball(file, audited) {
|
|
112
|
-
const buf = fs.readFileSync(file);
|
|
113
|
-
if (audited.tarball_sha256) {
|
|
114
|
-
const sha256 = crypto.createHash('sha256').update(buf).digest('hex');
|
|
115
|
-
if (sha256 !== audited.tarball_sha256) {
|
|
116
|
-
throw new Error(`tarball sha256 mismatch for ${version}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (audited.tarball_integrity) {
|
|
120
|
-
const sha512 = `sha512-${crypto.createHash('sha512').update(buf).digest('base64')}`;
|
|
121
|
-
if (sha512 !== audited.tarball_integrity) {
|
|
122
|
-
throw new Error(`tarball integrity mismatch for ${version}`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (audited.tarball_size !== undefined && Number(audited.tarball_size) !== buf.length) {
|
|
126
|
-
throw new Error(`tarball size mismatch for ${version}`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function validateOffsets(file, audited) {
|
|
131
|
-
if (audited.entry_format === 'esm-chunked') {
|
|
132
|
-
validateEsmChunkedOffsets(file, audited);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
validateLegacyCjsOffsets(file, audited);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function validateLegacyCjsOffsets(file, audited) {
|
|
139
|
-
const buf = fs.readFileSync(file);
|
|
140
|
-
const start = Number(audited.entry_js_offset);
|
|
141
|
-
const end = Number(audited.entry_end_offset);
|
|
142
|
-
const startMarker = Buffer.from('function(exports, require, module, __filename, __dirname) {// Claude Code is a Beta product');
|
|
143
|
-
const endMarker = Buffer.from('/$bunfs/root/image-processor.js');
|
|
144
|
-
|
|
145
|
-
if (!(start > 0 && end > start && end <= buf.length)) {
|
|
146
|
-
throw new Error(`invalid audited offsets for ${version}`);
|
|
147
|
-
}
|
|
148
|
-
if (!buf.subarray(start, start + startMarker.length).equals(startMarker)) {
|
|
149
|
-
throw new Error(`audited start offset validation failed for ${version}`);
|
|
150
|
-
}
|
|
151
|
-
if (!buf.subarray(end, end + endMarker.length).equals(endMarker)) {
|
|
152
|
-
throw new Error(`audited end offset validation failed for ${version}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function validateEsmChunkedOffsets(file, audited) {
|
|
157
|
-
// 371MB超のバイナリ全体をreadFileSyncしない(実機でOOM確認済み)。
|
|
158
|
-
// discoverModuleGraphは範囲readSyncのみでトレイラー・モジュールテーブルを検証する。
|
|
159
|
-
const { discoverModuleGraph, readEntryContentPrefix } = require(path.join(packageDir, 'lib', 'bunfs-extract.js'));
|
|
160
|
-
const graph = discoverModuleGraph(file);
|
|
161
|
-
try {
|
|
162
|
-
if (!(graph.numModules > 0)) {
|
|
163
|
-
throw new Error(`esm-chunked module graph is empty for ${version}`);
|
|
164
|
-
}
|
|
165
|
-
if (graph.entryName !== '/$bunfs/root/cli') {
|
|
166
|
-
throw new Error(`esm-chunked entry module name mismatch for ${version}: ${graph.entryName}`);
|
|
167
|
-
}
|
|
168
|
-
const prefix = readEntryContentPrefix(graph.fd, graph.entryModule, 256).toString('utf8');
|
|
169
|
-
const codeStart = prefix.replace(/^(\s*\/\/[^\n]*\n)+/, '').replace(/^\(/, '');
|
|
170
|
-
if (codeStart.startsWith('function(exports, require, module, __filename, __dirname) {')) {
|
|
171
|
-
throw new Error(`entry module for ${version} is legacy-cjs wrapped, but audited entry_format is esm-chunked`);
|
|
172
|
-
}
|
|
173
|
-
} finally {
|
|
174
|
-
fs.closeSync(graph.fd);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
@@ -665,7 +665,7 @@ function rewriteNativeChunkSource(source) {
|
|
|
665
665
|
|
|
666
666
|
async function esmChunkedMain() {
|
|
667
667
|
const { prepareProcessOwnedDir } = require(path.join(process.env.CLAUDE_TERMUX_PACKAGE_DIR, 'lib', 'bunfs-extract.js'));
|
|
668
|
-
const {
|
|
668
|
+
const { registerHooks } = require('node:module');
|
|
669
669
|
const { pathToFileURL } = require('node:url');
|
|
670
670
|
|
|
671
671
|
const { ownedDir, entryRelPath } = prepareProcessOwnedDir(sourceBin, workdir);
|
|
@@ -690,16 +690,27 @@ async function esmChunkedMain() {
|
|
|
690
690
|
globalThis.__claudeBunShim = globalThis.Bun;
|
|
691
691
|
globalThis.__claudeBun = globalThis.Bun;
|
|
692
692
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
693
|
+
let cycleHoists = [];
|
|
694
|
+
try {
|
|
695
|
+
const auditedVersions = require(path.join(process.env.CLAUDE_TERMUX_PACKAGE_DIR, 'config', 'claude-native-audited-versions.json'));
|
|
696
|
+
const entry = auditedVersions.versions?.[process.env.CURRENT_CLAUDE_VERSION];
|
|
697
|
+
if (entry && Array.isArray(entry.cycle_hoists)) {
|
|
698
|
+
cycleHoists = entry.cycle_hoists;
|
|
699
|
+
}
|
|
700
|
+
} catch {
|
|
701
|
+
// 読み込み失敗時は空配列のまま(fail-closed、既存の同期require経路にフォールバック)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const loaderMod = require(path.join(libDir, 'bunfs-esm-loader.mjs'));
|
|
705
|
+
loaderMod.initialize({
|
|
706
|
+
processOwnedDir: ownedDir,
|
|
707
|
+
sourceBin,
|
|
708
|
+
childProcessGuardPath: path.join(libDir, 'bunfs-child-process-guard.mjs'),
|
|
709
|
+
vmGuardPath: path.join(libDir, 'bunfs-vm-guard.mjs'),
|
|
710
|
+
wsStubPath: path.join(libDir, 'bunfs-ws-stub.mjs'),
|
|
711
|
+
cycleHoists,
|
|
702
712
|
});
|
|
713
|
+
registerHooks({ resolve: loaderMod.resolve, load: loaderMod.load });
|
|
703
714
|
|
|
704
715
|
const entryUrl = pathToFileURL(path.join(ownedDir, entryRelPath)).href;
|
|
705
716
|
|
|
@@ -1679,7 +1690,7 @@ function rewriteNativeChunkSource(source) {
|
|
|
1679
1690
|
|
|
1680
1691
|
async function esmChunkedMain() {
|
|
1681
1692
|
const { prepareProcessOwnedDir } = require(path.join(process.env.CLAUDE_TERMUX_PACKAGE_DIR, 'lib', 'bunfs-extract.js'));
|
|
1682
|
-
const {
|
|
1693
|
+
const { registerHooks } = require('node:module');
|
|
1683
1694
|
const { pathToFileURL } = require('node:url');
|
|
1684
1695
|
|
|
1685
1696
|
const { ownedDir, entryRelPath } = prepareProcessOwnedDir(sourceBin, workdir);
|
|
@@ -1704,16 +1715,27 @@ async function esmChunkedMain() {
|
|
|
1704
1715
|
globalThis.__claudeBunShim = globalThis.Bun;
|
|
1705
1716
|
globalThis.__claudeBun = globalThis.Bun;
|
|
1706
1717
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1718
|
+
let cycleHoists = [];
|
|
1719
|
+
try {
|
|
1720
|
+
const auditedVersions = require(path.join(process.env.CLAUDE_TERMUX_PACKAGE_DIR, 'config', 'claude-native-audited-versions.json'));
|
|
1721
|
+
const entry = auditedVersions.versions?.[process.env.CURRENT_CLAUDE_VERSION];
|
|
1722
|
+
if (entry && Array.isArray(entry.cycle_hoists)) {
|
|
1723
|
+
cycleHoists = entry.cycle_hoists;
|
|
1724
|
+
}
|
|
1725
|
+
} catch {
|
|
1726
|
+
// 読み込み失敗時は空配列のまま(fail-closed、既存の同期require経路にフォールバック)
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const loaderMod = require(path.join(libDir, 'bunfs-esm-loader.mjs'));
|
|
1730
|
+
loaderMod.initialize({
|
|
1731
|
+
processOwnedDir: ownedDir,
|
|
1732
|
+
sourceBin,
|
|
1733
|
+
childProcessGuardPath: path.join(libDir, 'bunfs-child-process-guard.mjs'),
|
|
1734
|
+
vmGuardPath: path.join(libDir, 'bunfs-vm-guard.mjs'),
|
|
1735
|
+
wsStubPath: path.join(libDir, 'bunfs-ws-stub.mjs'),
|
|
1736
|
+
cycleHoists,
|
|
1716
1737
|
});
|
|
1738
|
+
registerHooks({ resolve: loaderMod.resolve, load: loaderMod.load });
|
|
1717
1739
|
|
|
1718
1740
|
const entryUrl = pathToFileURL(path.join(ownedDir, entryRelPath)).href;
|
|
1719
1741
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bash0816/claude-code",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.248",
|
|
4
4
|
"description": "Unofficial Termux-native Claude Code wrapper with audited native replay",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"LICENSE"
|
|
16
16
|
],
|
|
17
17
|
"engines": {
|
|
18
|
-
"node": ">=
|
|
18
|
+
"node": ">=22.15.0 <23.0.0 || >=23.5.0"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
21
|
"claude-code",
|