@opendatalabs/connect 0.9.4 → 0.10.0-canary.2c05df0
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/dist/cli/auth.d.ts +105 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +277 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +265 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/core/cli-types.d.ts +3 -0
- package/dist/core/cli-types.d.ts.map +1 -1
- package/dist/core/cli-types.js +1 -1
- package/dist/core/cli-types.js.map +1 -1
- package/dist/personal-server/index.d.ts +1 -1
- package/dist/personal-server/index.d.ts.map +1 -1
- package/dist/personal-server/index.js +26 -2
- package/dist/personal-server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { findDataConnectorsDir, ManagedPlaywrightRuntime, } from "../runtime/ind
|
|
|
34
34
|
import { listAvailableSkills, installSkill, readInstalledSkills, } from "../skills/index.js";
|
|
35
35
|
import { queryStatus, querySources, queryDataList, queryDataShow, queryDoctor, } from "./queries.js";
|
|
36
36
|
import { checkForUpdate, readUpdateCheck, isNewerVersion, } from "./update-check.js";
|
|
37
|
+
import { loadCredentials, saveCredentials, clearCredentials, isExpired, formatAddress, formatExpiresIn, getAuthTarget, resolvePersonalServerUrl, runDeviceCodeFlow, runSelfHostedLoginFlow, } from "./auth.js";
|
|
37
38
|
function cleanDescription(desc) {
|
|
38
39
|
return desc
|
|
39
40
|
.replace(/ using Playwright browser automation\.?/i, ".")
|
|
@@ -73,6 +74,7 @@ export async function runCli(argv = process.argv) {
|
|
|
73
74
|
.showSuggestionAfterError(true)
|
|
74
75
|
.addHelpText("after", `
|
|
75
76
|
Quick start:
|
|
77
|
+
vana login Log in to your Vana account
|
|
76
78
|
vana connect Connect a source and collect data
|
|
77
79
|
vana sources Browse available sources
|
|
78
80
|
vana status Check system health
|
|
@@ -330,6 +332,19 @@ Examples:
|
|
|
330
332
|
.action(async (scope) => {
|
|
331
333
|
process.exitCode = await runServerData(scope, parsedOptions);
|
|
332
334
|
});
|
|
335
|
+
program
|
|
336
|
+
.command("login")
|
|
337
|
+
.description("Log in to your Vana account or a self-hosted Personal Server")
|
|
338
|
+
.option("-s, --server <url>", "Self-hosted Personal Server URL")
|
|
339
|
+
.action(async (loginOptions) => {
|
|
340
|
+
process.exitCode = await runLogin(parsedOptions, loginOptions.server);
|
|
341
|
+
});
|
|
342
|
+
program
|
|
343
|
+
.command("logout")
|
|
344
|
+
.description("Log out and remove saved credentials")
|
|
345
|
+
.action(async () => {
|
|
346
|
+
process.exitCode = await runLogout(parsedOptions);
|
|
347
|
+
});
|
|
333
348
|
program
|
|
334
349
|
.command("mcp")
|
|
335
350
|
.description("Start MCP server for agent integration")
|
|
@@ -425,7 +440,10 @@ Examples:
|
|
|
425
440
|
// The concurrent check may have populated the cache during this run.
|
|
426
441
|
if (shouldNotify) {
|
|
427
442
|
try {
|
|
428
|
-
await
|
|
443
|
+
await Promise.race([
|
|
444
|
+
updateCheckPromise,
|
|
445
|
+
new Promise((resolve) => setTimeout(resolve, 2000)),
|
|
446
|
+
]);
|
|
429
447
|
const cache = await readUpdateCheck();
|
|
430
448
|
if (cache && isNewerVersion(cliVersion, cache.latestVersion)) {
|
|
431
449
|
const { upgrade } = getLifecycleCommands(installMethod, getCliChannel(cliVersion));
|
|
@@ -1148,10 +1166,18 @@ async function runStatus(options) {
|
|
|
1148
1166
|
}
|
|
1149
1167
|
}
|
|
1150
1168
|
if (options.json) {
|
|
1169
|
+
const jsonAuthCreds = loadCredentials();
|
|
1151
1170
|
const compactJson = {
|
|
1152
1171
|
runtime: status.runtime,
|
|
1153
1172
|
personalServer: status.personalServer,
|
|
1154
1173
|
personalServerUrl: status.personalServerUrl,
|
|
1174
|
+
auth: jsonAuthCreds
|
|
1175
|
+
? {
|
|
1176
|
+
authenticated: !isExpired(jsonAuthCreds),
|
|
1177
|
+
address: jsonAuthCreds.account.address,
|
|
1178
|
+
expires_at: jsonAuthCreds.account.expires_at,
|
|
1179
|
+
}
|
|
1180
|
+
: { authenticated: false },
|
|
1155
1181
|
sources: {
|
|
1156
1182
|
connected: status.summary?.connectedCount ?? 0,
|
|
1157
1183
|
needsAttention: status.summary?.needsAttentionCount ?? 0,
|
|
@@ -1174,6 +1200,16 @@ async function runStatus(options) {
|
|
|
1174
1200
|
else {
|
|
1175
1201
|
emit.keyValue("Personal Server", "not connected", "warning");
|
|
1176
1202
|
}
|
|
1203
|
+
// Auth state
|
|
1204
|
+
const authCreds = loadCredentials();
|
|
1205
|
+
if (authCreds && !isExpired(authCreds)) {
|
|
1206
|
+
emit.keyValue("Account", formatAddress(authCreds.account.address), "success");
|
|
1207
|
+
emit.keyValue("Auth", `Authenticated (expires in ${formatExpiresIn(authCreds.account.expires_at)})`, "success");
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
emit.keyValue("Account", "Not logged in", "muted");
|
|
1211
|
+
emit.keyValue("Auth", "Run `vana login` to authenticate", "muted");
|
|
1212
|
+
}
|
|
1177
1213
|
const connectedCount = status.summary?.connectedCount ?? 0;
|
|
1178
1214
|
const attentionCount = status.summary?.needsAttentionCount ?? 0;
|
|
1179
1215
|
const sourceParts = [
|
|
@@ -1230,6 +1266,47 @@ async function runStatus(options) {
|
|
|
1230
1266
|
return 0;
|
|
1231
1267
|
}
|
|
1232
1268
|
}
|
|
1269
|
+
// Show attention-needing sources that weren't already displayed above
|
|
1270
|
+
const displayedSourceIds = new Set(connectedSources.map(([id]) => id));
|
|
1271
|
+
const hiddenAttentionSources = status.sources.filter((s) => rankSourceStatus(s) <= 4 && !displayedSourceIds.has(s.source));
|
|
1272
|
+
if (hiddenAttentionSources.length > 0) {
|
|
1273
|
+
if (connectedSources.length === 0) {
|
|
1274
|
+
emit.blank();
|
|
1275
|
+
}
|
|
1276
|
+
for (const source of hiddenAttentionSources) {
|
|
1277
|
+
const displayName = displaySource(source.source, sourceLabels);
|
|
1278
|
+
const presentation = getSourceStatusPresentation(source);
|
|
1279
|
+
const collectedAgo = source.lastCollectedAt
|
|
1280
|
+
? `collected ${formatRelativeTime(source.lastCollectedAt)}`
|
|
1281
|
+
: "";
|
|
1282
|
+
emit.keyValue(` ${displayName}`, `${presentation.label}${collectedAgo ? ` ${collectedAgo}` : ""}`, presentation.tone);
|
|
1283
|
+
// Show actionable detail line
|
|
1284
|
+
if (source.lastRunOutcome === CliOutcomeStatus.INGEST_FAILED &&
|
|
1285
|
+
source.ingestScopes) {
|
|
1286
|
+
const failedScopes = source.ingestScopes.filter((s) => s.status === "failed");
|
|
1287
|
+
for (const scope of failedScopes) {
|
|
1288
|
+
const errMsg = scope.error ?? "sync failed";
|
|
1289
|
+
emit.detail(` \u21b3 ${source.source}.${scope.scope}: ${errMsg}. Run \`vana connect ${source.source}\``);
|
|
1290
|
+
}
|
|
1291
|
+
if (failedScopes.length === 0) {
|
|
1292
|
+
emit.detail(` \u21b3 Sync failed. Run \`vana connect ${source.source}\``);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
else if (source.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
|
|
1296
|
+
emit.detail(` \u21b3 No connector available. Run \`vana sources\``);
|
|
1297
|
+
}
|
|
1298
|
+
else if (source.lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR) {
|
|
1299
|
+
const reason = source.lastError ?? "runtime error";
|
|
1300
|
+
emit.detail(` \u21b3 ${reason}. Run \`vana connect ${source.source}\``);
|
|
1301
|
+
}
|
|
1302
|
+
else if (source.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
|
|
1303
|
+
emit.detail(` \u21b3 Requires interactive login. Run \`vana connect ${source.source}\``);
|
|
1304
|
+
}
|
|
1305
|
+
else if (source.lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH) {
|
|
1306
|
+
emit.detail(` \u21b3 Manual auth step required. Run \`vana connect ${source.source}\``);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1233
1310
|
if (nextSteps.length > 0) {
|
|
1234
1311
|
emit.blank();
|
|
1235
1312
|
const command = extractCommand(nextSteps[0]);
|
|
@@ -1350,9 +1427,11 @@ async function runServerStatus(options) {
|
|
|
1350
1427
|
? "(auto-detected)"
|
|
1351
1428
|
: target.source === "config"
|
|
1352
1429
|
? "(saved)"
|
|
1353
|
-
: target.source === "
|
|
1354
|
-
? "(from
|
|
1355
|
-
:
|
|
1430
|
+
: target.source === "auth"
|
|
1431
|
+
? "(from vana login)"
|
|
1432
|
+
: target.source === "env"
|
|
1433
|
+
? "(from VANA_PERSONAL_SERVER_URL)"
|
|
1434
|
+
: `(${target.source ?? "unknown"})`;
|
|
1356
1435
|
emit.keyValue("URL", `${target.url} ${urlSuffix}`, "muted");
|
|
1357
1436
|
}
|
|
1358
1437
|
const stateLabel = target.state === "available" ? "healthy" : "Not connected";
|
|
@@ -2686,7 +2765,7 @@ export function formatHealthMessage(reason) {
|
|
|
2686
2765
|
return null;
|
|
2687
2766
|
const colonIndex = reason.indexOf(": ");
|
|
2688
2767
|
const prefix = colonIndex > 0 ? reason.slice(0, colonIndex) : reason;
|
|
2689
|
-
const detail = colonIndex > 0 ? reason.slice(colonIndex + 2) : "";
|
|
2768
|
+
const detail = colonIndex > 0 ? reason.slice(colonIndex + 2).replace(/\.$/, "") : "";
|
|
2690
2769
|
switch (prefix) {
|
|
2691
2770
|
case "needs-input":
|
|
2692
2771
|
return `Requires interactive login${detail ? `: ${detail}` : ""}.`;
|
|
@@ -3896,4 +3975,185 @@ async function runSkillShow(name, options) {
|
|
|
3896
3975
|
return 1;
|
|
3897
3976
|
}
|
|
3898
3977
|
}
|
|
3978
|
+
// ── Login / Logout ─────────────────────────────────────────────────────
|
|
3979
|
+
async function runLogin(options, serverUrl) {
|
|
3980
|
+
// Determine auth target: cloud (account.vana.org) or self-hosted (PS directly)
|
|
3981
|
+
const psUrl = serverUrl ?? resolvePersonalServerUrl() ?? null;
|
|
3982
|
+
const authTarget = getAuthTarget(psUrl);
|
|
3983
|
+
// If self-hosted, use /login/v2 flow against the PS
|
|
3984
|
+
if (authTarget === "self-hosted" && psUrl) {
|
|
3985
|
+
const emit = createEmitter(options);
|
|
3986
|
+
emit.blank();
|
|
3987
|
+
emit.info(` Logging in to ${psUrl}...`);
|
|
3988
|
+
emit.blank();
|
|
3989
|
+
try {
|
|
3990
|
+
const result = await runSelfHostedLoginFlow(psUrl, (url) => {
|
|
3991
|
+
emit.info(` ! Open this URL in your browser:`);
|
|
3992
|
+
emit.info(` ${url}`);
|
|
3993
|
+
emit.blank();
|
|
3994
|
+
emit.info(` Waiting for authorization...`);
|
|
3995
|
+
// Try to open browser — use spawn with args array to prevent shell injection
|
|
3996
|
+
// (a malicious self-hosted PS could return a URL with shell metacharacters)
|
|
3997
|
+
try {
|
|
3998
|
+
const { spawn } = require("node:child_process");
|
|
3999
|
+
const opener = process.platform === "darwin"
|
|
4000
|
+
? "open"
|
|
4001
|
+
: process.platform === "win32"
|
|
4002
|
+
? "start"
|
|
4003
|
+
: "xdg-open";
|
|
4004
|
+
spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
|
|
4005
|
+
}
|
|
4006
|
+
catch {
|
|
4007
|
+
// Browser open failed — user will open manually
|
|
4008
|
+
}
|
|
4009
|
+
});
|
|
4010
|
+
saveCredentials({
|
|
4011
|
+
account: {
|
|
4012
|
+
address: result.server,
|
|
4013
|
+
session_token: "",
|
|
4014
|
+
expires_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
4015
|
+
},
|
|
4016
|
+
personal_server: {
|
|
4017
|
+
url: psUrl,
|
|
4018
|
+
access_token: result.access_token,
|
|
4019
|
+
expires_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
4020
|
+
},
|
|
4021
|
+
});
|
|
4022
|
+
emit.success(`Logged in to ${psUrl}`);
|
|
4023
|
+
emit.success(`Credentials saved to ~/.vana/auth.json`);
|
|
4024
|
+
return 0;
|
|
4025
|
+
}
|
|
4026
|
+
catch (err) {
|
|
4027
|
+
emit.info(` ✗ Login failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4028
|
+
return 1;
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
// Cloud flow (account.vana.org)
|
|
4032
|
+
// Check env var shortcut
|
|
4033
|
+
const envToken = process.env.VANA_SESSION_TOKEN;
|
|
4034
|
+
if (envToken) {
|
|
4035
|
+
const creds = loadCredentials();
|
|
4036
|
+
if (creds) {
|
|
4037
|
+
if (options.json) {
|
|
4038
|
+
process.stdout.write(`${JSON.stringify({ status: "authenticated", source: "env", address: creds.account.address })}\n`);
|
|
4039
|
+
}
|
|
4040
|
+
else {
|
|
4041
|
+
const emit = createEmitter(options);
|
|
4042
|
+
emit.success(`Already authenticated via VANA_SESSION_TOKEN env var`);
|
|
4043
|
+
}
|
|
4044
|
+
return 0;
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
// Check if already logged in
|
|
4048
|
+
const existing = loadCredentials();
|
|
4049
|
+
if (existing && !isExpired(existing)) {
|
|
4050
|
+
if (options.json) {
|
|
4051
|
+
process.stdout.write(`${JSON.stringify({
|
|
4052
|
+
status: "authenticated",
|
|
4053
|
+
address: existing.account.address,
|
|
4054
|
+
personal_server: existing.personal_server?.url ?? null,
|
|
4055
|
+
expires_at: existing.account.expires_at,
|
|
4056
|
+
})}\n`);
|
|
4057
|
+
}
|
|
4058
|
+
else {
|
|
4059
|
+
const emit = createEmitter(options);
|
|
4060
|
+
emit.success(`Already logged in as ${formatAddress(existing.account.address)}`);
|
|
4061
|
+
if (existing.personal_server) {
|
|
4062
|
+
emit.keyValue("Personal Server", existing.personal_server.url, "success");
|
|
4063
|
+
}
|
|
4064
|
+
emit.info(` Auth expires in ${formatExpiresIn(existing.account.expires_at)}`);
|
|
4065
|
+
emit.blank();
|
|
4066
|
+
emit.info(" Run `vana logout` first to re-authenticate.");
|
|
4067
|
+
}
|
|
4068
|
+
return 0;
|
|
4069
|
+
}
|
|
4070
|
+
if (options.json) {
|
|
4071
|
+
// JSON mode: run flow and output result
|
|
4072
|
+
const creds = await runDeviceCodeFlow({
|
|
4073
|
+
onCode: (code, uri) => {
|
|
4074
|
+
process.stderr.write(JSON.stringify({ event: "device_code", code, uri }) + "\n");
|
|
4075
|
+
},
|
|
4076
|
+
onWaiting: () => { },
|
|
4077
|
+
onAuthorized: () => { },
|
|
4078
|
+
onExpired: () => {
|
|
4079
|
+
process.stdout.write(JSON.stringify({ status: "expired", error: "Device code expired" }) +
|
|
4080
|
+
"\n");
|
|
4081
|
+
},
|
|
4082
|
+
onError: (err) => {
|
|
4083
|
+
process.stdout.write(JSON.stringify({ status: "error", error: err.message }) + "\n");
|
|
4084
|
+
},
|
|
4085
|
+
});
|
|
4086
|
+
if (creds) {
|
|
4087
|
+
await saveCredentials(creds);
|
|
4088
|
+
process.stdout.write(`${JSON.stringify({
|
|
4089
|
+
status: "authenticated",
|
|
4090
|
+
address: creds.account.address,
|
|
4091
|
+
personal_server: creds.personal_server?.url ?? null,
|
|
4092
|
+
expires_at: creds.account.expires_at,
|
|
4093
|
+
})}\n`);
|
|
4094
|
+
return 0;
|
|
4095
|
+
}
|
|
4096
|
+
return 1;
|
|
4097
|
+
}
|
|
4098
|
+
// Interactive mode
|
|
4099
|
+
const emit = createEmitter(options);
|
|
4100
|
+
emit.blank();
|
|
4101
|
+
emit.info(" Logging in to Vana...");
|
|
4102
|
+
emit.blank();
|
|
4103
|
+
const creds = await runDeviceCodeFlow({
|
|
4104
|
+
onCode: (code, uri) => {
|
|
4105
|
+
emit.info(` ! Open this URL in your browser:`);
|
|
4106
|
+
emit.info(` ${uri}`);
|
|
4107
|
+
emit.blank();
|
|
4108
|
+
emit.info(` ! Enter this code: ${BOLD}${code}${RESET}`);
|
|
4109
|
+
emit.blank();
|
|
4110
|
+
},
|
|
4111
|
+
onWaiting: () => {
|
|
4112
|
+
emit.info(" Waiting for authorization...");
|
|
4113
|
+
},
|
|
4114
|
+
onAuthorized: async (authedCreds) => {
|
|
4115
|
+
await saveCredentials(authedCreds);
|
|
4116
|
+
emit.success(`Logged in as ${formatAddress(authedCreds.account.address)}`);
|
|
4117
|
+
if (authedCreds.personal_server) {
|
|
4118
|
+
emit.blank();
|
|
4119
|
+
emit.keyValue("Personal Server", authedCreds.personal_server.url, "success");
|
|
4120
|
+
}
|
|
4121
|
+
emit.success(`Credentials saved to ~/.vana/auth.json`);
|
|
4122
|
+
},
|
|
4123
|
+
onExpired: () => {
|
|
4124
|
+
emit.info(` Device code expired. Run ${emit.code("vana login")} to try again.`);
|
|
4125
|
+
},
|
|
4126
|
+
onError: (err) => {
|
|
4127
|
+
emit.info(` Error: ${err.message}`);
|
|
4128
|
+
},
|
|
4129
|
+
});
|
|
4130
|
+
return creds ? 0 : 1;
|
|
4131
|
+
}
|
|
4132
|
+
async function runLogout(options) {
|
|
4133
|
+
// Revoke the token server-side before clearing local credentials
|
|
4134
|
+
const creds = loadCredentials();
|
|
4135
|
+
if (creds?.personal_server?.url && creds.personal_server.access_token) {
|
|
4136
|
+
try {
|
|
4137
|
+
await fetch(`${creds.personal_server.url.replace(/\/$/, "")}/login/v2/token`, {
|
|
4138
|
+
method: "DELETE",
|
|
4139
|
+
headers: {
|
|
4140
|
+
Authorization: `Bearer ${creds.personal_server.access_token}`,
|
|
4141
|
+
},
|
|
4142
|
+
signal: AbortSignal.timeout(5000),
|
|
4143
|
+
});
|
|
4144
|
+
}
|
|
4145
|
+
catch {
|
|
4146
|
+
// Best-effort — server may be down, but we still clear local creds
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
await clearCredentials();
|
|
4150
|
+
if (options.json) {
|
|
4151
|
+
process.stdout.write(`${JSON.stringify({ status: "logged_out" })}\n`);
|
|
4152
|
+
}
|
|
4153
|
+
else {
|
|
4154
|
+
const emit = createEmitter(options);
|
|
4155
|
+
emit.success("Logged out. Credentials removed.");
|
|
4156
|
+
}
|
|
4157
|
+
return 0;
|
|
4158
|
+
}
|
|
3899
4159
|
//# sourceMappingURL=index.js.map
|