@netnodeag/kraftwerk 0.76.0 → 0.77.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 +33 -27
- package/dist/cli/cloud.d.ts +2 -0
- package/dist/cli/cloud.js +262 -0
- package/dist/cli/daemon.js +9 -0
- package/dist/cli/doctor.js +16 -8
- package/dist/cli/init.js +2 -2
- package/dist/cli/kraftwerk.js +4 -0
- package/dist/client/routes.d.ts +4 -0
- package/dist/client/routes.js +4 -0
- package/dist/config.d.ts +15 -28
- package/dist/config.js +5 -19
- package/dist/core/cloud.d.ts +115 -0
- package/dist/core/cloud.js +94 -0
- package/dist/server/api/cloud.d.ts +72 -0
- package/dist/server/api/cloud.js +38 -0
- package/dist/server/api/devices.d.ts +8 -8
- package/dist/server/api/index.d.ts +88 -10
- package/dist/server/api/index.js +2 -0
- package/dist/server/api/workspace.d.ts +16 -1
- package/dist/server/api/workspace.js +2 -1
- package/dist/server/cloud.d.ts +65 -33
- package/dist/server/cloud.js +207 -188
- package/dist/server/hub-context.d.ts +1 -1
- package/dist/server/hub-context.js +3 -3
- package/dist/server/relay.d.ts +13 -0
- package/dist/server/relay.js +29 -1
- package/dist/server/server.d.ts +3 -2
- package/dist/server/server.js +6 -6
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +4 -4
- package/web/dist/assets/{dist-T1sNLC62.js → dist-BGHweZKi.js} +1 -1
- package/web/dist/assets/{dist-DFXmaDc5.js → dist-BVPxOqcx.js} +1 -1
- package/web/dist/assets/{dist-BYkcY-Vl.js → dist-CCNyRSAA.js} +1 -1
- package/web/dist/assets/{dist-flnLXEzU.js → dist-CXoAMU1b.js} +1 -1
- package/web/dist/assets/{dist-l7foWlFx.js → dist-DCFREMXk.js} +1 -1
- package/web/dist/assets/{dist-BQpWYkxm.js → dist-DT2Tjvwx.js} +1 -1
- package/web/dist/assets/{dist-BwIca_Uj.js → dist-DdA5PoVJ.js} +1 -1
- package/web/dist/assets/{dist-fgB7OEJy.js → dist-DoD3qX1P.js} +1 -1
- package/web/dist/assets/{dist-vuciRCSz.js → dist-FFA4-S6d.js} +1 -1
- package/web/dist/assets/{dist-BTVP-6On.js → dist-yGx0qYbw.js} +1 -1
- package/web/dist/assets/dist-zjUUP5a9.js +1 -0
- package/web/dist/assets/{editor-C6Ok8NPo.js → editor-CyWapLQr.js} +3 -3
- package/web/dist/assets/{index-z4tpCCzk.js → index-CNXcNoKJ.js} +4 -4
- package/web/dist/index.html +1 -1
- package/web/dist/assets/dist-BURf1l-A.js +0 -1
package/dist/server/cloud.d.ts
CHANGED
|
@@ -1,53 +1,85 @@
|
|
|
1
|
+
import { type CloudUrlSource } from "../core/cloud.js";
|
|
1
2
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* The machine client: the daemon registers THIS MACHINE with the kraftwerk
|
|
4
|
+
* cloud once and heartbeats while it runs; the workspaces it serves ride
|
|
5
|
+
* along as children of that record (see core/cloud.ts for the contract and
|
|
6
|
+
* the settings). One per process, and only the daemon starts it
|
|
7
|
+
* (startMachineCloud from `kraftwerk daemon`): a standalone `kraftwerk ui`
|
|
8
|
+
* and test fixtures never talk to the cloud.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
* ~/.kraftwerk/cloud
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* start ensure the machine key (relay.json, relay stays off), drop the
|
|
11
|
+
* retired per-workspace identities (~/.kraftwerk/cloud/), and,
|
|
12
|
+
* unless cloud.json or KRAFTWERK_CLOUD_URL=off says no, register
|
|
13
|
+
* — anonymous: the answer carries a claim code until someone
|
|
14
|
+
* claims the machine (`kraftwerk login`, or the code in the UI)
|
|
15
|
+
* heartbeat every `interval` the cloud asked for; the workspace list rides
|
|
16
|
+
* along whenever it changed (opened, closed, vetoed, claimed).
|
|
17
|
+
* A claim made in the cloud arrives in the answer
|
|
18
|
+
* privacy while unclaimed, entries carry name/slug/ws-id only — no
|
|
19
|
+
* roots; once claimed, roots too
|
|
20
|
+
* recovery a cloud that no longer knows the machine (data reset) gets a
|
|
21
|
+
* fresh registration; a cloud that is down is retried at the
|
|
22
|
+
* same cadence and never affects the UI
|
|
23
|
+
* goodbye on shutdown (bounded), and when `kraftwerk cloud off` reaches
|
|
24
|
+
* the running daemon
|
|
16
25
|
*/
|
|
17
26
|
export type CloudState = "off" | "connecting" | "connected" | "error";
|
|
18
27
|
export interface CloudStatus {
|
|
19
28
|
url: string;
|
|
20
29
|
state: CloudState;
|
|
21
|
-
/**
|
|
30
|
+
/** Why it is off, when it is. */
|
|
31
|
+
reason?: string;
|
|
32
|
+
/** Where the URL came from (env, cloud.json, a deprecated kraftwerk.yml cloud.url, the default). */
|
|
33
|
+
source?: CloudUrlSource;
|
|
34
|
+
/** Last error message while registering or heartbeating. */
|
|
22
35
|
error?: string;
|
|
23
|
-
/**
|
|
36
|
+
/** The machine's id in the cloud (its relay id). */
|
|
24
37
|
id?: string;
|
|
25
|
-
/**
|
|
38
|
+
/** This machine's handle@machine. */
|
|
39
|
+
machine?: string;
|
|
40
|
+
/** Email of the account that claimed the machine; null while unclaimed. */
|
|
26
41
|
account?: string | null;
|
|
27
|
-
/** Effective seconds between heartbeats (the cloud
|
|
42
|
+
/** Effective seconds between heartbeats (what the cloud asked for). */
|
|
28
43
|
interval?: number;
|
|
44
|
+
/** Last successful register or heartbeat. */
|
|
29
45
|
lastSeen?: string;
|
|
30
|
-
/** XXXX-XXXX
|
|
46
|
+
/** XXXX-XXXX that claims this machine in the cloud; null once claimed. */
|
|
31
47
|
claimCode?: string | null;
|
|
32
|
-
/** The cloud page that claims this
|
|
48
|
+
/** The cloud page that claims this machine after sign-in. */
|
|
33
49
|
claimUrl?: string | null;
|
|
50
|
+
/** Workspaces reported (vetoed ones left out). */
|
|
51
|
+
workspaces?: number;
|
|
34
52
|
}
|
|
53
|
+
/** What the daemon serves right now. */
|
|
54
|
+
export type WorkspaceSource = () => Array<{
|
|
55
|
+
root: string;
|
|
56
|
+
slug: string;
|
|
57
|
+
name: string;
|
|
58
|
+
}>;
|
|
35
59
|
export declare const cloudStatus: () => CloudStatus;
|
|
36
60
|
/**
|
|
37
|
-
* Start the client
|
|
38
|
-
*
|
|
39
|
-
* own afterwards.
|
|
61
|
+
* Start the machine client (the daemon, once its workspaces are open).
|
|
62
|
+
* Resolves after the first registration attempt; the heartbeat runs on its
|
|
63
|
+
* own afterwards. A second start in the same process is ignored.
|
|
64
|
+
*/
|
|
65
|
+
export declare function startMachineCloud(opts: {
|
|
66
|
+
version: string;
|
|
67
|
+
workspaces: WorkspaceSource;
|
|
68
|
+
owner?: unknown;
|
|
69
|
+
}): Promise<CloudStatus>;
|
|
70
|
+
/**
|
|
71
|
+
* Settings changed (`kraftwerk cloud on|off`, a URL): turn the client on or
|
|
72
|
+
* off to match; off says goodbye first. Without a started client (a
|
|
73
|
+
* standalone server) nothing happens.
|
|
40
74
|
*/
|
|
41
|
-
export declare function
|
|
42
|
-
/**
|
|
43
|
-
export declare function
|
|
75
|
+
export declare function applyMachineCloud(): Promise<CloudStatus>;
|
|
76
|
+
/** A round now (the CLI's `login` waits for a claim): registers when needed, else heartbeats. */
|
|
77
|
+
export declare function syncMachineCloud(): Promise<CloudStatus>;
|
|
78
|
+
/** Release the claim (`kraftwerk logout`): the cloud hands out a fresh claim code. */
|
|
79
|
+
export declare function unlinkMachineCloud(): Promise<CloudStatus>;
|
|
44
80
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
81
|
+
* Stop the client and tell the cloud the machine is going away (bounded:
|
|
82
|
+
* an exit handler waits for this). `owner`: only the client that owner
|
|
83
|
+
* started (a hub stopping in a test leaves another's alone).
|
|
48
84
|
*/
|
|
49
|
-
export declare function
|
|
50
|
-
/** Every served workspace: stop its heartbeat (the server closes). */
|
|
51
|
-
export declare function stopAllCloudSync(): void;
|
|
52
|
-
/** Every served workspace says goodbye, in parallel (still bounded per workspace). */
|
|
53
|
-
export declare function cloudGoodbyeAll(): Promise<void>;
|
|
85
|
+
export declare function stopMachineCloud(owner?: unknown): Promise<void>;
|
package/dist/server/cloud.js
CHANGED
|
@@ -1,232 +1,251 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { promises as fs } from "node:fs";
|
|
3
2
|
import os from "node:os";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/** The client's state, one per workspace: each served workspace registers and heartbeats on its own. */
|
|
15
|
-
const client = perWorkspace(() => ({
|
|
16
|
-
status: { url: "", state: "off" },
|
|
17
|
-
identity: null,
|
|
3
|
+
import { resolveWorkspace } from "../config.js";
|
|
4
|
+
import { claimUrlFor, machineCloud, machineGoodbye, machineHeartbeat, machineRegister, machineUnlink, retiredCloudDir, unknownMachine, } from "../core/cloud.js";
|
|
5
|
+
import { identityString, readIdentity } from "../core/identity.js";
|
|
6
|
+
import { machineCredentials } from "./relay.js";
|
|
7
|
+
const DEFAULT_INTERVAL = 60;
|
|
8
|
+
const STANDALONE = { url: "", state: "off", reason: "only the daemon registers this machine with the cloud (`kraftwerk daemon start`)" };
|
|
9
|
+
const m = {
|
|
10
|
+
status: { ...STANDALONE },
|
|
11
|
+
creds: null,
|
|
12
|
+
registered: false,
|
|
18
13
|
timer: null,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
14
|
+
started: false,
|
|
15
|
+
owner: null,
|
|
16
|
+
source: (() => []),
|
|
17
|
+
version: "",
|
|
18
|
+
/** The workspace list the cloud last acknowledged, as JSON: sent again only when it differs. */
|
|
19
|
+
reported: "",
|
|
20
|
+
/** One round at a time: a timer tick, a sync from the CLI and a workspace change never overlap. */
|
|
21
|
+
lock: Promise.resolve(),
|
|
22
|
+
warned: new Set(),
|
|
23
|
+
};
|
|
24
|
+
function serial(fn) {
|
|
25
|
+
const run = m.lock.then(fn, fn);
|
|
26
|
+
m.lock = run.catch(() => { });
|
|
27
|
+
return run;
|
|
37
28
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
export const cloudStatus = () => m.status;
|
|
30
|
+
/** The open workspaces as the cloud lists them: vetoed ones left out, roots only once claimed. */
|
|
31
|
+
async function entries(withRoots) {
|
|
32
|
+
const list = [];
|
|
33
|
+
const ymlUrls = [];
|
|
34
|
+
for (const o of m.source()) {
|
|
35
|
+
const ws = await resolveWorkspace(o.root).catch(() => null);
|
|
36
|
+
const c = ws?.config.cloud;
|
|
37
|
+
if (c?.enabled === false)
|
|
38
|
+
continue;
|
|
39
|
+
if (c?.url)
|
|
40
|
+
ymlUrls.push(c.url.replace(/\/+$/, ""));
|
|
41
|
+
list.push({ ...(ws?.config.id ? { id: ws.config.id } : {}), name: o.name, slug: o.slug, ...(withRoots ? { root: o.root } : {}) });
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
/** The agents and channels of this workspace, as the cloud lists them. */
|
|
46
|
-
async function roster() {
|
|
47
|
-
const [agents, channels] = await Promise.all([
|
|
48
|
-
listAgents().then((l) => l.map(toSummary)).catch(() => []),
|
|
49
|
-
listChannels().then((l) => l.map(toChannelSummary)).catch(() => []),
|
|
50
|
-
]);
|
|
51
|
-
return { agents, channels };
|
|
43
|
+
return { list, ymlUrls };
|
|
52
44
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
data = text ? JSON.parse(text) : {};
|
|
45
|
+
const claimed = () => typeof m.status.account === "string" && m.status.account.length > 0;
|
|
46
|
+
function warnYml(cloud, ymlUrls) {
|
|
47
|
+
if (!ymlUrls.length)
|
|
48
|
+
return;
|
|
49
|
+
const key = `${cloud.source}:${ymlUrls[0]}`;
|
|
50
|
+
if (m.warned.has(key))
|
|
51
|
+
return;
|
|
52
|
+
m.warned.add(key);
|
|
53
|
+
if (cloud.source === "kraftwerk.yml") {
|
|
54
|
+
console.warn(`⚠ kraftwerk.yml cloud.url is deprecated — the cloud is the machine's now: \`kraftwerk cloud on --url ${cloud.url}\` (using ${cloud.url} for now)`);
|
|
65
55
|
}
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
else {
|
|
57
|
+
console.warn(`⚠ kraftwerk.yml cloud.url (${ymlUrls[0]}) is ignored: the machine's cloud is ${cloud.url} (${cloud.source}); remove it from kraftwerk.yml`);
|
|
68
58
|
}
|
|
69
|
-
if (!r.ok)
|
|
70
|
-
throw new Error(data.error ?? `${r.status} from ${url}`);
|
|
71
|
-
return data;
|
|
72
59
|
}
|
|
73
|
-
/** Register (or re-register) this instance. Returns false when the cloud refused or is unreachable. */
|
|
74
60
|
async function register() {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
if (!
|
|
61
|
+
const { list, ymlUrls } = await entries(false);
|
|
62
|
+
const cloud = await machineCloud(ymlUrls);
|
|
63
|
+
warnYml(cloud, ymlUrls);
|
|
64
|
+
if (!cloud.enabled) {
|
|
65
|
+
m.status = { url: cloud.url, state: "off", source: cloud.source, ...(cloud.reason ? { reason: cloud.reason } : {}) };
|
|
79
66
|
return false;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const { agents, channels } = await roster();
|
|
83
|
-
c.lastRoster = JSON.stringify({ agents, channels });
|
|
84
|
-
const body = {
|
|
85
|
-
...(c.identity ?? {}),
|
|
86
|
-
name: project.config.name ?? path.basename(project.root),
|
|
87
|
-
icon: project.config.icon ?? "",
|
|
88
|
-
color: project.config.color ?? "",
|
|
89
|
-
root: project.root,
|
|
90
|
-
rootLabel: tildify(project.root),
|
|
91
|
-
hostname: os.hostname(),
|
|
92
|
-
platform: `${process.platform}-${process.arch}`,
|
|
93
|
-
version: c.selfVersion,
|
|
94
|
-
url: c.selfUrl,
|
|
95
|
-
// Part of the cloud's register contract; always "" since the Cloudflare Tunnel was removed (0.65).
|
|
96
|
-
publicUrl: "",
|
|
97
|
-
agents,
|
|
98
|
-
channels,
|
|
99
|
-
interval: cloud.interval,
|
|
100
|
-
startedAt: c.startedAt,
|
|
101
|
-
...(cloud.token ? { accountToken: cloud.token } : {}),
|
|
102
|
-
};
|
|
67
|
+
}
|
|
68
|
+
const identity = await readIdentity();
|
|
103
69
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
70
|
+
m.creds ??= await machineCredentials();
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
m.status = { url: cloud.url, source: cloud.source, state: "error", error: err.message };
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const base = { url: cloud.url, source: cloud.source, id: m.creds.id, machine: identityString(identity) };
|
|
77
|
+
if (m.status.state === "off" || m.status.url !== cloud.url)
|
|
78
|
+
m.status = { ...base, state: "connecting" };
|
|
79
|
+
try {
|
|
80
|
+
const r = await machineRegister(cloud.url, m.creds, {
|
|
81
|
+
name: identity.machine,
|
|
82
|
+
handle: identity.handle,
|
|
83
|
+
hostname: os.hostname(),
|
|
84
|
+
platform: `${process.platform}-${process.arch}`,
|
|
85
|
+
version: m.version,
|
|
86
|
+
}, list);
|
|
87
|
+
m.registered = true;
|
|
88
|
+
// An unclaimed register carries no roots: the list counts as reported only when that is all the cloud should know.
|
|
89
|
+
m.reported = r.account ? "" : JSON.stringify(list);
|
|
90
|
+
m.status = {
|
|
91
|
+
...base, state: "connected", account: r.account ?? null, interval: r.interval || DEFAULT_INTERVAL, lastSeen: new Date().toISOString(),
|
|
92
|
+
claimCode: r.claimCode ?? null, claimUrl: claimUrlFor(cloud.url, r.claimCode), workspaces: list.length,
|
|
112
93
|
};
|
|
94
|
+
// Already claimed: the roots follow at once.
|
|
95
|
+
if (claimed())
|
|
96
|
+
await heartbeat();
|
|
113
97
|
return true;
|
|
114
98
|
}
|
|
115
99
|
catch (err) {
|
|
116
|
-
|
|
100
|
+
m.registered = false;
|
|
101
|
+
m.status = { ...m.status, ...base, state: "error", error: err.message };
|
|
117
102
|
return false;
|
|
118
103
|
}
|
|
119
104
|
}
|
|
120
105
|
async function heartbeat() {
|
|
121
|
-
|
|
122
|
-
if (!c.identity || c.status.state === "off")
|
|
106
|
+
if (!m.creds || !m.registered)
|
|
123
107
|
return;
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
...c.identity,
|
|
128
|
-
version: c.selfVersion,
|
|
129
|
-
...(rosterNow !== c.lastRoster ? { agents, channels } : {}),
|
|
130
|
-
};
|
|
108
|
+
const wasClaimed = claimed();
|
|
109
|
+
const { list } = await entries(wasClaimed);
|
|
110
|
+
const now = JSON.stringify(list);
|
|
131
111
|
try {
|
|
132
|
-
const r = await
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
...
|
|
112
|
+
const r = await machineHeartbeat(m.status.url, m.creds, { version: m.version, ...(now !== m.reported ? { workspaces: list } : {}) });
|
|
113
|
+
m.reported = now;
|
|
114
|
+
m.status = {
|
|
115
|
+
...m.status, state: "connected", error: undefined, interval: r.interval || m.status.interval || DEFAULT_INTERVAL, lastSeen: new Date().toISOString(),
|
|
136
116
|
...(r.account !== undefined ? { account: r.account } : {}),
|
|
137
|
-
...(r.claimCode !== undefined ? { claimCode: r.claimCode, claimUrl: claimUrlFor(
|
|
117
|
+
...(r.claimCode !== undefined ? { claimCode: r.claimCode, claimUrl: claimUrlFor(m.status.url, r.claimCode) } : {}),
|
|
118
|
+
workspaces: list.length,
|
|
138
119
|
};
|
|
120
|
+
// Just claimed: the roots go out now, not a whole interval later.
|
|
121
|
+
if (!wasClaimed && claimed())
|
|
122
|
+
await heartbeat();
|
|
139
123
|
}
|
|
140
124
|
catch (err) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
c.identity = null;
|
|
146
|
-
// Not after a goodbye: a stopped client registers nothing again.
|
|
147
|
-
if (!c.stopped && workspaceOpen())
|
|
125
|
+
m.status = { ...m.status, state: "error", error: err.message };
|
|
126
|
+
if (unknownMachine(err)) {
|
|
127
|
+
m.registered = false;
|
|
128
|
+
if (m.started)
|
|
148
129
|
await register();
|
|
149
130
|
}
|
|
150
131
|
}
|
|
151
132
|
}
|
|
152
|
-
/**
|
|
133
|
+
/** One round: register when not registered, else heartbeat. */
|
|
134
|
+
const round = () => serial(async () => {
|
|
135
|
+
if (!m.started)
|
|
136
|
+
return;
|
|
137
|
+
if (m.registered)
|
|
138
|
+
await heartbeat();
|
|
139
|
+
else
|
|
140
|
+
await register();
|
|
141
|
+
});
|
|
153
142
|
function schedule() {
|
|
154
|
-
|
|
155
|
-
|
|
143
|
+
if (m.timer)
|
|
144
|
+
clearTimeout(m.timer);
|
|
145
|
+
m.timer = null;
|
|
146
|
+
if (!m.started || m.status.state === "off")
|
|
156
147
|
return;
|
|
157
|
-
const seconds =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (c.status.state === "connected" || c.status.state === "error") {
|
|
163
|
-
if (c.identity)
|
|
164
|
-
await heartbeat();
|
|
165
|
-
else
|
|
166
|
-
await register();
|
|
167
|
-
}
|
|
168
|
-
if (!c.stopped && workspaceOpen())
|
|
169
|
-
schedule();
|
|
148
|
+
const seconds = m.status.interval && m.status.interval > 0 ? m.status.interval : DEFAULT_INTERVAL;
|
|
149
|
+
m.timer = setTimeout(async () => {
|
|
150
|
+
m.timer = null;
|
|
151
|
+
await round();
|
|
152
|
+
schedule();
|
|
170
153
|
}, seconds * 1000);
|
|
171
|
-
|
|
154
|
+
m.timer.unref();
|
|
172
155
|
}
|
|
173
156
|
/**
|
|
174
|
-
* Start the client
|
|
175
|
-
*
|
|
176
|
-
* own afterwards.
|
|
157
|
+
* Start the machine client (the daemon, once its workspaces are open).
|
|
158
|
+
* Resolves after the first registration attempt; the heartbeat runs on its
|
|
159
|
+
* own afterwards. A second start in the same process is ignored.
|
|
177
160
|
*/
|
|
178
|
-
export async function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return;
|
|
192
|
-
c.status = { url: cloud.url, state: "connecting", interval: cloud.interval };
|
|
193
|
-
await register();
|
|
194
|
-
if (c.stopped || !workspaceOpen())
|
|
195
|
-
return;
|
|
196
|
-
// Until the cloud answers, retry at the configured cadence; once it does, at what it asked for.
|
|
197
|
-
if (c.status.state !== "connected")
|
|
198
|
-
c.status.interval = cloud.interval;
|
|
161
|
+
export async function startMachineCloud(opts) {
|
|
162
|
+
if (m.started)
|
|
163
|
+
return m.status;
|
|
164
|
+
m.started = true;
|
|
165
|
+
m.owner = opts.owner ?? null;
|
|
166
|
+
m.source = opts.workspaces;
|
|
167
|
+
m.version = opts.version;
|
|
168
|
+
m.status = { url: "", state: "off" };
|
|
169
|
+
// The per-workspace identities of before (one record per workspace) are retired with the machine record.
|
|
170
|
+
await fs.rm(retiredCloudDir(), { recursive: true, force: true }).catch(() => { });
|
|
171
|
+
// The machine's id exists from the daemon's first start on, with the relay still off.
|
|
172
|
+
m.creds = await machineCredentials().catch(() => null);
|
|
173
|
+
await serial(register);
|
|
199
174
|
schedule();
|
|
200
|
-
|
|
201
|
-
/** Stop the heartbeat loop (server close, tests). */
|
|
202
|
-
export function stopCloudSync() {
|
|
203
|
-
const c = client();
|
|
204
|
-
c.stopped = true;
|
|
205
|
-
if (c.timer)
|
|
206
|
-
clearTimeout(c.timer);
|
|
207
|
-
c.timer = null;
|
|
175
|
+
return m.status;
|
|
208
176
|
}
|
|
209
177
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
178
|
+
* Settings changed (`kraftwerk cloud on|off`, a URL): turn the client on or
|
|
179
|
+
* off to match; off says goodbye first. Without a started client (a
|
|
180
|
+
* standalone server) nothing happens.
|
|
213
181
|
*/
|
|
214
|
-
export async function
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
182
|
+
export async function applyMachineCloud() {
|
|
183
|
+
if (!m.started)
|
|
184
|
+
return m.status;
|
|
185
|
+
const cloud = await machineCloud((await entries(false)).ymlUrls);
|
|
186
|
+
if (!cloud.enabled || (m.registered && cloud.url !== m.status.url)) {
|
|
187
|
+
await serial(async () => {
|
|
188
|
+
if (m.registered && m.creds)
|
|
189
|
+
await machineGoodbye(m.status.url, m.creds, m.version).catch(() => { });
|
|
190
|
+
m.registered = false;
|
|
191
|
+
m.reported = "";
|
|
192
|
+
});
|
|
221
193
|
}
|
|
222
|
-
|
|
194
|
+
if (!cloud.enabled) {
|
|
195
|
+
if (m.timer)
|
|
196
|
+
clearTimeout(m.timer);
|
|
197
|
+
m.timer = null;
|
|
198
|
+
m.status = { url: cloud.url, state: "off", source: cloud.source, ...(cloud.reason ? { reason: cloud.reason } : {}) };
|
|
199
|
+
return m.status;
|
|
200
|
+
}
|
|
201
|
+
if (!m.registered)
|
|
202
|
+
await serial(register);
|
|
203
|
+
schedule();
|
|
204
|
+
return m.status;
|
|
205
|
+
}
|
|
206
|
+
/** A round now (the CLI's `login` waits for a claim): registers when needed, else heartbeats. */
|
|
207
|
+
export async function syncMachineCloud() {
|
|
208
|
+
if (!m.started)
|
|
209
|
+
return m.status;
|
|
210
|
+
if (m.status.state === "off")
|
|
211
|
+
return applyMachineCloud();
|
|
212
|
+
await round();
|
|
213
|
+
schedule();
|
|
214
|
+
return m.status;
|
|
223
215
|
}
|
|
224
|
-
/**
|
|
225
|
-
export function
|
|
226
|
-
|
|
227
|
-
|
|
216
|
+
/** Release the claim (`kraftwerk logout`): the cloud hands out a fresh claim code. */
|
|
217
|
+
export async function unlinkMachineCloud() {
|
|
218
|
+
if (!m.started || m.status.state === "off")
|
|
219
|
+
return m.status;
|
|
220
|
+
return serial(async () => {
|
|
221
|
+
if (!m.registered)
|
|
222
|
+
await register();
|
|
223
|
+
if (!m.creds || !m.registered)
|
|
224
|
+
throw new Error(m.status.error ?? "not registered with the cloud");
|
|
225
|
+
const r = await machineUnlink(m.status.url, m.creds, m.version);
|
|
226
|
+
// The next heartbeat sends the list again — without roots now.
|
|
227
|
+
m.reported = "";
|
|
228
|
+
m.status = { ...m.status, account: null, claimCode: r.claimCode ?? null, claimUrl: claimUrlFor(m.status.url, r.claimCode) };
|
|
229
|
+
return m.status;
|
|
230
|
+
});
|
|
228
231
|
}
|
|
229
|
-
/**
|
|
230
|
-
|
|
231
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Stop the client and tell the cloud the machine is going away (bounded:
|
|
234
|
+
* an exit handler waits for this). `owner`: only the client that owner
|
|
235
|
+
* started (a hub stopping in a test leaves another's alone).
|
|
236
|
+
*/
|
|
237
|
+
export async function stopMachineCloud(owner) {
|
|
238
|
+
if (!m.started || (owner !== undefined && owner !== m.owner))
|
|
239
|
+
return;
|
|
240
|
+
m.started = false;
|
|
241
|
+
if (m.timer)
|
|
242
|
+
clearTimeout(m.timer);
|
|
243
|
+
m.timer = null;
|
|
244
|
+
const { creds, registered } = m;
|
|
245
|
+
const url = m.status.url;
|
|
246
|
+
m.registered = false;
|
|
247
|
+
m.reported = "";
|
|
248
|
+
m.status = { ...STANDALONE };
|
|
249
|
+
if (registered && creds && url)
|
|
250
|
+
await machineGoodbye(url, creds, m.version).catch(() => { });
|
|
232
251
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Hub } from "./server.js";
|
|
2
2
|
export declare const runInHub: <T>(hub: Hub, fn: () => T) => T;
|
|
3
3
|
export declare function currentHub(): Hub;
|
|
4
|
-
/** API paths that need no workspace: the machine's own (the hub, devices and pairing, the protocol, the socket). */
|
|
4
|
+
/** API paths that need no workspace: the machine's own (the hub, devices and pairing, the cloud record, the protocol, the socket). */
|
|
5
5
|
export declare const MACHINE_PATHS: RegExp;
|
|
6
6
|
/** Route names callable over the socket without a workspace. */
|
|
7
7
|
export declare const MACHINE_ROUTES: RegExp;
|
|
@@ -12,7 +12,7 @@ export function currentHub() {
|
|
|
12
12
|
throw new Error("no hub here");
|
|
13
13
|
return hub;
|
|
14
14
|
}
|
|
15
|
-
/** API paths that need no workspace: the machine's own (the hub, devices and pairing, the protocol, the socket). */
|
|
16
|
-
export const MACHINE_PATHS = /^\/api\/(hub(\/|$)|pair$|devices(\/|$)|protocol$|ws$)/;
|
|
15
|
+
/** API paths that need no workspace: the machine's own (the hub, devices and pairing, the cloud record, the protocol, the socket). */
|
|
16
|
+
export const MACHINE_PATHS = /^\/api\/(hub(\/|$)|pair$|devices(\/|$)|cloud(\/|$)|protocol$|ws$)/;
|
|
17
17
|
/** Route names callable over the socket without a workspace. */
|
|
18
|
-
export const MACHINE_ROUTES = /^(hub|devices|protocol)\./;
|
|
18
|
+
export const MACHINE_ROUTES = /^(hub|devices|cloud|protocol)\./;
|
package/dist/server/relay.d.ts
CHANGED
|
@@ -44,6 +44,19 @@ export interface RelayStatus {
|
|
|
44
44
|
export declare function readRelaySettings(): Promise<RelaySettings | null>;
|
|
45
45
|
/** Turn the relay on or off (creating the machine's key on first use); `url` switches relays. */
|
|
46
46
|
export declare function setRelay(enabled: boolean, url?: string): Promise<RelaySettings>;
|
|
47
|
+
/**
|
|
48
|
+
* The machine's key pair and secret, created (with the relay staying off)
|
|
49
|
+
* when relay.json is missing: the daemon's first start, `kraftwerk login`.
|
|
50
|
+
* The cloud knows the machine by the same id and secret the relay does; a
|
|
51
|
+
* damaged file is refused, never overwritten (it holds the id).
|
|
52
|
+
*/
|
|
53
|
+
export declare function ensureRelayKeys(): Promise<RelaySettings>;
|
|
54
|
+
/** The machine's id (its relay id), public key and secret: what the cloud knows it by. */
|
|
55
|
+
export declare function machineCredentials(): Promise<{
|
|
56
|
+
id: string;
|
|
57
|
+
key: string;
|
|
58
|
+
secret: string;
|
|
59
|
+
}>;
|
|
47
60
|
/** wss://host/api/relay/<role> for a cloud URL. */
|
|
48
61
|
export declare const relayEndpoint: (cloudUrl: string, role: "server" | "client") => string;
|
|
49
62
|
/**
|