vigthoria-cli 1.13.23 → 1.13.25

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 (39) hide show
  1. package/completions/_vigthoria +1 -1
  2. package/completions/vigthoria.fish +1 -1
  3. package/dist/commands/auth.js +69 -17
  4. package/dist/commands/chat.js +41 -1
  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 +19 -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/product-run-registration.js +4 -2
  15. package/dist/commands/repo.d.ts +2 -0
  16. package/dist/commands/repo.js +37 -0
  17. package/dist/commands/security.d.ts +3 -0
  18. package/dist/commands/security.js +29 -9
  19. package/dist/index.js +16 -2
  20. package/dist/utils/api.js +14 -11
  21. package/dist/utils/chat-prompt-policy.js +1 -1
  22. package/dist/utils/code-operations-service.js +43 -14
  23. package/dist/utils/config.js +5 -2
  24. package/dist/utils/local-security-service.d.ts +23 -0
  25. package/dist/utils/local-security-service.js +210 -0
  26. package/dist/utils/model-transport-service.js +66 -6
  27. package/dist/utils/network-policy.d.ts +1 -1
  28. package/dist/utils/network-policy.js +3 -2
  29. package/dist/utils/preview-screenshot-adapter.js +2 -2
  30. package/dist/utils/secret-policy.js +27 -18
  31. package/dist/utils/subscription-policy.d.ts +11 -0
  32. package/dist/utils/subscription-policy.js +32 -0
  33. package/dist/utils/v3-workspace-service.js +5 -1
  34. package/dist/utils/vigflow-client.js +2 -2
  35. package/package.json +3 -11
  36. package/release-policy.json +2 -1
  37. package/scripts/release/generate-release-evidence.mjs +49 -0
  38. package/scripts/release/publish-cli-release.mjs +19 -8
  39. package/scripts/release/validate-no-go-gates.sh +2 -0
