@cantinasecurity/apex-cli 0.1.16 → 0.1.18
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/.claude/skills/apex-cli/SKILL.md +33 -11
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.claude.json +1 -1
- package/.mcp.codex.json +1 -1
- package/MARKETPLACE.md +1 -1
- package/README.md +109 -8
- package/dist/apex.js +15 -1
- package/dist/api-client.js +255 -35
- package/dist/args.js +8 -1
- package/dist/auth.js +171 -54
- package/dist/claim-proof.js +65 -0
- package/dist/commands.js +287 -23
- package/dist/config.js +357 -23
- package/dist/findings.js +50 -9
- package/dist/help.js +21 -5
- package/dist/local-source-scan.js +200 -24
- package/dist/mcp.js +132 -23
- package/dist/onboarding.js +661 -0
- package/dist/redaction.js +34 -0
- package/dist/session.js +10 -1
- package/dist/shell.js +13 -6
- package/dist/telemetry.js +98 -14
- package/dist/workspace-binding.js +16 -6
- package/package.json +3 -1
- package/skills/apex-cli/SKILL.md +45 -11
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const CLAIM_DEADLINE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;
|
|
2
|
+
function isLeapYear(year) {
|
|
3
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
4
|
+
}
|
|
5
|
+
function daysInMonth(year, month) {
|
|
6
|
+
if (month === 2)
|
|
7
|
+
return isLeapYear(year) ? 29 : 28;
|
|
8
|
+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
9
|
+
}
|
|
10
|
+
/** Parses only the RFC3339 shape emitted by the onboarding API. */
|
|
11
|
+
export function parseClaimDeadlineMs(value) {
|
|
12
|
+
if (typeof value !== "string")
|
|
13
|
+
return null;
|
|
14
|
+
const match = CLAIM_DEADLINE_PATTERN.exec(value);
|
|
15
|
+
if (!match)
|
|
16
|
+
return null;
|
|
17
|
+
const year = Number(match[1]);
|
|
18
|
+
const month = Number(match[2]);
|
|
19
|
+
const day = Number(match[3]);
|
|
20
|
+
const hour = Number(match[4]);
|
|
21
|
+
const minute = Number(match[5]);
|
|
22
|
+
const second = Number(match[6]);
|
|
23
|
+
const offset = match[7] ?? "";
|
|
24
|
+
if (year < 1 ||
|
|
25
|
+
month < 1 ||
|
|
26
|
+
month > 12 ||
|
|
27
|
+
day < 1 ||
|
|
28
|
+
day > daysInMonth(year, month) ||
|
|
29
|
+
hour > 23 ||
|
|
30
|
+
minute > 59 ||
|
|
31
|
+
second > 59) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (offset !== "Z") {
|
|
35
|
+
const offsetHour = Number(offset.slice(1, 3));
|
|
36
|
+
const offsetMinute = Number(offset.slice(4, 6));
|
|
37
|
+
if (offsetHour > 23 || offsetMinute > 59)
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const parsed = Date.parse(value);
|
|
41
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Classifies the locally stored claim proof without mutating it.
|
|
45
|
+
*
|
|
46
|
+
* A missing or malformed deadline is treated conservatively as available: an
|
|
47
|
+
* older CLI or interrupted write may have left a valid proof without usable
|
|
48
|
+
* deadline metadata, and only the server can safely declare that proof dead.
|
|
49
|
+
* A known deadline is exclusive, so the proof expires exactly at that instant.
|
|
50
|
+
*/
|
|
51
|
+
export function getClaimProofState(credentials, nowMs = Date.now()) {
|
|
52
|
+
if (!credentials?.claimToken?.trim())
|
|
53
|
+
return "absent";
|
|
54
|
+
const rawDeadline = credentials.claimExpiresAt;
|
|
55
|
+
const deadline = typeof rawDeadline === "string" ? rawDeadline.trim() : "";
|
|
56
|
+
if (!deadline)
|
|
57
|
+
return "available";
|
|
58
|
+
const deadlineMs = parseClaimDeadlineMs(deadline);
|
|
59
|
+
if (deadlineMs === null || !Number.isFinite(nowMs))
|
|
60
|
+
return "available";
|
|
61
|
+
return deadlineMs > nowMs ? "available" : "expired";
|
|
62
|
+
}
|
|
63
|
+
export function isClaimProofAvailable(credentials, nowMs = Date.now()) {
|
|
64
|
+
return getClaimProofState(credentials, nowMs) === "available";
|
|
65
|
+
}
|
package/dist/commands.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { canPromptInteractively, getFlagList, getFlagString, isJsonMode, } from "./args.js";
|
|
2
|
-
import { getMe, logout } from "./auth.js";
|
|
3
|
-
import { ApiError } from "./api-client.js";
|
|
2
|
+
import { getMe, login, logout } from "./auth.js";
|
|
3
|
+
import { ApiError, formatApiError, getApiErrorCode, } from "./api-client.js";
|
|
4
4
|
import { openInBrowser } from "./browser.js";
|
|
5
|
-
import {
|
|
5
|
+
import { getClaimProofState, isClaimProofAvailable, parseClaimDeadlineMs, } from "./claim-proof.js";
|
|
6
|
+
import { loadConfig, updateConfig } from "./config.js";
|
|
6
7
|
import { createFindingComment, fetchScanExport, fetchScanFindings, isFindingUuid, normalizeFindingRef, resolveFindingSelection, resolveScanSelection, startFindingFixReviewScan, submitFindingFeedback, } from "./findings.js";
|
|
7
8
|
import { prepareExplicitScanSources, supportsLegacyRemoteFlow, } from "./local-source-scan.js";
|
|
8
9
|
import { logLine, printJson, withLoadingIndicator } from "./output.js";
|
|
10
|
+
import { claimOnboardingSession, isProofOnlyOnboardingCredentials, redeemOnboardingCode, revokeGuestOnboardingSession, } from "./onboarding.js";
|
|
9
11
|
import { confirm } from "./prompt.js";
|
|
10
12
|
import { cancelScan, fetchScanProgress, fetchWorkspaceScans, findMostRelevantActiveScan, getScanDisplayLabel, getScanDisplayId, getTrackedScanId, isActiveScanStatus, matchesScanId, selectDefaultScanToCancel, } from "./scan.js";
|
|
11
13
|
import { createServiceApiKey, isServiceApiScope, listServiceApiKeys, revokeServiceApiKey, SERVICE_API_SCOPES, } from "./service-keys.js";
|
|
@@ -13,7 +15,7 @@ import { chooseCompany, createWorkspaceBinding, ensureAuthenticated, remediateMi
|
|
|
13
15
|
import { createWorkspaceBindingFromSummary, fetchCompanyCredits, fetchCompanyWorkspaces, findWorkspaceByRef, } from "./workspaces.js";
|
|
14
16
|
import { loadWorkspaceBinding, saveWorkspaceBinding } from "./workspace-binding.js";
|
|
15
17
|
import { commandSetup as runCliSetup } from "./setup.js";
|
|
16
|
-
import { getTelemetryStatus, setTelemetryEnabled, } from "./telemetry.js";
|
|
18
|
+
import { getTelemetryStatus, setTelemetryAuthKind, setTelemetryEnabled, setTelemetryOnboardingOutcome, } from "./telemetry.js";
|
|
17
19
|
import { commandUpdate as runCliUpdate } from "./update.js";
|
|
18
20
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
19
21
|
import path from "node:path";
|
|
@@ -54,13 +56,94 @@ const SERVICE_KEY_NAME_SCHEMA = z
|
|
|
54
56
|
.min(1)
|
|
55
57
|
.max(100)
|
|
56
58
|
.refine((value) => !SERVICE_KEY_NAME_CONTROL_CHARACTER_PATTERN.test(value));
|
|
59
|
+
function getRedeemTelemetryOutcome(error) {
|
|
60
|
+
switch (getApiErrorCode(error)) {
|
|
61
|
+
case "INVALID_CODE_FORMAT":
|
|
62
|
+
case "CODE_NOT_FOUND":
|
|
63
|
+
return "invalid";
|
|
64
|
+
case "CODE_EXPIRED":
|
|
65
|
+
case "CODE_REVOKED":
|
|
66
|
+
case "CODE_PAUSED":
|
|
67
|
+
return "expired";
|
|
68
|
+
case "CODE_EXHAUSTED":
|
|
69
|
+
return "exhausted";
|
|
70
|
+
case "RATE_LIMITED":
|
|
71
|
+
return "rate_limited";
|
|
72
|
+
case "TRIAL_CAPACITY":
|
|
73
|
+
return "capacity";
|
|
74
|
+
case "ACTIVE_GUEST_SESSION":
|
|
75
|
+
return "active_guest_session";
|
|
76
|
+
default:
|
|
77
|
+
return error instanceof Error && /trial code (?:format is invalid|is required)/i.test(error.message)
|
|
78
|
+
? "invalid"
|
|
79
|
+
: "error";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function getClaimTelemetryOutcome(error) {
|
|
83
|
+
switch (getApiErrorCode(error)) {
|
|
84
|
+
case "CLAIM_INVALID":
|
|
85
|
+
return "invalid";
|
|
86
|
+
case "CLAIM_EXPIRED":
|
|
87
|
+
return "claim_expired";
|
|
88
|
+
case "ALREADY_CLAIMED":
|
|
89
|
+
return "already_claimed";
|
|
90
|
+
case "SCAN_IN_PROGRESS":
|
|
91
|
+
return "scan_in_progress";
|
|
92
|
+
case "EMAIL_UNVERIFIED":
|
|
93
|
+
return "email_unverified";
|
|
94
|
+
case "FINGERPRINT_MISMATCH":
|
|
95
|
+
return "fingerprint_mismatch";
|
|
96
|
+
default:
|
|
97
|
+
return "error";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function getCommandAuthState(client, me) {
|
|
101
|
+
let credentials = null;
|
|
102
|
+
try {
|
|
103
|
+
credentials = await client.getCredentials();
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Local metadata is only for UX and telemetry; it must not break scan commands.
|
|
107
|
+
}
|
|
108
|
+
const guest = credentials?.guest === true || me.user.role?.trim().toLowerCase() === "guest";
|
|
109
|
+
return {
|
|
110
|
+
authKind: guest ? "guest" : "user",
|
|
111
|
+
// Ordinary device login intentionally keeps an unclaimed trial proof. Keep
|
|
112
|
+
// reminding the user after that auth-kind transition until claim consumes it.
|
|
113
|
+
guestCredentials: credentials &&
|
|
114
|
+
(credentials.guest === true || isClaimProofAvailable(credentials))
|
|
115
|
+
? credentials
|
|
116
|
+
: null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function logGuestScanFooter(authState, flags) {
|
|
120
|
+
if (isJsonMode(flags) || !authState.guestCredentials)
|
|
121
|
+
return;
|
|
122
|
+
const claimState = getClaimProofState(authState.guestCredentials);
|
|
123
|
+
if (claimState === "expired") {
|
|
124
|
+
logLine("Guest trial claim window expired — run 'apex login' to continue with a Cantina account.", flags);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const deadlineTime = parseClaimDeadlineMs(authState.guestCredentials.claimExpiresAt);
|
|
128
|
+
const deadline = deadlineTime !== null
|
|
129
|
+
? new Date(deadlineTime).toISOString()
|
|
130
|
+
: "the claim deadline";
|
|
131
|
+
if (claimState === "absent") {
|
|
132
|
+
logLine(`Guest trial claim proof is missing — run 'apex redeem <code>' again on this machine before ${deadline}, then run 'apex claim'.`, flags);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const message = authState.authKind === "guest"
|
|
136
|
+
? `Guest scan — create a Cantina account and run 'apex claim' by ${deadline} to keep it.`
|
|
137
|
+
: `Trial results are still unclaimed — run 'apex claim' by ${deadline} to keep them.`;
|
|
138
|
+
logLine(message, flags);
|
|
139
|
+
}
|
|
140
|
+
export function getRedeemExitCode(result) {
|
|
141
|
+
return result.redeemed ? 0 : 2;
|
|
142
|
+
}
|
|
57
143
|
async function saveCompanyDefault(companyId) {
|
|
58
144
|
const config = await loadConfig();
|
|
59
145
|
if (config.defaultCompanyId !== companyId) {
|
|
60
|
-
await
|
|
61
|
-
...config,
|
|
62
|
-
defaultCompanyId: companyId,
|
|
63
|
-
});
|
|
146
|
+
await updateConfig({ defaultCompanyId: companyId });
|
|
64
147
|
}
|
|
65
148
|
}
|
|
66
149
|
function formatCommandArgument(value) {
|
|
@@ -184,6 +267,28 @@ function formatFixReviewScanBalance(scanBalance) {
|
|
|
184
267
|
}
|
|
185
268
|
return `Fix review scans: ${detailParts.join(", ")}`;
|
|
186
269
|
}
|
|
270
|
+
function formatLiteScanBalance(scanBalance) {
|
|
271
|
+
const purchased = getOptionalCount(scanBalance.litePurchased);
|
|
272
|
+
const used = getOptionalCount(scanBalance.liteUsed);
|
|
273
|
+
const remaining = getOptionalCount(scanBalance.liteRemaining);
|
|
274
|
+
const available = remaining ??
|
|
275
|
+
(purchased !== null && used !== null ? Math.max(0, purchased - used) : null);
|
|
276
|
+
if (purchased === null && used === null && available === null) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const detailParts = [];
|
|
280
|
+
if (purchased !== null) {
|
|
281
|
+
detailParts.push(`${purchased} purchased`);
|
|
282
|
+
}
|
|
283
|
+
if (used !== null) {
|
|
284
|
+
detailParts.push(`${used} used`);
|
|
285
|
+
}
|
|
286
|
+
if (available !== null) {
|
|
287
|
+
const detailSuffix = detailParts.length ? ` (${detailParts.join(", ")})` : "";
|
|
288
|
+
return `Lite scan credits: ${available} available${detailSuffix}`;
|
|
289
|
+
}
|
|
290
|
+
return `Lite scan credits: ${detailParts.join(", ")}`;
|
|
291
|
+
}
|
|
187
292
|
function normalizeRequestedScanMode(value) {
|
|
188
293
|
if (value === "ultra" || value === "audit") {
|
|
189
294
|
return "audit";
|
|
@@ -191,6 +296,9 @@ function normalizeRequestedScanMode(value) {
|
|
|
191
296
|
if (value === "pr") {
|
|
192
297
|
return "pr";
|
|
193
298
|
}
|
|
299
|
+
if (value === "lite") {
|
|
300
|
+
return "lite";
|
|
301
|
+
}
|
|
194
302
|
return "standard";
|
|
195
303
|
}
|
|
196
304
|
function getFlagValues(flags, key) {
|
|
@@ -539,7 +647,13 @@ export async function initializeInteractiveSession(client, cwd, flags) {
|
|
|
539
647
|
});
|
|
540
648
|
}
|
|
541
649
|
export async function commandLogin(client, flags) {
|
|
542
|
-
|
|
650
|
+
// Explicit login is the escape hatch from an unavailable guest session. It
|
|
651
|
+
// intentionally bypasses ordinary-command claim steering while preserving
|
|
652
|
+
// the proof for an authoritative `apex claim` attempt.
|
|
653
|
+
const me = await login(client, {
|
|
654
|
+
noOpen: flags["no-open"] === true,
|
|
655
|
+
quiet: isJsonMode(flags),
|
|
656
|
+
});
|
|
543
657
|
if (isJsonMode(flags)) {
|
|
544
658
|
printJson(me);
|
|
545
659
|
return me;
|
|
@@ -547,6 +661,75 @@ export async function commandLogin(client, flags) {
|
|
|
547
661
|
logLine(`Authenticated as ${me.user.username ?? me.user.id}`, flags);
|
|
548
662
|
return me;
|
|
549
663
|
}
|
|
664
|
+
export async function commandRedeem(client, flags, code) {
|
|
665
|
+
try {
|
|
666
|
+
if (flags.replace === true) {
|
|
667
|
+
const warning = "Warning: --replace revokes the current guest trial and queues its unclaimed source, scans, and findings for deletion before redeeming the new code.";
|
|
668
|
+
if (isJsonMode(flags)) {
|
|
669
|
+
process.stderr.write(`${warning}\n`);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
logLine(warning, flags);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const result = await redeemOnboardingCode(client, {
|
|
676
|
+
code,
|
|
677
|
+
replace: flags.replace === true,
|
|
678
|
+
});
|
|
679
|
+
setTelemetryOnboardingOutcome(result.outcome);
|
|
680
|
+
if (isJsonMode(flags)) {
|
|
681
|
+
printJson(result);
|
|
682
|
+
return result;
|
|
683
|
+
}
|
|
684
|
+
if (!result.redeemed) {
|
|
685
|
+
logLine(`Trial redemption skipped: ${result.warning}.`, flags);
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
logLine(`Trial ${result.outcome}: ${result.grant.scans} standard scan${result.grant.scans === 1 ? "" : "s"}.`, flags);
|
|
689
|
+
logLine(`Guest session expires: ${result.guestSessionExpiresAt}`, flags);
|
|
690
|
+
logLine(`Claim deadline: ${result.claimDeadline}`, flags);
|
|
691
|
+
logLine(result.notice, flags);
|
|
692
|
+
logLine(`Terms and retention: ${result.termsUrl}`, flags);
|
|
693
|
+
return result;
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
setTelemetryOnboardingOutcome(getRedeemTelemetryOutcome(error));
|
|
697
|
+
throw error;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
export async function commandClaim(client, cwd, flags) {
|
|
701
|
+
const json = isJsonMode(flags);
|
|
702
|
+
try {
|
|
703
|
+
const result = await claimOnboardingSession(client, cwd, {
|
|
704
|
+
noOpen: json || flags["no-open"] === true,
|
|
705
|
+
quiet: json,
|
|
706
|
+
...(json
|
|
707
|
+
? {
|
|
708
|
+
onVerification: async (login) => {
|
|
709
|
+
process.stderr.write(`Complete Cantina account setup with code ${escapeTerminalControlCharacters(login.userCode)} at ${escapeTerminalControlCharacters(login.verificationUrl)}\n`);
|
|
710
|
+
},
|
|
711
|
+
}
|
|
712
|
+
: {}),
|
|
713
|
+
});
|
|
714
|
+
setTelemetryOnboardingOutcome("claimed");
|
|
715
|
+
if (json) {
|
|
716
|
+
printJson(result);
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
logLine(`Claimed into: ${result.company.name ?? result.company.handle ?? result.company.id}`, flags);
|
|
720
|
+
if (result.scanUrl) {
|
|
721
|
+
logLine(`View: ${result.scanUrl}`, flags);
|
|
722
|
+
}
|
|
723
|
+
if (result.discountCode) {
|
|
724
|
+
logLine(`First credit-pack discount: ${result.discountCode}`, flags);
|
|
725
|
+
}
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
setTelemetryOnboardingOutcome(getClaimTelemetryOutcome(error));
|
|
730
|
+
throw error;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
550
733
|
export async function commandCredits(client, cwd, flags) {
|
|
551
734
|
const me = await ensureAuthenticated(client, flags);
|
|
552
735
|
const binding = await loadWorkspaceBinding(cwd);
|
|
@@ -574,14 +757,57 @@ export async function commandCredits(client, cwd, flags) {
|
|
|
574
757
|
if (redeemableFixReviewScans && redeemableFixReviewScans > 0) {
|
|
575
758
|
logLine(`Redeemable fix review scans from standard credits: ${redeemableFixReviewScans}`, flags);
|
|
576
759
|
}
|
|
760
|
+
const liteBalance = formatLiteScanBalance(payload.scanBalance);
|
|
761
|
+
if (liteBalance) {
|
|
762
|
+
logLine(liteBalance, flags);
|
|
763
|
+
}
|
|
577
764
|
logLine(`Scans enabled: ${payload.scansEnabled ? "yes" : "no"}`, flags);
|
|
578
765
|
return payload;
|
|
579
766
|
}
|
|
580
767
|
export async function commandLogout(client, flags) {
|
|
581
|
-
await
|
|
768
|
+
const credentials = await client.getCredentials().catch(() => null);
|
|
769
|
+
const proofOnly = isProofOnlyOnboardingCredentials(credentials);
|
|
770
|
+
const hasGuestLifecycle = credentials?.guest === true || proofOnly;
|
|
771
|
+
if (hasGuestLifecycle && flags.force !== true) {
|
|
772
|
+
if (!canPromptForConfirmation(flags)) {
|
|
773
|
+
throw new Error("Logging out revokes this guest trial and queues its unclaimed data for deletion. Re-run with --force to confirm.");
|
|
774
|
+
}
|
|
775
|
+
const approved = await confirm("Logging out revokes this guest trial and queues all unclaimed source, scans, and findings for deletion. Continue?", false);
|
|
776
|
+
if (!approved) {
|
|
777
|
+
logLine("Guest logout cancelled; the trial and claim proof were kept.", flags);
|
|
778
|
+
return {
|
|
779
|
+
loggedOut: false,
|
|
780
|
+
guestTrialRevoked: false,
|
|
781
|
+
confirmationRequired: true,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const guestTrialRevoked = hasGuestLifecycle
|
|
786
|
+
? await revokeGuestOnboardingSession(client, {
|
|
787
|
+
clearLocalOnTerminal: true,
|
|
788
|
+
expectedCredentials: credentials ?? undefined,
|
|
789
|
+
})
|
|
790
|
+
: false;
|
|
791
|
+
if (!hasGuestLifecycle) {
|
|
792
|
+
await logout(client, credentials);
|
|
793
|
+
}
|
|
794
|
+
const serverRevocationUnavailable = hasGuestLifecycle && !guestTrialRevoked;
|
|
582
795
|
if (!isJsonMode(flags)) {
|
|
583
|
-
logLine(
|
|
796
|
+
logLine(guestTrialRevoked
|
|
797
|
+
? "Guest trial revoked; unclaimed trial data is queued for deletion."
|
|
798
|
+
: serverRevocationUnavailable
|
|
799
|
+
? proofOnly
|
|
800
|
+
? "Guest trial proof cleared locally, but server revocation could not be confirmed."
|
|
801
|
+
: "Guest access cleared locally, but the terminal server session could not be revoked."
|
|
802
|
+
: "Logged out", flags);
|
|
584
803
|
}
|
|
804
|
+
return {
|
|
805
|
+
loggedOut: true,
|
|
806
|
+
guestTrialRevoked,
|
|
807
|
+
...(serverRevocationUnavailable
|
|
808
|
+
? { serverRevocationUnavailable: true }
|
|
809
|
+
: {}),
|
|
810
|
+
};
|
|
585
811
|
}
|
|
586
812
|
export async function commandServiceKeyCreate(client, flags) {
|
|
587
813
|
const name = normalizeServiceKeyName(getFlagString(flags, "name"));
|
|
@@ -745,7 +971,7 @@ export async function commandDoctor(client, cwd, flags) {
|
|
|
745
971
|
if (isJsonMode(flags)) {
|
|
746
972
|
const payload = {
|
|
747
973
|
ok: false,
|
|
748
|
-
error:
|
|
974
|
+
error: formatApiError(error),
|
|
749
975
|
};
|
|
750
976
|
printJson(payload);
|
|
751
977
|
if (error instanceof Error) {
|
|
@@ -757,7 +983,9 @@ export async function commandDoctor(client, cwd, flags) {
|
|
|
757
983
|
}
|
|
758
984
|
}
|
|
759
985
|
export async function commandStatus(client, cwd, flags) {
|
|
760
|
-
await ensureAuthenticated(client, flags);
|
|
986
|
+
const me = await ensureAuthenticated(client, flags);
|
|
987
|
+
const authState = await getCommandAuthState(client, me);
|
|
988
|
+
setTelemetryAuthKind(authState.authKind);
|
|
761
989
|
const binding = await requireWorkspaceBinding(cwd);
|
|
762
990
|
const requestedScanId = getFlagString(flags, "scan");
|
|
763
991
|
const scanId = requestedScanId ?? binding.lastScanId;
|
|
@@ -778,6 +1006,7 @@ export async function commandStatus(client, cwd, flags) {
|
|
|
778
1006
|
if (binding.lastScanUrl && (!requestedScanId || requestedScanId === binding.lastScanId)) {
|
|
779
1007
|
logLine(`View: ${binding.lastScanUrl}`, flags);
|
|
780
1008
|
}
|
|
1009
|
+
logGuestScanFooter(authState, flags);
|
|
781
1010
|
return { binding, scanId, progress };
|
|
782
1011
|
}
|
|
783
1012
|
export async function commandConnect(client, cwd, flags, provider) {
|
|
@@ -809,6 +1038,8 @@ export async function commandScan(client, cwd, flags) {
|
|
|
809
1038
|
throw new Error("PR scans require at least one --pr <number> selection.");
|
|
810
1039
|
}
|
|
811
1040
|
const me = await ensureAuthenticated(client, flags);
|
|
1041
|
+
const authState = await getCommandAuthState(client, me);
|
|
1042
|
+
setTelemetryAuthKind(authState.authKind);
|
|
812
1043
|
const selection = await selectWorkspaceTarget(cwd, me, flags);
|
|
813
1044
|
const result = await withLoadingIndicator("Resolving workspace and scan plan...", flags, () => resolveWorkspaceSelection(client, selection));
|
|
814
1045
|
if (!result.resolve.workspaceId) {
|
|
@@ -855,16 +1086,17 @@ export async function commandScan(client, cwd, flags) {
|
|
|
855
1086
|
},
|
|
856
1087
|
}));
|
|
857
1088
|
}
|
|
858
|
-
else if (requestedMode === "audit") {
|
|
1089
|
+
else if (requestedMode === "audit" || requestedMode === "lite") {
|
|
1090
|
+
const modeLabel = requestedMode === "audit" ? "Audit" : "Lite";
|
|
859
1091
|
if (!supportsLegacyRemoteFlow(result.resolve.plannedSources)) {
|
|
860
|
-
throw new Error(
|
|
1092
|
+
throw new Error(`${modeLabel} scans currently require provider-backed GitHub or GitLab repositories without local snapshot fallbacks.`);
|
|
861
1093
|
}
|
|
862
|
-
const legacyResolve = await withLoadingIndicator(
|
|
1094
|
+
const legacyResolve = await withLoadingIndicator(`Preparing ${modeLabel.toLowerCase()} scan...`, flags, () => resolveWorkspaceSelectionLegacy(client, selection, resolvedWorkspaceId));
|
|
863
1095
|
if (!legacyResolve.workspaceId) {
|
|
864
1096
|
throw new Error("Workspace resolution did not return a workspaceId.");
|
|
865
1097
|
}
|
|
866
1098
|
workspaceId = legacyResolve.workspaceId;
|
|
867
|
-
scan = await withLoadingIndicator(
|
|
1099
|
+
scan = await withLoadingIndicator(`Starting ${modeLabel.toLowerCase()} scan...`, flags, () => client.request(`/api/cli/v1/local-workspaces/${encodeURIComponent(workspaceId)}/scan`, {
|
|
868
1100
|
method: "POST",
|
|
869
1101
|
json: {
|
|
870
1102
|
mode: requestedMode,
|
|
@@ -890,14 +1122,19 @@ export async function commandScan(client, cwd, flags) {
|
|
|
890
1122
|
});
|
|
891
1123
|
});
|
|
892
1124
|
}
|
|
1125
|
+
const trackedScanId = getTrackedScanId(scan);
|
|
893
1126
|
const binding = {
|
|
894
1127
|
...createWorkspaceBinding(result, result.binding),
|
|
895
1128
|
workspaceId,
|
|
896
|
-
lastScanId:
|
|
1129
|
+
lastScanId: trackedScanId,
|
|
897
1130
|
lastScanUrl: scan.scanUrl ?? null,
|
|
898
1131
|
};
|
|
899
|
-
await
|
|
900
|
-
|
|
1132
|
+
const [workspaceBindingWrite, companyDefaultWrite] = await Promise.allSettled([
|
|
1133
|
+
Promise.resolve().then(() => saveWorkspaceBinding(cwd, binding)),
|
|
1134
|
+
Promise.resolve().then(() => saveCompanyDefault(result.company.id)),
|
|
1135
|
+
]);
|
|
1136
|
+
const workspaceBindingSaved = workspaceBindingWrite.status === "fulfilled";
|
|
1137
|
+
const companyDefaultSaved = companyDefaultWrite.status === "fulfilled";
|
|
901
1138
|
const summarySources = prScanSource
|
|
902
1139
|
? getPrScanSelectedSources(result.sources, prScanSource)
|
|
903
1140
|
: result.sources;
|
|
@@ -916,6 +1153,14 @@ export async function commandScan(client, cwd, flags) {
|
|
|
916
1153
|
apexScanId: scan.apexScanId ?? scan.bedrockScanId ?? null,
|
|
917
1154
|
},
|
|
918
1155
|
binding,
|
|
1156
|
+
...(!workspaceBindingSaved || !companyDefaultSaved
|
|
1157
|
+
? {
|
|
1158
|
+
localPersistence: {
|
|
1159
|
+
workspaceBindingSaved,
|
|
1160
|
+
companyDefaultSaved,
|
|
1161
|
+
},
|
|
1162
|
+
}
|
|
1163
|
+
: {}),
|
|
919
1164
|
};
|
|
920
1165
|
if (isJsonMode(flags)) {
|
|
921
1166
|
printJson(payload);
|
|
@@ -924,16 +1169,31 @@ export async function commandScan(client, cwd, flags) {
|
|
|
924
1169
|
logLine(`Authenticated as ${me.user.username ?? me.user.id}`, flags);
|
|
925
1170
|
logLine(`Company: ${result.company.handle ?? result.company.id}`, flags);
|
|
926
1171
|
logLine(`Workspace: ${result.workspaceName}`, flags);
|
|
1172
|
+
if (!workspaceBindingSaved) {
|
|
1173
|
+
logLine(`Warning: the scan started, but Apex could not save this directory's workspace binding. Keep scan ID ${trackedScanId ?? "from the result"}; repair local repository permissions and bind this directory again before using status or findings commands here.`, flags);
|
|
1174
|
+
}
|
|
1175
|
+
if (!companyDefaultSaved) {
|
|
1176
|
+
logLine("Warning: the scan started, but Apex could not save the default company. Pass --company explicitly until local Apex config permissions are repaired.", flags);
|
|
1177
|
+
}
|
|
927
1178
|
logLine(`Sources selected: ${summarySources.length}`, flags);
|
|
928
1179
|
logLine(`Plan: ${summaryPlannedSources.length - localSnapshotCount} remote, ${localSnapshotCount} local snapshot`, flags);
|
|
929
|
-
logLine(`Scan: ${scan.status}${
|
|
1180
|
+
logLine(`Scan: ${scan.status}${trackedScanId ? ` (${trackedScanId})` : ""}`, flags);
|
|
930
1181
|
if (scan.scanUrl) {
|
|
931
1182
|
logLine(`View: ${scan.scanUrl}`, flags);
|
|
932
1183
|
}
|
|
933
1184
|
if (scan.fullScanRecommendation) {
|
|
934
1185
|
logLine(`Recommendation: ${scan.fullScanRecommendation}`, flags);
|
|
935
1186
|
}
|
|
936
|
-
|
|
1187
|
+
if (workspaceBindingSaved) {
|
|
1188
|
+
logLine("Next: run `apex status` to watch progress, then `apex findings` when the scan finishes.", flags);
|
|
1189
|
+
}
|
|
1190
|
+
else if (trackedScanId) {
|
|
1191
|
+
logLine(`Next: repair local repository permissions, run \`apex workspace use ${workspaceId}\`, then use \`apex status --scan ${trackedScanId}\` and \`apex findings --scan ${trackedScanId}\`.`, flags);
|
|
1192
|
+
}
|
|
1193
|
+
else {
|
|
1194
|
+
logLine(`Next: repair local repository permissions, then run \`apex workspace use ${workspaceId}\` before using status or findings.`, flags);
|
|
1195
|
+
}
|
|
1196
|
+
logGuestScanFooter(authState, flags);
|
|
937
1197
|
return payload;
|
|
938
1198
|
}
|
|
939
1199
|
export async function commandWorkspaces(client, cwd, flags) {
|
|
@@ -1131,7 +1391,9 @@ export async function commandCancelScan(client, cwd, flags, requestedScanId) {
|
|
|
1131
1391
|
return payload;
|
|
1132
1392
|
}
|
|
1133
1393
|
export async function commandFindings(client, cwd, flags) {
|
|
1134
|
-
await ensureAuthenticated(client, flags);
|
|
1394
|
+
const me = await ensureAuthenticated(client, flags);
|
|
1395
|
+
const authState = await getCommandAuthState(client, me);
|
|
1396
|
+
setTelemetryAuthKind(authState.authKind);
|
|
1135
1397
|
const binding = await requireWorkspaceBinding(cwd);
|
|
1136
1398
|
const { scan, response } = await withLoadingIndicator("Loading findings...", flags, async () => {
|
|
1137
1399
|
const scan = await resolveScanSelection({
|
|
@@ -1160,6 +1422,7 @@ export async function commandFindings(client, cwd, flags) {
|
|
|
1160
1422
|
logLine(`Scan: ${response.scan.displayName} (${response.scan.status})`, flags);
|
|
1161
1423
|
if (response.findings.length === 0) {
|
|
1162
1424
|
logLine("Findings: none for this scan.", flags);
|
|
1425
|
+
logGuestScanFooter(authState, flags);
|
|
1163
1426
|
return payload;
|
|
1164
1427
|
}
|
|
1165
1428
|
for (const finding of response.findings) {
|
|
@@ -1169,6 +1432,7 @@ export async function commandFindings(client, cwd, flags) {
|
|
|
1169
1432
|
logLine(`View: ${finding.findingUrl}`, flags);
|
|
1170
1433
|
}
|
|
1171
1434
|
}
|
|
1435
|
+
logGuestScanFooter(authState, flags);
|
|
1172
1436
|
return payload;
|
|
1173
1437
|
}
|
|
1174
1438
|
export async function commandFindingComment(client, cwd, flags, requestedFindingRef, content, parentCommentId) {
|