darkmesh-node 0.1.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/CHANGELOG.md +12 -0
- package/README.md +89 -0
- package/assets/darksol-banner.png +0 -0
- package/package.json +35 -0
- package/src/cli.js +366 -0
- package/src/client.js +565 -0
- package/src/config.js +64 -0
- package/src/identity.js +26 -0
- package/src/index.js +400 -0
- package/src/operator.js +113 -0
- package/src/providers.js +19 -0
- package/src/service.js +148 -0
package/src/client.js
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
2
|
+
import { loadIdentity, ensureIdentity } from './identity.js';
|
|
3
|
+
import { createOperatorBindingAuth, ensureOperatorIdentity, fetchOperatorAccount, registerOperatorAccount } from './operator.js';
|
|
4
|
+
import { createProviderAdapter } from './providers.js';
|
|
5
|
+
import { createId } from '../../../packages/shared/src/index.js';
|
|
6
|
+
import { DarkMeshNode } from './index.js';
|
|
7
|
+
import { clearServiceStopRequest, isPidRunning, loadServiceState, readServiceLog, refreshServiceState, saveServiceState, serviceStopRequested, writeServiceStopRequest } from './service.js';
|
|
8
|
+
|
|
9
|
+
export async function initNodeClient(customHome, overrides = {}) {
|
|
10
|
+
const { saveConfig, getDefaultConfig } = await import('./config.js');
|
|
11
|
+
const current = await loadConfig(customHome);
|
|
12
|
+
const nextConfig = {
|
|
13
|
+
...getDefaultConfig(customHome),
|
|
14
|
+
...current,
|
|
15
|
+
...overrides,
|
|
16
|
+
provider: {
|
|
17
|
+
...current.provider,
|
|
18
|
+
...(overrides.provider || {})
|
|
19
|
+
},
|
|
20
|
+
models: Array.isArray(overrides.models) && overrides.models.length ? overrides.models : current.models
|
|
21
|
+
};
|
|
22
|
+
const identity = await ensureIdentity(customHome);
|
|
23
|
+
const operator = await ensureOperatorIdentity(customHome, {
|
|
24
|
+
operatorId: nextConfig.operatorId,
|
|
25
|
+
label: nextConfig.label
|
|
26
|
+
});
|
|
27
|
+
await saveConfig(nextConfig, customHome);
|
|
28
|
+
return {
|
|
29
|
+
home: customHome,
|
|
30
|
+
config: nextConfig,
|
|
31
|
+
operator: {
|
|
32
|
+
operatorId: operator.operatorId,
|
|
33
|
+
label: operator.label,
|
|
34
|
+
publicKey: operator.publicKey,
|
|
35
|
+
createdAt: operator.createdAt
|
|
36
|
+
},
|
|
37
|
+
identity: {
|
|
38
|
+
publicKey: identity.publicKey,
|
|
39
|
+
createdAt: identity.createdAt
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function whoAmI(customHome) {
|
|
45
|
+
const config = await loadConfig(customHome);
|
|
46
|
+
const identity = await loadIdentity(customHome);
|
|
47
|
+
const operator = await ensureOperatorIdentity(customHome, {
|
|
48
|
+
operatorId: config.operatorId,
|
|
49
|
+
label: config.label
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
label: config.label,
|
|
53
|
+
operatorId: config.operatorId,
|
|
54
|
+
nodeId: config.nodeId,
|
|
55
|
+
coordinatorUrl: config.coordinatorUrl,
|
|
56
|
+
privacyClass: config.privacyClass,
|
|
57
|
+
models: config.models,
|
|
58
|
+
operatorAccount: {
|
|
59
|
+
operatorId: operator.operatorId,
|
|
60
|
+
label: operator.label,
|
|
61
|
+
publicKey: operator.publicKey,
|
|
62
|
+
createdAt: operator.createdAt
|
|
63
|
+
},
|
|
64
|
+
hasIdentity: Boolean(identity?.publicKey),
|
|
65
|
+
publicKey: identity?.publicKey || null,
|
|
66
|
+
identityCreatedAt: identity?.createdAt || null
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function fetchNodeStatus(customHome) {
|
|
71
|
+
const config = await loadConfig(customHome);
|
|
72
|
+
const service = await refreshServiceState(customHome);
|
|
73
|
+
const baseStatus = {
|
|
74
|
+
configured: true,
|
|
75
|
+
registration: {
|
|
76
|
+
local: Boolean(config.nodeId),
|
|
77
|
+
nodeId: config.nodeId || null
|
|
78
|
+
},
|
|
79
|
+
runtime: {
|
|
80
|
+
serviceInstalled: service.installed,
|
|
81
|
+
serviceStatus: service.status,
|
|
82
|
+
serviceRunning: service.running,
|
|
83
|
+
servicePid: service.pid || null,
|
|
84
|
+
servicePidRunning: service.pidRunning || false,
|
|
85
|
+
stopRequested: service.stopRequested || false
|
|
86
|
+
},
|
|
87
|
+
coordinatorUrl: config.coordinatorUrl,
|
|
88
|
+
label: config.label,
|
|
89
|
+
operatorId: config.operatorId,
|
|
90
|
+
provider: config.provider,
|
|
91
|
+
models: config.models,
|
|
92
|
+
privacyClass: config.privacyClass,
|
|
93
|
+
availableCredits: 0,
|
|
94
|
+
lifetimeEarnedCredits: 0,
|
|
95
|
+
recentJobs: []
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (!config.nodeId) {
|
|
99
|
+
return {
|
|
100
|
+
...baseStatus,
|
|
101
|
+
registered: false,
|
|
102
|
+
connected: false,
|
|
103
|
+
connectivity: 'unregistered',
|
|
104
|
+
registrationState: 'unregistered',
|
|
105
|
+
nodeId: null,
|
|
106
|
+
health: null
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const res = await fetch(`${config.coordinatorUrl}/v1/nodes/${encodeURIComponent(config.nodeId)}`);
|
|
111
|
+
if (res.status === 404) {
|
|
112
|
+
return {
|
|
113
|
+
...baseStatus,
|
|
114
|
+
registered: false,
|
|
115
|
+
connected: false,
|
|
116
|
+
connectivity: 'not_found',
|
|
117
|
+
registrationState: 'stale_local_registration',
|
|
118
|
+
nodeId: config.nodeId,
|
|
119
|
+
health: null
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!res.ok) throw new Error(`status_fetch_failed_${res.status}`);
|
|
124
|
+
const data = await res.json();
|
|
125
|
+
const coordinatorConnected = Boolean(data.status.connected);
|
|
126
|
+
const registrationState = coordinatorConnected ? 'registered' : 'registered_offline';
|
|
127
|
+
return {
|
|
128
|
+
...baseStatus,
|
|
129
|
+
registered: true,
|
|
130
|
+
connected: coordinatorConnected,
|
|
131
|
+
connectivity: coordinatorConnected ? 'connected' : 'offline',
|
|
132
|
+
registrationState,
|
|
133
|
+
nodeId: data.node.id,
|
|
134
|
+
label: data.node.label,
|
|
135
|
+
operatorId: data.node.operatorId,
|
|
136
|
+
provider: {
|
|
137
|
+
type: data.node.providerType || data.node.provider || config.provider.type,
|
|
138
|
+
baseUrl: config.provider.baseUrl
|
|
139
|
+
},
|
|
140
|
+
models: data.node.models || config.models,
|
|
141
|
+
verifiedModels: data.node.verifiedModels || [],
|
|
142
|
+
rejectedModels: data.node.rejectedModels || [],
|
|
143
|
+
verificationStatus: data.node.verificationStatus || 'pending',
|
|
144
|
+
verificationReason: data.node.verificationReason || null,
|
|
145
|
+
lastVerifiedAt: data.node.lastVerifiedAt || null,
|
|
146
|
+
privacyClass: data.node.privacyClass,
|
|
147
|
+
health: data.node.health,
|
|
148
|
+
activeJobId: data.status.activeJobId,
|
|
149
|
+
availableCredits: data.status.availableCredits,
|
|
150
|
+
lifetimeEarnedCredits: data.status.lifetimeEarnedCredits,
|
|
151
|
+
recentJobs: data.status.recentJobs || []
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function connectNodeClient(customHome) {
|
|
156
|
+
const config = await loadConfig(customHome);
|
|
157
|
+
const identity = await ensureIdentity(customHome);
|
|
158
|
+
const operator = await ensureOperatorIdentity(customHome, {
|
|
159
|
+
operatorId: config.operatorId,
|
|
160
|
+
label: config.label
|
|
161
|
+
});
|
|
162
|
+
const adapter = createProviderAdapter({
|
|
163
|
+
providerType: config.provider.type,
|
|
164
|
+
baseUrl: config.provider.baseUrl,
|
|
165
|
+
model: config.models?.[0],
|
|
166
|
+
models: config.models
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const models = await adapter.listModels();
|
|
170
|
+
const requestedNodeId = config.nodeId || createId('node');
|
|
171
|
+
let operatorAccount = await fetchOperatorAccount({
|
|
172
|
+
coordinatorUrl: config.coordinatorUrl,
|
|
173
|
+
operatorId: operator.operatorId
|
|
174
|
+
});
|
|
175
|
+
if (!operatorAccount) {
|
|
176
|
+
const registered = await registerOperatorAccount({
|
|
177
|
+
coordinatorUrl: config.coordinatorUrl,
|
|
178
|
+
operator
|
|
179
|
+
});
|
|
180
|
+
operatorAccount = registered.operator;
|
|
181
|
+
}
|
|
182
|
+
const operatorAuth = await createOperatorBindingAuth({
|
|
183
|
+
coordinatorUrl: config.coordinatorUrl,
|
|
184
|
+
operator,
|
|
185
|
+
nodeId: requestedNodeId,
|
|
186
|
+
nodePublicKey: identity.publicKey,
|
|
187
|
+
label: config.label
|
|
188
|
+
});
|
|
189
|
+
const res = await fetch(`${config.coordinatorUrl}/v1/nodes/register`, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: { 'content-type': 'application/json' },
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
nodeId: requestedNodeId,
|
|
194
|
+
operatorId: config.operatorId,
|
|
195
|
+
label: config.label,
|
|
196
|
+
publicKey: identity.publicKey,
|
|
197
|
+
privacyClass: config.privacyClass,
|
|
198
|
+
operatorAuth
|
|
199
|
+
})
|
|
200
|
+
});
|
|
201
|
+
if (!res.ok) throw new Error(`node_register_failed_${res.status}`);
|
|
202
|
+
const data = await res.json();
|
|
203
|
+
config.nodeId = data.nodeId;
|
|
204
|
+
await saveConfig(config, customHome);
|
|
205
|
+
return {
|
|
206
|
+
info: {
|
|
207
|
+
nodeId: data.nodeId,
|
|
208
|
+
models,
|
|
209
|
+
wsPath: data.wsPath,
|
|
210
|
+
operatorAccount
|
|
211
|
+
},
|
|
212
|
+
config
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function fetchNodeJobs(customHome) {
|
|
217
|
+
const status = await fetchNodeStatus(customHome);
|
|
218
|
+
return {
|
|
219
|
+
nodeId: status.nodeId,
|
|
220
|
+
activeJobId: status.activeJobId || null,
|
|
221
|
+
jobs: status.recentJobs || []
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function fetchNodeEarnings(customHome) {
|
|
226
|
+
const status = await fetchNodeStatus(customHome);
|
|
227
|
+
const completedJobs = (status.recentJobs || []).filter(job => job.status === 'completed');
|
|
228
|
+
return {
|
|
229
|
+
nodeId: status.nodeId,
|
|
230
|
+
availableCredits: status.availableCredits || 0,
|
|
231
|
+
lifetimeEarnedCredits: status.lifetimeEarnedCredits || 0,
|
|
232
|
+
completedJobs: completedJobs.length,
|
|
233
|
+
recentPayouts: completedJobs.map(job => ({
|
|
234
|
+
jobId: job.id,
|
|
235
|
+
payoutCredits: job.payoutCredits,
|
|
236
|
+
completedAt: job.completedAt
|
|
237
|
+
}))
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function fetchNodeBalance(customHome) {
|
|
242
|
+
const config = await loadConfig(customHome);
|
|
243
|
+
if (!config.nodeId) {
|
|
244
|
+
return {
|
|
245
|
+
nodeId: null,
|
|
246
|
+
availableCredits: 0,
|
|
247
|
+
escrowCredits: 0,
|
|
248
|
+
lifetimeEarnedCredits: 0,
|
|
249
|
+
uptimeCredits: 0,
|
|
250
|
+
jobCredits: 0
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const res = await fetch(`${config.coordinatorUrl}/v1/ledger/balance?nodeId=${encodeURIComponent(config.nodeId)}`);
|
|
255
|
+
if (!res.ok) throw new Error(`balance_fetch_failed_${res.status}`);
|
|
256
|
+
const data = await res.json();
|
|
257
|
+
return data.balance;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function fetchNodeLedgerEvents(customHome) {
|
|
261
|
+
const config = await loadConfig(customHome);
|
|
262
|
+
if (!config.nodeId) {
|
|
263
|
+
return {
|
|
264
|
+
nodeId: null,
|
|
265
|
+
entries: []
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const res = await fetch(`${config.coordinatorUrl}/v1/ledger/events?nodeId=${encodeURIComponent(config.nodeId)}`);
|
|
270
|
+
if (!res.ok) throw new Error(`ledger_events_fetch_failed_${res.status}`);
|
|
271
|
+
const data = await res.json();
|
|
272
|
+
return {
|
|
273
|
+
nodeId: config.nodeId,
|
|
274
|
+
entries: data.entries || []
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function fetchNodeClaims(customHome, options = {}) {
|
|
279
|
+
const config = await loadConfig(customHome);
|
|
280
|
+
if (!config.nodeId) {
|
|
281
|
+
return {
|
|
282
|
+
nodeId: null,
|
|
283
|
+
claims: []
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const params = new URLSearchParams({ nodeId: config.nodeId });
|
|
288
|
+
if (options.status) params.set('status', options.status);
|
|
289
|
+
const res = await fetch(`${config.coordinatorUrl}/v1/ledger/claims?${params.toString()}`);
|
|
290
|
+
if (!res.ok) throw new Error(`claims_fetch_failed_${res.status}`);
|
|
291
|
+
const data = await res.json();
|
|
292
|
+
return {
|
|
293
|
+
nodeId: config.nodeId,
|
|
294
|
+
claims: data.claims || []
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function reviewNodeClaim(customHome, claimId, options = {}) {
|
|
299
|
+
const config = await loadConfig(customHome);
|
|
300
|
+
if (!config.nodeId) throw new Error('node_not_registered');
|
|
301
|
+
if (!claimId) throw new Error('claim_id_required');
|
|
302
|
+
const action = options.action === 'approve' ? 'approve' : options.action === 'reject' ? 'reject' : null;
|
|
303
|
+
if (!action) throw new Error('invalid_claim_review_action');
|
|
304
|
+
|
|
305
|
+
const res = await fetch(`${config.coordinatorUrl}/v1/ledger/claims/${encodeURIComponent(claimId)}/review`, {
|
|
306
|
+
method: 'POST',
|
|
307
|
+
headers: { 'content-type': 'application/json' },
|
|
308
|
+
body: JSON.stringify({
|
|
309
|
+
action,
|
|
310
|
+
reviewerId: options.reviewerId || config.operatorId || null,
|
|
311
|
+
requesterId: options.requesterId || config.operatorId || null,
|
|
312
|
+
reason: options.reason || null
|
|
313
|
+
})
|
|
314
|
+
});
|
|
315
|
+
const data = await res.json();
|
|
316
|
+
if (!res.ok) throw new Error(data.error || `claim_review_failed_${res.status}`);
|
|
317
|
+
return data;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function requestNodePayout(customHome, options = {}) {
|
|
321
|
+
const config = await loadConfig(customHome);
|
|
322
|
+
if (!config.nodeId) throw new Error('node_not_registered');
|
|
323
|
+
const amountCredits = Number(options.amountCredits);
|
|
324
|
+
if (!Number.isFinite(amountCredits) || amountCredits <= 0) throw new Error('invalid_claim_amount');
|
|
325
|
+
|
|
326
|
+
const res = await fetch(`${config.coordinatorUrl}/v1/ledger/claims/request`, {
|
|
327
|
+
method: 'POST',
|
|
328
|
+
headers: { 'content-type': 'application/json' },
|
|
329
|
+
body: JSON.stringify({
|
|
330
|
+
nodeId: config.nodeId,
|
|
331
|
+
amountCredits,
|
|
332
|
+
destination: options.destination || null,
|
|
333
|
+
requesterId: options.requesterId || config.operatorId || null
|
|
334
|
+
})
|
|
335
|
+
});
|
|
336
|
+
const data = await res.json();
|
|
337
|
+
if (!res.ok) throw new Error(data.error || `claim_request_failed_${res.status}`);
|
|
338
|
+
return data;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function createRuntimeNode(customHome) {
|
|
342
|
+
const config = await loadConfig(customHome);
|
|
343
|
+
const identity = await ensureIdentity(customHome);
|
|
344
|
+
const operator = await ensureOperatorIdentity(customHome, {
|
|
345
|
+
operatorId: config.operatorId,
|
|
346
|
+
label: config.label
|
|
347
|
+
});
|
|
348
|
+
const adapter = createProviderAdapter({
|
|
349
|
+
providerType: config.provider.type,
|
|
350
|
+
baseUrl: config.provider.baseUrl,
|
|
351
|
+
model: config.models?.[0],
|
|
352
|
+
models: config.models
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const node = new DarkMeshNode({
|
|
356
|
+
coordinatorBaseUrl: config.coordinatorUrl,
|
|
357
|
+
operatorId: config.operatorId,
|
|
358
|
+
label: config.label,
|
|
359
|
+
privacyClass: config.privacyClass,
|
|
360
|
+
privacySecret: config.privacySecret,
|
|
361
|
+
keyPair: {
|
|
362
|
+
publicKey: identity.publicKey,
|
|
363
|
+
privateKey: identity.privateKey
|
|
364
|
+
},
|
|
365
|
+
operatorKeyPair: {
|
|
366
|
+
publicKey: operator.publicKey,
|
|
367
|
+
privateKey: operator.privateKey
|
|
368
|
+
},
|
|
369
|
+
nodeId: config.nodeId,
|
|
370
|
+
adapter
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return { node, config };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export async function runNodeClient(customHome) {
|
|
377
|
+
const { node, config } = await createRuntimeNode(customHome);
|
|
378
|
+
const info = await node.connect();
|
|
379
|
+
if (config.nodeId !== info.nodeId) {
|
|
380
|
+
config.nodeId = info.nodeId;
|
|
381
|
+
await saveConfig(config, customHome);
|
|
382
|
+
}
|
|
383
|
+
return { node, info, config };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function disconnectNodeClient(customHome, options = {}) {
|
|
387
|
+
const config = await loadConfig(customHome);
|
|
388
|
+
if (!config.nodeId) {
|
|
389
|
+
return {
|
|
390
|
+
nodeId: null,
|
|
391
|
+
disconnected: true,
|
|
392
|
+
changed: false,
|
|
393
|
+
reason: 'not_registered'
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const current = await fetchNodeStatus(customHome);
|
|
398
|
+
const wasRegistered = current.registered;
|
|
399
|
+
const wasConnected = current.connected;
|
|
400
|
+
config.nodeId = null;
|
|
401
|
+
await saveConfig(config, customHome);
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
nodeId: current.nodeId,
|
|
405
|
+
disconnected: true,
|
|
406
|
+
changed: wasRegistered,
|
|
407
|
+
wasConnected,
|
|
408
|
+
preserveRemoteRegistration: Boolean(options.preserveRemoteRegistration),
|
|
409
|
+
reason: wasRegistered ? 'local_registration_cleared' : 'already_unregistered'
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function getServiceStatus(customHome) {
|
|
414
|
+
return refreshServiceState(customHome);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function getServiceLogs(customHome, options = {}) {
|
|
418
|
+
return readServiceLog(customHome, options);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export async function startNodeService(customHome, options = {}) {
|
|
422
|
+
const state = await refreshServiceState(customHome);
|
|
423
|
+
if (state.running) {
|
|
424
|
+
return {
|
|
425
|
+
alreadyRunning: true,
|
|
426
|
+
service: state
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
await clearServiceStopRequest(customHome);
|
|
431
|
+
const nextState = await saveServiceState(customHome, {
|
|
432
|
+
...state,
|
|
433
|
+
pid: options.pid || state.pid || null,
|
|
434
|
+
status: options.status || 'starting',
|
|
435
|
+
startedAt: state.startedAt || new Date().toISOString(),
|
|
436
|
+
stoppedAt: null
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
alreadyRunning: false,
|
|
441
|
+
service: nextState
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export async function restartNodeService(customHome, options = {}) {
|
|
446
|
+
const before = await refreshServiceState(customHome);
|
|
447
|
+
const stopped = await stopNodeService(customHome);
|
|
448
|
+
if (stopped.running) {
|
|
449
|
+
throw new Error('service_restart_failed_to_stop');
|
|
450
|
+
}
|
|
451
|
+
const pid = typeof options.spawn === 'function'
|
|
452
|
+
? await options.spawn()
|
|
453
|
+
: (options.pid || null);
|
|
454
|
+
return {
|
|
455
|
+
restarted: true,
|
|
456
|
+
previouslyRunning: before.running,
|
|
457
|
+
stop: stopped,
|
|
458
|
+
...await startNodeService(customHome, { ...options, pid })
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export async function stopNodeService(customHome) {
|
|
463
|
+
const state = await refreshServiceState(customHome);
|
|
464
|
+
if (!state.installed || !state.pid) {
|
|
465
|
+
await saveServiceState(customHome, {
|
|
466
|
+
...(await loadServiceState(customHome) || {}),
|
|
467
|
+
status: 'stopped',
|
|
468
|
+
pid: null,
|
|
469
|
+
stoppedAt: new Date().toISOString()
|
|
470
|
+
});
|
|
471
|
+
return {
|
|
472
|
+
requested: false,
|
|
473
|
+
running: false,
|
|
474
|
+
signalSent: false,
|
|
475
|
+
service: await refreshServiceState(customHome)
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (state.pidRunning === false) {
|
|
480
|
+
await saveServiceState(customHome, {
|
|
481
|
+
...(await loadServiceState(customHome) || {}),
|
|
482
|
+
status: 'stopped',
|
|
483
|
+
pid: null,
|
|
484
|
+
stoppedAt: new Date().toISOString()
|
|
485
|
+
});
|
|
486
|
+
await clearServiceStopRequest(customHome);
|
|
487
|
+
return {
|
|
488
|
+
requested: false,
|
|
489
|
+
running: false,
|
|
490
|
+
signalSent: false,
|
|
491
|
+
service: await refreshServiceState(customHome)
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
await writeStopAndSignal(customHome, state.pid);
|
|
496
|
+
await saveServiceState(customHome, {
|
|
497
|
+
...state,
|
|
498
|
+
status: 'stopping'
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const stopped = await waitForPidExit(state.pid, 5000, 50);
|
|
502
|
+
if (stopped) {
|
|
503
|
+
await saveServiceState(customHome, {
|
|
504
|
+
...(await loadServiceState(customHome) || {}),
|
|
505
|
+
status: 'stopped',
|
|
506
|
+
pid: null,
|
|
507
|
+
stoppedAt: new Date().toISOString()
|
|
508
|
+
});
|
|
509
|
+
await clearServiceStopRequest(customHome);
|
|
510
|
+
return {
|
|
511
|
+
requested: true,
|
|
512
|
+
running: false,
|
|
513
|
+
signalSent: true,
|
|
514
|
+
service: await refreshServiceState(customHome)
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const next = await refreshServiceState(customHome);
|
|
519
|
+
return {
|
|
520
|
+
requested: true,
|
|
521
|
+
running: true,
|
|
522
|
+
signalSent: true,
|
|
523
|
+
service: next
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function writeStopAndSignal(customHome, pid) {
|
|
528
|
+
await writeServiceStopRequest(customHome);
|
|
529
|
+
try {
|
|
530
|
+
process.kill(pid, 'SIGTERM');
|
|
531
|
+
} catch {
|
|
532
|
+
// stale pid; refreshServiceState will normalize later
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function settleNodeServiceStopped(customHome, details = {}) {
|
|
537
|
+
await saveServiceState(customHome, {
|
|
538
|
+
...(await loadServiceState(customHome) || {}),
|
|
539
|
+
pid: null,
|
|
540
|
+
status: 'stopped',
|
|
541
|
+
stoppedAt: new Date().toISOString(),
|
|
542
|
+
lastSignal: details.signal || 'shutdown',
|
|
543
|
+
lastNodeId: details.nodeId || null
|
|
544
|
+
});
|
|
545
|
+
await clearServiceStopRequest(customHome);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export async function waitForServiceStopRequest(customHome, signalHandler, pollMs = 250) {
|
|
549
|
+
while (true) {
|
|
550
|
+
if (await serviceStopRequested(customHome)) {
|
|
551
|
+
await signalHandler('service_stop_request');
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function waitForPidExit(pid, timeoutMs = 5000, intervalMs = 50) {
|
|
559
|
+
const started = Date.now();
|
|
560
|
+
while (Date.now() - started < timeoutMs) {
|
|
561
|
+
if (!isPidRunning(pid)) return true;
|
|
562
|
+
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
563
|
+
}
|
|
564
|
+
return !isPidRunning(pid);
|
|
565
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { JsonFileStore } from '../../../packages/shared/src/store.js';
|
|
5
|
+
|
|
6
|
+
export function getDarkMeshHome(customHome) {
|
|
7
|
+
return customHome || process.env.DARKMESH_HOME || path.join(os.homedir(), '.darkmesh');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getDefaultConfig(customHome) {
|
|
11
|
+
return {
|
|
12
|
+
coordinatorUrl: 'http://127.0.0.1:8787',
|
|
13
|
+
operatorId: 'operator-local',
|
|
14
|
+
label: 'darkmesh-node',
|
|
15
|
+
provider: {
|
|
16
|
+
type: 'ollama',
|
|
17
|
+
baseUrl: 'http://127.0.0.1:11434/v1'
|
|
18
|
+
},
|
|
19
|
+
models: ['qwen3:latest'],
|
|
20
|
+
privacyClass: 'class-a-public-safe',
|
|
21
|
+
privacySecret: null,
|
|
22
|
+
nodeId: null
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getConfigPath(customHome) {
|
|
27
|
+
return path.join(getDarkMeshHome(customHome), 'config.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getIdentityPath(customHome) {
|
|
31
|
+
return path.join(getDarkMeshHome(customHome), 'identity.json');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getOperatorIdentityPath(customHome) {
|
|
35
|
+
return path.join(getDarkMeshHome(customHome), 'operator.json');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createConfigStore(customHome) {
|
|
39
|
+
return new JsonFileStore(getConfigPath(customHome));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function loadConfig(customHome) {
|
|
43
|
+
const store = createConfigStore(customHome);
|
|
44
|
+
const loaded = await store.load(getDefaultConfig(customHome));
|
|
45
|
+
return {
|
|
46
|
+
...getDefaultConfig(customHome),
|
|
47
|
+
...loaded,
|
|
48
|
+
provider: {
|
|
49
|
+
...getDefaultConfig(customHome).provider,
|
|
50
|
+
...(loaded.provider || {})
|
|
51
|
+
},
|
|
52
|
+
models: Array.isArray(loaded.models) && loaded.models.length ? loaded.models : getDefaultConfig(customHome).models
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function saveConfig(config, customHome) {
|
|
57
|
+
const store = createConfigStore(customHome);
|
|
58
|
+
await store.save(config);
|
|
59
|
+
return config;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function ensureDarkMeshHome(customHome) {
|
|
63
|
+
await fs.mkdir(getDarkMeshHome(customHome), { recursive: true });
|
|
64
|
+
}
|
package/src/identity.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createKeyPair } from '../../../packages/shared/src/index.js';
|
|
2
|
+
import { JsonFileStore } from '../../../packages/shared/src/store.js';
|
|
3
|
+
import { ensureDarkMeshHome, getIdentityPath } from './config.js';
|
|
4
|
+
|
|
5
|
+
function createIdentityStore(customHome) {
|
|
6
|
+
return new JsonFileStore(getIdentityPath(customHome));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function loadIdentity(customHome) {
|
|
10
|
+
const store = createIdentityStore(customHome);
|
|
11
|
+
return store.load(null);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function ensureIdentity(customHome) {
|
|
15
|
+
await ensureDarkMeshHome(customHome);
|
|
16
|
+
const store = createIdentityStore(customHome);
|
|
17
|
+
const existing = await store.load(null);
|
|
18
|
+
if (existing?.publicKey && existing?.privateKey) return existing;
|
|
19
|
+
const keyPair = createKeyPair();
|
|
20
|
+
const identity = {
|
|
21
|
+
...keyPair,
|
|
22
|
+
createdAt: new Date().toISOString()
|
|
23
|
+
};
|
|
24
|
+
await store.save(identity);
|
|
25
|
+
return identity;
|
|
26
|
+
}
|