@@ -0,0 +1,210 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { isSensitivePath } from './secret-policy.js';
5
+ import { WorkspaceBoundaryError } from './workspace-boundary.js';
6
+ const MAX_FILES = 5_000;
7
+ const MAX_FILE_BYTES = 2 * 1024 * 1024;
8
+ const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
9
+ const SCANNABLE_EXTENSIONS = new Set([
10
+ '.cjs', '.css', '.go', '.html', '.htm', '.java', '.js', '.jsx', '.json',
11
+ '.mjs', '.php', '.ps1', '.py', '.rb', '.sh', '.sql', '.ts', '.tsx',
12
+ '.vue', '.xml', '.yaml', '.yml',
13
+ ]);
14
+ const EXCLUDED_DIRECTORIES = new Set([
15
+ '.git', '.hg', '.svn', '.vigthoria', 'build', 'coverage', 'dist',
16
+ 'node_modules', 'target', 'vendor', '__pycache__', '.venv', 'venv',
17
+ ]);
18
+ // These are intentionally high-confidence source patterns. The scanner does
19
+ // not classify ordinary bind addresses, DOM APIs, or configuration examples as
20
+ // vulnerabilities without the context required to prove exploitability.
21
+ const RULES = [
22
+ {
23
+ id: 'hardcoded-private-key',
24
+ severity: 'critical',
25
+ expression: /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/g,
26
+ message: 'Private key material is embedded in source',
27
+ },
28
+ {
29
+ id: 'hardcoded-access-token',
30
+ severity: 'critical',
31
+ expression: /\b(?:ghp|gho|ghu|ghs|github_pat|xox[baprs])_[A-Za-z0-9_-]{20,}\b/g,
32
+ message: 'A high-confidence access token is embedded in source',
33
+ },
34
+ {
35
+ id: 'hardcoded-cloud-key',
36
+ severity: 'critical',
37
+ expression: /\bAKIA[0-9A-Z]{16}\b/g,
38
+ message: 'An AWS access key identifier is embedded in source',
39
+ },
40
+ {
41
+ id: 'child-process-shell',
42
+ severity: 'high',
43
+ expression: /\b(?:exec|execSync)\s*\(\s*(?:`|[^'"\s][^,\n]*)/g,
44
+ message: 'A child process appears to execute dynamically constructed shell text',
45
+ },
46
+ {
47
+ id: 'dynamic-code-evaluation',
48
+ severity: 'high',
49
+ expression: /\b(?:eval|Function)\s*\(\s*(?!['"](?:use strict)?['"])/g,
50
+ message: 'Dynamic code evaluation requires manual security review',
51
+ },
52
+ {
53
+ id: 'unsafe-tls-disable',
54
+ severity: 'high',
55
+ expression: /\b(?:NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0|rejectUnauthorized\s*:\s*false)\b/g,
56
+ message: 'TLS certificate validation is explicitly disabled',
57
+ },
58
+ ];
59
+ function assertScanRoot(input) {
60
+ const resolved = path.resolve(input || process.cwd());
61
+ if (process.platform === 'win32' && /^(?:\\\\[?.]\\|\\\\(?!localhost\\))/i.test(resolved)) {
62
+ throw new WorkspaceBoundaryError('UNC and Windows device paths are not valid security scan roots', 'UNSAFE_WINDOWS_PATH');
63
+ }
64
+ const stat = fs.lstatSync(resolved);
65
+ if (stat.isSymbolicLink()) {
66
+ throw new WorkspaceBoundaryError('A symbolic link or junction cannot be used as a security scan root', 'WORKSPACE_LINK_ESCAPE');
67
+ }
68
+ if (!stat.isDirectory())
69
+ throw new WorkspaceBoundaryError('Security scan root must be a directory', 'WORKSPACE_PATH_INVALID');
70
+ return fs.realpathSync(resolved);
71
+ }
72
+ function stableIssueId(rule, file, line) {
73
+ return `SEC-${createHash('sha256').update(`${rule}\0${file}\0${line}`).digest('hex').slice(0, 12).toUpperCase()}`;
74
+ }
75
+ function lineAt(content, offset) {
76
+ let line = 1;
77
+ for (let index = 0; index < offset; index += 1)
78
+ if (content.charCodeAt(index) === 10)
79
+ line += 1;
80
+ return line;
81
+ }
82
+ function scoreFromIssues(issues) {
83
+ const penalty = { critical: 20, high: 10, medium: 4, low: 1 };
84
+ return Math.max(0, 100 - issues.reduce((sum, issue) => sum + penalty[issue.severity], 0));
85
+ }
86
+ function gradeFromScore(score) {
87
+ if (score >= 90)
88
+ return 'A';
89
+ if (score >= 75)
90
+ return 'B';
91
+ if (score >= 50)
92
+ return 'C';
93
+ if (score >= 30)
94
+ return 'D';
95
+ return 'F';
96
+ }
97
+ export function scanLocalWorkspace(inputRoot = process.cwd()) {
98
+ const rootDir = assertScanRoot(inputRoot);
99
+ const stack = [rootDir];
100
+ const issues = [];
101
+ let scannedFiles = 0;
102
+ let scannedBytes = 0;
103
+ let skippedFiles = 0;
104
+ while (stack.length > 0) {
105
+ const directory = stack.pop();
106
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
107
+ for (const entry of entries) {
108
+ const absolute = path.join(directory, entry.name);
109
+ const relative = path.relative(rootDir, absolute).split(path.sep).join('/');
110
+ if (entry.isSymbolicLink()) {
111
+ skippedFiles += 1;
112
+ continue;
113
+ }
114
+ if (entry.isDirectory()) {
115
+ if (!EXCLUDED_DIRECTORIES.has(entry.name) && !isSensitivePath(relative))
116
+ stack.push(absolute);
117
+ continue;
118
+ }
119
+ if (!entry.isFile()) {
120
+ skippedFiles += 1;
121
+ continue;
122
+ }
123
+ if (isSensitivePath(relative)) {
124
+ const basename = path.basename(relative).toLowerCase();
125
+ if (basename === '.env' || basename.startsWith('.env.')) {
126
+ issues.push({
127
+ id: stableIssueId('sensitive-env-file', relative, 1),
128
+ rule: 'sensitive-env-file',
129
+ severity: 'medium',
130
+ file: relative,
131
+ line: 1,
132
+ message: 'Sensitive environment file exists in the scanned tree; verify it is excluded from version control and uploads',
133
+ });
134
+ }
135
+ skippedFiles += 1;
136
+ continue;
137
+ }
138
+ if (!SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase()))
139
+ continue;
140
+ if (scannedFiles >= MAX_FILES)
141
+ throw new Error(`Security scan file limit exceeded (${MAX_FILES})`);
142
+ const stat = fs.statSync(absolute);
143
+ if (stat.size > MAX_FILE_BYTES) {
144
+ skippedFiles += 1;
145
+ continue;
146
+ }
147
+ if (scannedBytes + stat.size > MAX_TOTAL_BYTES)
148
+ throw new Error(`Security scan byte limit exceeded (${MAX_TOTAL_BYTES})`);
149
+ const content = fs.readFileSync(absolute, 'utf8');
150
+ scannedFiles += 1;
151
+ scannedBytes += stat.size;
152
+ for (const rule of RULES) {
153
+ rule.expression.lastIndex = 0;
154
+ let match;
155
+ while ((match = rule.expression.exec(content)) !== null) {
156
+ const line = lineAt(content, match.index);
157
+ issues.push({
158
+ id: stableIssueId(rule.id, relative, line),
159
+ rule: rule.id,
160
+ severity: rule.severity,
161
+ file: relative,
162
+ line,
163
+ message: rule.message,
164
+ });
165
+ if (match[0].length === 0)
166
+ rule.expression.lastIndex += 1;
167
+ }
168
+ }
169
+ }
170
+ }
171
+ issues.sort((left, right) => left.file.localeCompare(right.file) || left.line - right.line || left.rule.localeCompare(right.rule));
172
+ const score = scoreFromIssues(issues);
173
+ return {
174
+ rootDir,
175
+ scannedFiles,
176
+ scannedBytes,
177
+ skippedFiles,
178
+ score,
179
+ grade: gradeFromScore(score),
180
+ issueCount: issues.length,
181
+ issues,
182
+ bounded: true,
183
+ localOnly: true,
184
+ };
185
+ }
186
+ export function localSecurityFixPlan(result, issueId, requestedApply = false) {
187
+ const selected = issueId ? result.issues.filter((issue) => issue.id === issueId) : result.issues;
188
+ if (issueId && selected.length === 0)
189
+ throw new Error(`Security issue was not found in the current scan: ${issueId}`);
190
+ const actions = selected.slice(0, 100).map((issue) => ({
191
+ issue_id: issue.id,
192
+ file: issue.file,
193
+ line: issue.line,
194
+ rule: issue.rule,
195
+ action: issue.rule === 'sensitive-env-file'
196
+ ? 'Confirm the file is ignored and excluded from every upload/context boundary; rotate any exposed values'
197
+ : 'Review the exact source location and replace the unsafe construct without weakening validation or trust boundaries',
198
+ }));
199
+ return {
200
+ success: true,
201
+ applied: false,
202
+ requestedApply,
203
+ confirmRequired: false,
204
+ plannedActions: actions,
205
+ message: requestedApply
206
+ ? 'No mutation was performed: local security fixes require an explicit reviewed source patch.'
207
+ : 'Local security fix plan generated; no files were changed.',
208
+ localOnly: true,
209
+ };
210
+ }
@@ -1,4 +1,4 @@
1
- import { scanOutboundContext } from './secret-policy.js';
1
+ import { redactSensitiveText, scanOutboundContext } from './secret-policy.js';
2
2
  export class ModelTransportService {
3
3
  dependencies;
4
4
  lastErrors = [];
@@ -28,15 +28,75 @@ export class ModelTransportService {
28
28
  }
29
29
  async complete(systemPrompt, userPrompt, model, maxTokens) {
30
30
  const resolvedModel = model ? this.dependencies.resolvePermittedModelId(model) : 'Vigthoria-v4-Code-27B';
31
- const response = await this.dependencies.postModels({
31
+ const messages = scanOutboundContext([{ role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }]).value;
32
+ const body = {
32
33
  model: resolvedModel,
33
- messages: scanOutboundContext([{ role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }]).value,
34
+ messages,
34
35
  max_tokens: maxTokens || this.dependencies.getMaxTokens() || 4096,
35
36
  temperature: 0.3,
36
37
  stream: false,
37
- });
38
- const content = response?.choices?.[0]?.message?.content || response?.choices?.[0]?.text || '';
39
- return typeof content === 'string' ? content : '';
38
+ };
39
+ const failures = [];
40
+ const safeFailure = (value) => redactSensitiveText(String(value || 'transport failed')).slice(0, 120);
41
+ const extract = (response) => {
42
+ const content = response?.choices?.[0]?.message?.content
43
+ || response?.choices?.[0]?.text
44
+ || response?.response
45
+ || response?.message
46
+ || response?.content;
47
+ return response?.success === false || typeof content !== 'string' ? '' : content.trim();
48
+ };
49
+ if (!this.dependencies.shouldSkipCloudRoutes(resolvedModel) && !this.dependencies.isCloudModelId(resolvedModel)) {
50
+ try {
51
+ const content = extract(await this.dependencies.postModels(body));
52
+ if (content)
53
+ return content;
54
+ failures.push('models:empty response');
55
+ }
56
+ catch (error) {
57
+ failures.push(`models:${safeFailure(error?.response?.data?.error || error?.message)}`);
58
+ }
59
+ }
60
+ if (this.dependencies.isAuthenticated() && !this.dependencies.shouldSkipCloudRoutes(resolvedModel)) {
61
+ const coderBody = {
62
+ messages,
63
+ model: resolvedModel,
64
+ maxTokens: body.max_tokens,
65
+ temperature: body.temperature,
66
+ cloudConsent: this.dependencies.isCloudModelId(resolvedModel),
67
+ };
68
+ try {
69
+ const content = extract(await this.dependencies.postCoder(coderBody));
70
+ if (content)
71
+ return content;
72
+ failures.push('coder:empty response');
73
+ }
74
+ catch (error) {
75
+ failures.push(`coder:${safeFailure(error?.response?.data?.error || error?.message)}`);
76
+ }
77
+ try {
78
+ const content = extract(await this.dependencies.postCanonicalCoder(coderBody));
79
+ if (content)
80
+ return content;
81
+ failures.push('coder-canonical:empty response');
82
+ }
83
+ catch (error) {
84
+ failures.push(`coder-canonical:${safeFailure(error?.response?.data?.error || error?.message)}`);
85
+ }
86
+ }
87
+ if (this.dependencies.hasSelfHostedTransport() && this.dependencies.shouldTrySelfHosted(resolvedModel, model || resolvedModel)) {
88
+ try {
89
+ const content = extract(await this.dependencies.postSelfHosted(body));
90
+ if (content)
91
+ return content;
92
+ failures.push('self-hosted:empty response');
93
+ }
94
+ catch (error) {
95
+ failures.push(`self-hosted:${safeFailure(error?.message)}`);
96
+ }
97
+ }
98
+ this.lastErrors = failures;
99
+ throw new Error(`No permitted code-completion backend returned a response${failures.length ? ` (${failures.join(' | ')})` : ''}.`);
40
100
  }
41
101
  response(data, model, requested, prefix) {
42
102
  const content = data?.choices?.[0]?.message?.content || data?.choices?.[0]?.text || data?.response || data?.message || data?.content;
@@ -1,6 +1,6 @@
1
1
  import type { AxiosInstance } from 'axios';
2
2
  export { redactSensitiveText } from './secret-policy.js';
3
- export type EndpointAudience = 'auto' | 'coder' | 'models' | 'hub' | 'community' | 'music' | 'brain-hub' | 'mcp' | 'v3' | 'template' | 'hyperloop' | 'vigflow' | 'bridge' | 'release' | 'public';
3
+ export type EndpointAudience = 'auto' | 'coder' | 'models' | 'operator' | 'hub' | 'community' | 'music' | 'brain-hub' | 'mcp' | 'v3' | 'template' | 'hyperloop' | 'vigflow' | 'bridge' | 'release' | 'public';
4
4
  export declare const DEFAULT_NETWORK_TIMEOUT_MS = 30000;
5
5
  export declare class NetworkPolicyError extends Error {
6
6
  readonly code: 'offline' | 'untrusted_endpoint' | 'credential_audience' | 'unsafe_redirect';
@@ -3,7 +3,8 @@ export { redactSensitiveText } from './secret-policy.js';
3
3
  const AUDIENCE_HOSTS = {
4
4
  coder: new Set(['coder.vigthoria.io']),
5
5
  models: new Set(['api.vigthoria.io']),
6
- hub: new Set(['api.vigthoria.io']),
6
+ operator: new Set(['api.vigthoria.io', 'operator.vigthoria.io']),
7
+ hub: new Set(['hub.vigthoria.io']),
7
8
  community: new Set(['community.vigthoria.io']),
8
9
  music: new Set(['music.vigthoria.io']),
9
10
  'brain-hub': new Set(['coder.vigthoria.io']),
@@ -11,7 +12,7 @@ const AUDIENCE_HOSTS = {
11
12
  v3: new Set(['coder.vigthoria.io']),
12
13
  template: new Set(['coder.vigthoria.io', 'template.vigthoria.io']),
13
14
  hyperloop: new Set(['coder.vigthoria.io', 'hyperloop.vigthoria.io']),
14
- vigflow: new Set(['coder.vigthoria.io', 'vigflow.vigthoria.io']),
15
+ vigflow: new Set(['coder.vigthoria.io', 'workflow.vigthoria.io']),
15
16
  bridge: new Set(['bridge.vigthoria.io', 'devtools.vigthoria.io']),
16
17
  release: new Set([
17
18
  'extension.vigthoria.io',
@@ -20,12 +20,12 @@ export class OptionalPuppeteerScreenshotAdapter {
20
20
  const loaded = await this.loadPuppeteer().catch(() => null);
21
21
  const puppeteer = (loaded?.default || loaded);
22
22
  if (!puppeteer || typeof puppeteer.launch !== 'function') {
23
- return { captured: false, error: 'puppeteer optional adapter is not installed' };
23
+ return { captured: false, error: 'required Puppeteer screenshot runtime is not installed' };
24
24
  }
25
25
  const browserProfile = this.allocateTemp('browser-');
26
26
  try {
27
27
  const browser = await puppeteer.launch({
28
- headless: true,
28
+ headless: 'shell',
29
29
  args: ['--no-sandbox', '--disable-setuid-sandbox'],
30
30
  userDataDir: browserProfile,
31
31
  });
@@ -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
+ }
@@ -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.23",
3
+ "version": "1.13.25",
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 });