@topogram/cli 0.3.39 → 0.3.41
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/package.json +1 -1
- package/src/cli.js +31 -7
- package/src/new-project.js +51 -0
- package/src/template-trust.js +109 -17
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
getTemplateTrustStatus,
|
|
33
33
|
implementationRequiresTrust,
|
|
34
34
|
TEMPLATE_TRUST_FILE,
|
|
35
|
+
templateTrustRecoveryGuidance,
|
|
35
36
|
validateProjectImplementationTrust,
|
|
36
37
|
writeTemplateTrustRecord
|
|
37
38
|
} from "./template-trust.js";
|
|
@@ -5951,6 +5952,15 @@ function diagnosticForTemplateCreateFailure(message, templateSpec, step) {
|
|
|
5951
5952
|
step
|
|
5952
5953
|
});
|
|
5953
5954
|
}
|
|
5955
|
+
if (message.includes("unsupported symlink")) {
|
|
5956
|
+
return templateCheckDiagnostic({
|
|
5957
|
+
code: "template_symlink_unsupported",
|
|
5958
|
+
message,
|
|
5959
|
+
path: path.isAbsolute(templateSpec) ? templateSpec : null,
|
|
5960
|
+
suggestedFix: "Replace template symlinks with real files or directories, then rerun `topogram new` or `topogram template check`.",
|
|
5961
|
+
step
|
|
5962
|
+
});
|
|
5963
|
+
}
|
|
5954
5964
|
return templateCheckDiagnostic({
|
|
5955
5965
|
code: "template_create_failed",
|
|
5956
5966
|
message,
|
|
@@ -5968,13 +5978,15 @@ function diagnosticForTemplateCreateFailure(message, templateSpec, step) {
|
|
|
5968
5978
|
*/
|
|
5969
5979
|
function diagnosticForStarterCheckFailure(error, step, configPath) {
|
|
5970
5980
|
const locFile = typeof error?.loc?.file === "string" ? error.loc.file : null;
|
|
5971
|
-
const isTrust = error.message.includes(TEMPLATE_TRUST_FILE)
|
|
5981
|
+
const isTrust = error.message.includes(TEMPLATE_TRUST_FILE) ||
|
|
5982
|
+
error.message.includes("unsupported symlink") ||
|
|
5983
|
+
error.message.includes("must be under implementation/");
|
|
5972
5984
|
return templateCheckDiagnostic({
|
|
5973
5985
|
code: isTrust ? "template_trust_invalid" : "starter_check_failed",
|
|
5974
5986
|
message: error.message,
|
|
5975
5987
|
path: locFile || configPath,
|
|
5976
5988
|
suggestedFix: isTrust
|
|
5977
|
-
?
|
|
5989
|
+
? templateTrustRecoveryGuidance(error.message)
|
|
5978
5990
|
: "Fix the generated Topogram source or topogram.project.json so topogram check passes.",
|
|
5979
5991
|
step
|
|
5980
5992
|
});
|
|
@@ -6170,13 +6182,13 @@ function buildTemplateCheckPayload(templateSpec) {
|
|
|
6170
6182
|
configDir: projectConfigInfo.configDir
|
|
6171
6183
|
}
|
|
6172
6184
|
: null;
|
|
6173
|
-
if (implementationInfo && implementationRequiresTrust(implementationInfo)) {
|
|
6185
|
+
if (implementationInfo && implementationRequiresTrust(implementationInfo, projectConfigInfo.config)) {
|
|
6174
6186
|
const trustStatus = getTemplateTrustStatus(implementationInfo, projectConfigInfo.config);
|
|
6175
6187
|
const trustDiagnostics = trustStatus.issues.map((issue) => templateCheckDiagnostic({
|
|
6176
6188
|
code: "template_trust_invalid",
|
|
6177
6189
|
message: issue,
|
|
6178
6190
|
path: trustStatus.trustPath,
|
|
6179
|
-
suggestedFix:
|
|
6191
|
+
suggestedFix: templateTrustRecoveryGuidance(issue),
|
|
6180
6192
|
step: "executable-implementation-trust"
|
|
6181
6193
|
}));
|
|
6182
6194
|
steps.push(templateCheckStep("executable-implementation-trust", trustStatus.ok, {
|
|
@@ -7796,7 +7808,7 @@ try {
|
|
|
7796
7808
|
configPath: projectConfigInfo.configPath,
|
|
7797
7809
|
configDir: projectConfigInfo.configDir
|
|
7798
7810
|
};
|
|
7799
|
-
if (implementationRequiresTrust(implementationInfo)) {
|
|
7811
|
+
if (implementationRequiresTrust(implementationInfo, projectConfigInfo.config)) {
|
|
7800
7812
|
const trustRecord = writeTemplateTrustRecord(projectConfigInfo.configDir, projectConfigInfo.config);
|
|
7801
7813
|
console.log(`Wrote ${TEMPLATE_TRUST_FILE} for ${trustRecord.implementation.module}.`);
|
|
7802
7814
|
if (trustRecord.template.id) {
|
|
@@ -7853,7 +7865,10 @@ try {
|
|
|
7853
7865
|
console.log(`Removed: ${filePath}`);
|
|
7854
7866
|
}
|
|
7855
7867
|
if (!status.ok) {
|
|
7856
|
-
|
|
7868
|
+
const guidance = templateTrustRecoveryGuidance(status.issues);
|
|
7869
|
+
if (guidance) {
|
|
7870
|
+
console.log(guidance);
|
|
7871
|
+
}
|
|
7857
7872
|
}
|
|
7858
7873
|
}
|
|
7859
7874
|
process.exit(status.ok ? 0 : 1);
|
|
@@ -7882,6 +7897,12 @@ try {
|
|
|
7882
7897
|
for (const issue of diff.status.issues) {
|
|
7883
7898
|
console.log(`Issue: ${issue}`);
|
|
7884
7899
|
}
|
|
7900
|
+
if (!diff.ok) {
|
|
7901
|
+
const guidance = templateTrustRecoveryGuidance(diff.status.issues);
|
|
7902
|
+
if (guidance) {
|
|
7903
|
+
console.log(guidance);
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7885
7906
|
} else {
|
|
7886
7907
|
console.log(diff.ok ? "Template trust diff: no implementation changes." : "Template trust diff: review required");
|
|
7887
7908
|
for (const file of diff.files) {
|
|
@@ -7906,7 +7927,10 @@ try {
|
|
|
7906
7927
|
}
|
|
7907
7928
|
if (!diff.ok) {
|
|
7908
7929
|
console.log("");
|
|
7909
|
-
|
|
7930
|
+
const guidance = templateTrustRecoveryGuidance(diff.status.issues);
|
|
7931
|
+
if (guidance) {
|
|
7932
|
+
console.log(guidance);
|
|
7933
|
+
}
|
|
7910
7934
|
}
|
|
7911
7935
|
}
|
|
7912
7936
|
process.exit(diff.ok ? 0 : 1);
|
package/src/new-project.js
CHANGED
|
@@ -32,6 +32,15 @@ const SURFACE_ORDER = new Map([
|
|
|
32
32
|
["native", 40]
|
|
33
33
|
]);
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} templateId
|
|
37
|
+
* @param {string} relativePath
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
function unsupportedTemplateSymlinkMessage(templateId, relativePath) {
|
|
41
|
+
return `Template '${templateId}' contains unsupported symlink '${relativePath}'. Template packs must copy real files because Topogram records hashes for copied topogram/ and implementation/ content; symlinks can point outside the trusted template root. Replace the symlink with a real file or directory before running topogram new or topogram template check.`;
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
/**
|
|
36
45
|
* @typedef {Object} CreateNewProjectOptions
|
|
37
46
|
* @property {string} targetPath
|
|
@@ -331,6 +340,34 @@ function readTemplateManifest(templateRoot) {
|
|
|
331
340
|
return validateTemplateManifest(JSON.parse(fs.readFileSync(manifestPath, "utf8")));
|
|
332
341
|
}
|
|
333
342
|
|
|
343
|
+
/**
|
|
344
|
+
* @param {string} root
|
|
345
|
+
* @param {string} currentDir
|
|
346
|
+
* @param {string} label
|
|
347
|
+
* @param {string} templateId
|
|
348
|
+
* @returns {void}
|
|
349
|
+
*/
|
|
350
|
+
function assertTemplateTreeHasNoSymlinks(root, currentDir, label, templateId) {
|
|
351
|
+
const rootStat = fs.lstatSync(currentDir);
|
|
352
|
+
const relativeRoot = path.relative(root, currentDir).replace(/\\/g, "/") || label;
|
|
353
|
+
if (rootStat.isSymbolicLink()) {
|
|
354
|
+
throw new Error(unsupportedTemplateSymlinkMessage(templateId, relativeRoot));
|
|
355
|
+
}
|
|
356
|
+
if (!rootStat.isDirectory()) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
360
|
+
const entryPath = path.join(currentDir, entry.name);
|
|
361
|
+
const relativePath = path.relative(root, entryPath).replace(/\\/g, "/");
|
|
362
|
+
if (entry.isSymbolicLink()) {
|
|
363
|
+
throw new Error(unsupportedTemplateSymlinkMessage(templateId, relativePath));
|
|
364
|
+
}
|
|
365
|
+
if (entry.isDirectory()) {
|
|
366
|
+
assertTemplateTreeHasNoSymlinks(root, entryPath, label, templateId);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
334
371
|
/**
|
|
335
372
|
* @param {string} templateRoot
|
|
336
373
|
* @returns {TemplateManifest}
|
|
@@ -339,19 +376,30 @@ function validateTemplateRoot(templateRoot) {
|
|
|
339
376
|
const manifest = readTemplateManifest(templateRoot);
|
|
340
377
|
const topogramRoot = path.join(templateRoot, "topogram");
|
|
341
378
|
const projectConfigPath = path.join(templateRoot, "topogram.project.json");
|
|
379
|
+
if (fs.existsSync(topogramRoot) && fs.lstatSync(topogramRoot).isSymbolicLink()) {
|
|
380
|
+
throw new Error(unsupportedTemplateSymlinkMessage(manifest.id, "topogram"));
|
|
381
|
+
}
|
|
382
|
+
if (fs.existsSync(projectConfigPath) && fs.lstatSync(projectConfigPath).isSymbolicLink()) {
|
|
383
|
+
throw new Error(unsupportedTemplateSymlinkMessage(manifest.id, "topogram.project.json"));
|
|
384
|
+
}
|
|
342
385
|
if (!fs.existsSync(topogramRoot) || !fs.statSync(topogramRoot).isDirectory()) {
|
|
343
386
|
throw new Error(`Template '${manifest.id}' is missing topogram/.`);
|
|
344
387
|
}
|
|
345
388
|
if (!fs.existsSync(projectConfigPath) || !fs.statSync(projectConfigPath).isFile()) {
|
|
346
389
|
throw new Error(`Template '${manifest.id}' is missing topogram.project.json.`);
|
|
347
390
|
}
|
|
391
|
+
assertTemplateTreeHasNoSymlinks(templateRoot, topogramRoot, "topogram", manifest.id);
|
|
348
392
|
if (manifest.includesExecutableImplementation) {
|
|
349
393
|
const implementationRoot = path.join(templateRoot, "implementation");
|
|
394
|
+
if (fs.existsSync(implementationRoot) && fs.lstatSync(implementationRoot).isSymbolicLink()) {
|
|
395
|
+
throw new Error(unsupportedTemplateSymlinkMessage(manifest.id, "implementation"));
|
|
396
|
+
}
|
|
350
397
|
if (!fs.existsSync(implementationRoot) || !fs.statSync(implementationRoot).isDirectory()) {
|
|
351
398
|
throw new Error(
|
|
352
399
|
`Template '${manifest.id}' declares executable implementation code but is missing implementation/.`
|
|
353
400
|
);
|
|
354
401
|
}
|
|
402
|
+
assertTemplateTreeHasNoSymlinks(templateRoot, implementationRoot, "implementation", manifest.id);
|
|
355
403
|
} else {
|
|
356
404
|
const implementationRoot = path.join(templateRoot, "implementation");
|
|
357
405
|
if (fs.existsSync(implementationRoot) && fs.statSync(implementationRoot).isDirectory()) {
|
|
@@ -1129,6 +1177,9 @@ function collectFiles(root, currentDir, files) {
|
|
|
1129
1177
|
continue;
|
|
1130
1178
|
}
|
|
1131
1179
|
const entryPath = path.join(currentDir, entry.name);
|
|
1180
|
+
if (entry.isSymbolicLink()) {
|
|
1181
|
+
throw new Error(`Template-owned files cannot include symlink '${path.relative(root, entryPath).replace(/\\/g, "/")}'. Template-owned files must be real files so Topogram can hash the exact content being trusted. Replace the symlink with a real file, then run topogram trust status, topogram trust diff, and topogram trust template after review.`);
|
|
1182
|
+
}
|
|
1132
1183
|
if (entry.isDirectory()) {
|
|
1133
1184
|
collectFiles(root, entryPath, files);
|
|
1134
1185
|
continue;
|
package/src/template-trust.js
CHANGED
|
@@ -19,6 +19,7 @@ export const TEMPLATE_TRUST_POLICY = "topogram-template-executable-implementatio
|
|
|
19
19
|
|
|
20
20
|
const IGNORED_IMPLEMENTATION_ENTRIES = new Set([".DS_Store", "node_modules", ".tmp"]);
|
|
21
21
|
const MAX_TEXT_DIFF_BYTES = 256 * 1024;
|
|
22
|
+
const TRUST_REVIEW_COMMANDS = "`topogram trust status`, `topogram trust diff`, and `topogram trust template`";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* @param {string} parent
|
|
@@ -46,6 +47,45 @@ function normalizeRelativePath(value) {
|
|
|
46
47
|
return value.replace(/\\/g, "/");
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} relativePath
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function unsupportedImplementationSymlinkMessage(relativePath) {
|
|
55
|
+
return `Template implementation contains unsupported symlink '${relativePath}'. Implementation trust hashes real files under implementation/; symlinks can point outside the trusted root. Replace symlinks with real files under implementation/, then run ${TRUST_REVIEW_COMMANDS} after review.`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} modulePath
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
function implementationOutsideRootMessage(modulePath) {
|
|
63
|
+
return `Template implementation module '${modulePath}' must be under implementation/ for template-attached projects. Keep executable template code inside implementation/ so the trust record covers what topogram generate may load. Move the module back under implementation/, then run ${TRUST_REVIEW_COMMANDS} after review.`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string|string[]} issueOrIssues
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
export function templateTrustRecoveryGuidance(issueOrIssues) {
|
|
71
|
+
const issues = Array.isArray(issueOrIssues) ? issueOrIssues : [issueOrIssues];
|
|
72
|
+
const text = issues.join("\n");
|
|
73
|
+
if (issues.length > 0 && issues.every((issue) =>
|
|
74
|
+
issue.includes("topogram trust status") &&
|
|
75
|
+
issue.includes("topogram trust diff") &&
|
|
76
|
+
issue.includes("topogram trust template")
|
|
77
|
+
)) {
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
if (text.includes("unsupported symlink")) {
|
|
81
|
+
return `Replace symlinks with real files under implementation/, then run ${TRUST_REVIEW_COMMANDS} after review.`;
|
|
82
|
+
}
|
|
83
|
+
if (text.includes("must be under implementation/")) {
|
|
84
|
+
return `Keep executable template code under implementation/ so it can be hashed and trusted; move the module back under implementation/, then run ${TRUST_REVIEW_COMMANDS} after review.`;
|
|
85
|
+
}
|
|
86
|
+
return `Run \`topogram trust status\` and \`topogram trust diff\` to review implementation changes; after review, run \`topogram trust template\` to trust the current files.`;
|
|
87
|
+
}
|
|
88
|
+
|
|
49
89
|
/**
|
|
50
90
|
* @param {string} value
|
|
51
91
|
* @returns {string}
|
|
@@ -185,12 +225,16 @@ function collectImplementationFiles(implementationRoot, currentDir, files) {
|
|
|
185
225
|
continue;
|
|
186
226
|
}
|
|
187
227
|
const entryPath = path.join(currentDir, entry.name);
|
|
228
|
+
const relativePath = normalizeRelativePath(path.relative(implementationRoot, entryPath));
|
|
229
|
+
if (entry.isSymbolicLink()) {
|
|
230
|
+
throw new Error(unsupportedImplementationSymlinkMessage(relativePath));
|
|
231
|
+
}
|
|
188
232
|
if (entry.isDirectory()) {
|
|
189
233
|
collectImplementationFiles(implementationRoot, entryPath, files);
|
|
190
234
|
continue;
|
|
191
235
|
}
|
|
192
236
|
if (entry.isFile()) {
|
|
193
|
-
files.push(
|
|
237
|
+
files.push(relativePath);
|
|
194
238
|
}
|
|
195
239
|
}
|
|
196
240
|
}
|
|
@@ -250,17 +294,38 @@ export function implementationTrustFingerprint(config) {
|
|
|
250
294
|
};
|
|
251
295
|
}
|
|
252
296
|
|
|
297
|
+
/**
|
|
298
|
+
* @param {{ config: Record<string, any>, configDir: string }} implementationInfo
|
|
299
|
+
* @param {Record<string, any>|null} [projectConfig]
|
|
300
|
+
* @returns {boolean}
|
|
301
|
+
*/
|
|
302
|
+
export function implementationRequiresTrust(implementationInfo, projectConfig = null) {
|
|
303
|
+
const fingerprint = implementationTrustFingerprint(implementationInfo.config);
|
|
304
|
+
const modulePath = path.resolve(implementationInfo.configDir, fingerprint.module);
|
|
305
|
+
const implementationRoot = path.resolve(implementationInfo.configDir, "implementation");
|
|
306
|
+
return isSameOrInside(implementationRoot, modulePath) || projectHasTemplateAttachment(projectConfig);
|
|
307
|
+
}
|
|
308
|
+
|
|
253
309
|
/**
|
|
254
310
|
* @param {{ config: Record<string, any>, configDir: string }} implementationInfo
|
|
255
311
|
* @returns {boolean}
|
|
256
312
|
*/
|
|
257
|
-
|
|
313
|
+
function implementationModuleIsUnderRoot(implementationInfo) {
|
|
258
314
|
const fingerprint = implementationTrustFingerprint(implementationInfo.config);
|
|
259
315
|
const modulePath = path.resolve(implementationInfo.configDir, fingerprint.module);
|
|
260
316
|
const implementationRoot = path.resolve(implementationInfo.configDir, "implementation");
|
|
261
317
|
return isSameOrInside(implementationRoot, modulePath);
|
|
262
318
|
}
|
|
263
319
|
|
|
320
|
+
/**
|
|
321
|
+
* @param {Record<string, any>|null} projectConfig
|
|
322
|
+
* @returns {boolean}
|
|
323
|
+
*/
|
|
324
|
+
function projectHasTemplateAttachment(projectConfig) {
|
|
325
|
+
const template = projectConfig?.template || null;
|
|
326
|
+
return Boolean(template?.id || template?.sourceSpec || template?.requested);
|
|
327
|
+
}
|
|
328
|
+
|
|
264
329
|
/**
|
|
265
330
|
* @param {string} configDir
|
|
266
331
|
* @returns {TemplateTrustRecord|null}
|
|
@@ -312,6 +377,14 @@ export function writeTemplateTrustRecord(configDir, projectConfig) {
|
|
|
312
377
|
if (!implementationConfig) {
|
|
313
378
|
throw new Error("Cannot trust template implementation because topogram.project.json has no implementation config.");
|
|
314
379
|
}
|
|
380
|
+
const implementationInfo = {
|
|
381
|
+
config: implementationConfig,
|
|
382
|
+
configDir
|
|
383
|
+
};
|
|
384
|
+
if (!implementationModuleIsUnderRoot(implementationInfo)) {
|
|
385
|
+
const implementation = implementationTrustFingerprint(implementationConfig);
|
|
386
|
+
throw new Error(implementationOutsideRootMessage(implementation.module));
|
|
387
|
+
}
|
|
315
388
|
const implementation = implementationTrustFingerprint(implementationConfig);
|
|
316
389
|
const record = buildTrustRecord(configDir, projectConfig, implementation);
|
|
317
390
|
fs.writeFileSync(path.join(configDir, TEMPLATE_TRUST_FILE), `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
@@ -329,8 +402,9 @@ export function assertTrustedImplementation(implementationInfo, projectConfig =
|
|
|
329
402
|
return;
|
|
330
403
|
}
|
|
331
404
|
const firstIssue = status.issues[0] || "implementation trust is invalid";
|
|
405
|
+
const guidance = templateTrustRecoveryGuidance(firstIssue);
|
|
332
406
|
throw new Error(
|
|
333
|
-
`${firstIssue}.
|
|
407
|
+
guidance ? `${firstIssue}. ${guidance}` : firstIssue
|
|
334
408
|
);
|
|
335
409
|
}
|
|
336
410
|
|
|
@@ -393,7 +467,8 @@ function implementationReviewFile(configDir, relativePath, file) {
|
|
|
393
467
|
* @returns {{ ok: boolean, requiresTrust: boolean, trustPath: string, trustRecord: TemplateTrustRecord|null, template: { id: string|null, version: string|null, source: string|null, sourceSpec: string|null, requested: string|null, sourceRoot: string|null, catalog?: Record<string, any>|null, includesExecutableImplementation: boolean|null }, implementation: { id: string|null, module: string|null, export: string|null }, content: { trustedDigest: string|null, currentDigest: string|null, added: string[], removed: string[], changed: string[] }, issues: string[] }}
|
|
394
468
|
*/
|
|
395
469
|
export function getTemplateTrustStatus(implementationInfo, projectConfig = null) {
|
|
396
|
-
|
|
470
|
+
const templateAttached = projectHasTemplateAttachment(projectConfig);
|
|
471
|
+
if (!implementationRequiresTrust(implementationInfo, projectConfig)) {
|
|
397
472
|
return {
|
|
398
473
|
ok: true,
|
|
399
474
|
requiresTrust: false,
|
|
@@ -406,6 +481,7 @@ export function getTemplateTrustStatus(implementationInfo, projectConfig = null)
|
|
|
406
481
|
};
|
|
407
482
|
}
|
|
408
483
|
const fingerprint = implementationTrustFingerprint(implementationInfo.config);
|
|
484
|
+
const moduleInsideImplementation = implementationModuleIsUnderRoot(implementationInfo);
|
|
409
485
|
const trustRecord = readTemplateTrustRecord(implementationInfo.configDir);
|
|
410
486
|
const configLabel = implementationInfo.configPath || "topogram.project.json";
|
|
411
487
|
const trustPath = path.join(implementationInfo.configDir, TEMPLATE_TRUST_FILE);
|
|
@@ -415,6 +491,10 @@ export function getTemplateTrustStatus(implementationInfo, projectConfig = null)
|
|
|
415
491
|
/** @type {{ trustedDigest: string|null, currentDigest: string|null, added: string[], removed: string[], changed: string[] }} */
|
|
416
492
|
const contentStatus = { trustedDigest: null, currentDigest: null, added: [], removed: [], changed: [] };
|
|
417
493
|
|
|
494
|
+
if (templateAttached && !moduleInsideImplementation) {
|
|
495
|
+
issues.push(implementationOutsideRootMessage(fingerprint.module));
|
|
496
|
+
}
|
|
497
|
+
|
|
418
498
|
if (!trustRecord) {
|
|
419
499
|
issues.push(
|
|
420
500
|
`Refusing to load executable implementation '${fingerprint.module}' from ${normalizeRoot(configLabel)} without ${TEMPLATE_TRUST_FILE}`
|
|
@@ -441,22 +521,29 @@ export function getTemplateTrustStatus(implementationInfo, projectConfig = null)
|
|
|
441
521
|
issues.push(`${TEMPLATE_TRUST_FILE} trusts template version '${trustRecord.template?.version}', but topogram.project.json declares '${projectTemplate.version}'`);
|
|
442
522
|
}
|
|
443
523
|
|
|
444
|
-
if (!
|
|
524
|
+
if (!moduleInsideImplementation) {
|
|
525
|
+
// The module itself is outside the only supported trust root. Do not
|
|
526
|
+
// pretend the implementation/ content digest covers the executable code.
|
|
527
|
+
} else if (!trustRecord.content) {
|
|
445
528
|
issues.push(`${TEMPLATE_TRUST_FILE} is missing implementation content hashes`);
|
|
446
529
|
} else if (trustRecord.content.algorithm !== "sha256") {
|
|
447
530
|
issues.push(`${TEMPLATE_TRUST_FILE} uses unsupported content hash algorithm '${trustRecord.content.algorithm}'`);
|
|
448
531
|
} else {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
532
|
+
try {
|
|
533
|
+
const currentContent = hashImplementationContent(implementationInfo.configDir);
|
|
534
|
+
contentStatus.trustedDigest = trustRecord.content.digest;
|
|
535
|
+
contentStatus.currentDigest = currentContent.digest;
|
|
536
|
+
const trustedByPath = new Map((trustRecord.content.files || []).map((file) => [file.path, file]));
|
|
537
|
+
const currentByPath = new Map(currentContent.files.map((file) => [file.path, file]));
|
|
538
|
+
const diff = diffContentFiles(trustedByPath, currentByPath);
|
|
539
|
+
contentStatus.added = diff.added;
|
|
540
|
+
contentStatus.removed = diff.removed;
|
|
541
|
+
contentStatus.changed = diff.changed;
|
|
542
|
+
if (trustRecord.content.digest !== currentContent.digest) {
|
|
543
|
+
issues.push(`${TEMPLATE_TRUST_FILE} implementation content changed since it was last trusted`);
|
|
544
|
+
}
|
|
545
|
+
} catch (error) {
|
|
546
|
+
issues.push(error instanceof Error ? error.message : String(error));
|
|
460
547
|
}
|
|
461
548
|
}
|
|
462
549
|
}
|
|
@@ -494,7 +581,12 @@ export function getTemplateTrustDiff(implementationInfo, projectConfig = null) {
|
|
|
494
581
|
if (!status.requiresTrust || !status.trustRecord?.content) {
|
|
495
582
|
return { ok: status.ok, requiresTrust: status.requiresTrust, status, files: [] };
|
|
496
583
|
}
|
|
497
|
-
|
|
584
|
+
let currentContent;
|
|
585
|
+
try {
|
|
586
|
+
currentContent = hashImplementationContent(implementationInfo.configDir);
|
|
587
|
+
} catch (_error) {
|
|
588
|
+
return { ok: false, requiresTrust: status.requiresTrust, status, files: [] };
|
|
589
|
+
}
|
|
498
590
|
const trustedByPath = new Map((status.trustRecord.content.files || []).map((file) => [file.path, file]));
|
|
499
591
|
const currentByPath = new Map(currentContent.files.map((file) => [file.path, file]));
|
|
500
592
|
/** @type {Array<{ path: string, kind: "added"|"removed"|"changed", trusted: { path: string, sha256: string|null, size: number|null }|null, current: { path: string, sha256: string|null, size: number|null, binary: boolean, diffOmitted: boolean }|null, binary: boolean, diffOmitted: boolean, unifiedDiff: string|null }>} */
|