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.
- package/completions/_vigthoria +1 -1
- package/completions/vigthoria.fish +1 -1
- package/dist/commands/auth.js +14 -5
- package/dist/commands/chat.js +51 -2
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +33 -9
- package/dist/commands/game.d.ts +4 -0
- package/dist/commands/game.js +23 -1
- package/dist/commands/hub.d.ts +2 -0
- package/dist/commands/hub.js +61 -78
- package/dist/commands/legion.d.ts +1 -0
- package/dist/commands/legion.js +7 -7
- package/dist/commands/platform-registration.js +1 -1
- package/dist/commands/preview.d.ts +1 -0
- package/dist/commands/preview.js +30 -14
- package/dist/commands/product-run-registration.js +4 -2
- package/dist/commands/repo.d.ts +37 -2
- package/dist/commands/repo.js +99 -32
- package/dist/commands/security.d.ts +3 -0
- package/dist/commands/security.js +29 -9
- package/dist/commands/update-registration.js +2 -1
- package/dist/index.js +16 -2
- package/dist/utils/api.js +26 -22
- package/dist/utils/chat-prompt-policy.js +1 -1
- package/dist/utils/code-operations-service.js +43 -14
- package/dist/utils/config.js +5 -2
- package/dist/utils/local-security-service.d.ts +23 -0
- package/dist/utils/local-security-service.js +210 -0
- package/dist/utils/model-transport-service.js +66 -6
- package/dist/utils/network-policy.d.ts +1 -1
- package/dist/utils/network-policy.js +3 -2
- package/dist/utils/preview-screenshot-adapter.d.ts +42 -1
- package/dist/utils/preview-screenshot-adapter.js +66 -14
- package/dist/utils/runtime-temp.d.ts +1 -0
- package/dist/utils/runtime-temp.js +22 -2
- package/dist/utils/secret-policy.js +27 -18
- package/dist/utils/subscription-policy.d.ts +11 -0
- package/dist/utils/subscription-policy.js +32 -0
- package/dist/utils/v3-stream-events.d.ts +8 -0
- package/dist/utils/v3-stream-events.js +62 -0
- package/dist/utils/v3-workspace-service.js +5 -1
- package/dist/utils/vigflow-client.js +2 -2
- package/package.json +3 -11
- package/release-policy.json +2 -1
- package/scripts/release/generate-release-evidence.mjs +49 -0
- package/scripts/release/publish-cli-release.mjs +19 -8
- 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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
125
|
+
const objectValue = value;
|
|
126
|
+
if (ancestors.has(objectValue))
|
|
123
127
|
throw new OutboundContextError([`${location}: cyclic value`]);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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`, {
|
|
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.
|
|
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
|
},
|
package/release-policy.json
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
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,
|
|
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
|