vigthoria-cli 1.13.24 → 1.13.26

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.
Files changed (47) hide show
  1. package/completions/_vigthoria +1 -1
  2. package/completions/vigthoria.fish +1 -1
  3. package/dist/commands/auth.js +14 -5
  4. package/dist/commands/chat.js +51 -2
  5. package/dist/commands/config.d.ts +1 -0
  6. package/dist/commands/config.js +33 -9
  7. package/dist/commands/game.d.ts +4 -0
  8. package/dist/commands/game.js +23 -1
  9. package/dist/commands/hub.d.ts +2 -0
  10. package/dist/commands/hub.js +61 -78
  11. package/dist/commands/legion.d.ts +1 -0
  12. package/dist/commands/legion.js +7 -7
  13. package/dist/commands/platform-registration.js +1 -1
  14. package/dist/commands/preview.d.ts +1 -0
  15. package/dist/commands/preview.js +30 -14
  16. package/dist/commands/product-run-registration.js +4 -2
  17. package/dist/commands/repo.d.ts +37 -2
  18. package/dist/commands/repo.js +99 -32
  19. package/dist/commands/security.d.ts +3 -0
  20. package/dist/commands/security.js +29 -9
  21. package/dist/commands/update-registration.js +2 -1
  22. package/dist/index.js +16 -2
  23. package/dist/utils/api.js +26 -22
  24. package/dist/utils/chat-prompt-policy.js +1 -1
  25. package/dist/utils/code-operations-service.js +43 -14
  26. package/dist/utils/config.js +5 -2
  27. package/dist/utils/local-security-service.d.ts +23 -0
  28. package/dist/utils/local-security-service.js +210 -0
  29. package/dist/utils/model-transport-service.js +66 -6
  30. package/dist/utils/network-policy.d.ts +1 -1
  31. package/dist/utils/network-policy.js +3 -2
  32. package/dist/utils/preview-screenshot-adapter.d.ts +42 -1
  33. package/dist/utils/preview-screenshot-adapter.js +66 -14
  34. package/dist/utils/runtime-temp.d.ts +1 -0
  35. package/dist/utils/runtime-temp.js +22 -2
  36. package/dist/utils/secret-policy.js +27 -18
  37. package/dist/utils/subscription-policy.d.ts +11 -0
  38. package/dist/utils/subscription-policy.js +32 -0
  39. package/dist/utils/v3-stream-events.d.ts +8 -0
  40. package/dist/utils/v3-stream-events.js +62 -0
  41. package/dist/utils/v3-workspace-service.js +5 -1
  42. package/dist/utils/vigflow-client.js +2 -2
  43. package/package.json +3 -11
  44. package/release-policy.json +2 -1
  45. package/scripts/release/generate-release-evidence.mjs +49 -0
  46. package/scripts/release/publish-cli-release.mjs +19 -8
  47. package/scripts/release/validate-no-go-gates.sh +2 -0
