contextecf-codeledger 0.10.80 → 0.10.82
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/bin/codeledger.js +161 -69
- package/package.json +3 -2
- package/scripts/postinstall.js +309 -138
- package/scripts/runtime-pin.js +125 -0
package/bin/codeledger.js
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/*
|
|
3
|
-
*
|
|
3
|
+
* CodeLedger npm launcher — thin shim.
|
|
4
4
|
*
|
|
5
|
-
* This
|
|
6
|
-
*
|
|
5
|
+
* This launcher and its installation helpers ship to npm under @codeledger/cli.
|
|
6
|
+
* It contains no engine code, no scoring logic, no formulas. All CodeLedger
|
|
7
|
+
* functionality lives inside the hardened binary that is downloaded during
|
|
8
|
+
* `postinstall` from the GitHub Release for this exact version, verified
|
|
9
|
+
* via SHA256, and cached under ~/.cache/codeledger/bin/.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* postinstall soft-fails (see scripts/postinstall.js) rather than blocking
|
|
12
|
+
* `npm install`, so the binary may not be cached yet the first time this
|
|
13
|
+
* shim runs. When that happens, this shim first looks for a repo-local
|
|
14
|
+
* runtime (an already-initialized repo's `.codeledger/bin/`) before making
|
|
15
|
+
* one synchronous, on-demand download attempt itself.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
'use strict';
|
|
16
19
|
|
|
17
20
|
const { spawnSync } = require('node:child_process');
|
|
18
|
-
const
|
|
19
|
-
const
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
const crypto = require('node:crypto');
|
|
24
|
+
const { resolveRuntimePin, compareRuntimeVersions, isRuntimePinTrusted } = require('../scripts/runtime-pin.js');
|
|
25
|
+
const { existsSync, realpathSync, statSync } = require('node:fs');
|
|
26
|
+
const { basename, dirname, join, resolve } = require('node:path');
|
|
20
27
|
const { homedir, platform } = require('node:os');
|
|
21
28
|
|
|
22
|
-
const { version } = require('../package.json');
|
|
29
|
+
const { name: packageName, version } = require('../package.json');
|
|
30
|
+
|
|
31
|
+
// `codeledger-mcp` bin alias (same file, no extra JS): for MCP registries
|
|
32
|
+
// that cannot pass arguments, it always runs `mcp start` + the user's args.
|
|
33
|
+
// Every other invocation forwards argv unchanged, including a bare launch,
|
|
34
|
+
// so the CLI's own bare-launch MCP detection applies.
|
|
35
|
+
const MCP_ALIAS = /^codeledger-mcp(\.c?js)?$/i.test(basename(process.argv[1] || ''));
|
|
36
|
+
const ARGS = MCP_ALIAS ? ['mcp', 'start', ...process.argv.slice(2)] : process.argv.slice(2);
|
|
23
37
|
|
|
24
38
|
const CACHE_DIR =
|
|
25
39
|
process.env.CODELEDGER_CACHE_DIR ||
|
|
@@ -37,14 +51,51 @@ function die(msg, code = 1) {
|
|
|
37
51
|
process.exit(code);
|
|
38
52
|
}
|
|
39
53
|
|
|
54
|
+
// Repair commands use the installed version, so an old pin cannot trap an upgrade.
|
|
55
|
+
function repairing() {
|
|
56
|
+
const commands = [];
|
|
57
|
+
for (let i = 0; i < ARGS.length && ARGS[i] !== '--'; i++) {
|
|
58
|
+
if (ARGS[i] === '--cwd') { i++; continue; }
|
|
59
|
+
if (!ARGS[i].startsWith('-')) commands.push(ARGS[i]);
|
|
60
|
+
}
|
|
61
|
+
return ['vendor', 'upgrade', 'update', 'init'].includes(commands[0]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function findPinnedRuntime(startDir) {
|
|
65
|
+
if (process.env.CODELEDGER_GLOBAL_VERSION_PROBE === '1' || repairing()) return null;
|
|
66
|
+
let root = resolve(startDir);
|
|
67
|
+
while (true) {
|
|
68
|
+
const pin = resolveRuntimePin(join(root, '.codeledger'), { fs, path, crypto }, compareRuntimeVersions);
|
|
69
|
+
if (pin) {
|
|
70
|
+
const trustDir = process.env.CODELEDGER_RUNTIME_TRUST_DIR || join(homedir(), '.cache', 'codeledger', 'runtime-trust');
|
|
71
|
+
if (isRuntimePinTrusted(root, pin.manifest, { fs, path, crypto }, trustDir)) return { kind: 'standalone', path: pin.path, repoRoot: root };
|
|
72
|
+
process.stderr.write('codeledger: repository runtime has no installed trust receipt; using the global installation. Run the installed codeledger vendor command to install and trust its verified runtime.\n');
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (existsSync(join(root, '.git'))) return null;
|
|
76
|
+
const parent = dirname(root);
|
|
77
|
+
if (parent === root) return null;
|
|
78
|
+
root = parent;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Repo-local fallback: an already-`codeledger init`'d repo vendors its own
|
|
83
|
+
// runtime under .codeledger/bin/. Checked before attempting a network
|
|
84
|
+
// download since it's instant and needs no connectivity — the global cache
|
|
85
|
+
// miss just means this particular machine hasn't installed globally yet,
|
|
86
|
+
// not that the user has no working CodeLedger available to them.
|
|
40
87
|
function findRepoLocalRuntime(startDir) {
|
|
41
88
|
if (process.env.CODELEDGER_GLOBAL_VERSION_PROBE === '1') return null;
|
|
42
89
|
let current = resolve(startDir);
|
|
43
90
|
while (true) {
|
|
91
|
+
if (existsSync(join(current, '.codeledger', 'runtime.json'))) return null;
|
|
92
|
+
// Legacy scripts require an explicit environment-level repository trust decision.
|
|
93
|
+
if (process.env.CODELEDGER_TRUST_LEGACY_RUNTIME !== '1') return null;
|
|
44
94
|
const wrapper = join(current, '.codeledger', 'bin', 'codeledger');
|
|
45
95
|
const standalone = join(current, '.codeledger', 'bin', 'codeledger-standalone.cjs');
|
|
46
96
|
if (existsSync(wrapper)) return { kind: 'wrapper', path: wrapper, repoRoot: current };
|
|
47
97
|
if (existsSync(standalone)) return { kind: 'standalone', path: standalone, repoRoot: current };
|
|
98
|
+
if (existsSync(join(current, '.git'))) return null;
|
|
48
99
|
const parent = dirname(current);
|
|
49
100
|
if (parent === current) return null;
|
|
50
101
|
current = parent;
|
|
@@ -54,20 +105,19 @@ function findRepoLocalRuntime(startDir) {
|
|
|
54
105
|
function runRepoLocal(runtime) {
|
|
55
106
|
process.stderr.write(
|
|
56
107
|
[
|
|
57
|
-
`codeledger:
|
|
108
|
+
`codeledger: using the repository runtime instead of the global v${version} installation.`,
|
|
58
109
|
`codeledger: using repo-local runtime at ${runtime.path}`,
|
|
59
110
|
'',
|
|
60
111
|
].join('\n'),
|
|
61
112
|
);
|
|
62
113
|
const command = runtime.kind === 'standalone' ? process.execPath : runtime.path;
|
|
63
114
|
const args = runtime.kind === 'standalone'
|
|
64
|
-
? [runtime.path, ...
|
|
65
|
-
:
|
|
115
|
+
? [runtime.path, ...ARGS]
|
|
116
|
+
: ARGS;
|
|
66
117
|
const result = spawnSync(command, args, {
|
|
67
118
|
stdio: 'inherit',
|
|
68
|
-
cwd:
|
|
119
|
+
cwd: process.cwd(),
|
|
69
120
|
env: { ...process.env, CODELEDGER_DELEGATED: '1' },
|
|
70
|
-
shell: runtime.kind === 'wrapper',
|
|
71
121
|
});
|
|
72
122
|
if (result.error) {
|
|
73
123
|
die(`failed to spawn repo-local runtime: ${result.error.message}`);
|
|
@@ -75,64 +125,106 @@ function runRepoLocal(runtime) {
|
|
|
75
125
|
process.exit(result.status ?? 1);
|
|
76
126
|
}
|
|
77
127
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
128
|
+
// One-shot, on-demand fetch, used only when the binary isn't cached yet and
|
|
129
|
+
// no repo-local runtime was found. Bounded well below postinstall's own
|
|
130
|
+
// window since a live invocation is blocked on this, not a background
|
|
131
|
+
// install step.
|
|
132
|
+
const ON_DEMAND_DEADLINE_MS = 30_000;
|
|
133
|
+
|
|
134
|
+
function launchDirectory() {
|
|
135
|
+
const args = ARGS;
|
|
136
|
+
let target;
|
|
137
|
+
for (let i = 0; i < args.length && args[i] !== '--'; i++) {
|
|
138
|
+
if (args[i].startsWith('--cwd=')) target = args[i].slice(6);
|
|
139
|
+
else if (args[i] === '--cwd') {
|
|
140
|
+
target = args[++i];
|
|
141
|
+
if (!target || target.startsWith('--')) die('--cwd requires a repository directory.', 2);
|
|
142
|
+
}
|
|
82
143
|
}
|
|
144
|
+
if (target === undefined && (args[0] === 'mcp' || (args.length === 0 && !process.stdin.isTTY))) target = process.env.CODELEDGER_REPO_ROOT;
|
|
145
|
+
if (target !== undefined && !target.trim()) die('Repository target cannot be empty.', 2);
|
|
146
|
+
try {
|
|
147
|
+
const root = realpathSync(resolve(target ?? process.cwd()));
|
|
148
|
+
if (!statSync(root).isDirectory()) throw new Error('Not a directory');
|
|
149
|
+
return root;
|
|
150
|
+
} catch { die('Repository target must be an existing directory. Check --cwd or CODELEDGER_REPO_ROOT.', 2); }
|
|
151
|
+
}
|
|
83
152
|
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
` 4. Re-run the install without --ignore-scripts`,
|
|
120
|
-
` 5. In an initialized repo, use: ./.codeledger/bin/codeledger ready`,
|
|
121
|
-
'',
|
|
122
|
-
'Or download the binary manually from:',
|
|
123
|
-
` https://github.com/codeledgerECF/codeledger/releases/tag/v${version}`,
|
|
124
|
-
].join('\n'),
|
|
153
|
+
async function main() {
|
|
154
|
+
const pinned = findPinnedRuntime(launchDirectory());
|
|
155
|
+
if (pinned) runRepoLocal(pinned);
|
|
156
|
+
if (existsSync(BINARY_PATH)) {
|
|
157
|
+
const problem = require('../scripts/postinstall.js').verifyCachedBinary(BINARY_PATH);
|
|
158
|
+
if (problem) {
|
|
159
|
+
process.stderr.write(`codeledger: cached binary at ${BINARY_PATH} failed verification (${problem}); discarding it and downloading a verified copy.\n`);
|
|
160
|
+
try { fs.unlinkSync(BINARY_PATH); } catch (error) { die(`could not remove the unverified cached binary: ${error.message}. Delete ${BINARY_PATH} and retry.`); }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!existsSync(BINARY_PATH)) {
|
|
164
|
+
const repoLocal = repairing() ? null : findRepoLocalRuntime(launchDirectory());
|
|
165
|
+
if (repoLocal) {
|
|
166
|
+
runRepoLocal(repoLocal);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { arch } = require('node:os');
|
|
170
|
+
const platformKey = `${platform()}-${arch()}`;
|
|
171
|
+
const installerPath = join(__dirname, '..', 'scripts', 'postinstall.js');
|
|
172
|
+
|
|
173
|
+
const { PLATFORM_MAP, downloadAndVerifyBinary, unsupportedPlatformMessage, readInstallFailure, unpublishedAssetsMessage } = require('../scripts/postinstall.js');
|
|
174
|
+
if (!PLATFORM_MAP[platformKey]) {
|
|
175
|
+
// Don't waste time on a download attempt that can only ever fail —
|
|
176
|
+
// getBinaryPaths() would reject this platformKey immediately anyway.
|
|
177
|
+
// See postinstall.js's matching message for why this is "no
|
|
178
|
+
// committed timeline", not "try again later".
|
|
179
|
+
die(unsupportedPlatformMessage(platformKey));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const priorFailure = readInstallFailure();
|
|
183
|
+
if (priorFailure) {
|
|
184
|
+
process.stderr.write(`codeledger: the install step could not fetch the hardened binary (${priorFailure.at}): ${priorFailure.message}\n`);
|
|
185
|
+
}
|
|
186
|
+
process.stderr.write(
|
|
187
|
+
`codeledger: hardened binary for v${version} not cached locally — downloading now (one-time)...\n`,
|
|
125
188
|
);
|
|
189
|
+
try {
|
|
190
|
+
await downloadAndVerifyBinary({ deadlineMs: ON_DEMAND_DEADLINE_MS });
|
|
191
|
+
} catch (err) {
|
|
192
|
+
const unpublished = unpublishedAssetsMessage(err);
|
|
193
|
+
die(
|
|
194
|
+
[
|
|
195
|
+
`hardened binary for v${version} not found at ${BINARY_PATH}, and the on-demand download just failed: ${err.message}`,
|
|
196
|
+
...(unpublished ? ['', unpublished,
|
|
197
|
+
'Retrying or reinstalling this version will not help until they are published.',
|
|
198
|
+
'To use a published version instead, list them with: npm view @codeledger/cli versions'] : [
|
|
199
|
+
'',
|
|
200
|
+
'The postinstall step may have been skipped (e.g. --ignore-scripts).']),
|
|
201
|
+
'Confirm the official scoped package is installed: @codeledger/cli.',
|
|
202
|
+
'Avoid the unrelated unscoped npm package: codeledger.',
|
|
203
|
+
'',
|
|
204
|
+
'Recover with one of:',
|
|
205
|
+
' 1. Retry: codeledger --version',
|
|
206
|
+
` 2. Reinstall this version: npm install -g ${packageName}@${version}`,
|
|
207
|
+
` 3. node "${installerPath}"`,
|
|
208
|
+
' 4. Re-run the install without --ignore-scripts',
|
|
209
|
+
' 5. In an initialized repo, use: ./.codeledger/bin/codeledger ready',
|
|
210
|
+
'',
|
|
211
|
+
`Release page for v${version} (it exists only once that release is published):`,
|
|
212
|
+
` https://github.com/codeledgerECF/codeledger/releases/tag/v${version}`,
|
|
213
|
+
].join('\n'),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
126
216
|
}
|
|
127
|
-
}
|
|
128
217
|
|
|
129
|
-
const result = spawnSync(BINARY_PATH,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
218
|
+
const result = spawnSync(BINARY_PATH, ARGS, {
|
|
219
|
+
stdio: 'inherit',
|
|
220
|
+
env: process.env,
|
|
221
|
+
});
|
|
133
222
|
|
|
134
|
-
if (result.error) {
|
|
135
|
-
|
|
223
|
+
if (result.error) {
|
|
224
|
+
die(`failed to spawn binary: ${result.error.message}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
process.exit(result.status ?? 1);
|
|
136
228
|
}
|
|
137
229
|
|
|
138
|
-
|
|
230
|
+
main().catch((error) => die(error.message));
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contextecf-codeledger",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.82",
|
|
4
4
|
"mcpName": "io.github.codeledgerECF/codeledger",
|
|
5
5
|
"description": "ContextECF CodeLedger — deterministic context selection for AI coding agents. Safe unscoped alias for @codeledger/cli; avoid the unrelated codeledger package.",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"bin": {
|
|
8
|
-
"codeledger": "./bin/codeledger.js"
|
|
8
|
+
"codeledger": "./bin/codeledger.js",
|
|
9
|
+
"codeledger-mcp": "./bin/codeledger.js"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
12
|
"postinstall": "node scripts/postinstall.js"
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/*
|
|
3
|
-
*
|
|
3
|
+
* CodeLedger postinstall — download + verify the hardened binary.
|
|
4
4
|
*
|
|
5
|
-
* Runs automatically on `npm install -g
|
|
5
|
+
* Runs automatically on `npm install -g @codeledger/cli`. Downloads the
|
|
6
6
|
* platform-specific hardened binary from the GitHub Release for this
|
|
7
7
|
* exact version, verifies its SHA256 against SHA256SUMS-v<version>.txt
|
|
8
8
|
* (also downloaded from the release), and writes it to the cache dir.
|
|
9
9
|
*
|
|
10
|
+
* This is best-effort and always exits 0: if the download can't complete
|
|
11
|
+
* within the propagation window (e.g. installing a version whose release
|
|
12
|
+
* assets haven't landed yet, or aren't going to), npm must NOT treat the
|
|
13
|
+
* install as failed — a failed postinstall can roll back an existing
|
|
14
|
+
* working install. bin/codeledger.js retries the same download on-demand,
|
|
15
|
+
* synchronously, the first time the binary is actually needed.
|
|
16
|
+
*
|
|
10
17
|
* Environment variables (all optional):
|
|
11
18
|
* CODELEDGER_SKIP_POSTINSTALL=1 — skip entirely (air-gapped installs)
|
|
19
|
+
* CODELEDGER_RUN_UPGRADE=1 — after the binary download succeeds,
|
|
20
|
+
* also run `codeledger upgrade`
|
|
12
21
|
* CODELEDGER_BINARY_URL=<url> — override the release download base URL
|
|
13
22
|
* (for corporate mirrors / internal CDNs).
|
|
14
23
|
* Must serve the same filenames and a
|
|
15
24
|
* SHA256SUMS-v<version>.txt sibling file.
|
|
16
25
|
* CODELEDGER_CACHE_DIR=<path> — override the cache directory
|
|
17
26
|
* (default: ~/.cache/codeledger/bin)
|
|
27
|
+
* CODELEDGER_INSTALL_DEADLINE_MS=<ms> — override the download propagation
|
|
28
|
+
* window (default: 150000 = 2.5 min)
|
|
18
29
|
*
|
|
19
30
|
* This file contains no engine logic. It only fetches, verifies, and writes
|
|
20
31
|
* bytes. Safe to inspect, safe to mirror, safe to audit.
|
|
@@ -31,113 +42,157 @@ const {
|
|
|
31
42
|
chmodSync,
|
|
32
43
|
existsSync,
|
|
33
44
|
unlinkSync,
|
|
45
|
+
readFileSync,
|
|
46
|
+
statSync,
|
|
34
47
|
} = require('node:fs');
|
|
35
48
|
const { join } = require('node:path');
|
|
36
49
|
const { homedir, platform, arch } = require('node:os');
|
|
37
50
|
|
|
38
51
|
const { version } = require('../package.json');
|
|
39
52
|
|
|
40
|
-
// ───
|
|
53
|
+
// ─── platform / path resolution (pure — no I/O, no process.exit) ──────────
|
|
54
|
+
|
|
55
|
+
// Only these native asset names are wired into this installer.
|
|
56
|
+
const PLATFORM_MAP = {
|
|
57
|
+
'darwin-arm64': 'darwin-arm64',
|
|
58
|
+
'linux-x64': 'linux-x64',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function unsupportedPlatformMessage(platformKey) {
|
|
62
|
+
return [
|
|
63
|
+
`${platformKey} has no pre-built CodeLedger hardened binary.`,
|
|
64
|
+
`Supported native routes: ${Object.keys(PLATFORM_MAP).join(', ')}.`,
|
|
65
|
+
'A successful npm install does not establish a working runtime or MCP connection.',
|
|
66
|
+
'For an already initialized repository with a trusted vendored runtime:',
|
|
67
|
+
' node .codeledger/bin/codeledger-standalone.cjs ready',
|
|
68
|
+
'For a fresh repository, use a supported host or a supported linux-x64 environment.',
|
|
69
|
+
'Skipping postinstall does not bootstrap a missing runtime; codeledger init cannot fix that loop.',
|
|
70
|
+
'Intel Macs cannot run an Apple Silicon binary through Rosetta.',
|
|
71
|
+
'Source-build prerequisites and limitations: https://github.com/codeledgerECF/codeledger/blob/main/docs/security/ENTERPRISE_ASSURANCE.md',
|
|
72
|
+
].join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getPlatformKey() {
|
|
76
|
+
return `${platform()}-${arch()}`;
|
|
77
|
+
}
|
|
41
78
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
79
|
+
function getCacheDir() {
|
|
80
|
+
return (
|
|
81
|
+
process.env.CODELEDGER_CACHE_DIR || join(homedir(), '.cache', 'codeledger', 'bin')
|
|
45
82
|
);
|
|
46
|
-
process.exit(0);
|
|
47
83
|
}
|
|
48
84
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
// Binaries are hosted on the public CodeLedger GitHub Releases page so
|
|
86
|
+
// `npm install -g @codeledger/cli` can fetch them without authentication.
|
|
87
|
+
function getBinaryPaths(platformKey) {
|
|
88
|
+
const platformSuffix = PLATFORM_MAP[platformKey];
|
|
89
|
+
if (!platformSuffix) return null;
|
|
90
|
+
|
|
91
|
+
const releaseBase =
|
|
92
|
+
process.env.CODELEDGER_BINARY_URL ||
|
|
93
|
+
`https://github.com/codeledgerECF/codeledger/releases/download/v${version}`;
|
|
94
|
+
const binaryRemoteName = `codeledger-hardened-${platformSuffix}-v${version}`;
|
|
95
|
+
const cacheDir = getCacheDir();
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
binaryUrl: `${releaseBase}/${binaryRemoteName}`,
|
|
99
|
+
checksumsUrl: `${releaseBase}/SHA256SUMS-v${version}.txt`,
|
|
100
|
+
binaryRemoteName,
|
|
101
|
+
cacheDir,
|
|
102
|
+
binaryLocalPath: join(cacheDir, `codeledger-hardened-v${version}`),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The verified SHA-256 is recorded beside the cached binary so every launch can
|
|
107
|
+
// reject a truncated, replaced, or tampered cache before executing it.
|
|
108
|
+
function receiptPath(binaryPath) {
|
|
109
|
+
return `${binaryPath}.sha256.json`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function writeVerifiedReceipt(binaryPath, sha256) {
|
|
113
|
+
const stat = statSync(binaryPath);
|
|
114
|
+
writeFileSync(receiptPath(binaryPath), JSON.stringify({ schema: 1, version, sha256, size: stat.size }) + '\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Returns null when the cached binary matches its install receipt, else the reason it does not. */
|
|
118
|
+
function verifyCachedBinary(binaryPath) {
|
|
119
|
+
let receipt;
|
|
120
|
+
try {
|
|
121
|
+
receipt = JSON.parse(readFileSync(receiptPath(binaryPath), 'utf8'));
|
|
122
|
+
} catch {
|
|
123
|
+
return 'no install receipt';
|
|
62
124
|
}
|
|
63
|
-
}
|
|
64
|
-
|
|
125
|
+
if (!receipt || receipt.schema !== 1 || receipt.version !== version || !/^[a-f0-9]{64}$/.test(String(receipt.sha256))) return 'invalid install receipt';
|
|
126
|
+
let stat;
|
|
127
|
+
try {
|
|
128
|
+
stat = statSync(binaryPath);
|
|
129
|
+
} catch {
|
|
130
|
+
return 'binary missing';
|
|
131
|
+
}
|
|
132
|
+
if (!stat.isFile() || stat.size === 0) return 'binary is empty or not a file';
|
|
133
|
+
if (stat.size !== receipt.size) return 'binary size changed since install';
|
|
134
|
+
// Hashed on every launch: size or timestamps can be preserved by a same-size replacement.
|
|
135
|
+
const actual = createHash('sha256').update(readFileSync(binaryPath)).digest('hex');
|
|
136
|
+
return actual === receipt.sha256 ? null : 'SHA256 mismatch with install receipt';
|
|
65
137
|
}
|
|
66
138
|
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const PLATFORM_MAP = {
|
|
73
|
-
'darwin-arm64': 'darwin-arm64',
|
|
74
|
-
'linux-x64': 'linux-x64',
|
|
75
|
-
};
|
|
139
|
+
// npm hides postinstall output by default, so a soft failure is recorded where
|
|
140
|
+
// the first `codeledger` run can report it.
|
|
141
|
+
function installFailurePath() {
|
|
142
|
+
return join(getCacheDir(), `install-failure-v${version}.json`);
|
|
143
|
+
}
|
|
76
144
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
console.log(' 4. Download a binary manually from:');
|
|
95
|
-
console.log(` https://github.com/codeledgerECF/codeledger/releases/tag/v${version}`);
|
|
96
|
-
console.log('');
|
|
97
|
-
} else {
|
|
98
|
-
console.error(
|
|
99
|
-
`codeledger: unsupported platform ${platformKey}. Supported: ${Object.keys(
|
|
100
|
-
PLATFORM_MAP,
|
|
101
|
-
).join(', ')}`,
|
|
102
|
-
);
|
|
103
|
-
console.error(
|
|
104
|
-
`codeledger: v${version} does not provide a hardened binary for ${platformKey}.`,
|
|
105
|
-
);
|
|
106
|
-
console.error(
|
|
107
|
-
'codeledger: this installer is for ContextECF CodeLedger; you can still use the published source package or a supported release asset.',
|
|
108
|
-
);
|
|
145
|
+
function recordInstallFailure(err) {
|
|
146
|
+
try {
|
|
147
|
+
mkdirSync(getCacheDir(), { recursive: true });
|
|
148
|
+
writeFileSync(installFailurePath(), JSON.stringify({
|
|
149
|
+
schema: 1, version, at: new Date().toISOString(),
|
|
150
|
+
status: typeof err.statusCode === 'number' ? err.statusCode : null,
|
|
151
|
+
message: String(err.message).split('\n')[0].slice(0, 300),
|
|
152
|
+
}) + '\n');
|
|
153
|
+
} catch { /* Reporting is best effort; the install stays a soft failure. */ }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readInstallFailure() {
|
|
157
|
+
try {
|
|
158
|
+
const record = JSON.parse(readFileSync(installFailurePath(), 'utf8'));
|
|
159
|
+
return record && record.schema === 1 && record.version === version ? record : null;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
109
162
|
}
|
|
110
|
-
// Fail softly: don't block `npm install` for unsupported platforms. The
|
|
111
|
-
// bin shim will tell the user what happened when they first run `codeledger`.
|
|
112
|
-
process.exit(0);
|
|
113
163
|
}
|
|
114
164
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
process.env.CODELEDGER_BINARY_URL ||
|
|
119
|
-
`https://github.com/codeledgerECF/codeledger/releases/download/v${version}`;
|
|
165
|
+
function clearInstallFailure() {
|
|
166
|
+
try { unlinkSync(installFailurePath()); } catch { /* Nothing recorded. */ }
|
|
167
|
+
}
|
|
120
168
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
169
|
+
/** A 404 after the propagation window means this version's release assets are not published. */
|
|
170
|
+
function unpublishedAssetsMessage(err) {
|
|
171
|
+
return err && err.statusCode === 404
|
|
172
|
+
? `The v${version} release assets were not found (HTTP 404): this version's hardened binaries are not published yet, or were withdrawn.`
|
|
173
|
+
: null;
|
|
174
|
+
}
|
|
124
175
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const BINARY_LOCAL_PATH = join(CACHE_DIR, BINARY_LOCAL_NAME);
|
|
176
|
+
function isBinaryCached() {
|
|
177
|
+
const paths = getBinaryPaths(getPlatformKey());
|
|
178
|
+
return Boolean(paths && existsSync(paths.binaryLocalPath) && verifyCachedBinary(paths.binaryLocalPath) === null);
|
|
179
|
+
}
|
|
130
180
|
|
|
131
181
|
// ─── http ────────────────────────────────────────────────────────────────────
|
|
132
182
|
|
|
133
|
-
|
|
183
|
+
const FETCH_TIMEOUT_MS = 60_000;
|
|
184
|
+
|
|
185
|
+
function fetchBufferOnce(url, redirectDepth = 0, expiresAt = Date.now() + FETCH_TIMEOUT_MS) {
|
|
134
186
|
if (redirectDepth > 5) {
|
|
135
187
|
return Promise.reject(new Error(`too many redirects fetching ${url}`));
|
|
136
188
|
}
|
|
137
189
|
return new Promise((resolve, reject) => {
|
|
190
|
+
const remaining = Math.min(FETCH_TIMEOUT_MS, expiresAt - Date.now());
|
|
191
|
+
if (remaining <= 0) { reject(new Error('Binary download deadline exceeded.')); return; }
|
|
138
192
|
const req = https.get(
|
|
139
193
|
url,
|
|
140
194
|
{
|
|
195
|
+
timeout: FETCH_TIMEOUT_MS,
|
|
141
196
|
headers: {
|
|
142
197
|
'User-Agent': `codeledger-installer/${version}`,
|
|
143
198
|
Accept: 'application/octet-stream',
|
|
@@ -153,7 +208,7 @@ function fetchBufferOnce(url, redirectDepth = 0) {
|
|
|
153
208
|
) {
|
|
154
209
|
res.resume();
|
|
155
210
|
const next = new URL(res.headers.location, url).toString();
|
|
156
|
-
fetchBufferOnce(next, redirectDepth + 1).then(resolve, reject);
|
|
211
|
+
fetchBufferOnce(next, redirectDepth + 1, expiresAt).then(resolve, reject);
|
|
157
212
|
return;
|
|
158
213
|
}
|
|
159
214
|
if (res.statusCode !== 200) {
|
|
@@ -169,34 +224,51 @@ function fetchBufferOnce(url, redirectDepth = 0) {
|
|
|
169
224
|
res.on('error', reject);
|
|
170
225
|
},
|
|
171
226
|
);
|
|
227
|
+
const timer = setTimeout(() => {
|
|
228
|
+
const error = new Error('Binary download deadline exceeded.');
|
|
229
|
+
req.destroy(error);
|
|
230
|
+
reject(error);
|
|
231
|
+
}, remaining);
|
|
172
232
|
req.on('error', reject);
|
|
173
|
-
req.
|
|
174
|
-
req.destroy(new Error(`timeout fetching ${url}`));
|
|
175
|
-
});
|
|
233
|
+
req.once('close', () => clearTimeout(timer));
|
|
176
234
|
});
|
|
177
235
|
}
|
|
178
236
|
|
|
237
|
+
// Default propagation window for the postinstall-time attempt. Deliberately
|
|
238
|
+
// a wall-clock deadline, not a fixed attempt count: the question is "how
|
|
239
|
+
// long is it plausible for a just-published release's assets to still be
|
|
240
|
+
// propagating," not "how many times did we try." A version whose GitHub
|
|
241
|
+
// Release will never exist (npm published ahead of a working release — the
|
|
242
|
+
// actual incident this retry policy is designed around) now fails within
|
|
243
|
+
// this window instead of retrying for ~8 minutes that were never going to
|
|
244
|
+
// succeed.
|
|
245
|
+
const DEFAULT_DEADLINE_MS = Number(process.env.CODELEDGER_INSTALL_DEADLINE_MS) || 150_000; // 2.5 minutes
|
|
246
|
+
|
|
179
247
|
// Retry wrapper that tolerates the short propagation window where a freshly
|
|
180
248
|
// published package may be visible before all release assets are available.
|
|
181
|
-
// A 404 is retried with exponential backoff
|
|
182
|
-
|
|
183
|
-
|
|
249
|
+
// A 404 is retried with exponential backoff bounded by deadlineMs;
|
|
250
|
+
// non-404 errors fail fast.
|
|
251
|
+
async function fetchBuffer(url, deadlineMs = DEFAULT_DEADLINE_MS) {
|
|
252
|
+
const start = Date.now();
|
|
253
|
+
let attempt = 0;
|
|
184
254
|
let lastErr;
|
|
185
|
-
for (
|
|
255
|
+
for (;;) {
|
|
186
256
|
try {
|
|
187
|
-
return await fetchBufferOnce(url);
|
|
257
|
+
return await fetchBufferOnce(url, 0, start + deadlineMs);
|
|
188
258
|
} catch (err) {
|
|
189
259
|
lastErr = err;
|
|
190
|
-
// Only retry on 404 — everything else is a hard failure
|
|
260
|
+
// Only retry on 404 — everything else is a hard failure.
|
|
191
261
|
if (err.statusCode !== 404) throw err;
|
|
192
|
-
|
|
193
|
-
const wait =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
262
|
+
const elapsed = Date.now() - start;
|
|
263
|
+
const wait = Math.min(20_000, 5_000 * 2 ** attempt);
|
|
264
|
+
if (elapsed + wait >= deadlineMs) break;
|
|
265
|
+
attempt += 1;
|
|
266
|
+
const remainingSec = Math.round((deadlineMs - elapsed) / 1000);
|
|
267
|
+
console.error(
|
|
268
|
+
`codeledger: Binary pending — retrying in ${Math.round(wait / 1000)}s ` +
|
|
269
|
+
`(${remainingSec}s left in propagation window).`,
|
|
198
270
|
);
|
|
199
|
-
await new Promise((r) => setTimeout(r, wait
|
|
271
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
200
272
|
}
|
|
201
273
|
}
|
|
202
274
|
throw lastErr;
|
|
@@ -221,72 +293,171 @@ function findExpectedSha(checksumsText, filename) {
|
|
|
221
293
|
return null;
|
|
222
294
|
}
|
|
223
295
|
|
|
224
|
-
// ───
|
|
296
|
+
// ─── download + verify (the reusable, side-effect-scoped core) ────────────
|
|
297
|
+
|
|
298
|
+
// Downloads, verifies, and caches the hardened binary for the current
|
|
299
|
+
// platform + this package's version. Throws on failure — does not exit the
|
|
300
|
+
// process. Callers decide what "failure" means for them: postinstall.js
|
|
301
|
+
// soft-fails (exit 0, retry later); bin/codeledger.js's on-demand path
|
|
302
|
+
// falls through to a hard failure message since the user is blocked right
|
|
303
|
+
// now.
|
|
304
|
+
async function downloadAndVerifyBinary({ deadlineMs = DEFAULT_DEADLINE_MS } = {}) {
|
|
305
|
+
const platformKey = getPlatformKey();
|
|
306
|
+
const paths = getBinaryPaths(platformKey);
|
|
307
|
+
if (!paths) {
|
|
308
|
+
throw new Error(`unsupported platform ${platformKey}`);
|
|
309
|
+
}
|
|
225
310
|
|
|
226
|
-
|
|
227
|
-
console.
|
|
228
|
-
console.log(` source: ${BINARY_URL}`);
|
|
311
|
+
console.error(`codeledger: installing v${version} hardened binary for ${platformKey}`);
|
|
312
|
+
console.error(` source: ${paths.binaryUrl}`);
|
|
229
313
|
|
|
230
314
|
const [checksumsBuf, binary] = await Promise.all([
|
|
231
|
-
fetchBuffer(
|
|
232
|
-
fetchBuffer(
|
|
315
|
+
fetchBuffer(paths.checksumsUrl, deadlineMs),
|
|
316
|
+
fetchBuffer(paths.binaryUrl, deadlineMs),
|
|
233
317
|
]);
|
|
234
318
|
const checksums = checksumsBuf.toString('utf8');
|
|
235
319
|
|
|
236
|
-
const expectedSha = findExpectedSha(checksums,
|
|
320
|
+
const expectedSha = findExpectedSha(checksums, paths.binaryRemoteName);
|
|
237
321
|
if (!expectedSha) {
|
|
238
322
|
throw new Error(
|
|
239
|
-
`${
|
|
323
|
+
`${paths.binaryRemoteName} not listed in SHA256SUMS-v${version}.txt — cannot verify binary integrity`,
|
|
240
324
|
);
|
|
241
325
|
}
|
|
242
326
|
|
|
243
327
|
const actualSha = createHash('sha256').update(binary).digest('hex');
|
|
244
328
|
if (actualSha !== expectedSha) {
|
|
245
329
|
throw new Error(
|
|
246
|
-
`SHA256 mismatch for ${
|
|
330
|
+
`SHA256 mismatch for ${paths.binaryRemoteName}:\n expected ${expectedSha}\n got ${actualSha}`,
|
|
247
331
|
);
|
|
248
332
|
}
|
|
249
333
|
|
|
250
|
-
mkdirSync(
|
|
334
|
+
mkdirSync(paths.cacheDir, { recursive: true });
|
|
251
335
|
|
|
252
336
|
// Atomic-ish write: write to tmp, rename
|
|
253
|
-
const tmpPath = `${
|
|
337
|
+
const tmpPath = `${paths.binaryLocalPath}.tmp`;
|
|
254
338
|
if (existsSync(tmpPath)) unlinkSync(tmpPath);
|
|
255
339
|
writeFileSync(tmpPath, binary);
|
|
256
340
|
chmodSync(tmpPath, 0o755);
|
|
257
|
-
if (existsSync(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
console.
|
|
263
|
-
console.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
console.log('codeledger: ready initializes, scans, and prepares ambient task context.');
|
|
341
|
+
if (existsSync(paths.binaryLocalPath)) unlinkSync(paths.binaryLocalPath);
|
|
342
|
+
require('node:fs').renameSync(tmpPath, paths.binaryLocalPath);
|
|
343
|
+
writeVerifiedReceipt(paths.binaryLocalPath, actualSha);
|
|
344
|
+
clearInstallFailure();
|
|
345
|
+
|
|
346
|
+
console.error(`codeledger: installed ${paths.binaryLocalPath}`);
|
|
347
|
+
console.error(` sha256: ${actualSha}`);
|
|
348
|
+
|
|
349
|
+
return paths.binaryLocalPath;
|
|
267
350
|
}
|
|
268
351
|
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
console.error('
|
|
283
|
-
|
|
284
|
-
|
|
352
|
+
// Optional post-download step: run `codeledger upgrade` in the most likely
|
|
353
|
+
// target repo so existing repos pick up refreshed ambient hooks/runtime
|
|
354
|
+
// without a second manual command. Opt-in only — never runs by default.
|
|
355
|
+
function maybeRunUpgrade() {
|
|
356
|
+
if (process.env.CODELEDGER_RUN_UPGRADE === '1') {
|
|
357
|
+
const { spawnSync } = require('node:child_process');
|
|
358
|
+
const path = require('node:path');
|
|
359
|
+
const fs = require('node:fs');
|
|
360
|
+
const codeledgerBin = path.join(__dirname, '..', 'bin', 'codeledger.js');
|
|
361
|
+
const initCwd = process.env['INIT_CWD'];
|
|
362
|
+
const homeCwd = process.env['HOME'] ?? process.env['USERPROFILE'] ?? process.cwd();
|
|
363
|
+
const upgradeCwd =
|
|
364
|
+
initCwd && fs.existsSync(path.join(initCwd, '.codeledger')) ? initCwd : homeCwd;
|
|
365
|
+
console.error('codeledger: CODELEDGER_RUN_UPGRADE=1 set, running upgrade...');
|
|
366
|
+
const upgradeResult = spawnSync(process.execPath, [codeledgerBin, 'upgrade'], {
|
|
367
|
+
stdio: 'inherit',
|
|
368
|
+
cwd: upgradeCwd,
|
|
369
|
+
env: { ...process.env, CODELEDGER_SKIP_AMBIENT_ACTIVATION: '1' },
|
|
370
|
+
});
|
|
371
|
+
if ((upgradeResult.status ?? 0) !== 0) {
|
|
372
|
+
console.error('');
|
|
373
|
+
console.error('codeledger: upgrade step encountered an issue. Run manually:');
|
|
374
|
+
console.error(' codeledger upgrade');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ─── postinstall entry point ───────────────────────────────────────────────
|
|
380
|
+
// All side-effecting decisions (env skip, dev-workspace skip, platform
|
|
381
|
+
// gating, process.exit) live here, gated behind `require.main === module`
|
|
382
|
+
// below — so requiring this file as a library (from bin/codeledger.js, or
|
|
383
|
+
// from tests) never has side effects.
|
|
384
|
+
|
|
385
|
+
async function runPostinstall() {
|
|
386
|
+
if (process.env.CODELEDGER_SKIP_POSTINSTALL === '1') {
|
|
285
387
|
console.error(
|
|
286
|
-
|
|
388
|
+
'codeledger: CODELEDGER_SKIP_POSTINSTALL=1 set, skipping ContextECF CodeLedger hardened binary download.',
|
|
287
389
|
);
|
|
288
|
-
|
|
289
|
-
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Skip when running inside this source workspace (dev mode). The presence
|
|
394
|
+
// of pnpm-workspace.yaml two directories up is a reliable signal that
|
|
395
|
+
// this is not a real end-user install, and the release binary for this
|
|
396
|
+
// version may not exist yet.
|
|
397
|
+
try {
|
|
398
|
+
const path = require('node:path');
|
|
399
|
+
const fs = require('node:fs');
|
|
400
|
+
const workspaceRoot = path.resolve(__dirname, '..', '..', '..');
|
|
401
|
+
if (fs.existsSync(path.join(workspaceRoot, 'pnpm-workspace.yaml'))) {
|
|
402
|
+
console.error(
|
|
403
|
+
'codeledger: in-workspace dev install detected, skipping ContextECF CodeLedger hardened binary download.',
|
|
404
|
+
);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
} catch {
|
|
408
|
+
// fall through to normal install
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const platformKey = getPlatformKey();
|
|
412
|
+
|
|
413
|
+
if (!PLATFORM_MAP[platformKey]) {
|
|
414
|
+
// Soft install failure does not create a fresh runtime on this platform.
|
|
415
|
+
console.error(`codeledger: ${unsupportedPlatformMessage(platformKey)}`);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
await downloadAndVerifyBinary();
|
|
421
|
+
console.error('');
|
|
422
|
+
console.error('codeledger: install complete.');
|
|
423
|
+
console.error(' Next: cd your-repo && codeledger ready');
|
|
424
|
+
console.error(' Existing repos can refresh ambient hooks/runtime with: codeledger upgrade');
|
|
425
|
+
console.error('codeledger: ready initializes, scans, and prepares ambient task context.');
|
|
426
|
+
maybeRunUpgrade();
|
|
427
|
+
} catch (err) {
|
|
428
|
+
// Soft-fail, deliberately: exit 0 either way. A failed postinstall
|
|
429
|
+
// makes npm treat the whole install as failed, which can roll back an
|
|
430
|
+
// existing working install (the exact incident this is designed
|
|
431
|
+
// around). bin/codeledger.js retries this same download on-demand,
|
|
432
|
+
// synchronously, the first time it's actually needed.
|
|
433
|
+
recordInstallFailure(err);
|
|
434
|
+
console.error(`codeledger: could not pre-fetch the hardened binary — ${err.message}`);
|
|
435
|
+
const unpublished = unpublishedAssetsMessage(err);
|
|
436
|
+
if (unpublished) console.error(`codeledger: ${unpublished}`);
|
|
437
|
+
console.error('codeledger: not fatal — it will download automatically on first "codeledger" invocation.');
|
|
438
|
+
console.error('codeledger: or, in an initialized repo, use: ./.codeledger/bin/codeledger ready');
|
|
439
|
+
}
|
|
290
440
|
}
|
|
291
441
|
|
|
292
|
-
|
|
442
|
+
// Only run the postinstall entry point when invoked directly (npm
|
|
443
|
+
// postinstall, node scripts/postinstall.js). When required — from
|
|
444
|
+
// bin/codeledger.js's on-demand download path, or from tests — this file
|
|
445
|
+
// has no side effects; only the exported pure/reusable functions are used.
|
|
446
|
+
if (require.main === module) {
|
|
447
|
+
runPostinstall().then(() => process.exit(0));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
module.exports = {
|
|
451
|
+
findExpectedSha,
|
|
452
|
+
PLATFORM_MAP,
|
|
453
|
+
downloadAndVerifyBinary,
|
|
454
|
+
getPlatformKey,
|
|
455
|
+
getBinaryPaths,
|
|
456
|
+
isBinaryCached,
|
|
457
|
+
unsupportedPlatformMessage,
|
|
458
|
+
verifyCachedBinary,
|
|
459
|
+
writeVerifiedReceipt,
|
|
460
|
+
readInstallFailure,
|
|
461
|
+
recordInstallFailure,
|
|
462
|
+
unpublishedAssetsMessage,
|
|
463
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* Generated by scripts/generate-runtime-resolver.mjs. Public installation contract only. */
|
|
2
|
+
'use strict';
|
|
3
|
+
module.exports = (() => {
|
|
4
|
+
/** Strict SemVer 2.0, shared by pin validation and explicit installation. */
|
|
5
|
+
function parseRuntimeVersion(value) {
|
|
6
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(value);
|
|
7
|
+
if (!match || match[0] !== value) throw new Error(`Invalid runtime version: ${value}. Use an exact semantic version.`);
|
|
8
|
+
const core = match.slice(1, 4).map(Number);
|
|
9
|
+
const prerelease = match[4]?.split('.') ?? [];
|
|
10
|
+
if (core.some((n) => !Number.isSafeInteger(n)) || prerelease.some((p) => /^\d+$/.test(p) && (p.length > 1 && p.startsWith('0')))) {
|
|
11
|
+
throw new Error(`Invalid runtime version: ${value}`);
|
|
12
|
+
}
|
|
13
|
+
return { core, prerelease };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function compareRuntimeVersions(left, right) {
|
|
17
|
+
const a = parseRuntimeVersion(left);
|
|
18
|
+
const b = parseRuntimeVersion(right);
|
|
19
|
+
for (let i = 0; i < 3; i++) {
|
|
20
|
+
if (a.core[i] !== b.core[i]) return a.core[i] < b.core[i] ? -1 : 1;
|
|
21
|
+
}
|
|
22
|
+
if (!a.prerelease.length || !b.prerelease.length) {
|
|
23
|
+
return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length ? -1 : 1;
|
|
24
|
+
}
|
|
25
|
+
for (let i = 0; i < Math.max(a.prerelease.length, b.prerelease.length); i++) {
|
|
26
|
+
const x = a.prerelease[i];
|
|
27
|
+
const y = b.prerelease[i];
|
|
28
|
+
if (x === y) continue;
|
|
29
|
+
if (x === undefined || y === undefined) return x === undefined ? -1 : 1;
|
|
30
|
+
const xn = /^\d+$/.test(x);
|
|
31
|
+
const yn = /^\d+$/.test(y);
|
|
32
|
+
if (xn && yn) return x.length !== y.length ? (x.length < y.length ? -1 : 1) : x < y ? -1 : 1;
|
|
33
|
+
if (xn !== yn) return xn ? -1 : 1;
|
|
34
|
+
return x < y ? -1 : 1;
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runtimeGenerationSha256(bytes, notices, crypto) {
|
|
40
|
+
return crypto.createHash('sha256').update(String(bytes.length) + ':').update(bytes).update(notices === null ? '-1:' : String(notices.length) + ':').update(notices || Buffer.alloc(0)).digest('hex');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveRuntimePin(
|
|
44
|
+
codeledgerDir,
|
|
45
|
+
io,
|
|
46
|
+
compare,
|
|
47
|
+
) {
|
|
48
|
+
const manifestPath = io.path.join(codeledgerDir, 'runtime.json');
|
|
49
|
+
if (!io.fs.existsSync(manifestPath)) return null;
|
|
50
|
+
if (io.fs.lstatSync(codeledgerDir).isSymbolicLink() || io.fs.lstatSync(manifestPath).isSymbolicLink()) throw new Error('Runtime state must not be symlinked. Run codeledger vendor from the owning repository.');
|
|
51
|
+
if (io.fs.statSync(manifestPath).size > 65536) throw new Error('runtime.json exceeds its 64 KiB contract bound. Repair with codeledger vendor.');
|
|
52
|
+
let raw;
|
|
53
|
+
try { raw = JSON.parse(io.fs.readFileSync(manifestPath, 'utf8')); } catch (error) { throw new Error('.codeledger/runtime.json is not valid JSON (' + error.message + '). Restore it from version control, or repair with codeledger vendor.'); }
|
|
54
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('Invalid runtime.json: expected an object. Repair with codeledger vendor.');
|
|
55
|
+
const m = raw;
|
|
56
|
+
if (m.schema_version !== 'codeledger/runtime/v1' || typeof m.installation_id !== 'string' || typeof m.version !== 'string' || typeof m.min_runtime !== 'string'
|
|
57
|
+
|| typeof m.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(m.sha256)
|
|
58
|
+
|| (m.distribution !== 'full' && m.distribution !== 'open') || !['fail-open-v1', 'legacy'].includes(m.hook_contract)
|
|
59
|
+
|| !m.source || typeof m.source.kind !== 'string' || typeof m.source.location !== 'string'
|
|
60
|
+
|| !m.build || m.build.content_sha256 !== m.sha256 || (m.build.revision !== null && typeof m.build.revision !== 'string')
|
|
61
|
+
|| (m.build.source_dirty !== undefined && m.build.source_dirty !== null && typeof m.build.source_dirty !== 'boolean')
|
|
62
|
+
|| (m.build.generation_sha256 === undefined && m.build.legal_sha256 !== undefined)
|
|
63
|
+
|| (m.build.generation_sha256 !== undefined && (typeof m.build.generation_sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(m.build.generation_sha256) || (m.build.legal_sha256 !== null && !/^[a-f0-9]{64}$/.test(m.build.legal_sha256))))
|
|
64
|
+
|| typeof m.entrypoint !== 'string' || m.entrypoint !== `bin/runtime/${m.version}-${m.build.generation_sha256 || m.sha256}/codeledger-standalone.cjs`) {
|
|
65
|
+
throw new Error('Invalid runtime.json contract. Repair with codeledger vendor.');
|
|
66
|
+
}
|
|
67
|
+
for (const reference of [m.previous, m.migration_fallback]) {
|
|
68
|
+
if (reference === undefined) continue;
|
|
69
|
+
if (!reference || typeof reference !== 'object' || !/^[a-f0-9]{64}$/.test(reference.sha256) || typeof reference.entrypoint !== 'string') throw new Error('Invalid previous runtime reference. Repair with codeledger vendor.');
|
|
70
|
+
if (reference.version !== null) compare(reference.version, reference.version);
|
|
71
|
+
const generation = reference.generation_sha256 || reference.sha256;
|
|
72
|
+
if (!/^[a-f0-9]{64}$/.test(generation) || (reference.entrypoint !== `bin/runtime/legacy-${reference.sha256}/codeledger-standalone.cjs` && (reference.version === null || reference.entrypoint !== `bin/runtime/${reference.version}-${generation}/codeledger-standalone.cjs`))) throw new Error('Invalid previous runtime path. Repair with codeledger vendor.');
|
|
73
|
+
}
|
|
74
|
+
if (compare(m.version, m.min_runtime) < 0) throw new Error('Pinned runtime is older than its configuration contract. Run codeledger upgrade.');
|
|
75
|
+
if (compare(m.version, '0.10.82-0') >= 0 && m.build.generation_sha256 === undefined) throw new Error('Runtime generation identity is missing. Repair with codeledger vendor.');
|
|
76
|
+
const root = io.fs.realpathSync(codeledgerDir);
|
|
77
|
+
if (!io.fs.existsSync(io.path.resolve(root, m.entrypoint))) throw new Error('Pinned runtime is missing: .codeledger/' + m.entrypoint + '. Restore it from version control (git checkout -- .codeledger/bin), or repair with codeledger vendor.');
|
|
78
|
+
const runtimePath = io.fs.realpathSync(io.path.resolve(root, m.entrypoint));
|
|
79
|
+
const rel = io.path.relative(root, runtimePath);
|
|
80
|
+
if (rel.startsWith('..') || io.path.isAbsolute(rel) || runtimePath !== io.path.resolve(root, m.entrypoint) || !io.fs.statSync(runtimePath).isFile()) {
|
|
81
|
+
throw new Error('Pinned runtime escapes the repository. Repair with codeledger vendor.');
|
|
82
|
+
}
|
|
83
|
+
const bytes = io.fs.readFileSync(runtimePath);
|
|
84
|
+
if (io.crypto.createHash('sha256').update(bytes).digest('hex') !== m.sha256) {
|
|
85
|
+
throw new Error('Pinned runtime checksum mismatch. Repair with codeledger vendor; do not execute the modified artifact.');
|
|
86
|
+
}
|
|
87
|
+
if (m.build.generation_sha256 !== undefined) {
|
|
88
|
+
let notices = null;
|
|
89
|
+
if (m.build.legal_sha256 !== null) {
|
|
90
|
+
const legal = runtimePath + '.LEGAL.txt';
|
|
91
|
+
if (!io.fs.existsSync(legal)) throw new Error('Pinned notices are missing: ' + legal + '. Restore them from version control, or repair with codeledger vendor.');
|
|
92
|
+
if (io.fs.lstatSync(legal).isSymbolicLink()) throw new Error('Pinned notices must not be symlinked. Repair with codeledger vendor.');
|
|
93
|
+
notices = io.fs.readFileSync(legal);
|
|
94
|
+
if (io.crypto.createHash('sha256').update(notices).digest('hex') !== m.build.legal_sha256) throw new Error('Pinned notices checksum mismatch. Repair with codeledger vendor.');
|
|
95
|
+
}
|
|
96
|
+
if (runtimeGenerationSha256(bytes, notices, io.crypto) !== m.build.generation_sha256) throw new Error('Pinned generation checksum mismatch. Repair with codeledger vendor.');
|
|
97
|
+
}
|
|
98
|
+
const header = bytes.subarray(0, 65536).toString('utf8');
|
|
99
|
+
const version = /^\/\* CODELEDGER_STANDALONE_VERSION:([^\s*]+) \*\//m.exec(header)?.[1];
|
|
100
|
+
const flavor = header.includes('/* CodeLedger Open CLI') ? 'open' : header.includes('/* CodeLedger Standalone CLI') ? 'full' : null;
|
|
101
|
+
if (version !== m.version || flavor !== m.distribution) throw new Error('Pinned runtime version or distribution does not match runtime.json.');
|
|
102
|
+
return { manifest: m, path: runtimePath };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
function runtimeTrustKey(root, manifest, io) {
|
|
107
|
+
return io.crypto.createHash('sha256').update(JSON.stringify([io.fs.realpathSync(root), manifest])).digest('hex');
|
|
108
|
+
}
|
|
109
|
+
function isRuntimePinTrusted(root, manifest, io, trustDir) {
|
|
110
|
+
if (!trustDir || trustDir === 'disabled') return false;
|
|
111
|
+
try {
|
|
112
|
+
const directory = io.fs.realpathSync(trustDir);
|
|
113
|
+
const repo = io.fs.realpathSync(root);
|
|
114
|
+
const relative = io.path.relative(repo, directory);
|
|
115
|
+
if (!relative || (!relative.startsWith('..' + io.path.sep) && relative !== '..' && !io.path.isAbsolute(relative))) return false;
|
|
116
|
+
const key = runtimeTrustKey(root, manifest, io);
|
|
117
|
+
const file = io.path.join(directory, key + '.json');
|
|
118
|
+
const stat = io.fs.lstatSync(file);
|
|
119
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4096) return false;
|
|
120
|
+
const receipt = JSON.parse(io.fs.readFileSync(file, 'utf8'));
|
|
121
|
+
return receipt.schema === 1 && receipt.key === key;
|
|
122
|
+
} catch { return false; }
|
|
123
|
+
}
|
|
124
|
+
return {parseRuntimeVersion, compareRuntimeVersions, resolveRuntimePin, runtimeGenerationSha256, runtimeTrustKey, isRuntimePinTrusted};
|
|
125
|
+
})();
|