sentinelayer-cli 0.46.3 → 0.46.5
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/README.md +16 -0
- package/package.json +1 -1
- package/src/auth/gate.js +14 -7
- package/src/auth/service.js +34 -15
- package/src/auth/session-store.js +68 -47
- package/src/commands/auth.js +24 -4
- package/src/commands/session.js +116 -56
- package/src/mcp/bridge-inputs.js +3 -0
- package/src/session/admission.js +222 -66
- package/src/session/agent-access.js +137 -0
- package/src/session/agent-registry.js +2 -1
- package/src/session/listener-process.js +427 -57
package/README.md
CHANGED
|
@@ -46,6 +46,22 @@ sl watch run-events --run-id <run-id>
|
|
|
46
46
|
|
|
47
47
|
Windows PowerShell note: `sl` is a built-in alias for `Set-Location`. Use `sentinelayer-cli` (or short alias `slc`) instead.
|
|
48
48
|
|
|
49
|
+
### Human sign-in and agent identity
|
|
50
|
+
|
|
51
|
+
`sl auth login --skip-browser-open` prints the approval URL before waiting;
|
|
52
|
+
in JSON mode the URL goes to stderr. A server outage during `auth status` is
|
|
53
|
+
unavailable validation, not logout. Do not delete credential encryption keys
|
|
54
|
+
or log out to repair a keyring/file-permission error.
|
|
55
|
+
|
|
56
|
+
An admitted agent receives its own AIdenID email, Ed25519-backed passport and
|
|
57
|
+
room-scoped credential after human approval. Its email and cryptographic
|
|
58
|
+
identity remain visible on reconnect; email alone never grants access.
|
|
59
|
+
An interrupted claim or missing credential can be recovered with the retained
|
|
60
|
+
agent key. For a stale active credential, repeat the original join with
|
|
61
|
+
`--recover-admission`, the same `--goal`, `--scope` and `--ttl`.
|
|
62
|
+
Recovery requires API support, preserves the original expiry, and cannot revive
|
|
63
|
+
revoked, stopped or expired permissions. It is not TTL renewal.
|
|
64
|
+
|
|
49
65
|
## MCP Live Entry Points And Incident Response
|
|
50
66
|
|
|
51
67
|
Live MCP entry points:
|
package/package.json
CHANGED
package/src/auth/gate.js
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import pc from "picocolors";
|
|
7
7
|
import { resolveActiveAuthSession } from "./service.js";
|
|
8
|
+
import { readStoredSessionMetadata } from "./session-store.js";
|
|
8
9
|
import { resolveAgentAdmissionTarget } from "../session/admission-auth.js";
|
|
9
10
|
import { authLoginHint } from "../ui/command-hints.js";
|
|
10
11
|
import { isAuthenticated } from "./credential-destinations.js";
|
|
@@ -378,7 +379,9 @@ export async function checkAuthGate(args) {
|
|
|
378
379
|
resolveError = "session_invalid";
|
|
379
380
|
}
|
|
380
381
|
} else {
|
|
381
|
-
|
|
382
|
+
// A keyring/decryption failure leaves the metadata intact. That is not
|
|
383
|
+
// absence, and recommending logout would destroy recoverable sign-in.
|
|
384
|
+
resolveError = (await readStoredSessionMetadata()) ? "session_token_missing" : "no_session";
|
|
382
385
|
}
|
|
383
386
|
} catch (error) {
|
|
384
387
|
resolveError = error instanceof Error ? `session_read_error: ${error.message}` : "session_read_error";
|
|
@@ -405,15 +408,19 @@ export function printAuthRequired(failureReason = null) {
|
|
|
405
408
|
if (reason === "session_expired") {
|
|
406
409
|
console.error(" Your stored session has expired. Log in again:");
|
|
407
410
|
} else if (reason === "session_token_missing") {
|
|
408
|
-
console.error(" Your session metadata is present but
|
|
409
|
-
console.error("
|
|
411
|
+
console.error(" Your session metadata is present but its credential cannot be read.");
|
|
412
|
+
console.error(" Check the current user/home, keyring, encryption key and file permissions.");
|
|
413
|
+
console.error(" Your stored credentials were not cleared; repair storage before signing in again.");
|
|
410
414
|
console.error("");
|
|
411
|
-
|
|
412
|
-
|
|
415
|
+
process.exitCode = 1;
|
|
416
|
+
return;
|
|
413
417
|
} else if (reason && reason.startsWith("session_read_error")) {
|
|
414
418
|
console.error(" Session read failed: " + pc.yellow(reason.replace(/^session_read_error:\s*/, "")));
|
|
415
|
-
console.error("
|
|
416
|
-
console.error("
|
|
419
|
+
console.error(" Check the current user/home and credential-file permissions.");
|
|
420
|
+
console.error(" Your stored credentials were not cleared; do not log out to repair a read error.");
|
|
421
|
+
console.error("");
|
|
422
|
+
process.exitCode = 1;
|
|
423
|
+
return;
|
|
417
424
|
} else {
|
|
418
425
|
console.error(" Log in to SentinelLayer to use CLI commands:");
|
|
419
426
|
}
|
package/src/auth/service.js
CHANGED
|
@@ -7,6 +7,7 @@ import open from "open";
|
|
|
7
7
|
import { loadConfig } from "../config/service.js";
|
|
8
8
|
import { scopedAdmissionAuth } from "./admission-scope.js";
|
|
9
9
|
import {
|
|
10
|
+
CredentialDestinationRefused,
|
|
10
11
|
DEFAULT_API_URL,
|
|
11
12
|
assertTrustedApiUrl,
|
|
12
13
|
credentialFor,
|
|
@@ -453,6 +454,7 @@ async function rotateStoredApiTokenIfNeeded({
|
|
|
453
454
|
tokenPrefix: issued.token_prefix || null,
|
|
454
455
|
tokenExpiresAt: issued.expires_at || null,
|
|
455
456
|
user: session.user,
|
|
457
|
+
aidenid: session.aidenid,
|
|
456
458
|
},
|
|
457
459
|
{ homeDir }
|
|
458
460
|
);
|
|
@@ -490,7 +492,8 @@ async function rotateStoredApiTokenIfNeeded({
|
|
|
490
492
|
* tokenTtlDays?: number,
|
|
491
493
|
* ide?: string,
|
|
492
494
|
* cliVersion?: string,
|
|
493
|
-
* homeDir?: string
|
|
495
|
+
* homeDir?: string,
|
|
496
|
+
* onAuthorize?: (details: { authorizeUrl: string, browserOpened: boolean }) => void | Promise<void>
|
|
494
497
|
* }} [options]
|
|
495
498
|
* @returns {Promise<{
|
|
496
499
|
* apiUrl: string,
|
|
@@ -521,6 +524,7 @@ export async function loginAndPersistSession({
|
|
|
521
524
|
ide = DEFAULT_IDE_NAME,
|
|
522
525
|
cliVersion = "",
|
|
523
526
|
homeDir,
|
|
527
|
+
onAuthorize = () => {},
|
|
524
528
|
} = {}) {
|
|
525
529
|
const apiUrl = await resolveApiUrl({ cwd, env, explicitApiUrl, homeDir });
|
|
526
530
|
// The token this flow issues is the user's, so the API that issues it must be the configured
|
|
@@ -548,6 +552,10 @@ export async function loginAndPersistSession({
|
|
|
548
552
|
}
|
|
549
553
|
}
|
|
550
554
|
|
|
555
|
+
// Headless hosts must receive the URL before approval polling can complete.
|
|
556
|
+
// The command renders this on stderr in JSON mode, keeping stdout parseable.
|
|
557
|
+
await onAuthorize({ authorizeUrl, browserOpened });
|
|
558
|
+
|
|
551
559
|
const approval = await pollCliAuthSession({
|
|
552
560
|
apiUrl,
|
|
553
561
|
sessionId: String(session.session_id || "").trim(),
|
|
@@ -768,7 +776,8 @@ export async function resolveActiveAuthSession({
|
|
|
768
776
|
* homeDir?: string
|
|
769
777
|
* }} [options]
|
|
770
778
|
* @returns {Promise<{
|
|
771
|
-
* authenticated: boolean,
|
|
779
|
+
* authenticated: boolean | null,
|
|
780
|
+
* validation: "none" | "not_checked" | "verified" | "rejected" | "unavailable" | "storage_unavailable",
|
|
772
781
|
* apiUrl: string,
|
|
773
782
|
* source: string | null,
|
|
774
783
|
* storage: string | null,
|
|
@@ -805,20 +814,25 @@ export async function getAuthStatus({
|
|
|
805
814
|
});
|
|
806
815
|
|
|
807
816
|
if (!session) {
|
|
817
|
+
// Encrypted metadata can survive a missing/unreadable key or keyring.
|
|
818
|
+
// Distinguish recoverable storage from actual absence without creating a
|
|
819
|
+
// replacement key, validating remotely, or claiming authentication.
|
|
820
|
+
const stored = await readStoredSessionMetadata({ homeDir });
|
|
808
821
|
return {
|
|
809
|
-
authenticated: false,
|
|
822
|
+
authenticated: stored ? null : false,
|
|
823
|
+
validation: stored ? "storage_unavailable" : "none",
|
|
810
824
|
apiUrl: await resolveApiUrl({ cwd, env, explicitApiUrl, homeDir }),
|
|
811
|
-
source: null,
|
|
812
|
-
storage: null,
|
|
813
|
-
user: null,
|
|
814
|
-
aidenid: null,
|
|
825
|
+
source: stored ? "storage_unknown" : null,
|
|
826
|
+
storage: stored?.storage || null,
|
|
827
|
+
user: stored ? normalizeUser(stored.user || {}) : null,
|
|
828
|
+
aidenid: stored?.aidenid || null,
|
|
815
829
|
remoteUser: null,
|
|
816
830
|
remoteError: null,
|
|
817
831
|
rotated: false,
|
|
818
|
-
tokenExpiresAt: null,
|
|
819
|
-
tokenPrefix: null,
|
|
820
|
-
tokenId: null,
|
|
821
|
-
filePath: null,
|
|
832
|
+
tokenExpiresAt: stored?.tokenExpiresAt || null,
|
|
833
|
+
tokenPrefix: stored?.tokenPrefix || null,
|
|
834
|
+
tokenId: stored?.tokenId || null,
|
|
835
|
+
filePath: stored?.filePath || null,
|
|
822
836
|
};
|
|
823
837
|
}
|
|
824
838
|
|
|
@@ -829,12 +843,12 @@ export async function getAuthStatus({
|
|
|
829
843
|
remoteUser = normalizeUser(await fetchCurrentUser({ apiUrl: session.apiUrl, credential: session.credential }));
|
|
830
844
|
} catch (error) {
|
|
831
845
|
remoteError =
|
|
832
|
-
error instanceof SentinelayerApiError
|
|
846
|
+
error instanceof SentinelayerApiError || error instanceof CredentialDestinationRefused
|
|
833
847
|
? {
|
|
834
848
|
code: error.code,
|
|
835
849
|
message: error.message,
|
|
836
|
-
status: error.status,
|
|
837
|
-
requestId: error.requestId,
|
|
850
|
+
status: error instanceof CredentialDestinationRefused ? 403 : error.status,
|
|
851
|
+
requestId: error.requestId || null,
|
|
838
852
|
}
|
|
839
853
|
: {
|
|
840
854
|
code: "UNKNOWN",
|
|
@@ -845,8 +859,13 @@ export async function getAuthStatus({
|
|
|
845
859
|
}
|
|
846
860
|
}
|
|
847
861
|
|
|
862
|
+
const validationUnavailable = remoteError && (
|
|
863
|
+
[408, 425, 429, 499].includes(remoteError.status) || remoteError.status >= 500
|
|
864
|
+
);
|
|
848
865
|
return {
|
|
849
|
-
|
|
866
|
+
// Unknown remote health is neither a successful validation nor a logout.
|
|
867
|
+
authenticated: validationUnavailable ? null : !remoteError,
|
|
868
|
+
validation: validationUnavailable ? "unavailable" : remoteError ? "rejected" : checkRemote ? "verified" : "not_checked",
|
|
850
869
|
apiUrl: session.apiUrl,
|
|
851
870
|
source: session.source,
|
|
852
871
|
storage: session.storage,
|
|
@@ -135,60 +135,68 @@ function resolveLegacyCredentialsKeyPath({ homeDir } = {}) {
|
|
|
135
135
|
return path.join(resolvedHome, ".sentinelayer", "credentials.key");
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
async function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
} catch (error) {
|
|
148
|
-
if (!(error && typeof error === "object" && error.code === "ENOENT")) {
|
|
138
|
+
async function readFileKeyAt(keyPath) {
|
|
139
|
+
// Exclusive-create fallback filesystems may expose a partially written key.
|
|
140
|
+
// Retry that bounded race, but never replace an invalid or unreadable key.
|
|
141
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
142
|
+
let raw;
|
|
143
|
+
try {
|
|
144
|
+
raw = await fsp.readFile(keyPath, "utf-8");
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error?.code === "ENOENT") return null;
|
|
149
147
|
throw error;
|
|
150
148
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
await fsp.mkdir(path.dirname(keyPath), { recursive: true, mode: 0o700 });
|
|
158
|
-
await fsp.writeFile(keyPath, legacyKey.toString("base64"), { encoding: "utf-8", mode: 0o600 });
|
|
159
|
-
try {
|
|
160
|
-
await fsp.chmod(keyPath, 0o600);
|
|
161
|
-
} catch {
|
|
162
|
-
// Windows does not reliably support POSIX chmod semantics.
|
|
163
|
-
}
|
|
164
|
-
let verified = false;
|
|
165
|
-
try {
|
|
166
|
-
const verifyRaw = await fsp.readFile(keyPath, "utf-8");
|
|
167
|
-
const verifyKey = Buffer.from(String(verifyRaw || "").trim(), "base64");
|
|
168
|
-
verified = verifyKey.length === 32 && verifyKey.equals(legacyKey);
|
|
169
|
-
} catch {
|
|
170
|
-
verified = false;
|
|
171
|
-
}
|
|
172
|
-
if (verified) {
|
|
173
|
-
await fsp.rm(legacyKeyPath, { force: true });
|
|
174
|
-
}
|
|
175
|
-
return legacyKey;
|
|
176
|
-
}
|
|
177
|
-
} catch (error) {
|
|
178
|
-
if (!(error && typeof error === "object" && error.code === "ENOENT")) {
|
|
149
|
+
const encoded = String(raw || "").trim();
|
|
150
|
+
const key = Buffer.from(encoded, "base64");
|
|
151
|
+
if (key.length === 32 && key.toString("base64") === encoded) return key;
|
|
152
|
+
if (attempt >= 5) {
|
|
153
|
+
const error = new Error("Stored credential encryption key is invalid; it was not replaced.");
|
|
154
|
+
error.code = "CREDENTIALS_KEY_INVALID";
|
|
179
155
|
throw error;
|
|
180
156
|
}
|
|
157
|
+
await delayMilliseconds(20);
|
|
181
158
|
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function readFileKey({ homeDir } = {}) {
|
|
162
|
+
return (await readFileKeyAt(resolveCredentialsKeyPath({ homeDir }))) ||
|
|
163
|
+
(await readFileKeyAt(resolveLegacyCredentialsKeyPath({ homeDir })));
|
|
164
|
+
}
|
|
182
165
|
|
|
183
|
-
|
|
166
|
+
async function loadOrCreateFileKey({ homeDir } = {}) {
|
|
167
|
+
const keyPath = resolveCredentialsKeyPath({ homeDir });
|
|
168
|
+
const existing = await readFileKeyAt(keyPath);
|
|
169
|
+
if (existing) return existing;
|
|
170
|
+
const legacyKey = await readFileKeyAt(resolveLegacyCredentialsKeyPath({ homeDir }));
|
|
171
|
+
const key = legacyKey || crypto.randomBytes(32);
|
|
172
|
+
const encoded = key.toString("base64");
|
|
184
173
|
await fsp.mkdir(path.dirname(keyPath), { recursive: true, mode: 0o700 });
|
|
185
|
-
|
|
174
|
+
const tmpPath = `${keyPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
186
175
|
try {
|
|
187
|
-
await fsp.
|
|
188
|
-
|
|
189
|
-
|
|
176
|
+
await fsp.writeFile(tmpPath, encoded, { encoding: "utf-8", mode: 0o600, flag: "wx" });
|
|
177
|
+
await syncFile(tmpPath);
|
|
178
|
+
try {
|
|
179
|
+
// Publish complete bytes once. Every concurrent writer then uses the winner.
|
|
180
|
+
await fsp.link(tmpPath, keyPath);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error?.code !== "EEXIST") {
|
|
183
|
+
if (!["ENOTSUP", "ENOSYS", "EXDEV", "EPERM"].includes(error?.code)) throw error;
|
|
184
|
+
try {
|
|
185
|
+
await fsp.writeFile(keyPath, encoded, { encoding: "utf-8", mode: 0o600, flag: "wx" });
|
|
186
|
+
} catch (fallbackError) {
|
|
187
|
+
if (fallbackError?.code !== "EEXIST") throw fallbackError;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const persisted = await readFileKeyAt(keyPath);
|
|
192
|
+
if (!persisted) throw new Error("Credential encryption key could not be persisted.");
|
|
193
|
+
await syncFile(keyPath);
|
|
194
|
+
await syncDirectory(path.dirname(keyPath));
|
|
195
|
+
// Keep the legacy key: deleting it can race a read-only legacy reader.
|
|
196
|
+
return persisted;
|
|
197
|
+
} finally {
|
|
198
|
+
await fsp.rm(tmpPath, { force: true }).catch(() => {});
|
|
190
199
|
}
|
|
191
|
-
return key;
|
|
192
200
|
}
|
|
193
201
|
|
|
194
202
|
function encryptToken(token, key) {
|
|
@@ -540,7 +548,11 @@ async function tryDecryptFileToken({ metadata, homeDir }) {
|
|
|
540
548
|
return null;
|
|
541
549
|
}
|
|
542
550
|
try {
|
|
543
|
-
const key = await
|
|
551
|
+
const key = await readFileKey({ homeDir });
|
|
552
|
+
if (!key) {
|
|
553
|
+
emitSessionWarning("FILE_CREDENTIAL_UNAVAILABLE", { reason: "key_missing", source: "file-encrypted" });
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
544
556
|
const token = decryptToken(
|
|
545
557
|
{
|
|
546
558
|
tokenEncrypted: metadata.tokenEncrypted,
|
|
@@ -550,7 +562,13 @@ async function tryDecryptFileToken({ metadata, homeDir }) {
|
|
|
550
562
|
key
|
|
551
563
|
);
|
|
552
564
|
return token || null;
|
|
553
|
-
} catch {
|
|
565
|
+
} catch (error) {
|
|
566
|
+
emitSessionWarning("FILE_CREDENTIAL_UNAVAILABLE", {
|
|
567
|
+
reason: error?.code === "EACCES" || error?.code === "EPERM"
|
|
568
|
+
? "key_unreadable"
|
|
569
|
+
: error?.code === "CREDENTIALS_KEY_INVALID" ? "key_invalid" : "decryption_failed",
|
|
570
|
+
source: "file-encrypted",
|
|
571
|
+
});
|
|
554
572
|
return null;
|
|
555
573
|
}
|
|
556
574
|
}
|
|
@@ -722,6 +740,7 @@ export async function readStoredSessionMetadata({ homeDir } = {}) {
|
|
|
722
740
|
* tokenPrefix?: string | null,
|
|
723
741
|
* tokenExpiresAt?: string | null,
|
|
724
742
|
* user?: Record<string, unknown>
|
|
743
|
+
* aidenid?: Record<string, unknown> | null
|
|
725
744
|
* }} [session]
|
|
726
745
|
* @param {{ homeDir?: string }} [options]
|
|
727
746
|
* @returns {Promise<{
|
|
@@ -754,6 +773,7 @@ export async function writeStoredSession(
|
|
|
754
773
|
tokenPrefix = null,
|
|
755
774
|
tokenExpiresAt = null,
|
|
756
775
|
user = {},
|
|
776
|
+
aidenid = null,
|
|
757
777
|
} = {},
|
|
758
778
|
{ homeDir } = {}
|
|
759
779
|
) {
|
|
@@ -778,6 +798,7 @@ export async function writeStoredSession(
|
|
|
778
798
|
tokenPrefix,
|
|
779
799
|
tokenExpiresAt,
|
|
780
800
|
user: normalizeUser(user),
|
|
801
|
+
aidenid,
|
|
781
802
|
createdAt: existingMetadata?.createdAt || updatedAt,
|
|
782
803
|
updatedAt,
|
|
783
804
|
});
|
package/src/commands/auth.js
CHANGED
|
@@ -133,6 +133,14 @@ export function registerAuthCommand(program) {
|
|
|
133
133
|
tokenLabel: options.tokenLabel,
|
|
134
134
|
tokenTtlDays,
|
|
135
135
|
cliVersion: CLI_VERSION,
|
|
136
|
+
onAuthorize: ({ authorizeUrl }) => {
|
|
137
|
+
// A spawned opener does not prove a browser exists on a headless host.
|
|
138
|
+
if (!authorizeUrl) return;
|
|
139
|
+
const safeUrl = String(authorizeUrl).replace(/[\u0000-\u001f\u007f-\u009f]/g, "").slice(0, 4096);
|
|
140
|
+
const line = `Open this URL to approve sign-in:\n${safeUrl}\n`;
|
|
141
|
+
if (shouldEmitJson(options, command)) process.stderr.write(line);
|
|
142
|
+
else console.log(line.trimEnd());
|
|
143
|
+
},
|
|
136
144
|
});
|
|
137
145
|
} catch (error) {
|
|
138
146
|
throw new Error(formatApiError(error));
|
|
@@ -159,10 +167,6 @@ export function registerAuthCommand(program) {
|
|
|
159
167
|
if (result.filePath) {
|
|
160
168
|
console.log(pc.gray(`Session metadata: ${result.filePath}`));
|
|
161
169
|
}
|
|
162
|
-
if (!result.browserOpened && result.authorizeUrl) {
|
|
163
|
-
console.log(pc.yellow("Open this URL to approve sign-in:"));
|
|
164
|
-
console.log(result.authorizeUrl);
|
|
165
|
-
}
|
|
166
170
|
});
|
|
167
171
|
|
|
168
172
|
auth
|
|
@@ -199,6 +203,13 @@ export function registerAuthCommand(program) {
|
|
|
199
203
|
|
|
200
204
|
console.log(pc.bold("Authentication status"));
|
|
201
205
|
console.log(pc.gray(`API: ${status.apiUrl}`));
|
|
206
|
+
if (status.validation === "storage_unavailable") {
|
|
207
|
+
console.log(pc.yellow("Stored sign-in metadata exists, but its credential cannot be read. Authentication is unknown."));
|
|
208
|
+
if (status.filePath) console.log(pc.gray(`Session metadata: ${status.filePath}`));
|
|
209
|
+
console.log(pc.gray("Check the current user/home, keyring availability, encryption key and file permissions."));
|
|
210
|
+
console.log(pc.gray("Credentials were not cleared. Repair storage instead of logging out or signing in again for this error."));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
202
213
|
if (!status.source) {
|
|
203
214
|
console.log(pc.yellow("No active session token found."));
|
|
204
215
|
printAuthHint();
|
|
@@ -254,6 +265,15 @@ export function registerAuthCommand(program) {
|
|
|
254
265
|
}`
|
|
255
266
|
)
|
|
256
267
|
);
|
|
268
|
+
if (status.validation === "unavailable") {
|
|
269
|
+
console.log(pc.yellow("Remote validation is temporarily unavailable; your existing credentials were not cleared."));
|
|
270
|
+
console.log(pc.gray("Retry auth status when the API is reachable. Do not sign in again solely for this validation error."));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (["CREDENTIAL_DESTINATION_REFUSED", "CREDENTIAL_PATH_REFUSED"].includes(status.remoteError.code)) {
|
|
274
|
+
console.log(pc.yellow("Credential destination was refused. Check the configured API; signing in again does not change this boundary."));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
257
277
|
} else {
|
|
258
278
|
console.log(pc.yellow("Remote validation was skipped (`--offline`)."));
|
|
259
279
|
}
|