enigma-memory 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +367 -379
- package/apps/cli/bin/enigma.mjs +138 -0
- package/package.json +3 -1
- package/packages/mcp-server/src/index.js +1 -1
- package/scripts/verify-registry-install.mjs +405 -0
package/apps/cli/bin/enigma.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
const DEFAULT_BUNDLE = '.enigma/bundle.json';
|
|
30
30
|
export const DEFAULT_RELAY_PORT = 8787;
|
|
31
31
|
export const DEFAULT_GATEWAY_PORT = 8797;
|
|
32
|
+
const DEFAULT_QUICKSTART_MEMORY = 'Enigma quickstart demo memory: local proof bundles can be created and verified without provider or cloud credentials.';
|
|
32
33
|
const PACKAGE_JSON_URL = new URL('../../../package.json', import.meta.url);
|
|
33
34
|
const SPECS_URL = new URL('../../../specs/', import.meta.url);
|
|
34
35
|
const IMPORTERS = Object.freeze({
|
|
@@ -145,6 +146,51 @@ async function fileExists(path) {
|
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
function pathFlag(flags, names, fallback) {
|
|
150
|
+
const value = getFlag(flags, names, fallback);
|
|
151
|
+
if (value === true || value === '') throw new Error(`Missing required --${names[0]}.`);
|
|
152
|
+
return String(value);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function quickstartPathDisplay(outDirInput, name) {
|
|
156
|
+
const base = String(outDirInput);
|
|
157
|
+
if (base === '' || base === '.') return name;
|
|
158
|
+
return `${base.replace(/[\\/]+$/, '')}/${name}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function ensureDistinctOutputPaths(paths) {
|
|
162
|
+
const normalized = paths.map((path) => (process.platform === 'win32' ? path.toLowerCase() : path));
|
|
163
|
+
if (new Set(normalized).size !== paths.length) {
|
|
164
|
+
throw new Error('Quickstart output paths must be distinct.');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function assertCanWriteQuickstartOutputs(outputs, overwrite) {
|
|
169
|
+
if (overwrite) return;
|
|
170
|
+
const existing = [];
|
|
171
|
+
for (const output of outputs) {
|
|
172
|
+
if (await fileExists(output.path)) existing.push(output.display);
|
|
173
|
+
}
|
|
174
|
+
if (existing.length > 0) {
|
|
175
|
+
throw new Error(`Quickstart output already exists: ${existing.join(', ')}. Pass --overwrite to replace it.`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function quickstartMemoryTextFromFlags(flags) {
|
|
180
|
+
const inlineText = getFlag(flags, ['memory-text', 'memoryText']);
|
|
181
|
+
const textFile = getFlag(flags, ['memory-file', 'memoryFile', 'text-file', 'textFile']);
|
|
182
|
+
if (inlineText !== undefined && textFile !== undefined) throw new Error('Use either --memory-text or --memory-file, not both.');
|
|
183
|
+
if (inlineText !== undefined) {
|
|
184
|
+
if (inlineText === true || inlineText === '') throw new Error('Missing required --memory-text.');
|
|
185
|
+
return String(inlineText);
|
|
186
|
+
}
|
|
187
|
+
if (textFile !== undefined) {
|
|
188
|
+
if (textFile === true || textFile === '') throw new Error('Missing required --memory-file.');
|
|
189
|
+
return readFile(resolve(String(textFile)), 'utf8');
|
|
190
|
+
}
|
|
191
|
+
return DEFAULT_QUICKSTART_MEMORY;
|
|
192
|
+
}
|
|
193
|
+
|
|
148
194
|
async function readPackageJson() {
|
|
149
195
|
return readJson(PACKAGE_JSON_URL);
|
|
150
196
|
}
|
|
@@ -361,6 +407,87 @@ async function initCommand(flags, io) {
|
|
|
361
407
|
return 0;
|
|
362
408
|
}
|
|
363
409
|
|
|
410
|
+
export async function quickstartCommand(flags, io) {
|
|
411
|
+
const bundleInput = pathFlag(flags, ['bundle', 'file'], DEFAULT_BUNDLE);
|
|
412
|
+
const outDirInput = pathFlag(flags, ['out-dir', 'outDir'], dirname(bundleInput));
|
|
413
|
+
const bundlePath = resolve(bundleInput);
|
|
414
|
+
const outDirPath = resolve(outDirInput);
|
|
415
|
+
const contextPackPath = resolve(outDirPath, 'context-pack.json');
|
|
416
|
+
const exportPath = resolve(outDirPath, 'export.json');
|
|
417
|
+
const verifyReportPath = resolve(outDirPath, 'verify-report.json');
|
|
418
|
+
const contextPackDisplay = quickstartPathDisplay(outDirInput, 'context-pack.json');
|
|
419
|
+
const exportDisplay = quickstartPathDisplay(outDirInput, 'export.json');
|
|
420
|
+
const verifyReportDisplay = quickstartPathDisplay(outDirInput, 'verify-report.json');
|
|
421
|
+
const outputs = [
|
|
422
|
+
{ path: bundlePath, display: bundleInput },
|
|
423
|
+
{ path: contextPackPath, display: contextPackDisplay },
|
|
424
|
+
{ path: exportPath, display: exportDisplay },
|
|
425
|
+
{ path: verifyReportPath, display: verifyReportDisplay },
|
|
426
|
+
];
|
|
427
|
+
ensureDistinctOutputPaths(outputs.map((output) => output.path));
|
|
428
|
+
const overwrite = getFlag(flags, ['overwrite'], false) === true || getFlag(flags, ['overwrite'], false) === 'true';
|
|
429
|
+
await assertCanWriteQuickstartOutputs(outputs, overwrite);
|
|
430
|
+
|
|
431
|
+
const vault = createVault({
|
|
432
|
+
subjectId: String(getFlag(flags, ['subject', 'subject-id'], 'local-user')),
|
|
433
|
+
displayName: String(getFlag(flags, ['display-name', 'name'], 'Local user')),
|
|
434
|
+
passphrase: String(getFlag(flags, ['passphrase'], 'local-development-passphrase')),
|
|
435
|
+
});
|
|
436
|
+
const passport = createPassport({
|
|
437
|
+
vault,
|
|
438
|
+
subjectId: vault.subject_id,
|
|
439
|
+
displayName: String(getFlag(flags, ['display-name', 'name'], 'Local user')),
|
|
440
|
+
});
|
|
441
|
+
remember({
|
|
442
|
+
vault,
|
|
443
|
+
passport,
|
|
444
|
+
text: await quickstartMemoryTextFromFlags(flags),
|
|
445
|
+
purpose: 'quickstart_local_proof',
|
|
446
|
+
purpose_tags: ['quickstart'],
|
|
447
|
+
metadata: { source: 'enigma quickstart' },
|
|
448
|
+
});
|
|
449
|
+
const contextPack = compileContextPack({
|
|
450
|
+
vault,
|
|
451
|
+
passport,
|
|
452
|
+
query: '',
|
|
453
|
+
purpose: 'quickstart_local_context',
|
|
454
|
+
limit: 8,
|
|
455
|
+
});
|
|
456
|
+
const exported = exportBundle({ vault, includePlaintext: false });
|
|
457
|
+
const bundle = exported.bundle ?? exported;
|
|
458
|
+
const verifyReport = verifyBundle(bundle);
|
|
459
|
+
|
|
460
|
+
await writeJson(bundlePath, bundle);
|
|
461
|
+
await writeJson(contextPackPath, contextPack);
|
|
462
|
+
await writeJson(exportPath, bundle);
|
|
463
|
+
await writeJson(verifyReportPath, verifyReport);
|
|
464
|
+
|
|
465
|
+
print({
|
|
466
|
+
ok: verifyReport.ok === true,
|
|
467
|
+
bundle: bundleInput,
|
|
468
|
+
context_pack: contextPackDisplay,
|
|
469
|
+
export: exportDisplay,
|
|
470
|
+
verify_report: verifyReportDisplay,
|
|
471
|
+
memory_count: Array.isArray(bundle.memory_objects) ? bundle.memory_objects.length : 0,
|
|
472
|
+
receipt_count: Array.isArray(bundle.receipts) ? bundle.receipts.length : 0,
|
|
473
|
+
context_item_count: Array.isArray(contextPack.memories) ? contextPack.memories.length : 0,
|
|
474
|
+
verify_ok: verifyReport.ok === true,
|
|
475
|
+
next_commands: [
|
|
476
|
+
`enigma verify --export ${exportDisplay}`,
|
|
477
|
+
`enigma connect generic-mcp --bundle ${bundleInput} --dry-run`,
|
|
478
|
+
],
|
|
479
|
+
claim_boundaries: {
|
|
480
|
+
local_only: true,
|
|
481
|
+
provider_credentials_required: false,
|
|
482
|
+
provider_deletion_proof: false,
|
|
483
|
+
model_forgetting_proof: false,
|
|
484
|
+
roi_or_savings_guarantee: false,
|
|
485
|
+
compliance_certification: false,
|
|
486
|
+
},
|
|
487
|
+
}, io);
|
|
488
|
+
return verifyReport.ok === true ? 0 : 1;
|
|
489
|
+
}
|
|
490
|
+
|
|
364
491
|
async function rememberCommand(flags, io) {
|
|
365
492
|
const bundlePath = resolve(String(getFlag(flags, ['bundle', 'file'], DEFAULT_BUNDLE)));
|
|
366
493
|
const { vault, passport } = await loadState(bundlePath);
|
|
@@ -946,6 +1073,7 @@ function usage() {
|
|
|
946
1073
|
usage: 'enigma <command> [options]',
|
|
947
1074
|
commands: [
|
|
948
1075
|
'init',
|
|
1076
|
+
'quickstart',
|
|
949
1077
|
'doctor',
|
|
950
1078
|
'install',
|
|
951
1079
|
'connect <client>',
|
|
@@ -990,6 +1118,15 @@ function usage() {
|
|
|
990
1118
|
'--text <text>': 'Inline local memory text. Avoid for private content because argv can be logged by process tooling.',
|
|
991
1119
|
'--text-file <path>': 'Read local memory text from a file so private smoke input is not exposed in shell argv. Aliases: --memory-file, --textFile, --memoryFile.',
|
|
992
1120
|
},
|
|
1121
|
+
quickstart_options: {
|
|
1122
|
+
'--bundle <path>': 'Bundle JSON to create. Defaults to .enigma/bundle.json.',
|
|
1123
|
+
'--out-dir <path>': 'Directory for context-pack.json, export.json, and verify-report.json. Defaults to the bundle directory.',
|
|
1124
|
+
'--subject <id>': 'Local subject id. Defaults to local-user.',
|
|
1125
|
+
'--display-name <name>': 'Local display name. Defaults to Local user.',
|
|
1126
|
+
'--memory-file <path>': 'Read local memory text from a file. Alias: --text-file.',
|
|
1127
|
+
'--memory-text <text>': 'Inline demo memory text for non-private demos only.',
|
|
1128
|
+
'--overwrite': 'Replace existing quickstart output files.',
|
|
1129
|
+
},
|
|
993
1130
|
native_host: {
|
|
994
1131
|
bin: 'enigma-native-host',
|
|
995
1132
|
host_name: 'com.enigma.native_host',
|
|
@@ -1064,6 +1201,7 @@ export async function main(argv = process.argv.slice(2), io = { stdout: process.
|
|
|
1064
1201
|
}
|
|
1065
1202
|
try {
|
|
1066
1203
|
if (command === 'init') return await initCommand(flags, io);
|
|
1204
|
+
if (command === 'quickstart') return await quickstartCommand(flags, io);
|
|
1067
1205
|
if (command === 'doctor') return await doctorCommand(flags, io);
|
|
1068
1206
|
if (command === 'install') return await installCommand(flags, io);
|
|
1069
1207
|
if (command === 'connect') return await connectCommand(subcommand, flags, io);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enigma-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Provider-agnostic AI memory passport and offline-verifiable proof layer.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"deploy/kubernetes/enigma-backend.example.yaml",
|
|
57
57
|
"scripts/check.mjs",
|
|
58
58
|
"scripts/install-enigma-local.mjs",
|
|
59
|
+
"scripts/verify-registry-install.mjs",
|
|
59
60
|
"scripts/build-production-readiness-manifest.mjs",
|
|
60
61
|
"scripts/build-production-backend-env-kit.mjs",
|
|
61
62
|
"scripts/cloudflare-secret-env.mjs",
|
|
@@ -116,6 +117,7 @@
|
|
|
116
117
|
"check": "node scripts/check.mjs",
|
|
117
118
|
"test": "node --test test/*.test.mjs",
|
|
118
119
|
"install:local": "node scripts/install-enigma-local.mjs",
|
|
120
|
+
"registry:verify": "node scripts/verify-registry-install.mjs",
|
|
119
121
|
"boundary": "node harnesses/boundary-sim/boundary_harness.mjs",
|
|
120
122
|
"release:audit": "node scripts/release-audit.mjs",
|
|
121
123
|
"provenance:local": "node scripts/release-provenance.mjs",
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
const DEFAULT_BUNDLE = '.enigma/bundle.json';
|
|
18
18
|
const JSONRPC_VERSION = '2.0';
|
|
19
19
|
const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
20
|
-
const SERVER_INFO = Object.freeze({ name: 'enigma-mcp-server', version: '0.1.
|
|
20
|
+
const SERVER_INFO = Object.freeze({ name: 'enigma-mcp-server', version: '0.1.1' });
|
|
21
21
|
const JSON_RPC_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
22
22
|
const JSON_RPC_ERROR = Object.freeze({
|
|
23
23
|
INVALID_REQUEST: -32600,
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { mkdir, mkdtemp } from 'node:fs/promises';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, isAbsolute, join, resolve as resolvePath } from 'node:path';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
export const REGISTRY_INSTALL_SCHEMA = 'enigma.registry_install_verifier.v1';
|
|
11
|
+
export const REQUIRED_NODE_MAJOR = 24;
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
15
|
+
const PACKAGE_DIR = resolvePath(dirname(SCRIPT_PATH), '..');
|
|
16
|
+
const PACKAGE_JSON = JSON.parse(readFileSync(resolvePath(PACKAGE_DIR, 'package.json'), 'utf8'));
|
|
17
|
+
const PACKAGE_BINS = Object.freeze({ ...PACKAGE_JSON.bin });
|
|
18
|
+
const PACKAGE_NAME_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u;
|
|
19
|
+
const PACKAGE_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
|
|
20
|
+
const PACKAGE_SPEC_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_REGISTRY_PACKAGE = PACKAGE_JSON.name ?? 'enigma-memory';
|
|
23
|
+
export const DEFAULT_REGISTRY_VERSION = PACKAGE_JSON.version;
|
|
24
|
+
export const REGISTRY_INSTALL_CHECKS = Object.freeze([
|
|
25
|
+
Object.freeze({ step: 'check_enigma_help', bin: 'enigma', args: Object.freeze(['--help']) }),
|
|
26
|
+
Object.freeze({ step: 'check_enigma_doctor', bin: 'enigma', args: Object.freeze(['doctor']) }),
|
|
27
|
+
Object.freeze({ step: 'check_enigma_relay_demo', bin: 'enigma-relay', args: Object.freeze(['demo']) }),
|
|
28
|
+
Object.freeze({ step: 'check_enigma_gateway_demo', bin: 'enigma-gateway', args: Object.freeze(['demo']) }),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function readRequiredValue(argv, index, flag) {
|
|
32
|
+
const value = argv[index + 1];
|
|
33
|
+
if (value === undefined || value.startsWith('--')) throw new Error(`Missing value for ${flag}.`);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function commandForPlatform(base, platform) {
|
|
38
|
+
return platform === 'win32' ? `${base}.cmd` : base;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rejectControlCharacters(value, label) {
|
|
42
|
+
const text = String(value ?? '');
|
|
43
|
+
if (text.length === 0) throw new Error(`${label} must not be empty.`);
|
|
44
|
+
if (text.includes('\0') || text.includes('\n') || text.includes('\r')) throw new Error(`${label} contains an invalid control character.`);
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function usage() {
|
|
49
|
+
return `Usage: node scripts/verify-registry-install.mjs [--execute] [--package <name>] [--version <version>] [--tmp-dir <path>] [--skip-network]\n\nDry-run is the default and prints the public-safe planned command set. Execute mode installs the package into a temporary prefix and runs selected installed bin files directly. --skip-network validates the plan without running npm install or installed bins.\n`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseRegistryInstallArgs(argv = process.argv.slice(2)) {
|
|
53
|
+
const options = {
|
|
54
|
+
dryRun: true,
|
|
55
|
+
execute: false,
|
|
56
|
+
packageName: DEFAULT_REGISTRY_PACKAGE,
|
|
57
|
+
packageVersion: DEFAULT_REGISTRY_VERSION,
|
|
58
|
+
tmpDir: undefined,
|
|
59
|
+
skipNetwork: false,
|
|
60
|
+
};
|
|
61
|
+
let sawDryRun = false;
|
|
62
|
+
let sawExecute = false;
|
|
63
|
+
|
|
64
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
65
|
+
const arg = argv[index];
|
|
66
|
+
if (arg === '--help' || arg === '-h') {
|
|
67
|
+
options.help = true;
|
|
68
|
+
} else if (arg === '--dry-run') {
|
|
69
|
+
sawDryRun = true;
|
|
70
|
+
options.dryRun = true;
|
|
71
|
+
options.execute = false;
|
|
72
|
+
} else if (arg === '--execute') {
|
|
73
|
+
sawExecute = true;
|
|
74
|
+
options.dryRun = false;
|
|
75
|
+
options.execute = true;
|
|
76
|
+
} else if (arg === '--package') {
|
|
77
|
+
options.packageName = readRequiredValue(argv, index, arg);
|
|
78
|
+
index += 1;
|
|
79
|
+
} else if (arg === '--version') {
|
|
80
|
+
options.packageVersion = readRequiredValue(argv, index, arg);
|
|
81
|
+
index += 1;
|
|
82
|
+
} else if (arg === '--tmp-dir') {
|
|
83
|
+
options.tmpDir = readRequiredValue(argv, index, arg);
|
|
84
|
+
index += 1;
|
|
85
|
+
} else if (arg === '--skip-network') {
|
|
86
|
+
options.skipNetwork = true;
|
|
87
|
+
} else {
|
|
88
|
+
throw new Error('Unknown argument.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (sawDryRun && sawExecute) throw new Error('Use either --dry-run or --execute, not both.');
|
|
93
|
+
validatePackageName(options.packageName);
|
|
94
|
+
validatePackageVersion(options.packageVersion);
|
|
95
|
+
if (options.tmpDir !== undefined) rejectControlCharacters(options.tmpDir, 'Temporary directory');
|
|
96
|
+
return options;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function nodeMajor(version) {
|
|
100
|
+
const match = String(version ?? '').trim().match(/^v?(\d+)(?:\.\d+){0,2}(?:[-+].*)?$/u);
|
|
101
|
+
if (!match) throw new Error('Invalid Node version override.');
|
|
102
|
+
return Number(match[1]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function validateNodeVersion(version = process.versions.node, requiredMajor = REQUIRED_NODE_MAJOR) {
|
|
106
|
+
const major = nodeMajor(version);
|
|
107
|
+
if (!Number.isSafeInteger(major) || major < requiredMajor) {
|
|
108
|
+
throw new Error(`Node >=${requiredMajor} is required for the registry install verifier.`);
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, required: `>=${requiredMajor}`, current_major: major };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function validatePackageName(name) {
|
|
114
|
+
const value = rejectControlCharacters(name, 'Package name');
|
|
115
|
+
if (!PACKAGE_NAME_RE.test(value)) throw new Error('Package name must be a deterministic npm package name.');
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function validatePackageVersion(version) {
|
|
120
|
+
const value = rejectControlCharacters(version, 'Package version');
|
|
121
|
+
if (!PACKAGE_VERSION_RE.test(value)) throw new Error('Package version must be an exact semver version.');
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function packageSpec(packageName, packageVersion) {
|
|
126
|
+
return `${validatePackageName(packageName)}@${validatePackageVersion(packageVersion)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function packageInstallDirectory(prefix, packageName) {
|
|
130
|
+
const parts = packageName.startsWith('@') ? packageName.split('/') : [packageName];
|
|
131
|
+
return join(prefix, 'node_modules', ...parts);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeSeparators(value) {
|
|
135
|
+
return String(value).replace(/\\/gu, '/');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function replaceAllPathForms(value, from, to) {
|
|
139
|
+
if (!from || from.includes('<')) return value;
|
|
140
|
+
const normalizedFrom = normalizeSeparators(from);
|
|
141
|
+
const normalizedValue = normalizeSeparators(value);
|
|
142
|
+
if (normalizedValue === normalizedFrom) return to;
|
|
143
|
+
if (normalizedValue.startsWith(`${normalizedFrom}/`)) return `${to}${normalizedValue.slice(normalizedFrom.length)}`;
|
|
144
|
+
return normalizedValue.split(normalizedFrom).join(to);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function redactPublicPath(value, replacements = []) {
|
|
148
|
+
let redacted = String(value ?? '');
|
|
149
|
+
const sorted = [...replacements]
|
|
150
|
+
.filter((entry) => entry && typeof entry.path === 'string' && entry.path.length > 0)
|
|
151
|
+
.sort((left, right) => right.path.length - left.path.length);
|
|
152
|
+
for (const entry of sorted) redacted = replaceAllPathForms(redacted, entry.path, entry.label);
|
|
153
|
+
redacted = redacted.replace(/[A-Za-z]:[\\/][^\s"'\])},]+/gu, '<absolute-path>');
|
|
154
|
+
redacted = redacted.replace(/(^|[\s"'(\[{:,])\/(?:[^/\s"'\])},]+\/)+[^/\s"'\])},]+/gu, '$1<absolute-path>');
|
|
155
|
+
if (redacted.startsWith('/')) return '<absolute-path>';
|
|
156
|
+
if (redacted.startsWith('\\\\')) return '<absolute-path>';
|
|
157
|
+
return redacted;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function publicCommandFor(command) {
|
|
161
|
+
if (command.kind === 'npm_install') {
|
|
162
|
+
return {
|
|
163
|
+
command: 'npm',
|
|
164
|
+
args: ['install', '--prefix', '<temp-prefix>', command.package_spec],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
command: command.bin,
|
|
169
|
+
args: [...command.bin_args],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function npmInstallCommand(platform, nodeCommand, runtime, prefix, spec) {
|
|
174
|
+
const npmCliPath = runtime.npmCliPath ?? process.env.npm_execpath;
|
|
175
|
+
if (platform === 'win32' && typeof npmCliPath === 'string' && npmCliPath.length > 0) {
|
|
176
|
+
return {
|
|
177
|
+
command: nodeCommand,
|
|
178
|
+
args: [npmCliPath, 'install', '--prefix', prefix, spec],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
command: commandForPlatform('npm', platform),
|
|
183
|
+
args: ['install', '--prefix', prefix, spec],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function commandBasename(command) {
|
|
188
|
+
const normalized = normalizeSeparators(command);
|
|
189
|
+
return normalized.slice(normalized.lastIndexOf('/') + 1).toLowerCase();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function buildRegistryInstallPlan(options = {}, runtime = {}) {
|
|
193
|
+
const platform = runtime.platform ?? process.platform;
|
|
194
|
+
const cwd = runtime.cwd ?? process.cwd();
|
|
195
|
+
const packageName = validatePackageName(options.packageName ?? DEFAULT_REGISTRY_PACKAGE);
|
|
196
|
+
const packageVersion = validatePackageVersion(options.packageVersion ?? DEFAULT_REGISTRY_VERSION);
|
|
197
|
+
const spec = packageSpec(packageName, packageVersion);
|
|
198
|
+
const requestedTmpDir = options.tmpDir === undefined ? undefined : rejectControlCharacters(options.tmpDir, 'Temporary directory');
|
|
199
|
+
const prefix = requestedTmpDir === undefined ? (runtime.tmpDir ?? '<temp-prefix>') : (isAbsolute(requestedTmpDir) ? resolvePath(requestedTmpDir) : resolvePath(cwd, requestedTmpDir));
|
|
200
|
+
const installedPackageDir = packageInstallDirectory(prefix, packageName);
|
|
201
|
+
const nodeCommand = String(runtime.nodeCommand ?? process.execPath);
|
|
202
|
+
const installCommand = npmInstallCommand(platform, nodeCommand, runtime, prefix, spec);
|
|
203
|
+
const commands = [
|
|
204
|
+
{
|
|
205
|
+
step: 'install_package',
|
|
206
|
+
kind: 'npm_install',
|
|
207
|
+
command: installCommand.command,
|
|
208
|
+
args: installCommand.args,
|
|
209
|
+
package_spec: spec,
|
|
210
|
+
network: true,
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
for (const check of REGISTRY_INSTALL_CHECKS) {
|
|
215
|
+
const relativeBinPath = PACKAGE_BINS[check.bin];
|
|
216
|
+
if (!relativeBinPath) throw new Error(`Package bin ${check.bin} is not declared.`);
|
|
217
|
+
commands.push({
|
|
218
|
+
step: check.step,
|
|
219
|
+
kind: 'bin_check',
|
|
220
|
+
bin: check.bin,
|
|
221
|
+
command: nodeCommand,
|
|
222
|
+
args: [join(installedPackageDir, relativeBinPath), ...check.args],
|
|
223
|
+
bin_args: [...check.args],
|
|
224
|
+
network: false,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const plan = {
|
|
229
|
+
packageName,
|
|
230
|
+
packageVersion,
|
|
231
|
+
packageSpec: spec,
|
|
232
|
+
prefix,
|
|
233
|
+
prefixKind: requestedTmpDir === undefined ? 'temporary' : (isAbsolute(requestedTmpDir) ? 'absolute' : 'relative'),
|
|
234
|
+
execute: options.execute === true,
|
|
235
|
+
dryRun: options.execute === true ? false : true,
|
|
236
|
+
skipNetwork: options.skipNetwork === true,
|
|
237
|
+
commands,
|
|
238
|
+
};
|
|
239
|
+
validateRegistryInstallPlan(plan);
|
|
240
|
+
return plan;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function validateCommandSpec(spec) {
|
|
244
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) throw new Error('Registry verifier command spec must be an object.');
|
|
245
|
+
const command = rejectControlCharacters(spec.command, 'Command name');
|
|
246
|
+
const args = Array.isArray(spec.args) ? spec.args.map(String) : [];
|
|
247
|
+
for (const [index, arg] of args.entries()) rejectControlCharacters(arg, `Command argument ${index}`);
|
|
248
|
+
|
|
249
|
+
if (spec.kind === 'npm_install') {
|
|
250
|
+
const directNpmInstall = (command === 'npm' || command === 'npm.cmd')
|
|
251
|
+
&& args.length === 4
|
|
252
|
+
&& args[0] === 'install'
|
|
253
|
+
&& args[1] === '--prefix'
|
|
254
|
+
&& PACKAGE_SPEC_RE.test(args[3]);
|
|
255
|
+
const nodeNpmCliInstall = (commandBasename(command) === 'node' || commandBasename(command) === 'node.exe')
|
|
256
|
+
&& args.length === 5
|
|
257
|
+
&& args[1] === 'install'
|
|
258
|
+
&& args[2] === '--prefix'
|
|
259
|
+
&& PACKAGE_SPEC_RE.test(args[4]);
|
|
260
|
+
if (!directNpmInstall && !nodeNpmCliInstall) {
|
|
261
|
+
throw new Error('Registry install command must be exactly npm install --prefix <tmp> <package>@<version>.');
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (spec.kind === 'bin_check') {
|
|
267
|
+
if (!REGISTRY_INSTALL_CHECKS.some((check) => check.step === spec.step && check.bin === spec.bin)) throw new Error('Registry bin check is not allowlisted.');
|
|
268
|
+
if (args.length < 1) throw new Error('Registry bin check must run an installed bin file.');
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
throw new Error('Registry verifier command is not allowlisted.');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function validateRegistryInstallPlan(plan) {
|
|
276
|
+
if (!plan || typeof plan !== 'object' || Array.isArray(plan)) throw new Error('Registry install plan must be an object.');
|
|
277
|
+
if (plan.commands?.length !== REGISTRY_INSTALL_CHECKS.length + 1) throw new Error('Registry install plan has an unexpected command count.');
|
|
278
|
+
if (plan.commands[0]?.step !== 'install_package' || plan.commands[0]?.kind !== 'npm_install') throw new Error('Registry install plan must begin with package installation.');
|
|
279
|
+
for (const [index, check] of REGISTRY_INSTALL_CHECKS.entries()) {
|
|
280
|
+
const command = plan.commands[index + 1];
|
|
281
|
+
if (command?.step !== check.step || command?.kind !== 'bin_check' || command?.bin !== check.bin) {
|
|
282
|
+
throw new Error('Registry install plan has an unexpected bin check order.');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const command of plan.commands) validateCommandSpec(command);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function statusForCommand(plan, result) {
|
|
290
|
+
if (plan.skipNetwork) return 'validated';
|
|
291
|
+
if (!plan.execute) return 'preview';
|
|
292
|
+
if (!result) return 'skipped';
|
|
293
|
+
return result.ok === true ? 'passed' : 'failed';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function summarizeRegistryInstallResult(plan, commandResults = [], node = validateNodeVersion()) {
|
|
297
|
+
const resultByStep = new Map(commandResults.map((result) => [result.step, result]));
|
|
298
|
+
const commands = plan.commands.map((command) => {
|
|
299
|
+
const publicSpec = publicCommandFor(command);
|
|
300
|
+
const result = resultByStep.get(command.step);
|
|
301
|
+
const record = {
|
|
302
|
+
step: command.step,
|
|
303
|
+
command: publicSpec.command,
|
|
304
|
+
args: publicSpec.args,
|
|
305
|
+
status: statusForCommand(plan, result),
|
|
306
|
+
network: command.network === true,
|
|
307
|
+
};
|
|
308
|
+
if (result?.exitCode !== undefined) record.exit_code = result.exitCode;
|
|
309
|
+
return record;
|
|
310
|
+
});
|
|
311
|
+
const expectedRuns = plan.execute && !plan.skipNetwork ? plan.commands.length : 0;
|
|
312
|
+
const executedAll = expectedRuns === 0 || commandResults.length === expectedRuns;
|
|
313
|
+
const commandOk = commandResults.every((result) => result.ok === true);
|
|
314
|
+
return {
|
|
315
|
+
schema: REGISTRY_INSTALL_SCHEMA,
|
|
316
|
+
ok: plan.skipNetwork ? true : (!plan.execute || (executedAll && commandOk)),
|
|
317
|
+
mode: plan.skipNetwork ? 'skip-network' : (plan.execute ? 'execute' : 'dry-run'),
|
|
318
|
+
dry_run: plan.dryRun,
|
|
319
|
+
execute: plan.execute,
|
|
320
|
+
skip_network: plan.skipNetwork,
|
|
321
|
+
public_safe: true,
|
|
322
|
+
node,
|
|
323
|
+
package: {
|
|
324
|
+
name: plan.packageName,
|
|
325
|
+
version: plan.packageVersion,
|
|
326
|
+
spec: plan.packageSpec,
|
|
327
|
+
},
|
|
328
|
+
install_prefix: '<temp-prefix>',
|
|
329
|
+
commands,
|
|
330
|
+
safety: {
|
|
331
|
+
default_dry_run: true,
|
|
332
|
+
requires_execute_for_mutation: true,
|
|
333
|
+
skip_network_runs_no_commands: plan.skipNetwork,
|
|
334
|
+
shell: false,
|
|
335
|
+
prints_local_absolute_paths: false,
|
|
336
|
+
prints_response_bodies: false,
|
|
337
|
+
prints_raw_memory: false,
|
|
338
|
+
prints_secrets: false,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function runCommand(command, execFileImpl) {
|
|
344
|
+
try {
|
|
345
|
+
await execFileImpl(command.command, command.args, { windowsHide: true });
|
|
346
|
+
return { step: command.step, ok: true, exitCode: 0 };
|
|
347
|
+
} catch (error) {
|
|
348
|
+
return {
|
|
349
|
+
step: command.step,
|
|
350
|
+
ok: false,
|
|
351
|
+
exitCode: Number.isInteger(error?.code) ? error.code : 1,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export async function runRegistryInstallVerification(argv = process.argv.slice(2), runtime = {}) {
|
|
357
|
+
const parsedOptions = Array.isArray(argv) ? parseRegistryInstallArgs(argv) : { ...argv };
|
|
358
|
+
const node = validateNodeVersion(parsedOptions.nodeVersion ?? runtime.nodeVersion ?? process.versions.node);
|
|
359
|
+
const needsTempPrefix = parsedOptions.execute === true && parsedOptions.skipNetwork !== true && parsedOptions.tmpDir === undefined;
|
|
360
|
+
const options = { ...parsedOptions };
|
|
361
|
+
|
|
362
|
+
if (needsTempPrefix) {
|
|
363
|
+
const mkdtempImpl = runtime.mkdtempImpl ?? mkdtemp;
|
|
364
|
+
const tmpRoot = runtime.tmpRoot ?? tmpdir();
|
|
365
|
+
options.tmpDir = await mkdtempImpl(join(tmpRoot, 'enigma-registry-install-'));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const plan = buildRegistryInstallPlan(options, runtime);
|
|
369
|
+
if (plan.skipNetwork || !plan.execute) return summarizeRegistryInstallResult(plan, [], node);
|
|
370
|
+
|
|
371
|
+
const mkdirImpl = runtime.mkdirImpl ?? mkdir;
|
|
372
|
+
await mkdirImpl(plan.prefix, { recursive: true });
|
|
373
|
+
const execFileImpl = runtime.execFileImpl ?? execFileAsync;
|
|
374
|
+
const commandResults = [];
|
|
375
|
+
for (const command of plan.commands) {
|
|
376
|
+
const result = await runCommand(command, execFileImpl);
|
|
377
|
+
commandResults.push(result);
|
|
378
|
+
if (result.ok !== true) break;
|
|
379
|
+
}
|
|
380
|
+
return summarizeRegistryInstallResult(plan, commandResults, node);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function safeErrorMessage(error) {
|
|
384
|
+
const message = String(error?.message ?? error);
|
|
385
|
+
if (/^(Missing value|Unknown argument|Use either|Invalid Node version|Node >=|Package name|Package version|Temporary directory|Registry verifier|Registry install|Registry bin|Package bin)/u.test(message)) return message;
|
|
386
|
+
return 'Registry install verification failed.';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function main() {
|
|
390
|
+
const args = parseRegistryInstallArgs();
|
|
391
|
+
if (args.help) {
|
|
392
|
+
process.stdout.write(usage());
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const output = await runRegistryInstallVerification(args);
|
|
396
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
|
397
|
+
if (output.ok !== true) process.exitCode = 1;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === resolvePath(process.argv[1])) {
|
|
401
|
+
main().catch((error) => {
|
|
402
|
+
process.stdout.write(`${JSON.stringify({ schema: REGISTRY_INSTALL_SCHEMA, ok: false, public_safe: true, error: { code: 'REGISTRY_INSTALL_VERIFICATION_FAILED', message: safeErrorMessage(error) } }, null, 2)}\n`);
|
|
403
|
+
process.exitCode = 1;
|
|
404
|
+
});
|
|
405
|
+
}
|