@evomap/evolver-proxy 2.0.0-beta.14 → 2.0.0-beta.16
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/dist/bin/evolver-proxy.d.ts +15 -2
- package/dist/bin/evolver-proxy.js +206 -37
- package/dist/daemon/atpConsent.js +5 -2
- package/dist/daemon/proxyDaemon.d.ts +2 -0
- package/dist/daemon/proxyDaemon.js +43 -2
- package/dist/daemon/selectHub.js +5 -3
- package/dist/private/adapterLoader.d.ts +8 -0
- package/dist/private/adapterLoader.js +186 -15
- package/dist/private/nodeCredentialStore.d.ts +23 -0
- package/dist/private/nodeCredentialStore.js +210 -0
- package/package.json +3 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { mailbox } from '@evomap/evolver-core';
|
|
3
|
+
import { PrivateNodeCredentialStore } from '../private/nodeCredentialStore.js';
|
|
3
4
|
import { loadEnvFileFromEnv } from './envFile.js';
|
|
4
5
|
import { type SelfUpdatePolicy } from '../selfUpdate/policy.js';
|
|
5
6
|
import { type ReleaseBinaryOptions } from '../selfUpdate/releaseBinary.js';
|
|
@@ -11,8 +12,9 @@ import type { AtpProxyClient, ProxyDaemonDeps, ProxyTickReport } from '../daemon
|
|
|
11
12
|
import type { HelloLifecycleMode, HelloResult, HeartbeatOptions, HeartbeatResult } from '../lifecycle/manager.js';
|
|
12
13
|
import type { InboundResult } from '../sync/engine.js';
|
|
13
14
|
/** evolver-proxy 系统级 daemon 入口(M6-7). EVOMAP_HUB_MODE/URL/NODE_SECRET 选址. */
|
|
14
|
-
interface RunProxyMainOptions {
|
|
15
|
+
export interface RunProxyMainOptions {
|
|
15
16
|
environmentPrepared?: boolean;
|
|
17
|
+
recoveryPrepared?: SelfUpdateRecoveryResult;
|
|
16
18
|
}
|
|
17
19
|
export declare function runProxyMain(options?: RunProxyMainOptions): Promise<void>;
|
|
18
20
|
export declare function recoverBoundDurableSelfUpdate(options: Omit<SelfUpdateRecoveryOptions, 'beforeJournalMutation'>): Promise<SelfUpdateRecoveryResult>;
|
|
@@ -45,7 +47,8 @@ export interface RunProxyCliOptions {
|
|
|
45
47
|
env?: NodeJS.ProcessEnv;
|
|
46
48
|
platform?: NodeJS.Platform;
|
|
47
49
|
processExecPath?: string;
|
|
48
|
-
runMain?: () => Promise<void>;
|
|
50
|
+
runMain?: (options?: RunProxyMainOptions) => Promise<void>;
|
|
51
|
+
recoverStartup?: typeof recoverBoundDurableSelfUpdate;
|
|
49
52
|
runUnixRecoveryController?: typeof maybeRunUnixRecoveryController;
|
|
50
53
|
runWindowsRecoveryController?: typeof maybeRunWindowsRecoveryController;
|
|
51
54
|
runWindowsUpdaterWorker?: typeof maybeRunWindowsUpdaterWorkerFromArgv;
|
|
@@ -73,14 +76,22 @@ export interface RuntimeDeps {
|
|
|
73
76
|
env?: Record<string, string | undefined>;
|
|
74
77
|
now?: () => number;
|
|
75
78
|
privateImporter?: PrivateAdapterImporter;
|
|
79
|
+
privateNodeCredentialStore?: Pick<PrivateNodeCredentialStore, 'read' | 'write'>;
|
|
76
80
|
}
|
|
77
81
|
export type PublicNodeSecretSource = 'env' | 'store' | 'hub_rotate' | 'legacy_file';
|
|
78
82
|
export interface PublicNodeSecretSelection {
|
|
79
83
|
nodeSecret: string | undefined;
|
|
84
|
+
nodeId?: string;
|
|
80
85
|
nodeSecretVersion?: number;
|
|
81
86
|
source: PublicNodeSecretSource;
|
|
82
87
|
storeSecret?: string;
|
|
83
88
|
}
|
|
89
|
+
export interface VerifiedPublicSender {
|
|
90
|
+
senderId: () => string | undefined;
|
|
91
|
+
adopt: (nodeId: string) => void;
|
|
92
|
+
}
|
|
93
|
+
export declare function createVerifiedPublicSender(initialNodeId?: string): VerifiedPublicSender;
|
|
94
|
+
export declare function adoptVerifiedPublicNodeId(store: mailbox.MailboxStore, selection: PublicNodeSecretSelection, sender: VerifiedPublicSender, nodeId: string): void;
|
|
84
95
|
export interface HubRuntime {
|
|
85
96
|
hub: ProxyDaemonDeps['hub'];
|
|
86
97
|
hello: (opts: {
|
|
@@ -113,6 +124,7 @@ export declare function runProxyLoop(daemon: ProxyLoopDaemon, options?: ProxyLoo
|
|
|
113
124
|
interface CreateProxyDaemonDepsOptions {
|
|
114
125
|
runtime: HubRuntime;
|
|
115
126
|
store: mailbox.MailboxStore;
|
|
127
|
+
hubMode?: 'public' | 'private';
|
|
116
128
|
ipcToken: string;
|
|
117
129
|
ipcPort?: number;
|
|
118
130
|
evolverVersion: string;
|
|
@@ -139,6 +151,7 @@ export declare function resolvePublicNodeSecret(deps: RuntimeDeps): PublicNodeSe
|
|
|
139
151
|
*/
|
|
140
152
|
export declare function clearDivergedPublicNodeSecret(store: mailbox.MailboxStore, env?: NodeJS.ProcessEnv): void;
|
|
141
153
|
export declare function persistSelectedPublicNodeSecret(store: mailbox.MailboxStore, selection: PublicNodeSecretSelection): void;
|
|
154
|
+
export declare function persistRotatedPublicNodeCredentials(store: mailbox.MailboxStore, selection: PublicNodeSecretSelection, secret: string, version: number | undefined): void;
|
|
142
155
|
export declare function persistPublicNodeSecretVersion(store: mailbox.MailboxStore, selection: PublicNodeSecretSelection, version: number | undefined): void;
|
|
143
156
|
export declare function connectHubRuntime(deps: RuntimeDeps): Promise<HubRuntime>;
|
|
144
157
|
export declare function resolveLegacyNodeSecret(envNodeSecret: string | undefined, storedNodeSecret: string | undefined, storedSource: string | undefined): string | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
3
3
|
import { readFileSync, realpathSync, rmSync } from 'node:fs';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join, resolve, win32 } from 'node:path';
|
|
@@ -10,12 +10,13 @@ import { ProxyDaemon } from '../daemon/proxyDaemon.js';
|
|
|
10
10
|
import { resolveHubMode, resolveHubUrl } from '../daemon/selectHub.js';
|
|
11
11
|
import { resolveIpcPort } from '../daemon/ipcConfig.js';
|
|
12
12
|
import { traceCollectionEnabled } from '../llm/traceConfig.js';
|
|
13
|
-
import { connectPrivateProxyHub } from '../private/adapterLoader.js';
|
|
13
|
+
import { connectPrivateProxyHub, resolvePrivateEnterpriseToken, resolvePrivateInvitationToken, resolvePrivateNodeSecret, } from '../private/adapterLoader.js';
|
|
14
|
+
import { PrivateNodeCredentialStore, PrivateNodeCredentialReadError, } from '../private/nodeCredentialStore.js';
|
|
14
15
|
import { createAtpOrderConsentGate } from '../daemon/atpConsent.js';
|
|
15
16
|
import { resolveProxyStorePath } from './proxyStorePath.js';
|
|
16
17
|
import { publishProxySettings } from './proxySettings.js';
|
|
17
18
|
import { expandHomePath, loadEnvFileFromEnv } from './envFile.js';
|
|
18
|
-
import { resolveProxyNodeId } from '../lifecycle/legacyNodeId.js';
|
|
19
|
+
import { readLegacyNodeId, resolveProxyNodeId } from '../lifecycle/legacyNodeId.js';
|
|
19
20
|
import { getCurrentVersion } from '../selfUpdate/version.js';
|
|
20
21
|
import { resolveSelfUpdatePolicy } from '../selfUpdate/policy.js';
|
|
21
22
|
import { atomicReplaceExecutable, downloadGithubReleaseArtifact, resolveGithubReleaseManifest, resolveSelfUpdateTarget, } from '../selfUpdate/releaseBinary.js';
|
|
@@ -30,19 +31,23 @@ export async function runProxyMain(options = {}) {
|
|
|
30
31
|
process.stdout.write(proxyUsage());
|
|
31
32
|
return;
|
|
32
33
|
}
|
|
33
|
-
if (!options.environmentPrepared) {
|
|
34
|
-
const envFile = loadProxyEnvFile(process.env);
|
|
35
|
-
if (envFile.error)
|
|
36
|
-
process.stderr.write(`[evolver-proxy] failed to load EVOLVER_ENV_FILE: ${safeLoopMessage(envFile.error)}\n`);
|
|
37
|
-
}
|
|
38
34
|
// Recovery must run before any hub/store/runtime initialization. In particular,
|
|
39
|
-
// a broken store must not prevent a pending-health update from restoring the old binary.
|
|
40
|
-
const recovery =
|
|
35
|
+
// a broken store or env-file pointer must not prevent a pending-health update from restoring the old binary.
|
|
36
|
+
const recovery = options.recoveryPrepared
|
|
37
|
+
?? await recoverBoundDurableSelfUpdate({
|
|
38
|
+
env: proxyRecoveryEnvironment(process.env),
|
|
39
|
+
processExecPath: process.execPath,
|
|
40
|
+
});
|
|
41
41
|
if (recovery.outcome === 'blocked') {
|
|
42
42
|
throw new Error(`self_update_recovery_blocked:${recovery.failureCode ?? 'unknown'}`);
|
|
43
43
|
}
|
|
44
44
|
if (recovery.restartRequired)
|
|
45
45
|
process.exit(78);
|
|
46
|
+
if (!options.environmentPrepared) {
|
|
47
|
+
const envFile = loadProxyEnvFile(process.env);
|
|
48
|
+
if (envFile.error)
|
|
49
|
+
throw new Error(`failed to load EVOLVER_ENV_FILE: ${safeLoopMessage(envFile.error)}`);
|
|
50
|
+
}
|
|
46
51
|
let storePath;
|
|
47
52
|
let store;
|
|
48
53
|
let proxyDaemon;
|
|
@@ -92,11 +97,21 @@ export async function runProxyMain(options = {}) {
|
|
|
92
97
|
},
|
|
93
98
|
});
|
|
94
99
|
};
|
|
95
|
-
const
|
|
100
|
+
const privateNodeCredentialStore = mode === 'private'
|
|
101
|
+
? new PrivateNodeCredentialStore(storePath)
|
|
102
|
+
: undefined;
|
|
103
|
+
const runtime = await connectHubRuntime({
|
|
104
|
+
mode,
|
|
105
|
+
hubUrl,
|
|
106
|
+
senderId,
|
|
107
|
+
store,
|
|
108
|
+
...(privateNodeCredentialStore ? { privateNodeCredentialStore } : {}),
|
|
109
|
+
});
|
|
96
110
|
proxyDaemon = new ProxyDaemon({
|
|
97
111
|
...createProxyDaemonDeps({
|
|
98
112
|
runtime,
|
|
99
113
|
store,
|
|
114
|
+
hubMode: mode,
|
|
100
115
|
ipcToken,
|
|
101
116
|
...(ipcPort !== undefined ? { ipcPort } : {}),
|
|
102
117
|
evolverVersion,
|
|
@@ -152,7 +167,17 @@ export function loadProxyEnvFile(env) {
|
|
|
152
167
|
const supervisor = env['EVOLVER_SELF_UPDATE_SUPERVISOR'];
|
|
153
168
|
const stateDir = env['EVOLVER_SELF_UPDATE_STATE_DIR'];
|
|
154
169
|
const targetPath = env['EVOLVER_SELF_UPDATE_TARGET_PATH'];
|
|
170
|
+
const systemRootBindings = Object.entries(env)
|
|
171
|
+
.filter(([key]) => key.toLowerCase() === 'systemroot');
|
|
155
172
|
const result = loadEnvFileFromEnv(env);
|
|
173
|
+
for (const key of Object.keys(env)) {
|
|
174
|
+
if (key.toLowerCase() === 'systemroot')
|
|
175
|
+
delete env[key];
|
|
176
|
+
}
|
|
177
|
+
for (const [key, value] of systemRootBindings) {
|
|
178
|
+
if (value !== undefined)
|
|
179
|
+
env[key] = value;
|
|
180
|
+
}
|
|
156
181
|
if (supervisor === undefined) {
|
|
157
182
|
delete env['EVOLVER_SELF_UPDATE_SUPERVISOR'];
|
|
158
183
|
}
|
|
@@ -165,6 +190,11 @@ export function loadProxyEnvFile(env) {
|
|
|
165
190
|
}
|
|
166
191
|
return result;
|
|
167
192
|
}
|
|
193
|
+
function proxyRecoveryEnvironment(env) {
|
|
194
|
+
const recoveryEnv = { ...env };
|
|
195
|
+
loadProxyEnvFile(recoveryEnv);
|
|
196
|
+
return recoveryEnv;
|
|
197
|
+
}
|
|
168
198
|
function finalizeRecoveryTelemetry(store, recovery) {
|
|
169
199
|
try {
|
|
170
200
|
finalizeSelfUpdateRecoveryLastUpdate(store, recovery);
|
|
@@ -302,6 +332,10 @@ export function prepareProxyCliEnvironment(argv, env) {
|
|
|
302
332
|
if (options.envFile)
|
|
303
333
|
env['EVOLVER_ENV_FILE'] = options.envFile;
|
|
304
334
|
const envFile = loadProxyEnvFile(env);
|
|
335
|
+
applyProxyCliPathOptions(options, env);
|
|
336
|
+
return { options, envFile };
|
|
337
|
+
}
|
|
338
|
+
function applyProxyCliPathOptions(options, env) {
|
|
305
339
|
if (options.home) {
|
|
306
340
|
env['EVOMAP_DIR'] = options.home;
|
|
307
341
|
env['EVOLVER_HOME'] = options.home;
|
|
@@ -320,7 +354,6 @@ export function prepareProxyCliEnvironment(argv, env) {
|
|
|
320
354
|
env['EVOLVER_PROXY_STORE'] = options.store;
|
|
321
355
|
if (options.settings)
|
|
322
356
|
env['EVOLVER_PROXY_SETTINGS_FILE'] = options.settings;
|
|
323
|
-
return { options, envFile };
|
|
324
357
|
}
|
|
325
358
|
export async function runProxyCli(options = {}) {
|
|
326
359
|
const argv = options.argv ?? process.argv.slice(2);
|
|
@@ -356,11 +389,28 @@ export async function runProxyCli(options = {}) {
|
|
|
356
389
|
process.stdout.write(proxyUsage(argv[0] === 'proxy' ? 'evolver proxy' : 'evolver-proxy'));
|
|
357
390
|
return 0;
|
|
358
391
|
}
|
|
392
|
+
if (cliOptions.envFile)
|
|
393
|
+
env['EVOLVER_ENV_FILE'] = cliOptions.envFile;
|
|
394
|
+
applyProxyCliPathOptions(cliOptions, env);
|
|
395
|
+
const recovery = options.recoverStartup || !options.runMain
|
|
396
|
+
? await (options.recoverStartup ?? recoverBoundDurableSelfUpdate)({
|
|
397
|
+
env: proxyRecoveryEnvironment(env),
|
|
398
|
+
processExecPath: options.processExecPath ?? process.execPath,
|
|
399
|
+
})
|
|
400
|
+
: undefined;
|
|
401
|
+
if (recovery?.outcome === 'blocked') {
|
|
402
|
+
throw new Error(`self_update_recovery_blocked:${recovery.failureCode ?? 'unknown'}`);
|
|
403
|
+
}
|
|
404
|
+
if (recovery?.restartRequired)
|
|
405
|
+
return 78;
|
|
359
406
|
const prepared = prepareProxyCliEnvironment(argv, env);
|
|
360
407
|
if (prepared.envFile.error) {
|
|
361
|
-
|
|
408
|
+
throw new Error(`failed to load EVOLVER_ENV_FILE: ${safeLoopMessage(prepared.envFile.error)}`);
|
|
362
409
|
}
|
|
363
|
-
await (options.runMain ??
|
|
410
|
+
await (options.runMain ?? runProxyMain)({
|
|
411
|
+
environmentPrepared: true,
|
|
412
|
+
...(recovery ? { recoveryPrepared: recovery } : {}),
|
|
413
|
+
});
|
|
364
414
|
return 0;
|
|
365
415
|
}
|
|
366
416
|
catch (error) {
|
|
@@ -376,6 +426,20 @@ if (isDirectRun(import.meta.url, process.argv[1])) {
|
|
|
376
426
|
process.exitCode = exitCode;
|
|
377
427
|
});
|
|
378
428
|
}
|
|
429
|
+
export function createVerifiedPublicSender(initialNodeId) {
|
|
430
|
+
let verifiedNodeId = initialNodeId;
|
|
431
|
+
return {
|
|
432
|
+
senderId: () => verifiedNodeId,
|
|
433
|
+
adopt: (nodeId) => {
|
|
434
|
+
verifiedNodeId = nodeId;
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
export function adoptVerifiedPublicNodeId(store, selection, sender, nodeId) {
|
|
439
|
+
store.setState('node_id', nodeId);
|
|
440
|
+
selection.nodeId = nodeId;
|
|
441
|
+
sender.adopt(nodeId);
|
|
442
|
+
}
|
|
379
443
|
export async function runProxyLoop(daemon, options = {}) {
|
|
380
444
|
const minDelayMs = options.minDelayMs ?? 1_000;
|
|
381
445
|
const errorDelayMs = options.errorDelayMs ?? 5_000;
|
|
@@ -451,6 +515,7 @@ export function createProxyDaemonDeps(options) {
|
|
|
451
515
|
const traceBackfill = resolveTraceBackfillConfig(options.env ?? process.env);
|
|
452
516
|
return {
|
|
453
517
|
hub: options.runtime.hub,
|
|
518
|
+
...(options.hubMode ? { hubMode: options.hubMode } : {}),
|
|
454
519
|
store: options.store,
|
|
455
520
|
ipcToken: options.ipcToken,
|
|
456
521
|
...(options.ipcPort !== undefined ? { ipcPort: options.ipcPort } : {}),
|
|
@@ -675,29 +740,80 @@ function selfUpdateSupervisorAttested(env) {
|
|
|
675
740
|
|| supervisor === 'windows-scheduled-task';
|
|
676
741
|
}
|
|
677
742
|
export function resolvePublicNodeSecret(deps) {
|
|
678
|
-
const
|
|
679
|
-
// version env precedence MUST mirror the node_secret precedence above (EVOMAP-first) so an operator
|
|
680
|
-
// who sets both env pairs always resolves a matched (secret, version). v2 standardizes on EVOMAP_*-first
|
|
681
|
-
// for BOTH secret and version; v1 uses A2A_*-first but pairs the two identically. Do not flip one alone.
|
|
682
|
-
const envNodeSecretVersion = parseNodeSecretVersion(process.env['EVOMAP_NODE_SECRET_VERSION'] ?? process.env['A2A_NODE_SECRET_VERSION']);
|
|
743
|
+
const explicit = resolveExplicitPublicNodeCredentials(process.env);
|
|
683
744
|
const storedNodeSecret = deps.store.getState('node_secret');
|
|
745
|
+
const storedNodeId = deps.store.getState('node_id')?.trim() || undefined;
|
|
684
746
|
const storedSource = deps.store.getState('node_secret_source');
|
|
685
747
|
const storedNodeSecretVersion = parseNodeSecretVersion(deps.store.getState('node_secret_version'));
|
|
686
|
-
const storeSecret =
|
|
687
|
-
|
|
688
|
-
|
|
748
|
+
const storeSecret = storedSource?.startsWith('pending_')
|
|
749
|
+
? undefined
|
|
750
|
+
: storedNodeSecret && isNodeSecret(storedNodeSecret) ? storedNodeSecret : undefined;
|
|
751
|
+
const legacy = readLegacyNodeSecret(process.env);
|
|
752
|
+
const pairedStoreNodeId = storedNodeId
|
|
753
|
+
?? (explicit.nodeSecret === storeSecret ? explicit.nodeId : undefined)
|
|
754
|
+
?? (legacy && legacy.nodeSecret === storeSecret ? legacy.nodeId : undefined);
|
|
755
|
+
const completeExplicitOverridesOrphan = Boolean(explicit.nodeId
|
|
756
|
+
&& explicit.nodeSecret
|
|
757
|
+
&& explicit.nodeSecret !== storeSecret
|
|
758
|
+
&& pairedStoreNodeId !== explicit.nodeId);
|
|
759
|
+
if (storedSource === 'hub_rotate' && storeSecret && !completeExplicitOverridesOrphan) {
|
|
760
|
+
return {
|
|
761
|
+
nodeSecret: storeSecret,
|
|
762
|
+
...(pairedStoreNodeId ? { nodeId: pairedStoreNodeId } : {}),
|
|
763
|
+
nodeSecretVersion: storedNodeSecretVersion,
|
|
764
|
+
source: 'hub_rotate',
|
|
765
|
+
storeSecret,
|
|
766
|
+
};
|
|
689
767
|
}
|
|
690
|
-
if (
|
|
691
|
-
const
|
|
692
|
-
|
|
768
|
+
if (explicit.nodeSecret) {
|
|
769
|
+
const pairedNodeId = explicit.nodeId
|
|
770
|
+
?? (legacy?.nodeSecret === explicit.nodeSecret ? legacy.nodeId : undefined);
|
|
771
|
+
const pairedStoreVersion = explicit.nodeSecret === storeSecret ? storedNodeSecretVersion : undefined;
|
|
772
|
+
return {
|
|
773
|
+
nodeSecret: explicit.nodeSecret,
|
|
774
|
+
...(pairedNodeId ? { nodeId: pairedNodeId } : {}),
|
|
775
|
+
nodeSecretVersion: explicit.nodeSecretVersion ?? pairedStoreVersion,
|
|
776
|
+
source: 'env',
|
|
777
|
+
storeSecret,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (storeSecret) {
|
|
781
|
+
return {
|
|
782
|
+
nodeSecret: storeSecret,
|
|
783
|
+
...(pairedStoreNodeId ? { nodeId: pairedStoreNodeId } : {}),
|
|
784
|
+
nodeSecretVersion: storedNodeSecretVersion,
|
|
785
|
+
source: 'store',
|
|
786
|
+
storeSecret,
|
|
787
|
+
};
|
|
693
788
|
}
|
|
694
|
-
if (storeSecret)
|
|
695
|
-
return { nodeSecret: storeSecret, nodeSecretVersion: storedNodeSecretVersion, source: 'store', storeSecret };
|
|
696
|
-
const legacy = readLegacyNodeSecret(process.env);
|
|
697
789
|
if (legacy)
|
|
698
790
|
return { ...legacy, source: 'legacy_file' };
|
|
699
791
|
return { nodeSecret: undefined, source: 'store' };
|
|
700
792
|
}
|
|
793
|
+
function resolveExplicitPublicNodeCredentials(env) {
|
|
794
|
+
const evomap = publicCredentialNamespace(env, 'EVOMAP');
|
|
795
|
+
const a2a = publicCredentialNamespace(env, 'A2A');
|
|
796
|
+
if (evomap.nodeId && evomap.nodeSecret)
|
|
797
|
+
return evomap;
|
|
798
|
+
if (a2a.nodeId && a2a.nodeSecret)
|
|
799
|
+
return a2a;
|
|
800
|
+
if (evomap.nodeId && a2a.nodeId && evomap.nodeId === a2a.nodeId) {
|
|
801
|
+
return evomap.nodeSecret ? evomap : a2a.nodeSecret ? a2a : {};
|
|
802
|
+
}
|
|
803
|
+
if (evomap.nodeId || a2a.nodeId)
|
|
804
|
+
return {};
|
|
805
|
+
return evomap.nodeSecret ? evomap : a2a.nodeSecret ? a2a : {};
|
|
806
|
+
}
|
|
807
|
+
function publicCredentialNamespace(env, prefix) {
|
|
808
|
+
const nodeId = env[`${prefix}_NODE_ID`]?.trim() || undefined;
|
|
809
|
+
const nodeSecret = env[`${prefix}_NODE_SECRET`]?.trim() || undefined;
|
|
810
|
+
const nodeSecretVersion = parseNodeSecretVersion(env[`${prefix}_NODE_SECRET_VERSION`]);
|
|
811
|
+
return {
|
|
812
|
+
...(nodeId ? { nodeId } : {}),
|
|
813
|
+
...(nodeSecret ? { nodeSecret } : {}),
|
|
814
|
+
...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}),
|
|
815
|
+
};
|
|
816
|
+
}
|
|
701
817
|
function setOptionalStoreState(store, key, value) {
|
|
702
818
|
store.setState(key, value ?? '');
|
|
703
819
|
}
|
|
@@ -732,8 +848,13 @@ function readLegacyNodeSecret(env = process.env) {
|
|
|
732
848
|
const nodeSecret = readTrimmedFile(join(home, 'node_secret'));
|
|
733
849
|
if (!nodeSecret || !isNodeSecret(nodeSecret))
|
|
734
850
|
continue;
|
|
851
|
+
const nodeId = readLegacyNodeId({ candidates: [join(home, 'node_id')] });
|
|
735
852
|
const nodeSecretVersion = parseNodeSecretVersion(readTrimmedFile(join(home, 'node_secret_version')));
|
|
736
|
-
return {
|
|
853
|
+
return {
|
|
854
|
+
...(nodeId ? { nodeId } : {}),
|
|
855
|
+
nodeSecret,
|
|
856
|
+
...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}),
|
|
857
|
+
};
|
|
737
858
|
}
|
|
738
859
|
return undefined;
|
|
739
860
|
}
|
|
@@ -768,9 +889,20 @@ export function clearDivergedPublicNodeSecret(store, env = process.env) {
|
|
|
768
889
|
export function persistSelectedPublicNodeSecret(store, selection) {
|
|
769
890
|
if (selection.source !== 'legacy_file' || !selection.nodeSecret)
|
|
770
891
|
return;
|
|
892
|
+
store.setState('node_secret_source', 'pending_legacy');
|
|
893
|
+
if (selection.nodeId)
|
|
894
|
+
store.setState('node_id', selection.nodeId);
|
|
771
895
|
store.setState('node_secret', selection.nodeSecret);
|
|
772
|
-
store.setState('node_secret_source', 'legacy_file');
|
|
773
896
|
setOptionalStoreState(store, 'node_secret_version', selection.nodeSecretVersion !== undefined ? String(selection.nodeSecretVersion) : undefined);
|
|
897
|
+
store.setState('node_secret_source', 'legacy_file');
|
|
898
|
+
}
|
|
899
|
+
export function persistRotatedPublicNodeCredentials(store, selection, secret, version) {
|
|
900
|
+
store.setState('node_secret_source', 'pending_rotate');
|
|
901
|
+
if (selection.nodeId)
|
|
902
|
+
store.setState('node_id', selection.nodeId);
|
|
903
|
+
store.setState('node_secret', secret);
|
|
904
|
+
setOptionalStoreState(store, 'node_secret_version', version !== undefined ? String(version) : undefined);
|
|
905
|
+
store.setState('node_secret_source', 'hub_rotate');
|
|
774
906
|
}
|
|
775
907
|
export function persistPublicNodeSecretVersion(store, selection, version) {
|
|
776
908
|
const currentStoreSecret = store.getState('node_secret');
|
|
@@ -801,10 +933,33 @@ function isDirectRun(metaUrl, argv1) {
|
|
|
801
933
|
}
|
|
802
934
|
export async function connectHubRuntime(deps) {
|
|
803
935
|
if (deps.mode === 'private') {
|
|
936
|
+
const storedInvitationFingerprint = deps.store.getState('private_invitation_fingerprint')?.trim();
|
|
937
|
+
const runtimeEnv = deps.env ?? process.env;
|
|
938
|
+
let storedNodeSecret;
|
|
939
|
+
try {
|
|
940
|
+
storedNodeSecret = deps.privateNodeCredentialStore?.read();
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
if (!(error instanceof PrivateNodeCredentialReadError)
|
|
944
|
+
|| !hasPrivateEnrollmentFallback(runtimeEnv, storedInvitationFingerprint)) {
|
|
945
|
+
throw error;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
804
948
|
const runtime = await connectPrivateProxyHub({
|
|
805
949
|
hubUrl: deps.hubUrl,
|
|
806
950
|
senderId: deps.senderId,
|
|
807
|
-
env:
|
|
951
|
+
env: runtimeEnv,
|
|
952
|
+
...(storedNodeSecret ? { storedNodeSecret } : {}),
|
|
953
|
+
...(storedInvitationFingerprint ? { storedInvitationFingerprint } : {}),
|
|
954
|
+
...(deps.privateNodeCredentialStore ? {
|
|
955
|
+
onNodeSecretAdopted: (nodeSecret) => {
|
|
956
|
+
deps.privateNodeCredentialStore?.write(nodeSecret);
|
|
957
|
+
deps.store.setState('private_node_secret_source', 'hub_enrollment');
|
|
958
|
+
},
|
|
959
|
+
} : {}),
|
|
960
|
+
onInvitationRedeemed: (fingerprint) => {
|
|
961
|
+
deps.store.setState('private_invitation_fingerprint', fingerprint);
|
|
962
|
+
},
|
|
808
963
|
...(deps.now ? { now: deps.now } : {}),
|
|
809
964
|
...(deps.privateImporter ? { importer: deps.privateImporter } : {}),
|
|
810
965
|
});
|
|
@@ -820,17 +975,17 @@ export async function connectHubRuntime(deps) {
|
|
|
820
975
|
if (!nodeSecret)
|
|
821
976
|
throw new Error('public legacy 模式需 EVOMAP_NODE_SECRET');
|
|
822
977
|
persistSelectedPublicNodeSecret(deps.store, selection);
|
|
978
|
+
const verifiedSender = createVerifiedPublicSender(selection.nodeId);
|
|
979
|
+
const senderId = verifiedSender.senderId;
|
|
823
980
|
const { hub, auth } = connectPublicHub({
|
|
824
981
|
hubUrl: deps.hubUrl,
|
|
825
982
|
authMode: 'legacy',
|
|
826
983
|
nodeSecret,
|
|
827
984
|
...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}),
|
|
828
|
-
senderId
|
|
985
|
+
senderId,
|
|
829
986
|
antiAbuse: { source: 'evolver-proxy', proxyPortConfigured: true },
|
|
830
987
|
onNodeSecretRotated: (secret, version) => {
|
|
831
|
-
deps.store
|
|
832
|
-
deps.store.setState('node_secret_source', 'hub_rotate');
|
|
833
|
-
setOptionalStoreState(deps.store, 'node_secret_version', version !== undefined ? String(version) : undefined);
|
|
988
|
+
persistRotatedPublicNodeCredentials(deps.store, selection, secret, version);
|
|
834
989
|
},
|
|
835
990
|
onNodeSecretVersionUpdated: (version) => {
|
|
836
991
|
persistPublicNodeSecretVersion(deps.store, selection, version);
|
|
@@ -843,11 +998,25 @@ export async function connectHubRuntime(deps) {
|
|
|
843
998
|
});
|
|
844
999
|
return {
|
|
845
1000
|
hub,
|
|
846
|
-
atp: new AtpHubClient({ baseUrl: deps.hubUrl, auth, fetchFn: globalFetchLike, senderId
|
|
847
|
-
hello: (opts) =>
|
|
1001
|
+
atp: new AtpHubClient({ baseUrl: deps.hubUrl, auth, fetchFn: globalFetchLike, senderId }),
|
|
1002
|
+
hello: async (opts) => {
|
|
1003
|
+
const result = await hub.hello(opts);
|
|
1004
|
+
if (result.nodeId) {
|
|
1005
|
+
adoptVerifiedPublicNodeId(deps.store, selection, verifiedSender, result.nodeId);
|
|
1006
|
+
}
|
|
1007
|
+
return result;
|
|
1008
|
+
},
|
|
848
1009
|
heartbeat: (opts) => hub.heartbeat(opts),
|
|
849
1010
|
};
|
|
850
1011
|
}
|
|
1012
|
+
function hasPrivateEnrollmentFallback(env, storedInvitationFingerprint) {
|
|
1013
|
+
if (resolvePrivateNodeSecret(env) || resolvePrivateEnterpriseToken(env))
|
|
1014
|
+
return true;
|
|
1015
|
+
const invitationToken = resolvePrivateInvitationToken(env);
|
|
1016
|
+
if (!invitationToken)
|
|
1017
|
+
return false;
|
|
1018
|
+
return createHash('sha256').update(invitationToken).digest('hex') !== storedInvitationFingerprint;
|
|
1019
|
+
}
|
|
851
1020
|
export function resolveLegacyNodeSecret(envNodeSecret, storedNodeSecret, storedSource) {
|
|
852
1021
|
if (envNodeSecret)
|
|
853
1022
|
return envNodeSecret;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
|
-
import { events } from '@evomap/evolver-core';
|
|
4
4
|
export class AtpProxySpendConsentError extends Error {
|
|
5
5
|
source;
|
|
6
6
|
constructor(source) {
|
|
@@ -10,7 +10,10 @@ export class AtpProxySpendConsentError extends Error {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
function atpProxyConsentPath(env = process.env) {
|
|
13
|
-
const home = env['EVOMAP_DIR']
|
|
13
|
+
const home = [env['EVOMAP_DIR'], env['EVOLVER_HOME'], env['EVOMAP_HOME']]
|
|
14
|
+
.map((value) => value?.trim())
|
|
15
|
+
.find((value) => Boolean(value))
|
|
16
|
+
?? join(homedir(), '.evomap');
|
|
14
17
|
return join(home, 'evolution', 'atp-autobuy-ack.json');
|
|
15
18
|
}
|
|
16
19
|
export function getAtpProxyConsent(env = process.env, ackPath = atpProxyConsentPath(env)) {
|
|
@@ -8,6 +8,8 @@ type AssetStoreProvider = assetstore.AssetStoreProvider;
|
|
|
8
8
|
export declare const DEFAULT_IPC_PORT = 19820;
|
|
9
9
|
export interface ProxyDaemonDeps {
|
|
10
10
|
hub: HubCapability;
|
|
11
|
+
/** Immutable Hub selection for IPC callers that must fail closed across public/private runtimes. */
|
|
12
|
+
hubMode?: 'public' | 'private';
|
|
11
13
|
/** 二选一: 传 storePath 让 ProxyDaemon 建 store, 或传已建 store(供 hub senderId 共享同一 node_id). */
|
|
12
14
|
storePath?: string;
|
|
13
15
|
store?: mailbox.MailboxStore;
|
|
@@ -585,6 +585,11 @@ export class ProxyDaemon {
|
|
|
585
585
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
586
586
|
}
|
|
587
587
|
async handleProxyRoute(ctx) {
|
|
588
|
+
const expectedHeader = singleHeader(ctx.req.headers['x-evomap-expected-hub-mode']);
|
|
589
|
+
if (hubModeMismatch(expectedHeader, this.deps.hubMode)) {
|
|
590
|
+
ctx.json(409, { error: 'proxy_hub_mode_mismatch' });
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
588
593
|
if (await this.collaborationFacade.handle(ctx))
|
|
589
594
|
return true;
|
|
590
595
|
const handledAtp = await this.handleAtpRoute(ctx);
|
|
@@ -593,6 +598,7 @@ export class ProxyDaemon {
|
|
|
593
598
|
if (ctx.route === 'GET /proxy/status') {
|
|
594
599
|
ctx.json(200, {
|
|
595
600
|
running: true,
|
|
601
|
+
hub_mode: this.deps.hubMode ?? 'public',
|
|
596
602
|
node_id: this.lifecycle.nodeId ?? null,
|
|
597
603
|
outbound_pending: this.store.countPending('proxy', this.deps.runtimeNamespace),
|
|
598
604
|
inbound_pending: this.store.countPending('agent', this.deps.runtimeNamespace) + this.store.countPending('core', this.deps.runtimeNamespace),
|
|
@@ -616,6 +622,10 @@ export class ProxyDaemon {
|
|
|
616
622
|
}
|
|
617
623
|
if (ctx.route === 'POST /asset/search') {
|
|
618
624
|
const body = (await ctx.readJson());
|
|
625
|
+
if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
|
|
626
|
+
ctx.json(409, { error: 'proxy_hub_mode_mismatch' });
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
619
629
|
const limit = Math.max(1, Math.min(Number(body.limit ?? 5), 25));
|
|
620
630
|
const rawSignals = Array.isArray(body.signals) ? body.signals : body.signalsAny;
|
|
621
631
|
const signalsAny = Array.isArray(rawSignals) ? rawSignals.filter((s) => typeof s === 'string') : undefined;
|
|
@@ -638,6 +648,10 @@ export class ProxyDaemon {
|
|
|
638
648
|
}
|
|
639
649
|
if (ctx.route === 'POST /asset/fetch') {
|
|
640
650
|
const body = (await ctx.readJson());
|
|
651
|
+
if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
|
|
652
|
+
ctx.json(409, { error: 'proxy_hub_mode_mismatch' });
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
641
655
|
const ids = uniqueStrings([
|
|
642
656
|
...(Array.isArray(body.asset_ids) ? body.asset_ids : []),
|
|
643
657
|
...(typeof body.asset_id === 'string' ? [body.asset_id] : []),
|
|
@@ -664,6 +678,10 @@ export class ProxyDaemon {
|
|
|
664
678
|
}
|
|
665
679
|
if (ctx.route === 'POST /asset/submit') {
|
|
666
680
|
const body = (await ctx.readJson());
|
|
681
|
+
if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
|
|
682
|
+
ctx.json(409, { stored: false, error: 'proxy_hub_mode_mismatch' });
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
667
685
|
if (!body.assets && !body.asset_id) {
|
|
668
686
|
ctx.json(400, { error: 'assets or asset_id is required' });
|
|
669
687
|
return true;
|
|
@@ -679,6 +697,10 @@ export class ProxyDaemon {
|
|
|
679
697
|
// Pre-publish dry-run against the hub's quality + content-safety gate (nothing stored, no credits).
|
|
680
698
|
// Same {assets:[…]} bundle shape as /asset/submit; the adapter wraps it in a GEP-A2A envelope.
|
|
681
699
|
const body = (await ctx.readJson());
|
|
700
|
+
if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
|
|
701
|
+
ctx.json(409, { valid: false, error: 'proxy_hub_mode_mismatch' });
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
682
704
|
const bundle = Array.isArray(body.assets)
|
|
683
705
|
? body.assets.filter((a) => Boolean(a && typeof a === 'object'))
|
|
684
706
|
: (body.asset && typeof body.asset === 'object' && !Array.isArray(body.asset) ? [body.asset] : []);
|
|
@@ -704,7 +726,12 @@ export class ProxyDaemon {
|
|
|
704
726
|
return true;
|
|
705
727
|
}
|
|
706
728
|
if (ctx.route === 'POST /asset/reuse-result') {
|
|
707
|
-
const
|
|
729
|
+
const body = await ctx.readJson();
|
|
730
|
+
if (hubModeMismatch(asRecord(body)['expected_hub_mode'], this.deps.hubMode)) {
|
|
731
|
+
ctx.json(409, { recorded: false, error: 'proxy_hub_mode_mismatch' });
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
const parsed = parseReuseResultReport(body);
|
|
708
735
|
if ('error' in parsed) {
|
|
709
736
|
ctx.json(400, { recorded: false, error: parsed.error });
|
|
710
737
|
return true;
|
|
@@ -723,6 +750,10 @@ export class ProxyDaemon {
|
|
|
723
750
|
}
|
|
724
751
|
if (ctx.route === 'POST /conversation/distill') {
|
|
725
752
|
const body = (await ctx.readJson());
|
|
753
|
+
if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
|
|
754
|
+
ctx.json(409, { error: 'proxy_hub_mode_mismatch' });
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
726
757
|
const distill = await hubNs.distillConversation(body, { persist: body.persist === true, store: this.assetStore });
|
|
727
758
|
if (!distill.ok) {
|
|
728
759
|
ctx.json(200, { ...distill, queued: false, submission: null });
|
|
@@ -929,7 +960,17 @@ function isValidator(value) {
|
|
|
929
960
|
return Boolean(value && typeof value === 'object' && typeof value.validate === 'function');
|
|
930
961
|
}
|
|
931
962
|
function assetMatchesId(asset, assetId) {
|
|
932
|
-
|
|
963
|
+
if (!asset)
|
|
964
|
+
return false;
|
|
965
|
+
return assetId.startsWith('sha256:')
|
|
966
|
+
? asset.asset_id === assetId
|
|
967
|
+
: asset.asset_id === assetId || asset['id'] === assetId;
|
|
968
|
+
}
|
|
969
|
+
function hubModeMismatch(expected, actual) {
|
|
970
|
+
return expected !== undefined && expected !== (actual ?? 'public');
|
|
971
|
+
}
|
|
972
|
+
function singleHeader(value) {
|
|
973
|
+
return Array.isArray(value) ? value[0] : value;
|
|
933
974
|
}
|
|
934
975
|
function uniqueStrings(values) {
|
|
935
976
|
const seen = new Set();
|
package/dist/daemon/selectHub.js
CHANGED
|
@@ -12,10 +12,12 @@ export function resolveHubUrl(env) {
|
|
|
12
12
|
return resolvePublicHubUrl(env);
|
|
13
13
|
}
|
|
14
14
|
function resolvePrivateHubUrl(env) {
|
|
15
|
-
|
|
15
|
+
const url = trimmed(env['EVOMAP_HUB_URL'])
|
|
16
16
|
?? trimmed(env['A2A_HUB_URL'])
|
|
17
|
-
?? trimmed(env['EVOLVER_DEFAULT_HUB_URL'])
|
|
18
|
-
|
|
17
|
+
?? trimmed(env['EVOLVER_DEFAULT_HUB_URL']);
|
|
18
|
+
if (!url)
|
|
19
|
+
throw new Error('private Hub URL is not configured');
|
|
20
|
+
return url;
|
|
19
21
|
}
|
|
20
22
|
function trimmed(value) {
|
|
21
23
|
const v = value?.trim();
|
|
@@ -52,6 +52,14 @@ export interface ConnectPrivateProxyHubOptions {
|
|
|
52
52
|
now?: () => number;
|
|
53
53
|
importer?: DynamicImporter;
|
|
54
54
|
fetchFn?: PrivateCompatibilityFetch;
|
|
55
|
+
/** Durable credential minted by a previous invitation/SSO enrollment. */
|
|
56
|
+
storedNodeSecret?: string;
|
|
57
|
+
/** Persist a newly minted credential before the process can restart. */
|
|
58
|
+
onNodeSecretAdopted?: (nodeSecret: string) => void;
|
|
59
|
+
/** Fingerprint of the last successfully redeemed one-shot invitation. */
|
|
60
|
+
storedInvitationFingerprint?: string;
|
|
61
|
+
/** Persist the non-secret fingerprint after an invitation is redeemed. */
|
|
62
|
+
onInvitationRedeemed?: (fingerprint: string) => void;
|
|
55
63
|
}
|
|
56
64
|
export declare function resolvePrivateEnterpriseToken(env: Record<string, string | undefined>): string | undefined;
|
|
57
65
|
/** One-shot invitation token (evoinv_…), matching the hub's official onboarding script (A2A_INVITATION_TOKEN).
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hub as hubNs } from '@evomap/evolver-core';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { withPrivateAccountAssetCompatibility, } from './accountAssetCompatibility.js';
|
|
3
4
|
const DEFAULT_PRIVATE_ADAPTER_MODULE = '@evomap/evolver-adapter-private';
|
|
4
5
|
export function resolvePrivateEnterpriseToken(env) {
|
|
@@ -17,19 +18,26 @@ export function resolvePrivateEnterpriseSubject(env) {
|
|
|
17
18
|
}
|
|
18
19
|
export async function connectPrivateProxyHub(opts) {
|
|
19
20
|
const invitationToken = resolvePrivateInvitationToken(opts.env);
|
|
21
|
+
const invitationFingerprint = invitationToken
|
|
22
|
+
? createHash('sha256').update(invitationToken).digest('hex')
|
|
23
|
+
: undefined;
|
|
24
|
+
const usableInvitationToken = invitationFingerprint !== opts.storedInvitationFingerprint
|
|
25
|
+
? invitationToken
|
|
26
|
+
: undefined;
|
|
20
27
|
const token = resolvePrivateEnterpriseToken(opts.env);
|
|
21
|
-
const
|
|
28
|
+
const configuredNodeSecret = resolvePrivateNodeSecret(opts.env);
|
|
29
|
+
const nodeSecret = configuredNodeSecret ?? opts.storedNodeSecret?.trim();
|
|
22
30
|
if (nodeSecret && !isNodeSecret(nodeSecret)) {
|
|
23
31
|
throw new Error('Private Hub node_secret 必须是 64 位十六进制字符串');
|
|
24
32
|
}
|
|
25
|
-
if (!token && !
|
|
33
|
+
if (!token && !usableInvitationToken && !nodeSecret) {
|
|
26
34
|
throw new Error('EVOMAP_HUB_MODE=private 需要 A2A_NODE_SECRET / EVOMAP_NODE_SECRET、A2A_INVITATION_TOKEN 或 EVOMAP_ENTERPRISE_TOKEN');
|
|
27
35
|
}
|
|
28
36
|
const moduleName = opts.env['EVOMAP_PRIVATE_ADAPTER_MODULE']?.trim() || DEFAULT_PRIVATE_ADAPTER_MODULE;
|
|
29
37
|
const connectPrivateHub = await loadConnectPrivateHub(moduleName, opts.importer ?? ((specifier) => import(specifier)));
|
|
30
38
|
const now = opts.now ?? (() => Date.now());
|
|
31
39
|
const subject = resolvePrivateEnterpriseSubject(opts.env);
|
|
32
|
-
const
|
|
40
|
+
const baseConnectionOptions = {
|
|
33
41
|
hubUrl: opts.hubUrl,
|
|
34
42
|
senderId: opts.senderId,
|
|
35
43
|
env: opts.env,
|
|
@@ -39,13 +47,34 @@ export async function connectPrivateProxyHub(opts) {
|
|
|
39
47
|
exchange: async () => ({ token: token ?? nodeSecret ?? '' }),
|
|
40
48
|
now,
|
|
41
49
|
},
|
|
42
|
-
...(nodeSecret ? { nodeSecret } : {}),
|
|
43
|
-
...(!nodeSecret && invitationToken ? { invitationToken } : {}),
|
|
44
50
|
...(opts.fetchFn ? { fetchFn: opts.fetchFn } : {}),
|
|
51
|
+
};
|
|
52
|
+
const primaryInvitationToken = !nodeSecret && !configuredNodeSecret ? usableInvitationToken : undefined;
|
|
53
|
+
const { hub, auth } = connectPrivateHub({
|
|
54
|
+
...baseConnectionOptions,
|
|
55
|
+
...(nodeSecret ? { nodeSecret } : {}),
|
|
56
|
+
...(primaryInvitationToken ? { invitationToken: primaryInvitationToken } : {}),
|
|
45
57
|
});
|
|
46
58
|
assertPrivateLifecycle(hub, moduleName);
|
|
59
|
+
const storedInvitationFallback = nodeSecret && !configuredNodeSecret && usableInvitationToken
|
|
60
|
+
? connectPrivateHub({ ...baseConnectionOptions, invitationToken: usableInvitationToken })
|
|
61
|
+
: undefined;
|
|
62
|
+
const enterpriseFallback = primaryInvitationToken && token
|
|
63
|
+
? connectPrivateHub(baseConnectionOptions)
|
|
64
|
+
: undefined;
|
|
65
|
+
if (storedInvitationFallback)
|
|
66
|
+
assertPrivateLifecycle(storedInvitationFallback.hub, moduleName);
|
|
67
|
+
if (enterpriseFallback)
|
|
68
|
+
assertPrivateLifecycle(enterpriseFallback.hub, moduleName);
|
|
47
69
|
if (nodeSecret)
|
|
48
70
|
await adoptReadyNodeSecret(auth, nodeSecret);
|
|
71
|
+
const primaryAdoptions = trackNodeSecretAdoptions(auth);
|
|
72
|
+
const storedInvitationAdoptions = storedInvitationFallback
|
|
73
|
+
? trackNodeSecretAdoptions(storedInvitationFallback.auth)
|
|
74
|
+
: undefined;
|
|
75
|
+
const enterpriseAdoptions = enterpriseFallback
|
|
76
|
+
? trackNodeSecretAdoptions(enterpriseFallback.auth)
|
|
77
|
+
: undefined;
|
|
49
78
|
if (!hub.agentDirectory) {
|
|
50
79
|
hub.agentDirectory = hubNs.unsupportedAgentDirectoryCapability('private_hub_agent_directory_not_supported');
|
|
51
80
|
}
|
|
@@ -56,14 +85,152 @@ export async function connectPrivateProxyHub(opts) {
|
|
|
56
85
|
env: opts.env,
|
|
57
86
|
...(opts.fetchFn ? { fetchFn: opts.fetchFn } : {}),
|
|
58
87
|
});
|
|
88
|
+
let readyHelloPending = Boolean(nodeSecret);
|
|
89
|
+
let invitationRedeemed = false;
|
|
90
|
+
let enterpriseFallbackSelected = false;
|
|
91
|
+
let helloInProgress = false;
|
|
92
|
+
const helloWithFallback = async (fallback, adoptions, helloOpts, rotate) => {
|
|
93
|
+
if (!adoptions?.available) {
|
|
94
|
+
throw new Error('private Hub adapter cannot report node_secret adoption');
|
|
95
|
+
}
|
|
96
|
+
if (rotate)
|
|
97
|
+
await fallback.auth.rotate();
|
|
98
|
+
const { result, adoptedNodeSecret } = await adoptions.capture(() => fallback.hub.hello(helloOpts));
|
|
99
|
+
if (!result.ok)
|
|
100
|
+
return result;
|
|
101
|
+
if (!adoptedNodeSecret)
|
|
102
|
+
throw new Error('private Hub fallback did not adopt a node_secret');
|
|
103
|
+
await adoptReadyNodeSecret(fallback.auth, adoptedNodeSecret);
|
|
104
|
+
await adoptReadyNodeSecret(auth, adoptedNodeSecret);
|
|
105
|
+
opts.onNodeSecretAdopted?.(adoptedNodeSecret);
|
|
106
|
+
return result;
|
|
107
|
+
};
|
|
108
|
+
const helloOnce = async (helloOpts) => {
|
|
109
|
+
if (enterpriseFallbackSelected || invitationRedeemed) {
|
|
110
|
+
if (!enterpriseFallback)
|
|
111
|
+
return { ok: false, error: 'private_invitation_reenrollment_required' };
|
|
112
|
+
return helloWithFallback(enterpriseFallback, enterpriseAdoptions, helloOpts, true);
|
|
113
|
+
}
|
|
114
|
+
if (primaryInvitationToken && !primaryAdoptions.available) {
|
|
115
|
+
throw new Error('private Hub adapter cannot report node_secret adoption');
|
|
116
|
+
}
|
|
117
|
+
let result;
|
|
118
|
+
let adoptedNodeSecret;
|
|
119
|
+
let usedAdapterHello = false;
|
|
120
|
+
if (nodeSecret && readyHelloPending) {
|
|
121
|
+
// A stored credential gets one ready probe before a fresh invitation takes over.
|
|
122
|
+
// Clear this before awaiting so a thrown probe cannot starve the fallback forever.
|
|
123
|
+
if (storedInvitationFallback)
|
|
124
|
+
readyHelloPending = false;
|
|
125
|
+
const tracked = await primaryAdoptions.capture(() => helloWithReadyPrivateCredential(compatibleHub, auth, nodeSecret, opts.senderId, helloOpts, primaryAdoptions.available));
|
|
126
|
+
readyHelloPending = false;
|
|
127
|
+
result = tracked.result;
|
|
128
|
+
adoptedNodeSecret = tracked.adoptedNodeSecret;
|
|
129
|
+
if (result.ok && !adoptedNodeSecret && !await authenticatesWithNodeSecret(auth, nodeSecret)) {
|
|
130
|
+
throw new Error('private Hub ready credential hello did not preserve an active node_secret');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (storedInvitationFallback) {
|
|
134
|
+
result = await helloWithFallback(storedInvitationFallback, storedInvitationAdoptions, helloOpts, false);
|
|
135
|
+
if (result.ok) {
|
|
136
|
+
invitationRedeemed = true;
|
|
137
|
+
if (invitationFingerprint)
|
|
138
|
+
opts.onInvitationRedeemed?.(invitationFingerprint);
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
usedAdapterHello = true;
|
|
144
|
+
const tracked = await primaryAdoptions.capture(() => compatibleHub.hello(helloOpts));
|
|
145
|
+
result = tracked.result;
|
|
146
|
+
adoptedNodeSecret = tracked.adoptedNodeSecret;
|
|
147
|
+
}
|
|
148
|
+
if (!result.ok && usedAdapterHello && primaryInvitationToken && enterpriseFallback) {
|
|
149
|
+
const fallbackResult = await helloWithFallback(enterpriseFallback, enterpriseAdoptions, helloOpts, true);
|
|
150
|
+
if (fallbackResult.ok)
|
|
151
|
+
enterpriseFallbackSelected = true;
|
|
152
|
+
return fallbackResult;
|
|
153
|
+
}
|
|
154
|
+
if (result.ok) {
|
|
155
|
+
if (adoptedNodeSecret && adoptedNodeSecret !== nodeSecret) {
|
|
156
|
+
await adoptReadyNodeSecret(auth, adoptedNodeSecret);
|
|
157
|
+
opts.onNodeSecretAdopted?.(adoptedNodeSecret);
|
|
158
|
+
}
|
|
159
|
+
if (usedAdapterHello && primaryInvitationToken) {
|
|
160
|
+
if (!adoptedNodeSecret) {
|
|
161
|
+
throw new Error('private Hub enrollment did not adopt a node_secret');
|
|
162
|
+
}
|
|
163
|
+
invitationRedeemed = true;
|
|
164
|
+
if (invitationFingerprint)
|
|
165
|
+
opts.onInvitationRedeemed?.(invitationFingerprint);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
};
|
|
170
|
+
const hello = async (helloOpts) => {
|
|
171
|
+
if (helloInProgress)
|
|
172
|
+
throw new Error('concurrent private Hub hello is not supported');
|
|
173
|
+
helloInProgress = true;
|
|
174
|
+
try {
|
|
175
|
+
return await helloOnce(helloOpts);
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
helloInProgress = false;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
59
181
|
return {
|
|
60
182
|
hub: compatibleHub,
|
|
61
183
|
auth,
|
|
62
|
-
hello
|
|
63
|
-
? async (helloOpts) => await helloWithReadyPrivateCredential(compatibleHub, auth, nodeSecret, opts.senderId, helloOpts)
|
|
64
|
-
: (helloOpts) => compatibleHub.hello(helloOpts),
|
|
184
|
+
hello,
|
|
65
185
|
};
|
|
66
186
|
}
|
|
187
|
+
const NODE_SECRET_ADOPTION_TRACKERS = new WeakMap();
|
|
188
|
+
function trackNodeSecretAdoptions(auth) {
|
|
189
|
+
const existing = NODE_SECRET_ADOPTION_TRACKERS.get(auth);
|
|
190
|
+
if (existing)
|
|
191
|
+
return existing;
|
|
192
|
+
const candidate = auth;
|
|
193
|
+
const original = candidate.adoptNodeSecret;
|
|
194
|
+
let capturing = false;
|
|
195
|
+
let available = false;
|
|
196
|
+
let adoptedNodeSecret;
|
|
197
|
+
if (typeof original === 'function') {
|
|
198
|
+
const wrapped = function (nodeSecret) {
|
|
199
|
+
original.call(this, nodeSecret);
|
|
200
|
+
if (capturing)
|
|
201
|
+
adoptedNodeSecret = isNodeSecret(nodeSecret) ? nodeSecret : undefined;
|
|
202
|
+
};
|
|
203
|
+
try {
|
|
204
|
+
candidate.adoptNodeSecret = wrapped;
|
|
205
|
+
available = candidate.adoptNodeSecret === wrapped;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// A frozen adapter cannot expose reliable adoption provenance. Enrollment will fail closed below.
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const tracker = {
|
|
212
|
+
get available() { return available; },
|
|
213
|
+
capture: async (operation) => {
|
|
214
|
+
if (capturing)
|
|
215
|
+
throw new Error('concurrent private Hub credential adoption is not supported');
|
|
216
|
+
capturing = true;
|
|
217
|
+
adoptedNodeSecret = undefined;
|
|
218
|
+
try {
|
|
219
|
+
const result = await operation();
|
|
220
|
+
return {
|
|
221
|
+
result,
|
|
222
|
+
...(adoptedNodeSecret ? { adoptedNodeSecret } : {}),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
capturing = false;
|
|
227
|
+
adoptedNodeSecret = undefined;
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
NODE_SECRET_ADOPTION_TRACKERS.set(auth, tracker);
|
|
232
|
+
return tracker;
|
|
233
|
+
}
|
|
67
234
|
async function loadConnectPrivateHub(moduleName, importer) {
|
|
68
235
|
let loaded;
|
|
69
236
|
try {
|
|
@@ -94,16 +261,21 @@ async function adoptReadyNodeSecret(auth, nodeSecret) {
|
|
|
94
261
|
throw new Error('private Hub adapter cannot activate the configured node_secret');
|
|
95
262
|
}
|
|
96
263
|
}
|
|
97
|
-
async function helloWithReadyPrivateCredential(hub, auth, nodeSecret, senderId, opts) {
|
|
98
|
-
|
|
99
|
-
|
|
264
|
+
async function helloWithReadyPrivateCredential(hub, auth, nodeSecret, senderId, opts, canTrackAdoption = true) {
|
|
265
|
+
const nodeId = trimmedSenderId(senderId);
|
|
266
|
+
if (nodeId && await authenticatesWithNodeSecret(auth, nodeSecret))
|
|
267
|
+
return readyPrivateHello(nodeId);
|
|
268
|
+
if (!canTrackAdoption)
|
|
269
|
+
throw new Error('private Hub adapter cannot report node_secret adoption');
|
|
100
270
|
return await hub.hello(opts);
|
|
101
271
|
}
|
|
102
272
|
async function authenticatesWithNodeSecret(auth, nodeSecret) {
|
|
103
273
|
const signed = await auth.authenticate({ method: 'GET', path: '/a2a/assets/published-by-me' });
|
|
104
274
|
const authorization = headerValue(signed.headers, 'authorization');
|
|
105
275
|
const bodySecret = signed.bodyFields?.['node_secret'];
|
|
106
|
-
return authorization
|
|
276
|
+
return authorization !== undefined
|
|
277
|
+
? authorization === `Bearer ${nodeSecret}`
|
|
278
|
+
: bodySecret === nodeSecret;
|
|
107
279
|
}
|
|
108
280
|
function headerValue(headers, name) {
|
|
109
281
|
const lower = name.toLowerCase();
|
|
@@ -116,9 +288,8 @@ function headerValue(headers, name) {
|
|
|
116
288
|
function trimmedSenderId(senderId) {
|
|
117
289
|
return senderId()?.trim() || undefined;
|
|
118
290
|
}
|
|
119
|
-
function readyPrivateHello(
|
|
120
|
-
|
|
121
|
-
return { ok: true, ...(nodeId ? { nodeId } : {}) };
|
|
291
|
+
function readyPrivateHello(nodeId) {
|
|
292
|
+
return { ok: true, nodeId };
|
|
122
293
|
}
|
|
123
294
|
function isNodeSecret(value) {
|
|
124
295
|
return /^[a-f0-9]{64}$/i.test(value);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface WindowsSecretProtector {
|
|
2
|
+
protect(secret: string): string;
|
|
3
|
+
unprotect(protectedValue: string): string;
|
|
4
|
+
preflight?(): void;
|
|
5
|
+
}
|
|
6
|
+
export interface PrivateNodeCredentialStoreOptions {
|
|
7
|
+
platform?: NodeJS.Platform;
|
|
8
|
+
windowsProtector?: WindowsSecretProtector;
|
|
9
|
+
}
|
|
10
|
+
export declare class PrivateNodeCredentialReadError extends Error {
|
|
11
|
+
constructor();
|
|
12
|
+
}
|
|
13
|
+
export declare class PrivateNodeCredentialStore {
|
|
14
|
+
private readonly directory;
|
|
15
|
+
private readonly path;
|
|
16
|
+
private readonly platform;
|
|
17
|
+
private readonly windowsProtector;
|
|
18
|
+
constructor(proxyStorePath: string, options?: PrivateNodeCredentialStoreOptions);
|
|
19
|
+
read(): string | undefined;
|
|
20
|
+
write(nodeSecret: string): void;
|
|
21
|
+
private prepareDirectory;
|
|
22
|
+
private assertRegularFile;
|
|
23
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
const DIRECTORY_MODE = 0o700;
|
|
6
|
+
const FILE_MODE = 0o600;
|
|
7
|
+
const NODE_SECRET_RE = /^[a-f0-9]{64}$/i;
|
|
8
|
+
const WINDOWS_PROTECTED_VALUE_RE = /^[a-z0-9+/]+={0,2}$/i;
|
|
9
|
+
const MAX_PROTECTED_VALUE_LENGTH = 16_384;
|
|
10
|
+
export class PrivateNodeCredentialReadError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super('stored private node credential is unreadable');
|
|
13
|
+
this.name = 'PrivateNodeCredentialReadError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class PrivateNodeCredentialStore {
|
|
17
|
+
directory;
|
|
18
|
+
path;
|
|
19
|
+
platform;
|
|
20
|
+
windowsProtector;
|
|
21
|
+
constructor(proxyStorePath, options = {}) {
|
|
22
|
+
this.platform = options.platform ?? process.platform;
|
|
23
|
+
this.windowsProtector = this.platform === 'win32'
|
|
24
|
+
? options.windowsProtector ?? createWindowsDpapiProtector()
|
|
25
|
+
: undefined;
|
|
26
|
+
if (this.windowsProtector)
|
|
27
|
+
verifyWindowsProtector(this.windowsProtector);
|
|
28
|
+
this.directory = join(dirname(resolve(proxyStorePath)), '.private-credentials');
|
|
29
|
+
this.path = join(this.directory, this.platform === 'win32' ? 'node-secret.dpapi' : 'node-secret');
|
|
30
|
+
this.prepareDirectory();
|
|
31
|
+
}
|
|
32
|
+
read() {
|
|
33
|
+
if (!existsSync(this.path))
|
|
34
|
+
return undefined;
|
|
35
|
+
this.assertRegularFile(this.path);
|
|
36
|
+
if (this.platform !== 'win32')
|
|
37
|
+
chmodSync(this.path, FILE_MODE);
|
|
38
|
+
const storedValue = readFileSync(this.path, 'utf8').trim();
|
|
39
|
+
try {
|
|
40
|
+
const value = this.windowsProtector
|
|
41
|
+
? this.windowsProtector.unprotect(assertWindowsProtectedValue(storedValue))
|
|
42
|
+
: storedValue;
|
|
43
|
+
if (!NODE_SECRET_RE.test(value))
|
|
44
|
+
throw new PrivateNodeCredentialReadError();
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new PrivateNodeCredentialReadError();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
write(nodeSecret) {
|
|
52
|
+
if (!NODE_SECRET_RE.test(nodeSecret))
|
|
53
|
+
throw new Error('private node credential is invalid');
|
|
54
|
+
const storedValue = this.windowsProtector
|
|
55
|
+
? assertWindowsProtectedValue(this.windowsProtector.protect(nodeSecret))
|
|
56
|
+
: nodeSecret;
|
|
57
|
+
this.prepareDirectory();
|
|
58
|
+
if (existsSync(this.path))
|
|
59
|
+
this.assertRegularFile(this.path);
|
|
60
|
+
const temporaryPath = join(this.directory, `.node-secret.${process.pid}.${randomBytes(8).toString('hex')}.tmp`);
|
|
61
|
+
let descriptor;
|
|
62
|
+
try {
|
|
63
|
+
descriptor = openSync(temporaryPath, 'wx', FILE_MODE);
|
|
64
|
+
writeFileSync(descriptor, storedValue, 'utf8');
|
|
65
|
+
fsyncSync(descriptor);
|
|
66
|
+
closeSync(descriptor);
|
|
67
|
+
descriptor = undefined;
|
|
68
|
+
renameSync(temporaryPath, this.path);
|
|
69
|
+
if (this.platform !== 'win32')
|
|
70
|
+
chmodSync(this.path, FILE_MODE);
|
|
71
|
+
if (this.platform !== 'win32')
|
|
72
|
+
syncDirectory(this.directory);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (descriptor !== undefined)
|
|
76
|
+
closeSync(descriptor);
|
|
77
|
+
if (existsSync(temporaryPath))
|
|
78
|
+
unlinkSync(temporaryPath);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
prepareDirectory() {
|
|
83
|
+
const parent = dirname(this.directory);
|
|
84
|
+
const parentStat = lstatSync(parent);
|
|
85
|
+
if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) {
|
|
86
|
+
throw new Error(`private credential parent must be a real directory: ${parent}`);
|
|
87
|
+
}
|
|
88
|
+
if (this.platform !== 'win32' && (parentStat.mode & 0o022) !== 0) {
|
|
89
|
+
throw new Error(`private credential parent must not be group/world-writable: ${parent}`);
|
|
90
|
+
}
|
|
91
|
+
const directoryExisted = existsSync(this.directory);
|
|
92
|
+
mkdirSync(this.directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
93
|
+
const stat = lstatSync(this.directory);
|
|
94
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
95
|
+
throw new Error(`private credential path must be a real directory: ${this.directory}`);
|
|
96
|
+
}
|
|
97
|
+
if (this.platform !== 'win32')
|
|
98
|
+
chmodSync(this.directory, DIRECTORY_MODE);
|
|
99
|
+
if (!directoryExisted && this.platform !== 'win32')
|
|
100
|
+
syncDirectory(parent);
|
|
101
|
+
}
|
|
102
|
+
assertRegularFile(path) {
|
|
103
|
+
const stat = lstatSync(path);
|
|
104
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
105
|
+
throw new Error(`private credential must be a regular file: ${path}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function assertWindowsProtectedValue(value) {
|
|
110
|
+
const trimmed = value.trim();
|
|
111
|
+
if (!trimmed || trimmed.length > MAX_PROTECTED_VALUE_LENGTH || !WINDOWS_PROTECTED_VALUE_RE.test(trimmed)) {
|
|
112
|
+
throw new Error('stored private node credential ciphertext is invalid');
|
|
113
|
+
}
|
|
114
|
+
return trimmed;
|
|
115
|
+
}
|
|
116
|
+
function createWindowsDpapiProtector() {
|
|
117
|
+
const systemRoot = process.env['SystemRoot']?.trim();
|
|
118
|
+
if (!systemRoot || !isAbsolute(systemRoot)) {
|
|
119
|
+
throw new Error('Windows private credential persistence requires an absolute SystemRoot');
|
|
120
|
+
}
|
|
121
|
+
const executable = join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
122
|
+
let executableIsSafe = false;
|
|
123
|
+
try {
|
|
124
|
+
const executableStat = lstatSync(executable);
|
|
125
|
+
executableIsSafe = executableStat.isFile() && !executableStat.isSymbolicLink();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Normalize filesystem errors so local paths are not included in startup logs.
|
|
129
|
+
}
|
|
130
|
+
if (!executableIsSafe) {
|
|
131
|
+
throw new Error('Windows private credential persistence requires Windows PowerShell');
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
preflight: () => {
|
|
135
|
+
const canary = randomBytes(32).toString('hex');
|
|
136
|
+
if (runWindowsPowerShell(executable, WINDOWS_DPAPI_PREFLIGHT_SCRIPT, canary) !== canary) {
|
|
137
|
+
throw new Error('Windows private credential protection preflight failed');
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
protect: (secret) => runWindowsPowerShell(executable, WINDOWS_DPAPI_PROTECT_SCRIPT, secret),
|
|
141
|
+
unprotect: (protectedValue) => runWindowsPowerShell(executable, WINDOWS_DPAPI_UNPROTECT_SCRIPT, protectedValue),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function verifyWindowsProtector(protector) {
|
|
145
|
+
try {
|
|
146
|
+
if (protector.preflight) {
|
|
147
|
+
protector.preflight();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const canary = randomBytes(32).toString('hex');
|
|
151
|
+
const protectedCanary = assertWindowsProtectedValue(protector.protect(canary));
|
|
152
|
+
if (protector.unprotect(protectedCanary) !== canary) {
|
|
153
|
+
throw new Error('round trip mismatch');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
throw new Error('Windows private credential protection preflight failed');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function runWindowsPowerShell(executable, script, input) {
|
|
161
|
+
try {
|
|
162
|
+
return execFileSync(executable, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], {
|
|
163
|
+
encoding: 'utf8',
|
|
164
|
+
input,
|
|
165
|
+
maxBuffer: 64 * 1024,
|
|
166
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
167
|
+
timeout: 15_000,
|
|
168
|
+
windowsHide: true,
|
|
169
|
+
}).trim();
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
throw new Error('Windows private credential protection failed');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const WINDOWS_DPAPI_PREFLIGHT_SCRIPT = [
|
|
176
|
+
"$ErrorActionPreference = 'Stop'",
|
|
177
|
+
'Add-Type -AssemblyName System.Security',
|
|
178
|
+
'$plain = [Console]::In.ReadToEnd()',
|
|
179
|
+
'$bytes = [Text.Encoding]::UTF8.GetBytes($plain)',
|
|
180
|
+
'$scope = [Security.Cryptography.DataProtectionScope]::CurrentUser',
|
|
181
|
+
'$protected = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, $scope)',
|
|
182
|
+
'$restored = [Security.Cryptography.ProtectedData]::Unprotect($protected, $null, $scope)',
|
|
183
|
+
'[Console]::Out.Write([Text.Encoding]::UTF8.GetString($restored))',
|
|
184
|
+
].join('; ');
|
|
185
|
+
const WINDOWS_DPAPI_PROTECT_SCRIPT = [
|
|
186
|
+
"$ErrorActionPreference = 'Stop'",
|
|
187
|
+
'Add-Type -AssemblyName System.Security',
|
|
188
|
+
'$plain = [Console]::In.ReadToEnd()',
|
|
189
|
+
'$bytes = [Text.Encoding]::UTF8.GetBytes($plain)',
|
|
190
|
+
'$scope = [Security.Cryptography.DataProtectionScope]::CurrentUser',
|
|
191
|
+
'$protected = [Security.Cryptography.ProtectedData]::Protect($bytes, $null, $scope)',
|
|
192
|
+
'[Console]::Out.Write([Convert]::ToBase64String($protected))',
|
|
193
|
+
].join('; ');
|
|
194
|
+
const WINDOWS_DPAPI_UNPROTECT_SCRIPT = [
|
|
195
|
+
"$ErrorActionPreference = 'Stop'",
|
|
196
|
+
'Add-Type -AssemblyName System.Security',
|
|
197
|
+
'$protected = [Convert]::FromBase64String([Console]::In.ReadToEnd())',
|
|
198
|
+
'$scope = [Security.Cryptography.DataProtectionScope]::CurrentUser',
|
|
199
|
+
'$bytes = [Security.Cryptography.ProtectedData]::Unprotect($protected, $null, $scope)',
|
|
200
|
+
'[Console]::Out.Write([Text.Encoding]::UTF8.GetString($bytes))',
|
|
201
|
+
].join('; ');
|
|
202
|
+
function syncDirectory(path) {
|
|
203
|
+
const descriptor = openSync(path, 'r');
|
|
204
|
+
try {
|
|
205
|
+
fsyncSync(descriptor);
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
closeSync(descriptor);
|
|
209
|
+
}
|
|
210
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver-proxy",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.16",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "系统级 mailbox/hub 同步 daemon (Node)",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@aws-sdk/client-bedrock-runtime": "^3.1053.0",
|
|
29
|
-
"@evomap/evolver-adapter-public": "2.0.0-beta.
|
|
30
|
-
"@evomap/evolver-core": "2.0.0-beta.
|
|
29
|
+
"@evomap/evolver-adapter-public": "2.0.0-beta.16",
|
|
30
|
+
"@evomap/evolver-core": "2.0.0-beta.16"
|
|
31
31
|
},
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|