Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/cli/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ export function readStoredAccountAddress(): string | null {
return readStoredAuthFile()?.address ?? null;
}

/**
* The saved account for Personal Server ownership checks, even after expiry.
* An env token has unknown identity and overrides the saved account. This
* address is never authorization; tokens keep their own validity checks.
*/
export function loadAccountAddressForOwnership(): string | null {
return process.env.VANA_SESSION_TOKEN ? null : readStoredAccountAddress();
}

/**
* Raw peek at auth.json ignoring expiry: the previous address and the
* previous personal_server block. Login uses it to detect an account switch
Expand Down
33 changes: 23 additions & 10 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ import {
isNewerVersion,
} from "./update-check.js";
import {
loadAccountAddressForOwnership,
loadCredentials,
loadPersonalServerSession,
readStoredAuthFile,
Expand Down Expand Up @@ -1832,14 +1833,14 @@ async function runConnect(
const credentials = loadCredentials();
const mismatch = personalServerOwnerMismatch(
target.health?.owner,
credentials?.account?.address,
loadAccountAddressForOwnership(),
);
if (mismatch) {
// An expired login names whoever signed in last, which may be long
// gone; say that, rather than claim a current identity.
renderer?.detail(
credentials && isExpired(credentials)
? `Your vana login has expired, so the CLI cannot check that the Personal Server at ${target.url} (owner ${formatAddress(mismatch.owner)}) is yours. Run \`vana login\` to check.`
!credentials
? `Your vana login has expired or is unavailable, so the CLI cannot check that the Personal Server at ${target.url} (owner ${formatAddress(mismatch.owner)}) is yours. Run \`vana login\` to check.`
: `The Personal Server at ${target.url} belongs to ${formatAddress(mismatch.owner)}, but you are signed in as ${formatAddress(mismatch.account)}.`,
);
if (cannotAskAboutForeignServer(options)) {
Expand Down Expand Up @@ -4766,12 +4767,22 @@ async function runCollectAll(options: GlobalOptions): Promise<number> {
// may be another account's.
const foreign = personalServerOwnerMismatch(
target.health?.owner,
loadCredentials()?.account?.address,
loadAccountAddressForOwnership(),
);
if (target.state === "available" && foreign && !options.json) {
process.stderr.write(
`Pending data not synced: the Personal Server at ${target.url} belongs to ${formatAddress(foreign.owner)}, not to you. Start yours with \`vana server start\`.\n`,
);
let blockedPendingCount = 0;
if (target.state === "available" && foreign) {
const after = await readCliState();
const error = `Pending data not synced: the Personal Server at ${target.url} belongs to ${formatAddress(foreign.owner)}, not to you. Start yours with \`vana server start\`.`;
for (const [source, stored] of Object.entries(after.sources)) {
if (!shouldRetryPendingSource(stored, "automatic")) continue;
blockedPendingCount++;
const prior = results.get(source)?.outcome;
if (prior === "collect_failed" || prior === "sync_failed") continue;
results.set(source, { source, outcome: "sync_pending", error });
}
if (blockedPendingCount > 0 && !options.json) {
process.stderr.write(`${error}\n`);
}
}
if (target.state === "available" && !foreign) {
const synced = await syncPendingSources(target, "automatic");
Expand Down Expand Up @@ -4806,7 +4817,9 @@ async function runCollectAll(options: GlobalOptions): Promise<number> {
const message =
syncedPendingCount > 0
? `Synced ${syncedPendingCount} pending dataset(s).`
: "No sources are due for collection.";
: blockedPendingCount > 0
? `${blockedPendingCount} pending dataset(s) not synced: the Personal Server belongs to another account.`
: "No sources are due for collection.";
process.stdout.write(
`${JSON.stringify({ message, count: 0, syncedPendingCount, sources: sourceResults })}\n`,
);
Expand Down Expand Up @@ -4868,7 +4881,7 @@ async function runServerSync(options: GlobalOptions): Promise<number> {

const mismatch = personalServerOwnerMismatch(
target.health?.owner,
loadCredentials()?.account?.address,
loadAccountAddressForOwnership(),
);
if (mismatch) {
let useIt = false;
Expand Down
11 changes: 6 additions & 5 deletions src/cli/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,9 +360,10 @@ export function classifyCollectedSource(

/**
* The exit code for a whole `collect --all`: OK only when every source it
* touched collected and synced. When the only problem is that no Personal
* Server answered, that is {@link CliExitCode.SERVER_UNAVAILABLE}; anything
* else is {@link CliExitCode.FAILURE}.
* touched collected and synced. When the only problem is sync pending because
* no usable Personal Server answered, that is
* {@link CliExitCode.SERVER_UNAVAILABLE}; anything else is
* {@link CliExitCode.FAILURE}.
*/
export function collectAllExitCode(
results: readonly CollectAllSourceResult[],
Expand All @@ -384,13 +385,13 @@ export function describeCollectAllFailures(
ok: "ok",
collect_failed: "collection failed",
sync_failed: "sync failed",
sync_pending: "not synced, no Personal Server answered",
sync_pending: "sync pending",
};
return [
`${failures.length} of ${results.length} source(s) did not collect and sync:`,
...failures.map(
(r) =>
` ${r.source}: ${label[r.outcome]}${r.error ? ` (${r.error})` : ""}`,
` ${r.source}: ${r.outcome === "sync_pending" && !r.error ? "not synced, no Personal Server answered" : label[r.outcome]}${r.error ? ` (${r.error})` : ""}`,
),
];
}
9 changes: 6 additions & 3 deletions src/personal-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { resolveScopes } from "./scope-resolver.js";
import { createPersonalServerClient } from "./client.js";
import { readCachedConnectorMetadata } from "../connectors/registry.js";
import { getConnectorCacheDir } from "../core/paths.js";
import { loadCredentials, loadPersonalServerSession } from "../cli/auth.js";
import {
loadAccountAddressForOwnership,
loadPersonalServerSession,
} from "../cli/auth.js";

export { createPersonalServerClient } from "./client.js";
export type {
Expand Down Expand Up @@ -96,7 +99,7 @@ export async function detectPersonalServerTarget(): Promise<PersonalServerTarget
// 2. Auth credentials (from `vana login`). The saved server can be another
// account's, kept across a login switch: then a server of this account
// found by the scan below wins over it.
const account = loadCredentials()?.account?.address ?? null;
const account = loadAccountAddressForOwnership();
const savedSession = loadPersonalServerSession();
let savedForeign: PersonalServerTarget | null = null;
if (savedSession?.url) {
Expand All @@ -118,7 +121,7 @@ export async function detectPersonalServerTarget(): Promise<PersonalServerTarget

// 4. Localhost port scan. One machine can host servers for several
// identities, so the first port to answer is not necessarily ours; prefer
// one the signed-in account owns and only fall back to a stranger's.
// one the saved account owns and only fall back to a stranger's.
let unowned: PersonalServerTarget | null = null;
for (const port of DEFAULT_PORTS) {
const url = `http://localhost:${port}`;
Expand Down
Loading
Loading