vana-cli 0.25.0 → 0.27.0
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 +8 -0
- package/dist/cli/auth.d.ts +2 -0
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +2 -1
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +40 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/server-start.d.ts +51 -0
- package/dist/cli/server-start.d.ts.map +1 -0
- package/dist/cli/server-start.js +337 -0
- package/dist/cli/server-start.js.map +1 -0
- package/dist/personal-server/index.d.ts +1 -0
- package/dist/personal-server/index.d.ts.map +1 -1
- package/dist/personal-server/index.js +17 -2
- package/dist/personal-server/index.js.map +1 -1
- package/dist/personal-server/local/config.d.ts +28 -0
- package/dist/personal-server/local/config.d.ts.map +1 -0
- package/dist/personal-server/local/config.js +53 -0
- package/dist/personal-server/local/config.js.map +1 -0
- package/dist/personal-server/local/frpc.d.ts +59 -0
- package/dist/personal-server/local/frpc.d.ts.map +1 -0
- package/dist/personal-server/local/frpc.js +191 -0
- package/dist/personal-server/local/frpc.js.map +1 -0
- package/dist/personal-server/local/owner-binding.d.ts +53 -0
- package/dist/personal-server/local/owner-binding.d.ts.map +1 -0
- package/dist/personal-server/local/owner-binding.js +164 -0
- package/dist/personal-server/local/owner-binding.js.map +1 -0
- package/dist/personal-server/local/owner-secret.d.ts +17 -0
- package/dist/personal-server/local/owner-secret.d.ts.map +1 -0
- package/dist/personal-server/local/owner-secret.js +119 -0
- package/dist/personal-server/local/owner-secret.js.map +1 -0
- package/dist/personal-server/local/registration.d.ts +42 -0
- package/dist/personal-server/local/registration.d.ts.map +1 -0
- package/dist/personal-server/local/registration.js +81 -0
- package/dist/personal-server/local/registration.js.map +1 -0
- package/dist/personal-server/local/runtime-pkg/entry.mjs +271 -0
- package/dist/personal-server/local/runtime-pkg/package-lock.json +2342 -0
- package/dist/personal-server/local/runtime-pkg/package.json +13 -0
- package/dist/personal-server/local/runtime.d.ts +9 -0
- package/dist/personal-server/local/runtime.d.ts.map +1 -0
- package/dist/personal-server/local/runtime.js +66 -0
- package/dist/personal-server/local/runtime.js.map +1 -0
- package/dist/personal-server/local/server.d.ts +64 -0
- package/dist/personal-server/local/server.d.ts.map +1 -0
- package/dist/personal-server/local/server.js +279 -0
- package/dist/personal-server/local/server.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describeFailure, runSigningExchange, } from "./owner-binding.js";
|
|
2
|
+
export const SERVER_REGISTRATION_INTENT = "personal_server.server_registration.v1";
|
|
3
|
+
const SIGN_PATH = "/api/v1/intents/personal-server-registration/sign";
|
|
4
|
+
/** Account needs the browser: the silent path is closed for this owner or client. */
|
|
5
|
+
export class RegistrationNeedsBrowserError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code) {
|
|
8
|
+
super(`Registering needs you to confirm in a browser (${code}). Run without --no-input.`);
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** The fields Account signs, from the server's own registration request. */
|
|
13
|
+
export function registrationPayload(request) {
|
|
14
|
+
const message = request.typedData?.message;
|
|
15
|
+
const domain = request.typedData?.domain;
|
|
16
|
+
if (!(message?.serverAddress && message.publicKey && message.serverUrl)) {
|
|
17
|
+
throw new Error("The server's registration request is incomplete.");
|
|
18
|
+
}
|
|
19
|
+
const chainId = Number(domain?.chainId);
|
|
20
|
+
return {
|
|
21
|
+
serverAddress: message.serverAddress,
|
|
22
|
+
serverPublicKey: message.publicKey,
|
|
23
|
+
serverUrl: message.serverUrl,
|
|
24
|
+
...(Number.isInteger(chainId) ? { chainId } : {}),
|
|
25
|
+
...(typeof domain?.verifyingContract === "string"
|
|
26
|
+
? { verifyingContract: domain.verifyingContract }
|
|
27
|
+
: {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Have Account sign the server's registration as its owner. Account signs
|
|
32
|
+
* silently when the owner's wallet lives in Account and the trust token from
|
|
33
|
+
* the owner confirmation is valid, as it does for Desktop; otherwise the
|
|
34
|
+
* person confirms in the browser.
|
|
35
|
+
*/
|
|
36
|
+
export async function signServerRegistration(input, deps) {
|
|
37
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
38
|
+
const account = input.accountUrl.replace(/\/+$/, "");
|
|
39
|
+
const payload = registrationPayload(input.request);
|
|
40
|
+
const response = await fetchImpl(`${account}${SIGN_PATH}`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"content-type": "application/json",
|
|
44
|
+
authorization: `Bearer ${input.accessToken}`,
|
|
45
|
+
...(input.trustToken
|
|
46
|
+
? { "x-vana-desktop-trust-token": input.trustToken }
|
|
47
|
+
: {}),
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({ intent: SERVER_REGISTRATION_INTENT, ...payload }),
|
|
50
|
+
});
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new Error(await describeFailure("sign the registration", response));
|
|
53
|
+
}
|
|
54
|
+
const body = (await response.json());
|
|
55
|
+
if (body.status === "signed" && body.signature) {
|
|
56
|
+
return {
|
|
57
|
+
signature: body.signature,
|
|
58
|
+
signerAddress: body.signerAddress ?? null,
|
|
59
|
+
via: "silent",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (body.status !== "confirmation_required") {
|
|
63
|
+
throw new Error(`Account did not sign the registration (${body.code ?? body.status ?? "unknown"}).`);
|
|
64
|
+
}
|
|
65
|
+
if (!input.allowBrowser) {
|
|
66
|
+
throw new RegistrationNeedsBrowserError(body.code ?? "confirmation_required");
|
|
67
|
+
}
|
|
68
|
+
const redeemed = await runSigningExchange({
|
|
69
|
+
accountUrl: input.accountUrl,
|
|
70
|
+
accessToken: input.accessToken,
|
|
71
|
+
intent: SERVER_REGISTRATION_INTENT,
|
|
72
|
+
payload,
|
|
73
|
+
step: "registration",
|
|
74
|
+
}, deps);
|
|
75
|
+
return {
|
|
76
|
+
signature: redeemed.signature,
|
|
77
|
+
signerAddress: redeemed.signerAddress ?? null,
|
|
78
|
+
via: "browser",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=registration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registration.js","sourceRoot":"","sources":["../../../src/personal-server/local/registration.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// The Personal Server process `vana server start` runs, modeled on Vana
|
|
2
|
+
// Desktop's sidecar (apps/desktop/personal-server/index.js in unity-surfaces).
|
|
3
|
+
//
|
|
4
|
+
// The first JSON line on stdin is its configuration, so the owner signature
|
|
5
|
+
// never appears in the process list or environment:
|
|
6
|
+
// { rootPath, port, ownerSignature, ownerAddress, network: { gatewayUrl, chainId,
|
|
7
|
+
// contracts, storageApiUrl }, tunnel: { binaryPath, serverAddr, serverPort } | null }
|
|
8
|
+
// Later lines are commands from the CLI, which holds the Account session:
|
|
9
|
+
// { type: "prepare-registration" } -> registration-request
|
|
10
|
+
// { type: "submit-registration", signature } -> registration-submitted
|
|
11
|
+
// It writes one JSON object per line on stdout: ready, public, tunnel,
|
|
12
|
+
// registration-request, registration-submitted, command-failed, error,
|
|
13
|
+
// stopped. Logs go to stderr.
|
|
14
|
+
|
|
15
|
+
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import readline from "node:readline";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
loadConfig,
|
|
21
|
+
startPersonalServer,
|
|
22
|
+
} from "@opendatalabs/personal-server-ts/node";
|
|
23
|
+
|
|
24
|
+
const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
25
|
+
|
|
26
|
+
function isPlainObject(value) {
|
|
27
|
+
return (
|
|
28
|
+
typeof value === "object" &&
|
|
29
|
+
value !== null &&
|
|
30
|
+
!Array.isArray(value) &&
|
|
31
|
+
Object.getPrototypeOf(value) !== null
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Derived leaves win and every other persisted key survives, as in Desktop's
|
|
36
|
+
// applyDerivedServerConfig.
|
|
37
|
+
function applyDerived(persisted, derived) {
|
|
38
|
+
if (!isPlainObject(persisted)) return structuredClone(derived);
|
|
39
|
+
const merged = { ...persisted };
|
|
40
|
+
for (const [key, value] of Object.entries(derived)) {
|
|
41
|
+
merged[key] =
|
|
42
|
+
isPlainObject(value) && isPlainObject(merged[key])
|
|
43
|
+
? applyDerived(merged[key], value)
|
|
44
|
+
: structuredClone(value);
|
|
45
|
+
}
|
|
46
|
+
return merged;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Everything that follows from the network and the tunnel client. Without a
|
|
50
|
+
// tunnel client the server is local only: nothing outside can reach it, so
|
|
51
|
+
// it is never registered and has nothing to sync. With one, the server
|
|
52
|
+
// reserves its public URL but dials the relay only once registered, and sync
|
|
53
|
+
// waits for registration too.
|
|
54
|
+
function derivedConfig(network, tunnel) {
|
|
55
|
+
return {
|
|
56
|
+
devUi: { enabled: true },
|
|
57
|
+
gateway: {
|
|
58
|
+
url: network.gatewayUrl,
|
|
59
|
+
chainId: network.chainId,
|
|
60
|
+
contracts: network.contracts,
|
|
61
|
+
},
|
|
62
|
+
inference: {
|
|
63
|
+
baseUrl: `${network.gatewayUrl.replace(/\/+$/, "")}/v1/inference`,
|
|
64
|
+
},
|
|
65
|
+
logging: { level: "info", pretty: false },
|
|
66
|
+
storage: {
|
|
67
|
+
backend: "vana",
|
|
68
|
+
config: { vana: { apiUrl: network.storageApiUrl } },
|
|
69
|
+
},
|
|
70
|
+
sync: { enabled: Boolean(tunnel) },
|
|
71
|
+
tunnel: tunnel
|
|
72
|
+
? {
|
|
73
|
+
enabled: true,
|
|
74
|
+
serverAddr: tunnel.serverAddr,
|
|
75
|
+
serverPort: tunnel.serverPort,
|
|
76
|
+
binaryPath: tunnel.binaryPath,
|
|
77
|
+
}
|
|
78
|
+
: { enabled: false },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const lines = readline
|
|
83
|
+
.createInterface({ input: process.stdin })
|
|
84
|
+
[Symbol.asyncIterator]();
|
|
85
|
+
|
|
86
|
+
async function readInput() {
|
|
87
|
+
for (;;) {
|
|
88
|
+
const { value, done } = await lines.next();
|
|
89
|
+
if (done) throw new Error("No configuration on stdin.");
|
|
90
|
+
if (value.trim()) return JSON.parse(value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let ps = null;
|
|
95
|
+
|
|
96
|
+
// Only a tunnel URL may be registered: without one the server offers its
|
|
97
|
+
// localhost origin, which no app could ever reach.
|
|
98
|
+
function publicState(info) {
|
|
99
|
+
const tunnel = info.details?.tunnel ?? null;
|
|
100
|
+
return {
|
|
101
|
+
registered: Boolean(info.registration?.registered),
|
|
102
|
+
serverAddress: info.server?.address ?? null,
|
|
103
|
+
serverUrl: info.urls?.public ?? null,
|
|
104
|
+
tunnel: tunnel
|
|
105
|
+
? {
|
|
106
|
+
status: tunnel.status ?? null,
|
|
107
|
+
routable: tunnel.routable !== false,
|
|
108
|
+
warning: tunnel.warning ?? null,
|
|
109
|
+
}
|
|
110
|
+
: null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let watching = false;
|
|
115
|
+
|
|
116
|
+
// The server dials the relay on its own once it sees the registration (it
|
|
117
|
+
// polls the gateway); report when the public URL answers, as Desktop does.
|
|
118
|
+
async function watchTunnel() {
|
|
119
|
+
if (watching) return;
|
|
120
|
+
watching = true;
|
|
121
|
+
const started = Date.now();
|
|
122
|
+
let last = null;
|
|
123
|
+
while (Date.now() - started < 10 * 60_000) {
|
|
124
|
+
try {
|
|
125
|
+
last = publicState(await ps.info());
|
|
126
|
+
if (last.tunnel?.status === "connected" && last.tunnel.routable) {
|
|
127
|
+
send({ type: "tunnel", status: "connected", url: last.serverUrl });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
process.stderr.write(`[entry] tunnel check failed: ${error}\n`);
|
|
132
|
+
}
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, 3_000));
|
|
134
|
+
}
|
|
135
|
+
send({
|
|
136
|
+
type: "tunnel",
|
|
137
|
+
status: last?.tunnel?.status ?? "unknown",
|
|
138
|
+
url: last?.serverUrl ?? null,
|
|
139
|
+
warning: last?.tunnel?.warning ?? "The tunnel did not connect in time.",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Typed data can carry bigints, which JSON.stringify refuses.
|
|
144
|
+
const plain = (value) =>
|
|
145
|
+
JSON.parse(
|
|
146
|
+
JSON.stringify(value, (_key, item) =>
|
|
147
|
+
typeof item === "bigint" ? item.toString() : item,
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
async function handleCommands() {
|
|
152
|
+
for (;;) {
|
|
153
|
+
const { value, done } = await lines.next();
|
|
154
|
+
if (done) return;
|
|
155
|
+
if (!value.trim()) continue;
|
|
156
|
+
let command;
|
|
157
|
+
try {
|
|
158
|
+
command = JSON.parse(value);
|
|
159
|
+
} catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
if (command.type === "prepare-registration") {
|
|
164
|
+
const { serverUrl } = publicState(await ps.info());
|
|
165
|
+
if (!serverUrl) throw new Error("The server has no public URL.");
|
|
166
|
+
const request = await ps.prepareRegistration({ serverUrl });
|
|
167
|
+
send({
|
|
168
|
+
type: "registration-request",
|
|
169
|
+
request: { typedData: plain(request.typedData) },
|
|
170
|
+
});
|
|
171
|
+
} else if (command.type === "submit-registration") {
|
|
172
|
+
const result = await ps.submitRegistration({
|
|
173
|
+
signature: command.signature,
|
|
174
|
+
});
|
|
175
|
+
send({
|
|
176
|
+
type: "registration-submitted",
|
|
177
|
+
serverId: result?.serverId ?? null,
|
|
178
|
+
alreadyRegistered: Boolean(result?.alreadyRegistered),
|
|
179
|
+
});
|
|
180
|
+
void watchTunnel();
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
send({
|
|
184
|
+
type: "command-failed",
|
|
185
|
+
command: command.type,
|
|
186
|
+
message: error instanceof Error ? error.message : String(error),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function main() {
|
|
193
|
+
const input = await readInput();
|
|
194
|
+
const { rootPath, port, ownerSignature, ownerAddress, network, tunnel } =
|
|
195
|
+
input;
|
|
196
|
+
const configPath = join(rootPath, "server.json");
|
|
197
|
+
const derived = derivedConfig(network, tunnel ?? null);
|
|
198
|
+
|
|
199
|
+
// Persist the derived block the way Desktop does, so a network change can
|
|
200
|
+
// never leave an old gateway or storage host behind in server.json.
|
|
201
|
+
let persisted = null;
|
|
202
|
+
try {
|
|
203
|
+
persisted = JSON.parse(await readFile(configPath, "utf8"));
|
|
204
|
+
} catch {
|
|
205
|
+
persisted = null;
|
|
206
|
+
}
|
|
207
|
+
const next = applyDerived(persisted, derived);
|
|
208
|
+
const temporary = `${configPath}.${process.pid}.tmp`;
|
|
209
|
+
await writeFile(temporary, `${JSON.stringify(next, null, 2)}\n`, {
|
|
210
|
+
mode: 0o600,
|
|
211
|
+
});
|
|
212
|
+
await rename(temporary, configPath);
|
|
213
|
+
|
|
214
|
+
// configDefaults merges shallowly for some sections, so hand back whole
|
|
215
|
+
// sections from the schema-complete loaded config.
|
|
216
|
+
const loaded = await loadConfig({ configPath, rootPath });
|
|
217
|
+
const configDefaults = {
|
|
218
|
+
server: {
|
|
219
|
+
...loaded.server,
|
|
220
|
+
port,
|
|
221
|
+
...(ownerAddress ? { address: ownerAddress } : {}),
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
for (const key of Object.keys(derived)) {
|
|
225
|
+
configDefaults[key] = applyDerived(loaded[key], derived[key]);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
ps = await startPersonalServer({
|
|
229
|
+
configPath,
|
|
230
|
+
rootPath,
|
|
231
|
+
port,
|
|
232
|
+
ownerSignature,
|
|
233
|
+
mcpOAuthApprovalUrl: "vana://mcp-authorization",
|
|
234
|
+
configDefaults,
|
|
235
|
+
onStatus: (status) => process.stderr.write(`[status] ${status}\n`),
|
|
236
|
+
});
|
|
237
|
+
const info = await ps.ready();
|
|
238
|
+
send({
|
|
239
|
+
type: "ready",
|
|
240
|
+
port,
|
|
241
|
+
url: info.urls?.local ?? `http://localhost:${port}`,
|
|
242
|
+
});
|
|
243
|
+
void handleCommands();
|
|
244
|
+
if (!tunnel) return;
|
|
245
|
+
|
|
246
|
+
// Waits for the gateway registration check and the tunnel URL reservation.
|
|
247
|
+
const state = publicState(await ps.ready({ publicUrl: true }));
|
|
248
|
+
send({ type: "public", ...state });
|
|
249
|
+
if (state.registered) void watchTunnel();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function shutdown(signal) {
|
|
253
|
+
process.stderr.write(`[entry] ${signal}, stopping\n`);
|
|
254
|
+
try {
|
|
255
|
+
await ps?.stop();
|
|
256
|
+
} finally {
|
|
257
|
+
send({ type: "stopped" });
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
263
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
264
|
+
|
|
265
|
+
main().catch((error) => {
|
|
266
|
+
send({
|
|
267
|
+
type: "error",
|
|
268
|
+
message: error instanceof Error ? error.message : String(error),
|
|
269
|
+
});
|
|
270
|
+
process.exit(1);
|
|
271
|
+
});
|