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/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
+ }
@@ -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
+ }