@mmmbuto/nexuscrew 0.8.19 → 0.8.21
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 +43 -4
- package/frontend/dist/assets/index-DQbVJLji.css +32 -0
- package/frontend/dist/assets/index-DaFQbMHq.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/init.js +9 -1
- package/lib/decks/routes.js +2 -2
- package/lib/decks/store.js +34 -5
- package/lib/fleet/builtin.js +18 -4
- package/lib/fleet/catalogs/openrouter-kimi-k3.json +44 -0
- package/lib/fleet/managed.js +109 -5
- package/lib/fleet/openrouter-auth-helper.js +21 -0
- package/lib/nodes/commands.js +35 -2
- package/lib/nodes/peering.js +45 -3
- package/lib/nodes/store.js +51 -10
- package/lib/nodes/tunnel-supervisor.js +47 -4
- package/lib/nodes/tunnel.js +46 -1
- package/lib/proxy/federation.js +123 -18
- package/lib/server.js +11 -0
- package/lib/settings/pairing-coordinator.js +3 -3
- package/lib/settings/public-peering-routes.js +39 -11
- package/lib/settings/routes.js +54 -36
- package/lib/tmux/list.js +11 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-DOHx885g.js +0 -91
- package/frontend/dist/assets/index-kxpt4ezv.css +0 -32
package/frontend/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="NexusCrew" />
|
|
12
12
|
<link rel="manifest" href="/manifest.json" />
|
|
13
13
|
<title>NexusCrew</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DaFQbMHq.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DQbVJLji.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.8.
|
|
1
|
+
{"version":"0.8.21"}
|
package/lib/cli/init.js
CHANGED
|
@@ -152,13 +152,21 @@ function runInit(opts = {}) {
|
|
|
152
152
|
try {
|
|
153
153
|
const nstore = require('../nodes/store.js');
|
|
154
154
|
const nodesPath = opts.nodesPath || path.join(configDir, 'nodes.json');
|
|
155
|
-
const st = nstore.
|
|
155
|
+
const st = nstore.initStore(nodesPath);
|
|
156
156
|
actions.push(`nodes.json ok (${nodesPath}, nodeId ${st.nodeId.slice(0, 8)}…)`);
|
|
157
157
|
const mig = nstore.migrateLegacyNodes(configPath, nodesPath);
|
|
158
158
|
if (mig.migrated) actions.push(`nodes: migrati ${mig.count} nodi legacy da config.json`);
|
|
159
159
|
} catch (e) {
|
|
160
160
|
actions.push(`WARN: nodes.json init/migrazione fallita: ${e.message} (init prosegue)`);
|
|
161
161
|
}
|
|
162
|
+
try {
|
|
163
|
+
const dstore = require('../decks/store.js');
|
|
164
|
+
const decksPath = opts.decksPath || path.join(configDir, 'decks.json');
|
|
165
|
+
dstore.initStore(decksPath);
|
|
166
|
+
actions.push(`decks.json ok (${decksPath})`);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
actions.push(`WARN: decks.json init fallita: ${e.message} (init prosegue)`);
|
|
169
|
+
}
|
|
162
170
|
}
|
|
163
171
|
|
|
164
172
|
// service generation
|
package/lib/decks/routes.js
CHANGED
|
@@ -20,7 +20,7 @@ function decksRoutes({ cfg = {}, decksPath }) {
|
|
|
20
20
|
const found = store.loadStore(decksPath);
|
|
21
21
|
if (found) return found;
|
|
22
22
|
if (readonly() && !fs.existsSync(decksPath)) return store.emptyStore();
|
|
23
|
-
return store.
|
|
23
|
+
return store.loadStoreStrict(decksPath);
|
|
24
24
|
};
|
|
25
25
|
const expected = (body) => {
|
|
26
26
|
const n = body && body.expectedRevision;
|
|
@@ -33,7 +33,7 @@ function decksRoutes({ cfg = {}, decksPath }) {
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
r.get('/', (_req, res) => {
|
|
36
|
-
try { res.json(read()); } catch (e) { res.status(500).json({ error: String(e.message || e) }); }
|
|
36
|
+
try { res.json(read()); } catch (e) { res.status(e.status || 500).json({ error: String(e.message || e) }); }
|
|
37
37
|
});
|
|
38
38
|
r.post('/', mutate((req, res) => {
|
|
39
39
|
const name = String(req.body && req.body.name || '');
|
package/lib/decks/store.js
CHANGED
|
@@ -113,14 +113,43 @@ function atomicWrite(p, data) {
|
|
|
113
113
|
return parsed;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
function
|
|
116
|
+
function storeUnavailable(code, message) {
|
|
117
|
+
const e = new Error(message); e.status = 503; e.code = code; return e;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function loadStoreStrict(p) {
|
|
121
|
+
let st;
|
|
122
|
+
try { st = fs.lstatSync(p); }
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (e.code === 'ENOENT') {
|
|
125
|
+
throw storeUnavailable('DECKS_STORE_MISSING',
|
|
126
|
+
`decks.json assente (${p}): esegui \`nexuscrew init\` per inizializzare esplicitamente lo store`);
|
|
127
|
+
}
|
|
128
|
+
throw e;
|
|
129
|
+
}
|
|
130
|
+
if (st.isSymbolicLink()) throw storeUnavailable('DECKS_STORE_INVALID', 'decks.json e\' un symlink (rifiutato)');
|
|
131
|
+
if (!st.isFile()) throw storeUnavailable('DECKS_STORE_INVALID', 'decks.json non e\' un file regolare');
|
|
117
132
|
const found = loadStore(p);
|
|
118
|
-
if (found)
|
|
119
|
-
|
|
120
|
-
|
|
133
|
+
if (!found) {
|
|
134
|
+
throw storeUnavailable('DECKS_STORE_INVALID',
|
|
135
|
+
'decks.json presente ma invalido: ripristina un backup valido; non viene sovrascritto');
|
|
136
|
+
}
|
|
137
|
+
return found;
|
|
121
138
|
}
|
|
122
139
|
|
|
140
|
+
function initStore(p) {
|
|
141
|
+
try { fs.lstatSync(p); }
|
|
142
|
+
catch (e) {
|
|
143
|
+
if (e.code === 'ENOENT') return atomicWrite(p, emptyStore());
|
|
144
|
+
throw e;
|
|
145
|
+
}
|
|
146
|
+
return loadStoreStrict(p);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Compatibilita' interna: nessuna creazione implicita fuori da initStore().
|
|
150
|
+
function loadOrCreate(p) { return loadStoreStrict(p); }
|
|
151
|
+
|
|
123
152
|
module.exports = {
|
|
124
153
|
SCHEMA_VERSION, MAX_DECKS, MAX_TILES, NAME_RE, OWNER_ID_RE,
|
|
125
|
-
defaultDecksPath, emptyStore, parseLayout, parseStore, loadStore, loadOrCreate, atomicWrite,
|
|
154
|
+
defaultDecksPath, emptyStore, parseLayout, parseStore, loadStore, loadStoreStrict, initStore, loadOrCreate, atomicWrite,
|
|
126
155
|
};
|
package/lib/fleet/builtin.js
CHANGED
|
@@ -33,7 +33,7 @@ const {
|
|
|
33
33
|
loadDefinitions, atomicWrite, CAPS, MAX_CELLS, validTmuxName,
|
|
34
34
|
} = require('./definitions.js');
|
|
35
35
|
const {
|
|
36
|
-
publicCatalog, describeManaged,
|
|
36
|
+
publicCatalog, describeManaged, describeCatalogCredential,
|
|
37
37
|
} = require('./managed.js');
|
|
38
38
|
const { validEnvKey } = require('./env-key.js');
|
|
39
39
|
const { setCredential, removeCredential } = require('./credentials.js');
|
|
@@ -462,6 +462,20 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
462
462
|
// Vista editabile ma secret-safe: gli env values restano write-only.
|
|
463
463
|
function definitions() {
|
|
464
464
|
const defs = reloadDefs();
|
|
465
|
+
const { map: credentialMap } = credentialRequirements(defs);
|
|
466
|
+
const managedCatalog = publicCatalog().map((profile) => {
|
|
467
|
+
if (typeof profile.credentialEnv !== 'string') return profile;
|
|
468
|
+
const status = describeCatalogCredential(
|
|
469
|
+
profile.client, profile.provider, profile.credentialProfile, { ...cfg, home },
|
|
470
|
+
);
|
|
471
|
+
const usedBy = credentialMap.get(profile.credentialEnv)?.engines || [];
|
|
472
|
+
return {
|
|
473
|
+
...profile,
|
|
474
|
+
authConfigured: status?.authConfigured === true,
|
|
475
|
+
credentialSource: status?.credentialSource || 'missing',
|
|
476
|
+
credentialUsedBy: [...usedBy].sort(),
|
|
477
|
+
};
|
|
478
|
+
});
|
|
465
479
|
return {
|
|
466
480
|
schemaVersion: defs.schemaVersion,
|
|
467
481
|
engines: defs.engines.map((e) => {
|
|
@@ -475,7 +489,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
475
489
|
...(c.models ? { models: { ...c.models } } : {}),
|
|
476
490
|
...(c.permissionPolicies ? { permissionPolicies: { ...c.permissionPolicies } } : {}),
|
|
477
491
|
})),
|
|
478
|
-
managedCatalog
|
|
492
|
+
managedCatalog,
|
|
479
493
|
managedConfig: {
|
|
480
494
|
providerSecretsPath: cfg.providerSecretsPath || path.join(home, '.nexuscrew', 'providers.env'),
|
|
481
495
|
providerShellPath: cfg.providerShellPath || path.join(home, '.config', 'ai-shell', 'providers.zsh'),
|
|
@@ -486,8 +500,8 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
486
500
|
};
|
|
487
501
|
}
|
|
488
502
|
|
|
489
|
-
function credentialRequirements() {
|
|
490
|
-
const defs = reloadDefs(); const map = new Map();
|
|
503
|
+
function credentialRequirements(existingDefs) {
|
|
504
|
+
const defs = existingDefs || reloadDefs(); const map = new Map();
|
|
491
505
|
for (const engine of defs.engines) {
|
|
492
506
|
if (!engine.managed) continue;
|
|
493
507
|
const info = describeManaged(engine.managed, { ...cfg, home });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"models": [
|
|
3
|
+
{
|
|
4
|
+
"slug": "moonshotai/kimi-k3",
|
|
5
|
+
"display_name": "Kimi K3",
|
|
6
|
+
"description": "Kimi K3 through OpenRouter Responses",
|
|
7
|
+
"default_reasoning_level": "max",
|
|
8
|
+
"supported_reasoning_levels": [
|
|
9
|
+
{
|
|
10
|
+
"effort": "max",
|
|
11
|
+
"description": "Maximum reasoning"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"shell_type": "default",
|
|
15
|
+
"visibility": "list",
|
|
16
|
+
"supported_in_api": true,
|
|
17
|
+
"priority": 50,
|
|
18
|
+
"availability_nux": null,
|
|
19
|
+
"upgrade": null,
|
|
20
|
+
"base_instructions": "",
|
|
21
|
+
"supports_reasoning_summaries": true,
|
|
22
|
+
"default_reasoning_summary": "none",
|
|
23
|
+
"support_verbosity": false,
|
|
24
|
+
"default_verbosity": null,
|
|
25
|
+
"apply_patch_tool_type": null,
|
|
26
|
+
"web_search_tool_type": "text",
|
|
27
|
+
"truncation_policy": {
|
|
28
|
+
"mode": "tokens",
|
|
29
|
+
"limit": 1048576
|
|
30
|
+
},
|
|
31
|
+
"supports_parallel_tool_calls": false,
|
|
32
|
+
"supports_image_detail_original": true,
|
|
33
|
+
"context_window": 1048576,
|
|
34
|
+
"max_context_window": 1048576,
|
|
35
|
+
"effective_context_window_percent": 95,
|
|
36
|
+
"experimental_supported_tools": [],
|
|
37
|
+
"input_modalities": [
|
|
38
|
+
"text",
|
|
39
|
+
"image"
|
|
40
|
+
],
|
|
41
|
+
"supports_search_tool": false
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
package/lib/fleet/managed.js
CHANGED
|
@@ -7,7 +7,7 @@ const path = require('node:path');
|
|
|
7
7
|
const crypto = require('node:crypto');
|
|
8
8
|
const { execFile } = require('node:child_process');
|
|
9
9
|
const { ENV_KEY_RE } = require('./env-key.js');
|
|
10
|
-
const { readCredentialStore } = require('./credentials.js');
|
|
10
|
+
const { readCredentialStore, safePrivateDir } = require('./credentials.js');
|
|
11
11
|
|
|
12
12
|
const OLLAMA_CLOUD_MODELS = Object.freeze([
|
|
13
13
|
'glm-5.2', 'kimi-k2.7-code', 'deepseek-v4-pro', 'minimax-m3',
|
|
@@ -36,6 +36,8 @@ function validBaseUrl(value) {
|
|
|
36
36
|
const CATALOG = Object.freeze([
|
|
37
37
|
// Claude Code
|
|
38
38
|
{ id: 'claude.native', client: 'claude', provider: 'native', label: 'Anthropic / Claude account', auth: 'login', endpoint: 'Anthropic account', protocol: 'anthropic_messages', rc: true, default: true, core: true },
|
|
39
|
+
{ id: 'claude.openrouter', client: 'claude', provider: 'openrouter', label: 'OpenRouter', auth: 'OPENROUTER_API_KEY', endpoint: 'https://openrouter.ai/api', protocol: 'anthropic_messages', requiresModel: true, core: true, notice: 'claude-openrouter' },
|
|
40
|
+
{ id: 'claude.kimi-code', client: 'claude', provider: 'kimi-code', label: 'Kimi Code', auth: 'KIMI_API_KEY', endpoint: 'https://api.kimi.com/coding/', protocol: 'anthropic_messages', model: 'k3[1m]', models: ['k3', 'k3[1m]', 'kimi-for-coding', 'kimi-for-coding-highspeed'], strictModels: true, core: true, notice: 'claude-kimi-code' },
|
|
39
41
|
{ id: 'claude.bedrock', client: 'claude', provider: 'bedrock', label: 'Amazon Bedrock', auth: 'login', endpoint: 'AWS Bedrock', protocol: 'anthropic_messages', core: true, providerEnv: { CLAUDE_CODE_USE_BEDROCK: '1' } },
|
|
40
42
|
{ id: 'claude.vertex', client: 'claude', provider: 'vertex', label: 'Google Vertex AI', auth: 'login', endpoint: 'Google Vertex AI', protocol: 'anthropic_messages', core: true, providerEnv: { CLAUDE_CODE_USE_VERTEX: '1' } },
|
|
41
43
|
{ id: 'claude.foundry', client: 'claude', provider: 'foundry', label: 'Microsoft Foundry', auth: 'login', endpoint: 'Microsoft Foundry', protocol: 'anthropic_messages', core: true, providerEnv: { CLAUDE_CODE_USE_FOUNDRY: '1' } },
|
|
@@ -52,6 +54,7 @@ const CATALOG = Object.freeze([
|
|
|
52
54
|
{ id: 'codex-vl.native', client: 'codex-vl', provider: 'native', label: 'OpenAI / ChatGPT account', auth: 'login', endpoint: 'OpenAI account', protocol: 'openai_responses', default: true, core: true },
|
|
53
55
|
{ id: 'codex.openai-api', client: 'codex', provider: 'openai-api', label: 'OpenAI API', auth: 'OPENAI_API_KEY', endpoint: 'https://api.openai.com/v1', protocol: 'openai_responses', core: true },
|
|
54
56
|
{ id: 'codex-vl.openai-api', client: 'codex-vl', provider: 'openai-api', label: 'OpenAI API', auth: 'OPENAI_API_KEY', endpoint: 'https://api.openai.com/v1', protocol: 'openai_responses', core: true },
|
|
57
|
+
{ id: 'codex-vl.openrouter', client: 'codex-vl', provider: 'openrouter', label: 'OpenRouter', auth: 'OPENROUTER_API_KEY', endpoint: 'https://openrouter.ai/api/v1', protocol: 'openai_responses', requiresModel: true, core: true, notice: 'codex-openrouter' },
|
|
55
58
|
{ id: 'codex.ollama', client: 'codex', provider: 'ollama', label: 'Ollama local', auth: 'none', endpoint: 'local provider', protocol: 'openai_responses', localProvider: 'ollama', core: true },
|
|
56
59
|
{ id: 'codex-vl.ollama', client: 'codex-vl', provider: 'ollama', label: 'Ollama local', auth: 'none', endpoint: 'local provider', protocol: 'openai_responses', localProvider: 'ollama', core: true },
|
|
57
60
|
{ id: 'codex.lmstudio', client: 'codex', provider: 'lmstudio', label: 'LM Studio', auth: 'none', endpoint: 'local provider', protocol: 'openai_responses', localProvider: 'lmstudio', core: true },
|
|
@@ -103,6 +106,7 @@ function normalizeManagedSpec(value) {
|
|
|
103
106
|
const model = value.model === undefined ? (profile.model || '') : value.model;
|
|
104
107
|
if (typeof model !== 'string' || model.length > 128 || /[\x00-\x1f\x7f]/.test(model)) return null;
|
|
105
108
|
if (profile.requiresModel && !model) return null;
|
|
109
|
+
if (profile.strictModels && !(profile.models || []).includes(model)) return null;
|
|
106
110
|
const permissionPolicy = value.permissionPolicy === undefined ? (profile.client === 'claude' ? 'unsafe' : 'standard') : value.permissionPolicy;
|
|
107
111
|
if (permissionPolicy !== 'standard' && permissionPolicy !== 'unsafe') return null;
|
|
108
112
|
if (value.client === 'pi' && permissionPolicy !== 'standard') return null;
|
|
@@ -388,6 +392,22 @@ function describeManaged(spec, cfg = {}) {
|
|
|
388
392
|
};
|
|
389
393
|
}
|
|
390
394
|
|
|
395
|
+
// Target-local, value-free status for a fixed catalog credential. The caller
|
|
396
|
+
// supplies only a profile already present in the public catalog, so this cannot
|
|
397
|
+
// be used as an arbitrary environment-variable oracle.
|
|
398
|
+
function describeCatalogCredential(client, provider, credentialProfile = '', cfg = {}) {
|
|
399
|
+
const profile = profileFor(client, provider, credentialProfile);
|
|
400
|
+
if (!profile || profile.auth === 'dynamic' || profile.auth === 'login' || profile.auth === 'none'
|
|
401
|
+
|| !ENV_KEY_RE.test(profile.auth || '')) return null;
|
|
402
|
+
const home = cfg.home || require('node:os').homedir();
|
|
403
|
+
const cred = credential(profile, {}, cfg, home);
|
|
404
|
+
return {
|
|
405
|
+
envKey: cred.envKey,
|
|
406
|
+
authConfigured: !!cred.value,
|
|
407
|
+
credentialSource: cred.value ? cred.source : 'missing',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
391
411
|
function codexProviderArgs(id, name, baseUrl, envKey) {
|
|
392
412
|
return [
|
|
393
413
|
'-c', `model_provider=${JSON.stringify(id)}`, '-c', `model_providers.${id}.name=${JSON.stringify(name)}`,
|
|
@@ -396,6 +416,56 @@ function codexProviderArgs(id, name, baseUrl, envKey) {
|
|
|
396
416
|
];
|
|
397
417
|
}
|
|
398
418
|
|
|
419
|
+
function codexCommandAuthProviderArgs(id, name, baseUrl, command, args) {
|
|
420
|
+
return [
|
|
421
|
+
'-c', `model_provider=${JSON.stringify(id)}`, '-c', `model_providers.${id}.name=${JSON.stringify(name)}`,
|
|
422
|
+
'-c', `model_providers.${id}.base_url=${JSON.stringify(baseUrl)}`,
|
|
423
|
+
'-c', `model_providers.${id}.wire_api="responses"`,
|
|
424
|
+
'-c', `model_providers.${id}.auth.command=${JSON.stringify(command)}`,
|
|
425
|
+
'-c', `model_providers.${id}.auth.args=${JSON.stringify(args)}`,
|
|
426
|
+
'-c', `model_providers.${id}.auth.timeout_ms=5000`,
|
|
427
|
+
'-c', `model_providers.${id}.auth.refresh_interval_ms=300000`,
|
|
428
|
+
];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function ensureKimiClaudeConfig(home) {
|
|
432
|
+
const nexusDir = path.join(home, '.nexuscrew');
|
|
433
|
+
const profilesDir = path.join(nexusDir, 'claude-profiles');
|
|
434
|
+
const configDir = path.join(profilesDir, 'kimi-code');
|
|
435
|
+
safePrivateDir(nexusDir, { create: true });
|
|
436
|
+
safePrivateDir(profilesDir, { create: true });
|
|
437
|
+
safePrivateDir(configDir, { create: true });
|
|
438
|
+
const file = path.join(configDir, '.claude.json');
|
|
439
|
+
let current = {};
|
|
440
|
+
try {
|
|
441
|
+
const st = fs.lstatSync(file);
|
|
442
|
+
if (st.isSymbolicLink() || !st.isFile() || (st.mode & 0o077) || st.size > 256 * 1024
|
|
443
|
+
|| (typeof process.getuid === 'function' && st.uid !== process.getuid())) {
|
|
444
|
+
throw new Error('unsafe Kimi Code Claude config');
|
|
445
|
+
}
|
|
446
|
+
current = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
447
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) throw new Error('invalid Kimi Code Claude config');
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (error.code !== 'ENOENT') throw error;
|
|
450
|
+
}
|
|
451
|
+
const next = { ...current, hasCompletedOnboarding: true, penguinModeOrgEnabled: true };
|
|
452
|
+
if (current.hasCompletedOnboarding === true && current.penguinModeOrgEnabled === true) return configDir;
|
|
453
|
+
const tmp = path.join(configDir, `.claude.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`);
|
|
454
|
+
let fd;
|
|
455
|
+
try {
|
|
456
|
+
fd = fs.openSync(tmp, 'wx', 0o600);
|
|
457
|
+
fs.writeFileSync(fd, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
458
|
+
fs.fsyncSync(fd);
|
|
459
|
+
fs.closeSync(fd); fd = undefined;
|
|
460
|
+
fs.chmodSync(tmp, 0o600);
|
|
461
|
+
fs.renameSync(tmp, file);
|
|
462
|
+
} finally {
|
|
463
|
+
if (fd !== undefined) { try { fs.closeSync(fd); } catch (_) {} }
|
|
464
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
465
|
+
}
|
|
466
|
+
return configDir;
|
|
467
|
+
}
|
|
468
|
+
|
|
399
469
|
function writePiProviderExtension(spec, home) {
|
|
400
470
|
const dir = path.join(home, '.nexuscrew', 'pi-providers');
|
|
401
471
|
try {
|
|
@@ -465,6 +535,29 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
465
535
|
if (engine.rc !== false) args.push('--remote-control', `Cloud_${cell.id}`);
|
|
466
536
|
} else if (profile.providerEnv) {
|
|
467
537
|
Object.assign(env, profile.providerEnv);
|
|
538
|
+
} else if (spec.provider === 'openrouter') {
|
|
539
|
+
Object.assign(env, {
|
|
540
|
+
ANTHROPIC_BASE_URL: profile.endpoint, ANTHROPIC_AUTH_TOKEN: cred.value, ANTHROPIC_API_KEY: '',
|
|
541
|
+
ANTHROPIC_MODEL: model, ANTHROPIC_DEFAULT_OPUS_MODEL: model,
|
|
542
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: model, ANTHROPIC_DEFAULT_HAIKU_MODEL: model,
|
|
543
|
+
CLAUDE_CODE_SUBAGENT_MODEL: model, API_TIMEOUT_MS: '3000000',
|
|
544
|
+
});
|
|
545
|
+
} else if (spec.provider === 'kimi-code') {
|
|
546
|
+
const contextWindow = model === 'k3[1m]' ? '1048576' : '262144';
|
|
547
|
+
Object.assign(env, {
|
|
548
|
+
CLAUDE_CONFIG_DIR: ensureKimiClaudeConfig(home),
|
|
549
|
+
ANTHROPIC_BASE_URL: profile.endpoint, ANTHROPIC_API_KEY: cred.value,
|
|
550
|
+
ANTHROPIC_MODEL: model, ANTHROPIC_DEFAULT_FABLE_MODEL: model,
|
|
551
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: model, ANTHROPIC_DEFAULT_SONNET_MODEL: model,
|
|
552
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: model, CLAUDE_CODE_SUBAGENT_MODEL: model,
|
|
553
|
+
CLAUDE_CODE_AUTO_COMPACT_WINDOW: contextWindow,
|
|
554
|
+
CLAUDE_CODE_MAX_CONTEXT_TOKENS: contextWindow,
|
|
555
|
+
API_TIMEOUT_MS: '3000000',
|
|
556
|
+
});
|
|
557
|
+
if (model === 'k3' || model === 'k3[1m]') {
|
|
558
|
+
env.CLAUDE_CODE_EFFORT_LEVEL = 'max';
|
|
559
|
+
env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = '1';
|
|
560
|
+
}
|
|
468
561
|
} else {
|
|
469
562
|
const endpoint = spec.baseUrl || profile.endpoint;
|
|
470
563
|
const token = profile.auth === 'none' ? 'ollama' : cred.value;
|
|
@@ -488,6 +581,16 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
|
|
|
488
581
|
args.push('-c', 'model_providers.ollama_cloud.stream_idle_timeout_ms=600000', '-c', `model_context_window=${OLLAMA_CONTEXT[model] || 200000}`);
|
|
489
582
|
const localCatalog = path.join(home, '.codex', 'ollama_cloud_model_catalog.json');
|
|
490
583
|
if (fs.existsSync(localCatalog)) args.push('-c', `model_catalog_json="${localCatalog}"`);
|
|
584
|
+
} else if (spec.provider === 'openrouter') {
|
|
585
|
+
env.OPENROUTER_API_KEY = cred.value;
|
|
586
|
+
const authHelper = path.join(__dirname, 'openrouter-auth-helper.js');
|
|
587
|
+
args.push(...codexCommandAuthProviderArgs('openrouter', 'OpenRouter', profile.endpoint, process.execPath, [authHelper, 'OPENROUTER_API_KEY']));
|
|
588
|
+
args.push('-c', 'model_providers.openrouter.stream_idle_timeout_ms=600000');
|
|
589
|
+
if (model === 'moonshotai/kimi-k3') {
|
|
590
|
+
const localCatalog = path.join(__dirname, 'catalogs', 'openrouter-kimi-k3.json');
|
|
591
|
+
args.push('-c', `model_catalog_json=${JSON.stringify(localCatalog)}`);
|
|
592
|
+
args.push('-c', 'model_context_window=1048576');
|
|
593
|
+
}
|
|
491
594
|
} else if (spec.provider === 'custom') {
|
|
492
595
|
env[spec.envKey] = cred.value;
|
|
493
596
|
args.push(...codexProviderArgs(spec.providerId, spec.displayName, spec.baseUrl, spec.envKey));
|
|
@@ -522,14 +625,15 @@ function publicCatalog() {
|
|
|
522
625
|
auth: p.auth, endpoint: p.endpoint || '', model: p.model || '', models: [...(p.models || [])],
|
|
523
626
|
protocols: [...(p.protocols || [p.protocol])], supportsUnsafe: p.client !== 'pi', requiresModel: !!p.requiresModel || !!p.custom,
|
|
524
627
|
permissionPolicyDefault: p.client === 'claude' ? 'unsafe' : 'standard',
|
|
525
|
-
rc: !!p.rc, custom: !!p.custom, default: !!p.default,
|
|
526
|
-
credentialEnv: !!p.credentialEnv
|
|
628
|
+
rc: !!p.rc, custom: !!p.custom, default: !!p.default, notice: p.notice || '',
|
|
629
|
+
credentialEnv: p.auth === 'dynamic' ? !!p.credentialEnv : (ENV_KEY_RE.test(p.auth || '') ? p.auth : false),
|
|
630
|
+
defaultEnvKey: p.defaultEnvKey || '',
|
|
527
631
|
}));
|
|
528
632
|
}
|
|
529
633
|
|
|
530
634
|
module.exports = {
|
|
531
635
|
CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT, CLIENT_LABELS, normalizeManagedSpec,
|
|
532
|
-
defaultDefinitions, describeManaged, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
|
|
636
|
+
defaultDefinitions, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
|
|
533
637
|
discoverPiModels, parseEnvFile, parseProviderShellFile, findBinary, publicCatalog, writePiProviderExtension,
|
|
534
|
-
providerKeyPaths, parseProviderKeyFiles, credentialSources, credential, ENV_KEY_RE,
|
|
638
|
+
providerKeyPaths, parseProviderKeyFiles, credentialSources, credential, ensureKimiClaudeConfig, ENV_KEY_RE,
|
|
535
639
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ALLOWED_ENV_KEY = 'OPENROUTER_API_KEY';
|
|
4
|
+
const MAX_TOKEN_BYTES = 16 * 1024;
|
|
5
|
+
|
|
6
|
+
function readOpenRouterCredential(name, env = process.env) {
|
|
7
|
+
if (name !== ALLOWED_ENV_KEY) throw new Error('unsupported credential request');
|
|
8
|
+
const value = env[name];
|
|
9
|
+
if (typeof value !== 'string' || !value || Buffer.byteLength(value) > MAX_TOKEN_BYTES
|
|
10
|
+
|| /[\x00-\x1f\x7f]/.test(value)) {
|
|
11
|
+
throw new Error('credential unavailable');
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (require.main === module) {
|
|
17
|
+
try { process.stdout.write(readOpenRouterCredential(process.argv[2])); }
|
|
18
|
+
catch (_) { process.exitCode = 1; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { ALLOWED_ENV_KEY, MAX_TOKEN_BYTES, readOpenRouterCredential };
|
package/lib/nodes/commands.js
CHANGED
|
@@ -16,6 +16,7 @@ const path = require('node:path');
|
|
|
16
16
|
const { execFileSync } = require('node:child_process');
|
|
17
17
|
const store = require('./store.js');
|
|
18
18
|
const tunnel = require('./tunnel.js');
|
|
19
|
+
const federation = require('../proxy/federation.js');
|
|
19
20
|
const { resolvePaths, DEFAULT_PORT } = require('../cli/url.js');
|
|
20
21
|
|
|
21
22
|
const LOCAL_PORT_BASE = 43001; // porte locali stabili per i forward (design: stabili da nodes.json)
|
|
@@ -127,8 +128,11 @@ function nodesAdd(opts) {
|
|
|
127
128
|
const sshPort = opts.sshPort === undefined ? undefined : Number(opts.sshPort);
|
|
128
129
|
|
|
129
130
|
let st;
|
|
130
|
-
try { st = store.
|
|
131
|
-
catch (e) {
|
|
131
|
+
try { st = store.loadStoreStrict(nodesPath); }
|
|
132
|
+
catch (e) {
|
|
133
|
+
log(`nodes add: ${e.message}`);
|
|
134
|
+
return { code: 1, reason: 'store invalido', status: e.status || 500, errorCode: e.code || null };
|
|
135
|
+
}
|
|
132
136
|
|
|
133
137
|
const localPort = opts.localPort ? Number(opts.localPort) : assignLocalPort(st);
|
|
134
138
|
const keyPath = opts.identityFile || opts.key || (typeof opts.keygen === 'function' ? defaultKeyPath(home, name) : undefined);
|
|
@@ -238,6 +242,35 @@ async function nodesTest(opts) {
|
|
|
238
242
|
const node = st ? store.getNode(st, name) : null;
|
|
239
243
|
if (!node) { log(`nodes test: nodo sconosciuto "${name}"`); return { code: 1, result: 'unknown-node' }; }
|
|
240
244
|
|
|
245
|
+
// An inbound peer is reached through the reverse listener owned by sshd on
|
|
246
|
+
// this hub; there is intentionally no local tunnel pidfile to inspect.
|
|
247
|
+
if (node.direction === 'inbound') {
|
|
248
|
+
if (node.shared !== true) {
|
|
249
|
+
log(`nodes test [${name}]: PASSIVO — Share disattivato, nessun canale inverso atteso`);
|
|
250
|
+
return { code: 0, result: 'passive' };
|
|
251
|
+
}
|
|
252
|
+
if (!node.token) {
|
|
253
|
+
log(`nodes test [${name}]: ASSOCIAZIONE INCOMPLETA — credenziale federation assente`);
|
|
254
|
+
return { code: 1, result: 'token-missing' };
|
|
255
|
+
}
|
|
256
|
+
const probe = opts.federationProbe || federation.probeHealth;
|
|
257
|
+
let health;
|
|
258
|
+
try {
|
|
259
|
+
health = await probe({
|
|
260
|
+
port: node.localPort, token: node.token, expectedInstanceId: node.nodeId || null,
|
|
261
|
+
fetchImpl: opts.fetchImpl, timeoutMs: opts.timeoutMs,
|
|
262
|
+
});
|
|
263
|
+
} catch (_) {
|
|
264
|
+
health = { status: 'down', detail: 'peer non raggiungibile' };
|
|
265
|
+
}
|
|
266
|
+
if (health && health.status === 'healthy') {
|
|
267
|
+
log(`nodes test [${name}]: OK — canale inverso e federation autenticati`);
|
|
268
|
+
return { code: 0, result: 'ok', health };
|
|
269
|
+
}
|
|
270
|
+
log(`nodes test [${name}]: FEDERATION KO — ${(health && health.detail) || 'peer non raggiungibile'}`);
|
|
271
|
+
return { code: 1, result: 'federation-ko', health };
|
|
272
|
+
}
|
|
273
|
+
|
|
241
274
|
const state = tunnel.readTunnelState(home, name);
|
|
242
275
|
if (state.status !== 'up') {
|
|
243
276
|
const diagnostic = tunnel.diagnoseTunnel(home, node, state);
|
package/lib/nodes/peering.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
4
|
const fs = require('node:fs');
|
|
5
|
+
const net = require('node:net');
|
|
5
6
|
const path = require('node:path');
|
|
6
7
|
const store = require('./store.js');
|
|
7
8
|
|
|
@@ -132,9 +133,14 @@ function consumeInvite({ invitesPath, invite, now = Date.now() }) {
|
|
|
132
133
|
return true;
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
function
|
|
136
|
+
function hasInvite({ invitesPath, invite, now = Date.now() }) {
|
|
137
|
+
const hash = crypto.createHash('sha256').update(String(invite || '')).digest('hex');
|
|
138
|
+
return readInvites(invitesPath, now).some((row) => safeEqual(row.hash, hash));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function allocateReversePort(nodes, reservations = []) {
|
|
136
142
|
const used = new Set();
|
|
137
|
-
for (const n of nodes || []) {
|
|
143
|
+
for (const n of [...(nodes || []), ...(reservations || [])]) {
|
|
138
144
|
if (store.isPort(n.localPort)) used.add(n.localPort);
|
|
139
145
|
if (store.isPort(n.reversePort)) used.add(n.reversePort);
|
|
140
146
|
}
|
|
@@ -144,6 +150,41 @@ function allocateReversePort(nodes) {
|
|
|
144
150
|
return p;
|
|
145
151
|
}
|
|
146
152
|
|
|
153
|
+
function canBindReversePort(port, createServerImpl = net.createServer) {
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const server = createServerImpl();
|
|
156
|
+
let settled = false;
|
|
157
|
+
const finish = (value, error = null) => {
|
|
158
|
+
if (settled) return;
|
|
159
|
+
settled = true;
|
|
160
|
+
server.removeAllListeners?.();
|
|
161
|
+
if (error) reject(error); else resolve(value);
|
|
162
|
+
};
|
|
163
|
+
server.once('error', (error) => {
|
|
164
|
+
if (error && (error.code === 'EADDRINUSE' || error.code === 'EACCES')) finish(false);
|
|
165
|
+
else finish(false, error);
|
|
166
|
+
});
|
|
167
|
+
server.once('listening', () => {
|
|
168
|
+
if (typeof server.unref === 'function') server.unref();
|
|
169
|
+
try { server.close(() => finish(true)); } catch (error) { finish(false, error); }
|
|
170
|
+
});
|
|
171
|
+
server.listen({ host: '127.0.0.1', port, exclusive: true });
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// The reverse listener will be owned by sshd, but allocation happens on the
|
|
176
|
+
// hub. Probe the real loopback bind before issuing a candidate so a stale or
|
|
177
|
+
// unrelated listener absent from nodes.json cannot be reallocated blindly.
|
|
178
|
+
async function allocateAvailableReversePort(nodes, reservations = [], opts = {}) {
|
|
179
|
+
const rejected = [];
|
|
180
|
+
while (rejected.length < (65535 - REVERSE_PORT_BASE)) {
|
|
181
|
+
const candidate = allocateReversePort(nodes, [...reservations, ...rejected]);
|
|
182
|
+
if (await canBindReversePort(candidate, opts.createServerImpl || net.createServer)) return candidate;
|
|
183
|
+
rejected.push({ localPort: candidate });
|
|
184
|
+
}
|
|
185
|
+
throw new Error('no reverse port available');
|
|
186
|
+
}
|
|
187
|
+
|
|
147
188
|
function createPending({ pendingPath, data, now = Date.now() }) {
|
|
148
189
|
const rows = readInvites(pendingPath, now);
|
|
149
190
|
const credential = crypto.randomBytes(32).toString('base64url');
|
|
@@ -269,6 +310,7 @@ async function probeTransportReady({
|
|
|
269
310
|
module.exports = {
|
|
270
311
|
INVITE_TTL_MS, REVERSE_PORT_BASE, defaultInvitesPath, defaultPendingPath, safeEqual,
|
|
271
312
|
readInvites, writeInvites, encodePairing, decodePairing, parsePairingUrl,
|
|
272
|
-
createInvite, consumeInvite,
|
|
313
|
+
createInvite, consumeInvite, hasInvite, allocateReversePort, canBindReversePort,
|
|
314
|
+
allocateAvailableReversePort, createPending, consumePending,
|
|
273
315
|
capabilityIdentity, capabilityProbeMaterial, probeTransportReady,
|
|
274
316
|
};
|
package/lib/nodes/store.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// - nessun token in output redatto (redactStore/redactNode)
|
|
10
10
|
//
|
|
11
11
|
// Modulo PURO al parse: parseStore/parseNode non toccano il filesystem.
|
|
12
|
-
// Tutto l'I/O vive in loadStore/atomicWriteStore/
|
|
12
|
+
// Tutto l'I/O vive in loadStore/loadStoreStrict/initStore/atomicWriteStore/
|
|
13
|
+
// migrateLegacyNodes.
|
|
13
14
|
// Principio fail-closed: qualunque dato malformato -> null (parse) o throw
|
|
14
15
|
// esplicito (mutazioni/write), mai un default silenzioso.
|
|
15
16
|
const fs = require('node:fs');
|
|
@@ -227,12 +228,15 @@ function parseStore(raw) {
|
|
|
227
228
|
|
|
228
229
|
const names = new Set();
|
|
229
230
|
const ids = new Set();
|
|
231
|
+
const localPorts = new Set();
|
|
230
232
|
const nodes = [];
|
|
231
233
|
for (const raw2 of d.nodes) {
|
|
232
234
|
const node = parseNode(raw2, d.schemaVersion);
|
|
233
235
|
if (!node) return null;
|
|
234
236
|
if (names.has(node.name)) return null; // name univoco
|
|
235
237
|
names.add(node.name);
|
|
238
|
+
if (localPorts.has(node.localPort)) return null; // ogni listener locale ha un solo owner
|
|
239
|
+
localPorts.add(node.localPort);
|
|
236
240
|
if (node.nodeId) {
|
|
237
241
|
if (node.nodeId === d.nodeId) return null; // self-reference nei dati salvati
|
|
238
242
|
if (ids.has(node.nodeId)) return null; // nodeId remoto univoco
|
|
@@ -306,22 +310,56 @@ function emptyStore(nodeId) {
|
|
|
306
310
|
return { schemaVersion: SCHEMA_VERSION, nodeId: nodeId || newNodeId(), nodes: [] };
|
|
307
311
|
}
|
|
308
312
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
+
function storeUnavailable(code, message) {
|
|
314
|
+
const e = new Error(message);
|
|
315
|
+
e.status = 503;
|
|
316
|
+
e.code = code;
|
|
317
|
+
return e;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Runtime strict: nodes.json e' identita' + credential store. Se sparisce non
|
|
321
|
+
// deve MAI essere rigenerato implicitamente da una route/command: un nuovo
|
|
322
|
+
// nodeId farebbe apparire l'installazione come un altro dispositivo e
|
|
323
|
+
// maschererebbe una perdita di stato. Solo `nexuscrew init` usa initStore().
|
|
324
|
+
function loadStoreStrict(p) {
|
|
313
325
|
let st;
|
|
314
326
|
try { st = fs.lstatSync(p); }
|
|
315
327
|
catch (e) {
|
|
316
|
-
if (e.code === 'ENOENT')
|
|
328
|
+
if (e.code === 'ENOENT') {
|
|
329
|
+
throw storeUnavailable('NODES_STORE_MISSING',
|
|
330
|
+
`nodes.json assente (${p}): esegui \`nexuscrew init\` per inizializzare esplicitamente lo store`);
|
|
331
|
+
}
|
|
317
332
|
throw e;
|
|
318
333
|
}
|
|
319
|
-
if (st.isSymbolicLink())
|
|
334
|
+
if (st.isSymbolicLink()) {
|
|
335
|
+
throw storeUnavailable('NODES_STORE_INVALID', 'nodes.json e\' un symlink (rifiutato)');
|
|
336
|
+
}
|
|
337
|
+
if (!st.isFile()) {
|
|
338
|
+
throw storeUnavailable('NODES_STORE_INVALID', 'nodes.json non e\' un file regolare');
|
|
339
|
+
}
|
|
320
340
|
const parsed = loadStore(p);
|
|
321
|
-
if (!parsed)
|
|
341
|
+
if (!parsed) {
|
|
342
|
+
throw storeUnavailable('NODES_STORE_INVALID',
|
|
343
|
+
'nodes.json presente ma invalido (schema strict): ripristina un backup valido; non viene sovrascritto');
|
|
344
|
+
}
|
|
322
345
|
return parsed;
|
|
323
346
|
}
|
|
324
347
|
|
|
348
|
+
// Init esplicito e idempotente. E' l'unico percorso autorizzato a creare uno
|
|
349
|
+
// store mancante; un file presente ma invalido resta fail-closed.
|
|
350
|
+
function initStore(p) {
|
|
351
|
+
try { fs.lstatSync(p); }
|
|
352
|
+
catch (e) {
|
|
353
|
+
if (e.code === 'ENOENT') return atomicWriteStore(p, emptyStore());
|
|
354
|
+
throw e;
|
|
355
|
+
}
|
|
356
|
+
return loadStoreStrict(p);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Compatibilita' API interna: il vecchio nome non inizializza piu' a runtime.
|
|
360
|
+
// Tenerlo strict impedisce che un call-site dimenticato reintroduca F1.
|
|
361
|
+
function loadOrInitStore(p) { return loadStoreStrict(p); }
|
|
362
|
+
|
|
325
363
|
// --- Mutazioni pure (ritornano un nuovo store; il caller lo scrive) ---------
|
|
326
364
|
|
|
327
365
|
function getNode(store, name) {
|
|
@@ -334,6 +372,9 @@ function addNode(store, entry) {
|
|
|
334
372
|
if (store.nodes.some((n) => n.name === node.name)) {
|
|
335
373
|
throw new Error(`nodo duplicato: name "${node.name}" gia' presente`);
|
|
336
374
|
}
|
|
375
|
+
if (store.nodes.some((n) => n.localPort === node.localPort)) {
|
|
376
|
+
throw new Error(`nodo duplicato: localPort ${node.localPort} gia' assegnata`);
|
|
377
|
+
}
|
|
337
378
|
if (node.nodeId) {
|
|
338
379
|
if (node.nodeId === store.nodeId) throw new Error('self-reference: il nodeId coincide con questa installazione');
|
|
339
380
|
if (store.nodes.some((n) => n.nodeId === node.nodeId)) {
|
|
@@ -425,7 +466,7 @@ function migrateLegacyNodes(configPath, nodesPath) {
|
|
|
425
466
|
if (!cfg || typeof cfg !== 'object' || !Array.isArray(cfg.nodes) || cfg.nodes.length === 0) {
|
|
426
467
|
return { migrated: false, count: 0, reason: 'nessun campo nodes legacy in config.json' };
|
|
427
468
|
}
|
|
428
|
-
const store =
|
|
469
|
+
const store = initStore(nodesPath);
|
|
429
470
|
if (store.nodes.length > 0) {
|
|
430
471
|
return { migrated: false, count: 0, reason: 'nodes.json gia\' popolato (no overwrite)' };
|
|
431
472
|
}
|
|
@@ -498,7 +539,7 @@ module.exports = {
|
|
|
498
539
|
// parse/validate
|
|
499
540
|
parseStore, parseNode, parseRendezvous, parseRoles, parseSsh, parseSshTarget, isPort, isAbsPath, validToken,
|
|
500
541
|
// I/O
|
|
501
|
-
defaultNodesPath, loadStore, atomicWriteStore, loadOrInitStore, emptyStore, newNodeId,
|
|
542
|
+
defaultNodesPath, loadStore, loadStoreStrict, initStore, atomicWriteStore, loadOrInitStore, emptyStore, newNodeId,
|
|
502
543
|
// mutazioni
|
|
503
544
|
getNode, addNode, removeNode, setNodeToken, updateNode,
|
|
504
545
|
// redazione
|