@@ -99,6 +99,7 @@ export class RuntimeTempManager {
99
99
  pid;
100
100
  isProcessAlive;
101
101
  systemTempRoot;
102
+ sharedTempRoots;
102
103
  configuredRoot;
103
104
  source;
104
105
  maxBytes;
@@ -121,7 +122,18 @@ export class RuntimeTempManager {
121
122
  this.now = options.now || Date.now;
122
123
  this.pid = options.pid || process.pid;
123
124
  this.isProcessAlive = options.isProcessAlive || defaultProcessAlive;
124
- this.systemTempRoot = options.systemTempDirectory || os.tmpdir();
125
+ const platformTemp = this.platform === 'win32'
126
+ ? String(this.environment.TEMP || this.environment.TMP || '').trim()
127
+ : String(this.environment.TMPDIR || '').trim();
128
+ this.systemTempRoot = options.systemTempDirectory || platformTemp || os.tmpdir();
129
+ this.sharedTempRoots = [
130
+ options.systemTempDirectory,
131
+ platformTemp,
132
+ this.environment.TEMP,
133
+ this.environment.TMP,
134
+ this.environment.TMPDIR,
135
+ this.platform === process.platform ? os.tmpdir() : undefined,
136
+ ].filter((value) => typeof value === 'string' && value.trim().length > 0);
125
137
  const resolved = resolveRuntimeTempRoot({ ...options, environment: this.environment, platform: this.platform, homeDirectory: this.homeDirectory });
126
138
  this.configuredRoot = resolved.root;
127
139
  this.source = resolved.source;
@@ -134,6 +146,14 @@ export class RuntimeTempManager {
134
146
  if (this.initializedRoot || this.initializationError)
135
147
  return this.status();
136
148
  try {
149
+ // Reject the shared OS temp path before touching the filesystem. This is
150
+ // also what makes cross-platform policy probes deterministic: a Windows
151
+ // policy instance running in a non-Windows test host must not attempt to
152
+ // create a drive-letter path before it can reject `%TEMP%`.
153
+ if (this.sharedTempRoots.some((root) => pathsEqual(this.configuredRoot, root, this.platform))
154
+ || (this.platform !== 'win32' && isKnownSharedPosixTemp(this.configuredRoot))) {
155
+ throw new RuntimeTempError('The shared operating-system temporary directory cannot be used for Vigthoria runtime storage.', 'TEMP_ROOT_SHARED');
156
+ }
137
157
  if (this.source === 'per-user-default' && !fs.existsSync(this.homeDirectory)) {
138
158
  throw new RuntimeTempError('The user home directory does not exist; managed temporary storage cannot be initialized.', 'TEMP_HOME_UNAVAILABLE');
139
159
  }
@@ -154,7 +174,7 @@ export class RuntimeTempManager {
154
174
  }
155
175
  }
156
176
  const pathApi = this.platform === 'win32' ? path.win32 : path.posix;
157
- if (pathsEqual(realRoot, this.systemTempRoot, this.platform)
177
+ if (this.sharedTempRoots.some((root) => pathsEqual(realRoot, root, this.platform))
158
178
  || (this.platform !== 'win32' && isKnownSharedPosixTemp(realRoot))) {
159
179
  throw new RuntimeTempError('The shared operating-system temporary directory cannot be used for Vigthoria runtime storage.', 'TEMP_ROOT_SHARED');
160
180
  }
@@ -104,7 +104,10 @@ export function installConsoleRedaction() {
104
104
  }
105
105
  export function scanOutboundContext(input) {
106
106
  const findings = new Set();
107
- const seen = new WeakSet();
107
+ // Track only the active recursion path. A WeakSet of every previously seen
108
+ // object incorrectly rejects ordinary DAGs where compatibility aliases point
109
+ // at the same context object. Only an ancestor reference is a real cycle.
110
+ const ancestors = new WeakSet();
108
111
  const visit = (value, location, depth) => {
109
112
  if (depth > 24)
110
113
  throw new OutboundContextError([`${location}: depth limit exceeded`]);
@@ -119,26 +122,32 @@ export function scanOutboundContext(input) {
119
122
  }
120
123
  if (!value || typeof value !== 'object')
121
124
  return value;
122
- if (seen.has(value))
125
+ const objectValue = value;
126
+ if (ancestors.has(objectValue))
123
127
  throw new OutboundContextError([`${location}: cyclic value`]);
124
- seen.add(value);
125
- if (Array.isArray(value))
126
- return value.map((entry, index) => visit(entry, `${location}[${index}]`, depth + 1));
127
- const output = {};
128
- for (const [key, entry] of Object.entries(value)) {
129
- const child = `${location}.${key}`;
130
- if (isSensitivePath(key)) {
131
- findings.add(`${child}: sensitive path excluded`);
132
- continue;
128
+ ancestors.add(objectValue);
129
+ try {
130
+ if (Array.isArray(value))
131
+ return value.map((entry, index) => visit(entry, `${location}[${index}]`, depth + 1));
132
+ const output = {};
133
+ for (const [key, entry] of Object.entries(value)) {
134
+ const child = `${location}.${key}`;
135
+ if (isSensitivePath(key)) {
136
+ findings.add(`${child}: sensitive path excluded`);
137
+ continue;
138
+ }
139
+ if (SENSITIVE_KEY.test(key)) {
140
+ findings.add(`${child}: credential field redacted`);
141
+ output[key] = REDACTED;
142
+ continue;
143
+ }
144
+ output[key] = visit(entry, child, depth + 1);
133
145
  }
134
- if (SENSITIVE_KEY.test(key)) {
135
- findings.add(`${child}: credential field redacted`);
136
- output[key] = REDACTED;
137
- continue;
138
- }
139
- output[key] = visit(entry, child, depth + 1);
146
+ return output;
147
+ }
148
+ finally {
149
+ ancestors.delete(objectValue);
140
150
  }
141
- return output;
142
151
  };
143
152
  const value = visit(input, '$', 0);
144
153
  if (containsHighConfidenceSecret(JSON.stringify(value))) {
@@ -0,0 +1,11 @@
1
+ export interface NormalizedSubscription {
2
+ plan: string;
3
+ status: string;
4
+ expiresAt?: string;
5
+ }
6
+ /**
7
+ * Normalize the subscription shapes emitted by the Coder, User Hub, and
8
+ * legacy authentication contracts. Missing plan data is never synthesized:
9
+ * callers must retain their prior authoritative value or fail closed.
10
+ */
11
+ export declare function normalizeSubscriptionResponse(payload: unknown): NormalizedSubscription | null;
@@ -0,0 +1,32 @@
1
+ function record(value) {
2
+ return value && typeof value === 'object' && !Array.isArray(value)
3
+ ? value
4
+ : {};
5
+ }
6
+ function firstString(...values) {
7
+ for (const value of values) {
8
+ if (typeof value === 'string' && value.trim())
9
+ return value.trim();
10
+ }
11
+ return undefined;
12
+ }
13
+ /**
14
+ * Normalize the subscription shapes emitted by the Coder, User Hub, and
15
+ * legacy authentication contracts. Missing plan data is never synthesized:
16
+ * callers must retain their prior authoritative value or fail closed.
17
+ */
18
+ export function normalizeSubscriptionResponse(payload) {
19
+ const body = record(payload);
20
+ const subscription = record(body.subscription);
21
+ const user = record(body.user);
22
+ const userSubscription = record(user.subscription);
23
+ const plan = firstString(body.plan, body.subscription_plan, body.subscriptionPlan, body.subscriptionTier, body.tier, subscription.plan, subscription.name, subscription.subscription_plan, subscription.subscriptionPlan, subscription.subscriptionTier, subscription.tier, user.plan, user.subscription_plan, user.subscriptionPlan, user.subscriptionTier, user.tier, userSubscription.plan, userSubscription.name, userSubscription.subscription_plan, userSubscription.subscriptionPlan, userSubscription.subscriptionTier, userSubscription.tier);
24
+ if (!plan)
25
+ return null;
26
+ const explicitStatus = firstString(body.status, body.subscription_status, body.subscriptionStatus, subscription.status, subscription.subscription_status, subscription.subscriptionStatus, user.status, user.subscription_status, user.subscriptionStatus, userSubscription.status, userSubscription.subscription_status, userSubscription.subscriptionStatus);
27
+ const activeFlag = [body.active, subscription.active, user.active, userSubscription.active]
28
+ .find((value) => typeof value === 'boolean');
29
+ const status = explicitStatus || (activeFlag === false ? 'inactive' : 'active');
30
+ const expiresAt = firstString(body.expiresAt, body.expires_at, subscription.expiresAt, subscription.expires_at, user.expiresAt, user.expires_at, userSubscription.expiresAt, userSubscription.expires_at);
31
+ return { plan, status: status.toLowerCase(), ...(expiresAt ? { expiresAt } : {}) };
32
+ }
@@ -3,3 +3,11 @@
3
3
  * connections alive — not substantive work the CLI should surface as progress.
4
4
  */
5
5
  export declare function isV3StreamKeepaliveEvent(event: unknown): boolean;
6
+ export declare class AgentPlanContractError extends Error {
7
+ readonly code = "AGENT_PLAN_INVALID";
8
+ constructor(message: string);
9
+ }
10
+ /** Validate a server-authored execution graph before any task can be credited.
11
+ * A cyclic, duplicate, self-referencing, or dangling graph is never runnable
12
+ * and must not reach the UI callback where it could later be counted as 2/2. */
13
+ export declare function assertValidAgentPlanEvent(event: unknown): void;
@@ -39,3 +39,65 @@ export function isV3StreamKeepaliveEvent(event) {
39
39
  }
40
40
  return false;
41
41
  }
42
+ export class AgentPlanContractError extends Error {
43
+ code = 'AGENT_PLAN_INVALID';
44
+ constructor(message) {
45
+ super(message);
46
+ this.name = 'AgentPlanContractError';
47
+ }
48
+ }
49
+ function dependencyId(value) {
50
+ if (typeof value === 'string' || typeof value === 'number')
51
+ return String(value).trim();
52
+ if (!value || typeof value !== 'object')
53
+ return '';
54
+ const record = value;
55
+ return String(record.id || record.task_id || record.taskId || '').trim();
56
+ }
57
+ /** Validate a server-authored execution graph before any task can be credited.
58
+ * A cyclic, duplicate, self-referencing, or dangling graph is never runnable
59
+ * and must not reach the UI callback where it could later be counted as 2/2. */
60
+ export function assertValidAgentPlanEvent(event) {
61
+ if (!event || typeof event !== 'object')
62
+ return;
63
+ const record = event;
64
+ if (record.type !== 'plan' || !Array.isArray(record.plan?.tasks))
65
+ return;
66
+ const tasks = record.plan.tasks;
67
+ const ids = tasks.map((task, index) => {
68
+ const id = dependencyId(task.id ?? task.task_id ?? task.taskId);
69
+ if (!id)
70
+ throw new AgentPlanContractError(`Agent plan task at index ${index} has no stable ID.`);
71
+ return id;
72
+ });
73
+ if (new Set(ids).size !== ids.length)
74
+ throw new AgentPlanContractError('Agent plan contains duplicate task IDs.');
75
+ const idSet = new Set(ids);
76
+ const graph = new Map();
77
+ tasks.forEach((task, index) => {
78
+ const raw = task.depends_on ?? task.dependsOn ?? task.dependencies ?? [];
79
+ const dependencies = (Array.isArray(raw) ? raw : [raw]).map(dependencyId).filter(Boolean);
80
+ for (const dependency of dependencies) {
81
+ if (dependency === ids[index])
82
+ throw new AgentPlanContractError(`Agent plan task ${ids[index]} depends on itself.`);
83
+ if (!idSet.has(dependency))
84
+ throw new AgentPlanContractError(`Agent plan task ${ids[index]} depends on missing task ${dependency}.`);
85
+ }
86
+ graph.set(ids[index], dependencies);
87
+ });
88
+ const visiting = new Set();
89
+ const visited = new Set();
90
+ const visit = (id, trail) => {
91
+ if (visiting.has(id))
92
+ throw new AgentPlanContractError(`Agent plan contains a dependency cycle: ${[...trail, id].join(' -> ')}.`);
93
+ if (visited.has(id))
94
+ return;
95
+ visiting.add(id);
96
+ for (const dependency of graph.get(id) || [])
97
+ visit(dependency, [...trail, id]);
98
+ visiting.delete(id);
99
+ visited.add(id);
100
+ };
101
+ for (const id of ids)
102
+ visit(id, []);
103
+ }
@@ -135,7 +135,11 @@ export class V3WorkspaceService {
135
135
  const candidates = new Set();
136
136
  for (const value of [message, context.rawMessage, context.agentPrompt]) {
137
137
  const text = String(value || '');
138
- for (const pattern of [/`([^`]+\.(?:html|css|js|jsx|ts|tsx|json|md|py|sh))`/gi, /\b([A-Za-z0-9_./-]+\.(?:html|css|js|jsx|ts|tsx|json|md|py|sh))\b/g]) {
138
+ const extensions = 'c|cc|cpp|cxx|h|hpp|cs|css|go|html|htm|ini|java|js|jsx|json|kt|kts|md|mjs|cjs|php|ps1|py|rb|rs|scss|sh|sql|svelte|toml|ts|tsx|txt|vue|xml|yaml|yml';
139
+ for (const pattern of [
140
+ new RegExp('`([^`]+\\.(?:' + extensions + '))`', 'gi'),
141
+ new RegExp('\\b([A-Za-z0-9_./-]+\\.(?:' + extensions + '))\\b', 'gi'),
142
+ ]) {
139
143
  let match;
140
144
  while ((match = pattern.exec(text)) !== null) {
141
145
  const filePath = match[1].trim().replace(/^\.\//, '');
@@ -16,8 +16,8 @@ export class VigFlowClient {
16
16
  const authToken = this.dependencies.getAccessToken();
17
17
  if (!authToken)
18
18
  throw new Error('Not authenticated. Run vigthoria login first.');
19
- const response = await this.transport.post(`${baseUrl}/api/auth/sso`, { token: authToken }, {
20
- headers: { 'Content-Type': 'application/json' }, timeout: 30_000,
19
+ const response = await this.transport.post(`${baseUrl}/api/auth/sso`, {}, {
20
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authToken}` }, timeout: 30_000,
21
21
  });
22
22
  const token = String(response.data.token || '').trim();
23
23
  if (!token)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.13.24",
3
+ "version": "1.13.26",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -93,7 +93,7 @@
93
93
  "test:pitfall:context": "npm run build && node scripts/test-pitfall-context-smoke.js",
94
94
  "test:game:command": "node scripts/test-game-command.mjs",
95
95
  "test:security:dependencies": "node scripts/test-dependency-security.mjs",
96
- "test:security:network": "npm run build && node scripts/test-network-policy.mjs && node scripts/test-installer-trust-policy.mjs",
96
+ "test:security:network": "npm run build && node scripts/test-network-policy.mjs && node scripts/test-installer-trust-policy.mjs && node scripts/test-local-security-service.mjs",
97
97
  "test:security:workspace": "npm run build && node scripts/test-workspace-archive-containment.mjs",
98
98
  "test:security:process": "npm run build && node scripts/test-process-approval-safety.mjs",
99
99
  "test:security:mutation-preview": "npm run build && node scripts/test-mutation-preview-transaction.mjs",
@@ -131,6 +131,7 @@
131
131
  "marked": "^11.0.0",
132
132
  "marked-terminal": "^6.2.0",
133
133
  "ora": "^7.0.1",
134
+ "puppeteer": "^24.40.0",
134
135
  "ws": "^8.14.2"
135
136
  },
136
137
  "devDependencies": {
@@ -138,18 +139,9 @@
138
139
  "@types/inquirer": "^9.0.7",
139
140
  "@types/node": "^20.10.0",
140
141
  "@types/ws": "^8.5.10",
141
- "puppeteer": "^24.40.0",
142
142
  "ts-node": "^10.9.2",
143
143
  "typescript": "^5.3.2"
144
144
  },
145
- "peerDependencies": {
146
- "puppeteer": "^24.40.0"
147
- },
148
- "peerDependenciesMeta": {
149
- "puppeteer": {
150
- "optional": true
151
- }
152
- },
153
145
  "engines": {
154
146
  "node": ">=20.19.0"
155
147
  },
@@ -27,7 +27,8 @@
27
27
  "signature": {
28
28
  "algorithm": "Ed25519",
29
29
  "trustedKeys": {
30
- "vigthoria-release-2026-01": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAotTXdcHwZ+D1z5lJMdgAAeMU99BRdMCaCZ465DN2aug=\n-----END PUBLIC KEY-----\n"
30
+ "vigthoria-release-2026-01": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAotTXdcHwZ+D1z5lJMdgAAeMU99BRdMCaCZ465DN2aug=\n-----END PUBLIC KEY-----\n",
31
+ "vigthoria-release-2026-02": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAwzjKsJCJhF387ZPbQoGTciJtOLyJFXp4zpOKrrhQm8I=\n-----END PUBLIC KEY-----\n"
31
32
  }
32
33
  },
33
34
  "channels": {
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { spawnSync } from 'node:child_process';
6
+
7
+ const archive = path.resolve(String(process.argv[2] || ''));
8
+ const outputDirectory = path.resolve(String(process.argv[3] || ''));
9
+ if (!fs.statSync(archive, { throwIfNoEntry: false })?.isFile()) throw new Error('Usage: generate-release-evidence.mjs <archive.tgz> <output-directory>');
10
+ fs.mkdirSync(outputDirectory, { recursive: true });
11
+ const sha256 = createHash('sha256').update(fs.readFileSync(archive)).digest('hex');
12
+ const readPackage = spawnSync('tar', ['-xOf', archive, 'package/package.json'], { encoding: 'utf8', maxBuffer: 1024 * 1024 });
13
+ if (readPackage.status !== 0) throw new Error(`Unable to inspect archive: ${readPackage.stderr || readPackage.stdout}`);
14
+ const pkg = JSON.parse(readPackage.stdout);
15
+ if (pkg.name !== 'vigthoria-cli' || !pkg.version) throw new Error('Archive package identity is invalid');
16
+ const timestamp = new Date().toISOString();
17
+ const component = {
18
+ type: 'application',
19
+ 'bom-ref': `pkg:npm/${pkg.name}@${pkg.version}`,
20
+ name: pkg.name,
21
+ version: pkg.version,
22
+ purl: `pkg:npm/${pkg.name}@${pkg.version}`,
23
+ hashes: [{ alg: 'SHA-256', content: sha256 }],
24
+ };
25
+ const components = Object.entries(pkg.dependencies || {}).sort(([left], [right]) => left.localeCompare(right)).map(([name, version]) => ({
26
+ type: 'library', name, version: String(version), purl: `pkg:npm/${encodeURIComponent(name)}@${encodeURIComponent(String(version))}`,
27
+ scope: 'required',
28
+ }));
29
+ const sbom = {
30
+ bomFormat: 'CycloneDX', specVersion: '1.5', serialNumber: `urn:uuid:${randomUUID()}`, version: 1,
31
+ metadata: { timestamp, tools: { components: [{ type: 'application', name: 'vigthoria-cli-release-evidence', version: '1' }] }, component },
32
+ components,
33
+ };
34
+ const provenance = {
35
+ _type: 'https://in-toto.io/Statement/v1',
36
+ subject: [{ name: path.basename(archive), digest: { sha256 } }],
37
+ predicateType: 'https://vigthoria.io/provenance/verified-existing-artifact/v1',
38
+ predicate: {
39
+ package: { name: pkg.name, version: pkg.version },
40
+ verification: { method: 'sha256-and-embedded-package-identity', verifiedAt: timestamp },
41
+ claimBoundary: 'This record verifies an existing immutable artifact; it does not reconstruct or attest to its original build environment.',
42
+ },
43
+ };
44
+ const stem = `${pkg.name}-${pkg.version}.tgz`;
45
+ const sbomPath = path.join(outputDirectory, `${stem}.sbom.json`);
46
+ const provenancePath = path.join(outputDirectory, `${stem}.provenance.json`);
47
+ fs.writeFileSync(sbomPath, `${JSON.stringify(sbom, null, 2)}\n`, { mode: 0o644 });
48
+ fs.writeFileSync(provenancePath, `${JSON.stringify(provenance, null, 2)}\n`, { mode: 0o644 });
49
+ console.log(JSON.stringify({ packageName: pkg.name, version: pkg.version, sha256, size: fs.statSync(archive).size, sbomPath, provenancePath }, null, 2));
@@ -20,6 +20,9 @@ const privateKeyFile = String(process.env.RELEASE_SIGNING_PRIVATE_KEY_FILE || ''
20
20
  const keyId = String(process.env.RELEASE_SIGNING_KEY_ID || '');
21
21
  const sbomFile = String(process.env.RELEASE_SBOM_FILE || '');
22
22
  const provenanceFile = String(process.env.RELEASE_PROVENANCE_FILE || '');
23
+ const externalArchive = String(process.env.RELEASE_ARCHIVE_FILE || '');
24
+ const republishExisting = process.env.RELEASE_REPUBLISH_EXISTING === '1';
25
+ const resetManifest = process.env.RELEASE_RESET_MANIFEST === '1';
23
26
  const policy = getReleasePolicy();
24
27
 
25
28
  function stop(message) { throw new Error(message); }
@@ -41,14 +44,17 @@ requiredFile(sbomFile, 'RELEASE_SBOM_FILE');
41
44
  requiredFile(provenanceFile, 'RELEASE_PROVENANCE_FILE');
42
45
  if (!Object.hasOwn(policy.signature.trustedKeys, keyId)) stop(`RELEASE_SIGNING_KEY_ID is not trusted by release-policy.json: ${keyId || '(unset)'}`);
43
46
 
44
- const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
45
- const lock = JSON.parse(fs.readFileSync(path.join(root, 'package-lock.json'), 'utf8'));
46
- if ([pkg.version, lock.version, lock.packages?.['']?.version].some((identity) => identity !== version)) stop('Requested version conflicts with package.json or package-lock.json.');
47
- const runtime = spawnSync(process.execPath, ['dist/index.js', '--version'], { cwd: root, encoding: 'utf8', env: { ...process.env, VIGTHORIA_OFFLINE: '1', VIGTHORIA_NO_BANNER: '1' } });
48
- if (runtime.status !== 0 || runtime.stdout.trim() !== version) stop('Requested version conflicts with the built CLI banner.');
47
+ if (externalArchive && !republishExisting) stop('RELEASE_ARCHIVE_FILE requires RELEASE_REPUBLISH_EXISTING=1.');
48
+ if (!externalArchive) {
49
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
50
+ const lock = JSON.parse(fs.readFileSync(path.join(root, 'package-lock.json'), 'utf8'));
51
+ if ([pkg.version, lock.version, lock.packages?.['']?.version].some((identity) => identity !== version)) stop('Requested version conflicts with package.json or package-lock.json.');
52
+ const runtime = spawnSync(process.execPath, ['dist/index.js', '--version'], { cwd: root, encoding: 'utf8', env: { ...process.env, VIGTHORIA_OFFLINE: '1', VIGTHORIA_NO_BANNER: '1' } });
53
+ if (runtime.status !== 0 || runtime.stdout.trim() !== version) stop('Requested version conflicts with the built CLI banner.');
54
+ }
49
55
 
50
56
  const archiveFilename = `${policy.packageName}-${version}.tgz`;
51
- const archive = path.join(root, archiveFilename);
57
+ const archive = externalArchive ? path.resolve(externalArchive) : path.join(root, archiveFilename);
52
58
  requiredFile(archive, 'Release archive');
53
59
  const metadata = exactPackageMetadata(archive);
54
60
  if (metadata.name !== policy.packageName || metadata.version !== version) stop('Requested version conflicts with archive metadata.');
@@ -87,16 +93,21 @@ try {
87
93
  verifyReleaseSignature(channel, entry, detached);
88
94
 
89
95
  let manifest = { schemaVersion: policy.manifest.schemaVersion, channels: {} };
90
- if (fs.existsSync(manifestFile)) manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
96
+ if (!resetManifest && fs.existsSync(manifestFile)) manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
91
97
  manifest.channels = { ...manifest.channels, [channel]: entry };
92
98
  parseReleaseManifest(manifest, channel);
93
99
 
94
100
  const inputs = [[archive, archiveFilename], [sbomFile, sbomFilename], [provenanceFile, provenanceFilename]];
95
101
  const staged = [];
96
102
  for (const [source, filename] of inputs) {
103
+ const destination = path.join(downloadsDirectory, filename);
104
+ if (fs.existsSync(destination)) {
105
+ if (republishExisting && digest(destination) === digest(source) && fs.statSync(destination).size === fs.statSync(source).size) continue;
106
+ stop(`Refusing to overwrite published release component: ${destination}`);
107
+ }
97
108
  const temporary = path.join(downloadsDirectory, `.${filename}.${suffix}.tmp`);
98
109
  fs.copyFileSync(source, temporary, fs.constants.COPYFILE_EXCL);
99
- staged.push([temporary, path.join(downloadsDirectory, filename)]);
110
+ staged.push([temporary, destination]);
100
111
  }
101
112
  const signatureTemporary = path.join(downloadsDirectory, `.${signatureFilename}.${suffix}.tmp`);
102
113
  fs.writeFileSync(signatureTemporary, `${detached}\n`, { flag: 'wx', mode: 0o644 });
@@ -66,6 +66,8 @@ node scripts/verify-package-hygiene.js
66
66
  echo "[0.7] authoritative Phase 1 security contracts"
67
67
  node scripts/test-dependency-security.mjs
68
68
  node scripts/test-network-policy.mjs
69
+ node scripts/test-local-security-service.mjs
70
+ node scripts/test-config-noninteractive.mjs
69
71
  node scripts/test-installer-trust-policy.mjs
70
72
  node scripts/test-workspace-archive-containment.mjs
71
73
  node scripts/test-public-repo-clone.mjs