@evomap/evolver-proxy 2.0.0-beta.2 → 2.0.0-beta.22

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.
Files changed (69) hide show
  1. package/dist/bin/evolver-llm-proxy.js +0 -0
  2. package/dist/bin/evolver-proxy.d.ts +105 -7
  3. package/dist/bin/evolver-proxy.js +877 -121
  4. package/dist/daemon/atpConsent.js +5 -2
  5. package/dist/daemon/collaborationFacade.js +26 -16
  6. package/dist/daemon/proxyDaemon.d.ts +65 -0
  7. package/dist/daemon/proxyDaemon.js +1384 -29
  8. package/dist/daemon/publishRecallVerifier.d.ts +114 -0
  9. package/dist/daemon/publishRecallVerifier.js +495 -0
  10. package/dist/daemon/selectHub.js +5 -3
  11. package/dist/daemon/systemdNotifier.d.ts +48 -0
  12. package/dist/daemon/systemdNotifier.js +163 -0
  13. package/dist/index.d.ts +4 -1
  14. package/dist/index.js +4 -1
  15. package/dist/lifecycle/claimNudge.d.ts +20 -0
  16. package/dist/lifecycle/claimNudge.js +124 -0
  17. package/dist/lifecycle/legacyNodeId.d.ts +11 -13
  18. package/dist/lifecycle/legacyNodeId.js +35 -20
  19. package/dist/lifecycle/manager.d.ts +4 -0
  20. package/dist/lifecycle/manager.js +15 -2
  21. package/dist/llm/server.js +24 -4
  22. package/dist/llm/traceControl.js +1 -1
  23. package/dist/llm/upstream.d.ts +5 -1
  24. package/dist/llm/upstream.js +72 -2
  25. package/dist/private/accountAssetCompatibility.d.ts +29 -0
  26. package/dist/private/accountAssetCompatibility.js +196 -0
  27. package/dist/private/adapterLoader.d.ts +21 -1
  28. package/dist/private/adapterLoader.js +242 -7
  29. package/dist/private/nodeCredentialStore.d.ts +23 -0
  30. package/dist/private/nodeCredentialStore.js +210 -0
  31. package/dist/router/messagesRoute.js +9 -3
  32. package/dist/router/providerRoutes.js +7 -3
  33. package/dist/selfUpdate/bootstrap.d.ts +162 -0
  34. package/dist/selfUpdate/bootstrap.js +3524 -0
  35. package/dist/selfUpdate/bootstrapReadiness.d.ts +9 -0
  36. package/dist/selfUpdate/bootstrapReadiness.js +153 -0
  37. package/dist/selfUpdate/builtinKey.d.ts +4 -0
  38. package/dist/selfUpdate/builtinKey.js +16 -0
  39. package/dist/selfUpdate/controllerLifecycleAuthority.d.ts +45 -0
  40. package/dist/selfUpdate/controllerLifecycleAuthority.js +61 -0
  41. package/dist/selfUpdate/executor.d.ts +27 -11
  42. package/dist/selfUpdate/executor.js +233 -58
  43. package/dist/selfUpdate/failureCodes.d.ts +10 -0
  44. package/dist/selfUpdate/failureCodes.js +13 -0
  45. package/dist/selfUpdate/index.d.ts +5 -1
  46. package/dist/selfUpdate/index.js +5 -1
  47. package/dist/selfUpdate/lastUpdate.d.ts +3 -1
  48. package/dist/selfUpdate/lastUpdate.js +37 -6
  49. package/dist/selfUpdate/migration.d.ts +158 -0
  50. package/dist/selfUpdate/migration.js +2672 -0
  51. package/dist/selfUpdate/policy.d.ts +19 -2
  52. package/dist/selfUpdate/policy.js +76 -2
  53. package/dist/selfUpdate/recoveryChildStartGate.d.ts +29 -0
  54. package/dist/selfUpdate/recoveryChildStartGate.js +319 -0
  55. package/dist/selfUpdate/releaseBinary.d.ts +13 -0
  56. package/dist/selfUpdate/releaseBinary.js +93 -10
  57. package/dist/selfUpdate/transaction.d.ts +117 -0
  58. package/dist/selfUpdate/transaction.js +1322 -0
  59. package/dist/selfUpdate/unixController.d.ts +23 -0
  60. package/dist/selfUpdate/unixController.js +514 -0
  61. package/dist/selfUpdate/version.d.ts +6 -2
  62. package/dist/selfUpdate/version.js +5 -3
  63. package/dist/selfUpdate/windowsController.d.ts +35 -0
  64. package/dist/selfUpdate/windowsController.js +655 -0
  65. package/dist/selfUpdate/windowsUpdater.d.ts +104 -0
  66. package/dist/selfUpdate/windowsUpdater.js +882 -0
  67. package/dist/sync/engine.d.ts +12 -0
  68. package/dist/sync/engine.js +255 -64
  69. package/package.json +10 -3
@@ -32,6 +32,56 @@ async function loadDefaultBedrockRuntime() {
32
32
  function isOpenAiMode(upstreamMode) {
33
33
  return upstreamMode === 'openai';
34
34
  }
35
+ /**
36
+ * Detect deprecated OPENAI_COMPATIBLE_BASE_URLS env var (#671).
37
+ * V1 multi-base lists are ignored for routing; V2 OpenAI base must pass *.api.openai.com/v1 allowlist
38
+ * via EVOLVER_LLM_OPENAI_BASE_URL / EVOMAP_OPENAI_BASE_URL / OPENAI_BASE_URL.
39
+ * Warn once per process to avoid per-request spam.
40
+ */
41
+ let warnedDeprecatedOpenAICompatible = false;
42
+ function hasDeprecatedOpenAICompatibleConfig(env) {
43
+ return [
44
+ env['EVOLVER_OPENAI_COMPATIBLE_BASE_URLS'],
45
+ env['EVOMAP_OPENAI_COMPATIBLE_BASE_URLS'],
46
+ ].some((value) => typeof value === 'string' && value.trim() !== '');
47
+ }
48
+ function explicitOpenAIBaseUrl(env) {
49
+ return env['EVOLVER_LLM_OPENAI_BASE_URL'] || env['EVOMAP_OPENAI_BASE_URL'] || env['OPENAI_BASE_URL'];
50
+ }
51
+ function hasCanonicalOpenAIMigrationPair(env) {
52
+ const baseUrl = env['EVOLVER_LLM_OPENAI_BASE_URL']?.trim();
53
+ if (!baseUrl || !env['EVOLVER_LLM_OPENAI_API_KEY']?.trim())
54
+ return false;
55
+ try {
56
+ normalizeOpenAIBaseUrl(baseUrl);
57
+ return true;
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ function warnDeprecatedOpenAICompatible(env) {
64
+ if (!hasDeprecatedOpenAICompatibleConfig(env) || warnedDeprecatedOpenAICompatible)
65
+ return;
66
+ warnedDeprecatedOpenAICompatible = true;
67
+ console.warn('[proxy] DEPRECATED: EVOMAP_OPENAI_COMPATIBLE_BASE_URLS / EVOLVER_OPENAI_COMPATIBLE_BASE_URLS is ignored for routing. ' +
68
+ 'Manual migration is required: explicitly bind one official https://*.api.openai.com/v1 endpoint and its credential ' +
69
+ 'with EVOLVER_LLM_OPENAI_BASE_URL and EVOLVER_LLM_OPENAI_API_KEY. ' +
70
+ 'LiteLLM, OpenRouter, Azure OpenAI, MiniMax, DeepSeek, Moonshot, and other custom OpenAI-compatible hosts have no drop-in V2 route. ' +
71
+ 'Do not reuse their credentials as an OpenAI credential; keep that workload outside this proxy until a supported provider-specific route exists.');
72
+ }
73
+ function assertDeprecatedOpenAICompatibleMigrated(env) {
74
+ warnDeprecatedOpenAICompatible(env);
75
+ if (hasDeprecatedOpenAICompatibleConfig(env) && !hasCanonicalOpenAIMigrationPair(env)) {
76
+ throw new Error('[proxy] manual migration of deprecated OpenAI-compatible base URLs requires an explicit '
77
+ + 'EVOLVER_LLM_OPENAI_BASE_URL and '
78
+ + 'EVOLVER_LLM_OPENAI_API_KEY migration pair before the OpenAI route can start');
79
+ }
80
+ }
81
+ /** Test helper: reset once-warn latch (unit tests only). */
82
+ function resetDeprecatedOpenAICompatibleWarning() {
83
+ warnedDeprecatedOpenAICompatible = false;
84
+ }
35
85
  function isAllowedOpenAIHostname(hostname) {
36
86
  const h = hostname.toLowerCase();
37
87
  return h === 'api.openai.com' || h.endsWith('.api.openai.com');
@@ -56,8 +106,23 @@ function normalizeOpenAIBaseUrl(raw) {
56
106
  }
57
107
  return value;
58
108
  }
109
+ function openAIBaseUrlIdentity(raw) {
110
+ return new URL(normalizeOpenAIBaseUrl(raw)).href;
111
+ }
112
+ function assertDeprecatedOpenAIRequestBaseBound(env, requestBaseUrl) {
113
+ if (!requestBaseUrl || !hasDeprecatedOpenAICompatibleConfig(env))
114
+ return;
115
+ const canonicalBaseUrl = env['EVOLVER_LLM_OPENAI_BASE_URL'];
116
+ if (typeof canonicalBaseUrl !== 'string'
117
+ || openAIBaseUrlIdentity(requestBaseUrl) !== openAIBaseUrlIdentity(canonicalBaseUrl)) {
118
+ throw new Error('[proxy] request-scoped OpenAI base URL must match EVOLVER_LLM_OPENAI_BASE_URL '
119
+ + 'while deprecated OpenAI-compatible routing remains configured');
120
+ }
121
+ }
59
122
  export function resolveOpenAIUpstreamUrl(env = process.env) {
60
- return normalizeOpenAIBaseUrl(env['EVOLVER_LLM_OPENAI_BASE_URL'] || env['EVOMAP_OPENAI_BASE_URL'] || env['OPENAI_BASE_URL'] || DEFAULT_OPENAI_UPSTREAM_URL);
123
+ assertDeprecatedOpenAICompatibleMigrated(env);
124
+ const explicitBaseUrl = explicitOpenAIBaseUrl(env);
125
+ return normalizeOpenAIBaseUrl(explicitBaseUrl || DEFAULT_OPENAI_UPSTREAM_URL);
61
126
  }
62
127
  function pathForOpenAIBase(path) {
63
128
  if (path === '/v1')
@@ -456,6 +521,10 @@ export function makeOpenAIUpstream(opts = {}) {
456
521
  const headersTimeoutMs = opts.headersTimeoutMs ?? DEFAULT_HEADERS_TIMEOUT_MS;
457
522
  return async (path, body, callOpts) => {
458
523
  const env = opts.env ?? process.env;
524
+ // A request-scoped base override is routing data, not evidence that the daemon's
525
+ // legacy provider credential/base pair was deliberately migrated.
526
+ assertDeprecatedOpenAICompatibleMigrated(env);
527
+ assertDeprecatedOpenAIRequestBaseBound(env, callOpts.baseUrl);
459
528
  const baseUrl = callOpts.baseUrl ? normalizeOpenAIBaseUrl(callOpts.baseUrl) : resolveOpenAIUpstreamUrl(env);
460
529
  return fetchUpstream(`${baseUrl}${path}`, body, callOpts, buildOpenAIHeaders(callOpts.inboundHeaders, env), fetchImpl, headersTimeoutMs, 'openai', (headers) => contentTypeIncludes(headers, 'text/event-stream'));
461
530
  };
@@ -488,4 +557,5 @@ export function makeVertexUpstream(opts = {}) {
488
557
  const env = opts.env ?? process.env;
489
558
  return fetchUpstream(`${baseUrl}${path}`, body, callOpts, buildVertexHeaders(env), fetchImpl, headersTimeoutMs, 'vertex', (headers, endpoint) => contentTypeIncludes(headers, 'text/event-stream') || /:streamGenerateContent(\b|\?|$)/.test(endpoint));
490
559
  };
491
- }
560
+ }
561
+ export { warnDeprecatedOpenAICompatible, resetDeprecatedOpenAICompatibleWarning };
@@ -0,0 +1,29 @@
1
+ import type { hub } from '@evomap/evolver-core';
2
+ import { type AccountAssetListOptions, type AccountAssetListResult } from '@evomap/evolver-adapter-public';
3
+ export interface PrivateAccountAssetHub {
4
+ listAccountAssets(opts: AccountAssetListOptions): Promise<AccountAssetListResult>;
5
+ }
6
+ interface PrivateCompatibilityResponse {
7
+ status: number;
8
+ json(): Promise<unknown>;
9
+ }
10
+ export type PrivateCompatibilityFetch = (url: string, init: {
11
+ method: string;
12
+ headers: Record<string, string>;
13
+ body?: string;
14
+ }) => Promise<PrivateCompatibilityResponse>;
15
+ interface PrivateAccountAssetCompatibilityOptions {
16
+ baseUrl: string;
17
+ auth: hub.AuthProvider;
18
+ senderId: () => string | undefined;
19
+ env: Record<string, string | undefined>;
20
+ fetchFn?: PrivateCompatibilityFetch;
21
+ }
22
+ /**
23
+ * Older official private adapters predate account inventory listing. Keep the
24
+ * compatibility wire at the private composition edge, and never replace a
25
+ * future adapter's native implementation.
26
+ */
27
+ export declare function withPrivateAccountAssetCompatibility<T extends object>(hubCapability: T, opts: PrivateAccountAssetCompatibilityOptions): T & PrivateAccountAssetHub;
28
+ export declare function normalizePrivateHubBaseUrl(raw: string, env: Record<string, string | undefined>): string;
29
+ export {};
@@ -0,0 +1,196 @@
1
+ import { AuthError, HubClientError, HubUnreachableError, } from '@evomap/evolver-adapter-public';
2
+ const PRIVATE_PUBLISHED_ASSETS_PATH = '/a2a/assets/published-by-me';
3
+ const PRIVATE_PUBLISHED_MAX_PAGE_SIZE = 500;
4
+ const PRIVATE_CURSOR_MAX_LENGTH = 4096;
5
+ /**
6
+ * Older official private adapters predate account inventory listing. Keep the
7
+ * compatibility wire at the private composition edge, and never replace a
8
+ * future adapter's native implementation.
9
+ */
10
+ export function withPrivateAccountAssetCompatibility(hubCapability, opts) {
11
+ const candidate = hubCapability;
12
+ if (typeof candidate['listAccountAssets'] === 'function') {
13
+ return hubCapability;
14
+ }
15
+ if (candidate['listAccountAssets'] !== undefined) {
16
+ throw new Error('private Hub adapter exposes an invalid account asset sync capability');
17
+ }
18
+ const client = new PrivateAccountAssetCompatibility(opts);
19
+ try {
20
+ Object.defineProperty(hubCapability, 'listAccountAssets', {
21
+ configurable: false,
22
+ enumerable: false,
23
+ writable: false,
24
+ value: (listOpts) => client.list(listOpts),
25
+ });
26
+ }
27
+ catch {
28
+ throw new Error('private Hub adapter cannot be extended with account asset sync compatibility');
29
+ }
30
+ return hubCapability;
31
+ }
32
+ class PrivateAccountAssetCompatibility {
33
+ opts;
34
+ baseUrl;
35
+ fetchFn;
36
+ constructor(opts) {
37
+ this.opts = opts;
38
+ this.baseUrl = normalizePrivateHubBaseUrl(opts.baseUrl, opts.env);
39
+ this.fetchFn = opts.fetchFn ?? globalPrivateCompatibilityFetch;
40
+ }
41
+ async list(opts) {
42
+ assertAccountAssetListOptions(opts);
43
+ if (opts.scope === 'purchased') {
44
+ // Private Hub has no marketplace/purchase ledger. An empty inventory is
45
+ // distinct from published assets and avoids importing arbitrary recall hits.
46
+ if (opts.cursor)
47
+ throw new HubClientError(400, { code: 'private_marketplace_cursor_unsupported' });
48
+ return { assets: [], count: 0, hasMore: false };
49
+ }
50
+ const limit = Math.min(opts.limit ?? 100, PRIVATE_PUBLISHED_MAX_PAGE_SIZE);
51
+ const query = new URLSearchParams({ limit: String(limit) });
52
+ const senderId = this.opts.senderId()?.trim();
53
+ if (senderId)
54
+ query.set('sender_id', senderId);
55
+ if (opts.cursor)
56
+ query.set('cursor', opts.cursor);
57
+ if (opts.type)
58
+ query.set('type', opts.type);
59
+ if (opts.status && opts.status !== 'all')
60
+ query.set('status', opts.status);
61
+ const signed = await this.opts.auth.authenticate({ method: 'GET', path: PRIVATE_PUBLISHED_ASSETS_PATH });
62
+ const headers = accountAssetHeaders(signed);
63
+ const url = `${this.baseUrl}${PRIVATE_PUBLISHED_ASSETS_PATH}?${query.toString()}`;
64
+ assertPrivateCompatibilityUrlSecure(url, this.opts.env);
65
+ let response;
66
+ try {
67
+ response = await this.fetchFn(url, { method: 'GET', headers });
68
+ }
69
+ catch (error) {
70
+ if (error instanceof AuthError || error instanceof HubClientError || error instanceof HubUnreachableError)
71
+ throw error;
72
+ throw new HubUnreachableError('Private Hub account asset request failed before a response arrived', {
73
+ context: `GET ${PRIVATE_PUBLISHED_ASSETS_PATH}`,
74
+ });
75
+ }
76
+ const body = await parseCompatibilityResponse(response);
77
+ if (response.status === 401 || response.status === 403)
78
+ throw new AuthError(response.status, body);
79
+ if (response.status >= 400 && response.status < 500)
80
+ throw new HubClientError(response.status, body);
81
+ if (response.status >= 500)
82
+ throw new Error(`private hub ${response.status} ${PRIVATE_PUBLISHED_ASSETS_PATH}`);
83
+ return parsePublishedPage(body, limit);
84
+ }
85
+ }
86
+ function assertAccountAssetListOptions(opts) {
87
+ if (!opts || (opts.scope !== 'purchased' && opts.scope !== 'published')) {
88
+ throw new HubClientError(400, { code: 'invalid_account_asset_scope' });
89
+ }
90
+ if (opts.limit !== undefined && (!Number.isSafeInteger(opts.limit) || opts.limit <= 0)) {
91
+ throw new HubClientError(400, { code: 'invalid_account_asset_limit' });
92
+ }
93
+ if (opts.cursor !== undefined && (!opts.cursor.trim() || opts.cursor.length > PRIVATE_CURSOR_MAX_LENGTH)) {
94
+ throw new HubClientError(400, { code: 'invalid_account_asset_cursor' });
95
+ }
96
+ if (opts.type !== undefined && opts.type !== 'Gene' && opts.type !== 'Capsule') {
97
+ throw new HubClientError(400, { code: 'invalid_account_asset_type' });
98
+ }
99
+ if (opts.status !== undefined && opts.status !== 'draft' && opts.status !== 'promoted' && opts.status !== 'all') {
100
+ throw new HubClientError(400, { code: 'invalid_account_asset_status' });
101
+ }
102
+ }
103
+ function accountAssetHeaders(signed) {
104
+ const headers = { accept: 'application/json', ...signed.headers };
105
+ const hasAuthorization = Object.keys(headers).some((key) => key.toLowerCase() === 'authorization');
106
+ const bodyNodeSecret = signed.bodyFields?.['node_secret'];
107
+ if (!hasAuthorization && typeof bodyNodeSecret === 'string' && bodyNodeSecret) {
108
+ headers['authorization'] = `Bearer ${bodyNodeSecret}`;
109
+ }
110
+ return headers;
111
+ }
112
+ async function parseCompatibilityResponse(response) {
113
+ if (!Number.isInteger(response.status) || response.status < 100 || response.status > 599) {
114
+ throw new HubUnreachableError('Private Hub account asset response has an invalid HTTP status', {
115
+ context: `GET ${PRIVATE_PUBLISHED_ASSETS_PATH}`,
116
+ });
117
+ }
118
+ try {
119
+ const parsed = await response.json();
120
+ const record = asRecord(parsed);
121
+ if (record)
122
+ return record;
123
+ }
124
+ catch {
125
+ // The normalized error below intentionally excludes response data.
126
+ }
127
+ throw new HubUnreachableError('Private Hub account asset response is not a JSON object', {
128
+ status: response.status,
129
+ context: `GET ${PRIVATE_PUBLISHED_ASSETS_PATH}`,
130
+ });
131
+ }
132
+ function parsePublishedPage(body, limit) {
133
+ const payload = asRecord(body['payload']) ?? body;
134
+ const assets = payload['assets'];
135
+ const hasMore = payload['has_more'] ?? payload['hasMore'];
136
+ const rawCursor = payload['next_cursor'] ?? payload['nextCursor'];
137
+ const count = payload['count'];
138
+ if (!Array.isArray(assets) || assets.length > limit || typeof hasMore !== 'boolean') {
139
+ throw malformedPublishedPage();
140
+ }
141
+ if (count !== undefined && (!Number.isSafeInteger(count) || count < 0)) {
142
+ throw malformedPublishedPage();
143
+ }
144
+ const nextCursor = rawCursor === null || rawCursor === undefined ? undefined : rawCursor;
145
+ if (nextCursor !== undefined && (typeof nextCursor !== 'string' || !nextCursor.trim() || nextCursor.length > PRIVATE_CURSOR_MAX_LENGTH)) {
146
+ throw malformedPublishedPage();
147
+ }
148
+ if (hasMore && nextCursor === undefined)
149
+ throw malformedPublishedPage();
150
+ return {
151
+ assets: assets,
152
+ ...(count !== undefined ? { count: count } : {}),
153
+ hasMore,
154
+ ...(nextCursor !== undefined ? { nextCursor } : {}),
155
+ };
156
+ }
157
+ function malformedPublishedPage() {
158
+ return new HubUnreachableError('Private Hub returned a malformed published asset page', {
159
+ context: `GET ${PRIVATE_PUBLISHED_ASSETS_PATH}`,
160
+ });
161
+ }
162
+ export function normalizePrivateHubBaseUrl(raw, env) {
163
+ const normalized = raw.trim().replace(/\/+$/, '');
164
+ assertPrivateCompatibilityUrlSecure(normalized, env);
165
+ const parsed = new URL(normalized);
166
+ if (parsed.username || parsed.password || normalized.includes('?') || normalized.includes('#')) {
167
+ throw new Error('Private Hub URL must not contain credentials, query parameters, or a fragment');
168
+ }
169
+ return parsed.href.replace(/[/]+$/, '');
170
+ }
171
+ function assertPrivateCompatibilityUrlSecure(url, env) {
172
+ let parsed;
173
+ try {
174
+ parsed = new URL(url);
175
+ }
176
+ catch {
177
+ throw new Error('Private Hub URL is invalid');
178
+ }
179
+ if (parsed.protocol === 'https:')
180
+ return;
181
+ if (parsed.protocol === 'http:' && env['EVOLVER_PRIVATE_ALLOW_INSECURE'] === '1')
182
+ return;
183
+ throw new Error('Private Hub URL must use https');
184
+ }
185
+ const globalPrivateCompatibilityFetch = async (url, init) => {
186
+ const response = await fetch(url, {
187
+ method: init.method,
188
+ headers: init.headers,
189
+ ...(init.body ? { body: init.body } : {}),
190
+ redirect: 'error',
191
+ });
192
+ return { status: response.status, json: () => response.json() };
193
+ };
194
+ function asRecord(value) {
195
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
196
+ }
@@ -1,5 +1,6 @@
1
1
  import { hub as hubNs } from '@evomap/evolver-core';
2
2
  import type { HelloResult, HeartbeatOptions, HeartbeatResult } from '../lifecycle/manager.js';
3
+ import { type PrivateAccountAssetHub, type PrivateCompatibilityFetch } from './accountAssetCompatibility.js';
3
4
  export type PrivateHubWithLifecycle = hubNs.HubCapability & {
4
5
  hello(opts: {
5
6
  rotate: boolean;
@@ -7,6 +8,7 @@ export type PrivateHubWithLifecycle = hubNs.HubCapability & {
7
8
  }): Promise<HelloResult>;
8
9
  heartbeat(opts?: HeartbeatOptions): Promise<HeartbeatResult>;
9
10
  };
11
+ export type PrivateProxyHub = PrivateHubWithLifecycle & PrivateAccountAssetHub;
10
12
  interface PrivateSsoExchange {
11
13
  identity: () => {
12
14
  subject: string;
@@ -29,11 +31,19 @@ export interface ConnectPrivateHubOptions {
29
31
  now?: () => number;
30
32
  /** One-shot invitation token (evoinv_…) — preferred over the SSO bearer for token_required hubs. */
31
33
  invitationToken?: string;
34
+ /** Ready credential from the standard Private Hub onboarding store. */
35
+ nodeSecret?: string;
36
+ fetchFn?: PrivateCompatibilityFetch;
32
37
  }
33
38
  type DynamicImporter = (specifier: string) => Promise<unknown>;
34
39
  export interface PrivateProxyHubRuntime {
35
- hub: PrivateHubWithLifecycle;
40
+ hub: PrivateProxyHub;
36
41
  auth: hubNs.AuthProvider;
42
+ /** Enrollment-aware lifecycle entrypoint. Ready node_secret credentials must not re-run hello. */
43
+ hello(opts: {
44
+ rotate: boolean;
45
+ evolverVersion?: string;
46
+ }): Promise<HelloResult>;
37
47
  }
38
48
  export interface ConnectPrivateProxyHubOptions {
39
49
  hubUrl: string;
@@ -41,11 +51,21 @@ export interface ConnectPrivateProxyHubOptions {
41
51
  env: Record<string, string | undefined>;
42
52
  now?: () => number;
43
53
  importer?: DynamicImporter;
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;
44
63
  }
45
64
  export declare function resolvePrivateEnterpriseToken(env: Record<string, string | undefined>): string | undefined;
46
65
  /** One-shot invitation token (evoinv_…), matching the hub's official onboarding script (A2A_INVITATION_TOKEN).
47
66
  * Preferred over the enterprise token for the default token_required enrollment mode. */
48
67
  export declare function resolvePrivateInvitationToken(env: Record<string, string | undefined>): string | undefined;
68
+ export declare function resolvePrivateNodeSecret(env: Record<string, string | undefined>): string | undefined;
49
69
  export declare function resolvePrivateEnterpriseSubject(env: Record<string, string | undefined>): string;
50
70
  export declare function connectPrivateProxyHub(opts: ConnectPrivateProxyHubOptions): Promise<PrivateProxyHubRuntime>;
51
71
  export {};
@@ -1,4 +1,6 @@
1
1
  import { hub as hubNs } from '@evomap/evolver-core';
2
+ import { createHash } from 'node:crypto';
3
+ import { normalizePrivateHubBaseUrl, withPrivateAccountAssetCompatibility, } from './accountAssetCompatibility.js';
2
4
  const DEFAULT_PRIVATE_ADAPTER_MODULE = '@evomap/evolver-adapter-private';
3
5
  export function resolvePrivateEnterpriseToken(env) {
4
6
  return firstEnv(env, 'EVOMAP_ENTERPRISE_TOKEN', 'EVOMAP_PRIVATE_HUB_TOKEN', 'PHUB_ENTERPRISE_TOKEN', 'PRIVATE_HUB_ENTERPRISE_TOKEN');
@@ -8,36 +10,227 @@ export function resolvePrivateEnterpriseToken(env) {
8
10
  export function resolvePrivateInvitationToken(env) {
9
11
  return firstEnv(env, 'A2A_INVITATION_TOKEN');
10
12
  }
13
+ export function resolvePrivateNodeSecret(env) {
14
+ return firstEnv(env, 'EVOMAP_NODE_SECRET', 'A2A_NODE_SECRET');
15
+ }
11
16
  export function resolvePrivateEnterpriseSubject(env) {
12
17
  return firstEnv(env, 'EVOMAP_ENTERPRISE_SUBJECT', 'EVOMAP_PRIVATE_SUBJECT', 'PHUB_ENTERPRISE_SUBJECT', 'USER') ?? 'evolver-proxy';
13
18
  }
14
19
  export async function connectPrivateProxyHub(opts) {
15
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;
16
27
  const token = resolvePrivateEnterpriseToken(opts.env);
17
- if (!token && !invitationToken) {
18
- throw new Error('EVOMAP_HUB_MODE=private 需要 A2A_INVITATION_TOKEN(推荐,对齐 PrivateHub onboarding)或 EVOMAP_ENTERPRISE_TOKEN(也兼容 EVOMAP_PRIVATE_HUB_TOKEN / PHUB_ENTERPRISE_TOKEN)');
28
+ const configuredNodeSecret = resolvePrivateNodeSecret(opts.env);
29
+ const nodeSecret = configuredNodeSecret ?? opts.storedNodeSecret?.trim();
30
+ if (nodeSecret && !isNodeSecret(nodeSecret)) {
31
+ throw new Error('Private Hub node_secret 必须是 64 位十六进制字符串');
32
+ }
33
+ if (!token && !usableInvitationToken && !nodeSecret) {
34
+ throw new Error('EVOMAP_HUB_MODE=private 需要 A2A_NODE_SECRET / EVOMAP_NODE_SECRET、A2A_INVITATION_TOKEN 或 EVOMAP_ENTERPRISE_TOKEN');
19
35
  }
36
+ const hubUrl = normalizePrivateHubBaseUrl(opts.hubUrl, opts.env);
20
37
  const moduleName = opts.env['EVOMAP_PRIVATE_ADAPTER_MODULE']?.trim() || DEFAULT_PRIVATE_ADAPTER_MODULE;
21
38
  const connectPrivateHub = await loadConnectPrivateHub(moduleName, opts.importer ?? ((specifier) => import(specifier)));
22
39
  const now = opts.now ?? (() => Date.now());
23
40
  const subject = resolvePrivateEnterpriseSubject(opts.env);
24
- const { hub, auth } = connectPrivateHub({
25
- hubUrl: opts.hubUrl,
41
+ const baseConnectionOptions = {
42
+ hubUrl,
26
43
  senderId: opts.senderId,
27
44
  env: opts.env,
28
45
  now,
29
46
  sso: {
30
47
  identity: () => ({ subject }),
31
- exchange: async () => ({ token: token ?? '' }),
48
+ exchange: async () => ({ token: token ?? nodeSecret ?? '' }),
32
49
  now,
33
50
  },
34
- ...(invitationToken ? { invitationToken } : {}),
51
+ ...(opts.fetchFn ? { fetchFn: opts.fetchFn } : {}),
52
+ };
53
+ const primaryInvitationToken = !nodeSecret && !configuredNodeSecret ? usableInvitationToken : undefined;
54
+ const { hub, auth } = connectPrivateHub({
55
+ ...baseConnectionOptions,
56
+ ...(nodeSecret ? { nodeSecret } : {}),
57
+ ...(primaryInvitationToken ? { invitationToken: primaryInvitationToken } : {}),
35
58
  });
36
59
  assertPrivateLifecycle(hub, moduleName);
60
+ const storedInvitationFallback = nodeSecret && !configuredNodeSecret && usableInvitationToken
61
+ ? connectPrivateHub({ ...baseConnectionOptions, invitationToken: usableInvitationToken })
62
+ : undefined;
63
+ const enterpriseFallback = primaryInvitationToken && token
64
+ ? connectPrivateHub(baseConnectionOptions)
65
+ : undefined;
66
+ if (storedInvitationFallback)
67
+ assertPrivateLifecycle(storedInvitationFallback.hub, moduleName);
68
+ if (enterpriseFallback)
69
+ assertPrivateLifecycle(enterpriseFallback.hub, moduleName);
70
+ if (nodeSecret)
71
+ await adoptReadyNodeSecret(auth, nodeSecret);
72
+ const primaryAdoptions = trackNodeSecretAdoptions(auth);
73
+ const storedInvitationAdoptions = storedInvitationFallback
74
+ ? trackNodeSecretAdoptions(storedInvitationFallback.auth)
75
+ : undefined;
76
+ const enterpriseAdoptions = enterpriseFallback
77
+ ? trackNodeSecretAdoptions(enterpriseFallback.auth)
78
+ : undefined;
37
79
  if (!hub.agentDirectory) {
38
80
  hub.agentDirectory = hubNs.unsupportedAgentDirectoryCapability('private_hub_agent_directory_not_supported');
39
81
  }
40
- return { hub, auth };
82
+ const compatibleHub = withPrivateAccountAssetCompatibility(hub, {
83
+ baseUrl: hubUrl,
84
+ auth,
85
+ senderId: opts.senderId,
86
+ env: opts.env,
87
+ ...(opts.fetchFn ? { fetchFn: opts.fetchFn } : {}),
88
+ });
89
+ let readyHelloPending = Boolean(nodeSecret);
90
+ let invitationRedeemed = false;
91
+ let enterpriseFallbackSelected = false;
92
+ let helloInProgress = false;
93
+ const helloWithFallback = async (fallback, adoptions, helloOpts, rotate) => {
94
+ if (!adoptions?.available) {
95
+ throw new Error('private Hub adapter cannot report node_secret adoption');
96
+ }
97
+ if (rotate)
98
+ await fallback.auth.rotate();
99
+ const { result, adoptedNodeSecret } = await adoptions.capture(() => fallback.hub.hello(helloOpts));
100
+ if (!result.ok)
101
+ return result;
102
+ if (!adoptedNodeSecret)
103
+ throw new Error('private Hub fallback did not adopt a node_secret');
104
+ await adoptReadyNodeSecret(fallback.auth, adoptedNodeSecret);
105
+ await adoptReadyNodeSecret(auth, adoptedNodeSecret);
106
+ opts.onNodeSecretAdopted?.(adoptedNodeSecret);
107
+ return result;
108
+ };
109
+ const helloOnce = async (helloOpts) => {
110
+ if (enterpriseFallbackSelected || invitationRedeemed) {
111
+ if (!enterpriseFallback)
112
+ return { ok: false, error: 'private_invitation_reenrollment_required' };
113
+ return helloWithFallback(enterpriseFallback, enterpriseAdoptions, helloOpts, true);
114
+ }
115
+ if (primaryInvitationToken && !primaryAdoptions.available) {
116
+ throw new Error('private Hub adapter cannot report node_secret adoption');
117
+ }
118
+ let result;
119
+ let adoptedNodeSecret;
120
+ let usedAdapterHello = false;
121
+ if (nodeSecret && readyHelloPending) {
122
+ // A stored credential gets one ready probe before a fresh invitation takes over.
123
+ // Clear this before awaiting so a thrown probe cannot starve the fallback forever.
124
+ if (storedInvitationFallback)
125
+ readyHelloPending = false;
126
+ const tracked = await primaryAdoptions.capture(() => helloWithReadyPrivateCredential(compatibleHub, auth, nodeSecret, opts.senderId, helloOpts, primaryAdoptions.available));
127
+ readyHelloPending = false;
128
+ result = tracked.result;
129
+ adoptedNodeSecret = tracked.adoptedNodeSecret;
130
+ if (result.ok && !adoptedNodeSecret && !await authenticatesWithNodeSecret(auth, nodeSecret)) {
131
+ throw new Error('private Hub ready credential hello did not preserve an active node_secret');
132
+ }
133
+ }
134
+ else if (storedInvitationFallback) {
135
+ result = await helloWithFallback(storedInvitationFallback, storedInvitationAdoptions, helloOpts, false);
136
+ if (result.ok) {
137
+ invitationRedeemed = true;
138
+ if (invitationFingerprint)
139
+ opts.onInvitationRedeemed?.(invitationFingerprint);
140
+ }
141
+ return result;
142
+ }
143
+ else {
144
+ usedAdapterHello = true;
145
+ const tracked = await primaryAdoptions.capture(() => compatibleHub.hello(helloOpts));
146
+ result = tracked.result;
147
+ adoptedNodeSecret = tracked.adoptedNodeSecret;
148
+ }
149
+ if (!result.ok && usedAdapterHello && primaryInvitationToken && enterpriseFallback) {
150
+ const fallbackResult = await helloWithFallback(enterpriseFallback, enterpriseAdoptions, helloOpts, true);
151
+ if (fallbackResult.ok)
152
+ enterpriseFallbackSelected = true;
153
+ return fallbackResult;
154
+ }
155
+ if (result.ok) {
156
+ if (adoptedNodeSecret && adoptedNodeSecret !== nodeSecret) {
157
+ await adoptReadyNodeSecret(auth, adoptedNodeSecret);
158
+ opts.onNodeSecretAdopted?.(adoptedNodeSecret);
159
+ }
160
+ if (usedAdapterHello && primaryInvitationToken) {
161
+ if (!adoptedNodeSecret) {
162
+ throw new Error('private Hub enrollment did not adopt a node_secret');
163
+ }
164
+ invitationRedeemed = true;
165
+ if (invitationFingerprint)
166
+ opts.onInvitationRedeemed?.(invitationFingerprint);
167
+ }
168
+ }
169
+ return result;
170
+ };
171
+ const hello = async (helloOpts) => {
172
+ if (helloInProgress)
173
+ throw new Error('concurrent private Hub hello is not supported');
174
+ helloInProgress = true;
175
+ try {
176
+ return await helloOnce(helloOpts);
177
+ }
178
+ finally {
179
+ helloInProgress = false;
180
+ }
181
+ };
182
+ return {
183
+ hub: compatibleHub,
184
+ auth,
185
+ hello,
186
+ };
187
+ }
188
+ const NODE_SECRET_ADOPTION_TRACKERS = new WeakMap();
189
+ function trackNodeSecretAdoptions(auth) {
190
+ const existing = NODE_SECRET_ADOPTION_TRACKERS.get(auth);
191
+ if (existing)
192
+ return existing;
193
+ const candidate = auth;
194
+ const original = candidate.adoptNodeSecret;
195
+ let capturing = false;
196
+ let available = false;
197
+ let adoptedNodeSecret;
198
+ if (typeof original === 'function') {
199
+ const wrapped = function (nodeSecret) {
200
+ original.call(this, nodeSecret);
201
+ if (capturing)
202
+ adoptedNodeSecret = isNodeSecret(nodeSecret) ? nodeSecret : undefined;
203
+ };
204
+ try {
205
+ candidate.adoptNodeSecret = wrapped;
206
+ available = candidate.adoptNodeSecret === wrapped;
207
+ }
208
+ catch {
209
+ // A frozen adapter cannot expose reliable adoption provenance. Enrollment will fail closed below.
210
+ }
211
+ }
212
+ const tracker = {
213
+ get available() { return available; },
214
+ capture: async (operation) => {
215
+ if (capturing)
216
+ throw new Error('concurrent private Hub credential adoption is not supported');
217
+ capturing = true;
218
+ adoptedNodeSecret = undefined;
219
+ try {
220
+ const result = await operation();
221
+ return {
222
+ result,
223
+ ...(adoptedNodeSecret ? { adoptedNodeSecret } : {}),
224
+ };
225
+ }
226
+ finally {
227
+ capturing = false;
228
+ adoptedNodeSecret = undefined;
229
+ }
230
+ },
231
+ };
232
+ NODE_SECRET_ADOPTION_TRACKERS.set(auth, tracker);
233
+ return tracker;
41
234
  }
42
235
  async function loadConnectPrivateHub(moduleName, importer) {
43
236
  let loaded;
@@ -60,6 +253,48 @@ function assertPrivateLifecycle(hub, moduleName) {
60
253
  throw new Error(`${moduleName} 的 hub 缺少 hello/heartbeat lifecycle 方法,无法接入 evolver-proxy`);
61
254
  }
62
255
  }
256
+ async function adoptReadyNodeSecret(auth, nodeSecret) {
257
+ const candidate = auth;
258
+ if (typeof candidate.adoptNodeSecret === 'function') {
259
+ candidate.adoptNodeSecret.call(auth, nodeSecret);
260
+ }
261
+ if (!await authenticatesWithNodeSecret(auth, nodeSecret)) {
262
+ throw new Error('private Hub adapter cannot activate the configured node_secret');
263
+ }
264
+ }
265
+ async function helloWithReadyPrivateCredential(hub, auth, nodeSecret, senderId, opts, canTrackAdoption = true) {
266
+ const nodeId = trimmedSenderId(senderId);
267
+ if (nodeId && await authenticatesWithNodeSecret(auth, nodeSecret))
268
+ return readyPrivateHello(nodeId);
269
+ if (!canTrackAdoption)
270
+ throw new Error('private Hub adapter cannot report node_secret adoption');
271
+ return await hub.hello(opts);
272
+ }
273
+ async function authenticatesWithNodeSecret(auth, nodeSecret) {
274
+ const signed = await auth.authenticate({ method: 'GET', path: '/a2a/assets/published-by-me' });
275
+ const authorization = headerValue(signed.headers, 'authorization');
276
+ const bodySecret = signed.bodyFields?.['node_secret'];
277
+ return authorization !== undefined
278
+ ? authorization === `Bearer ${nodeSecret}`
279
+ : bodySecret === nodeSecret;
280
+ }
281
+ function headerValue(headers, name) {
282
+ const lower = name.toLowerCase();
283
+ for (const [key, value] of Object.entries(headers)) {
284
+ if (key.toLowerCase() === lower)
285
+ return value;
286
+ }
287
+ return undefined;
288
+ }
289
+ function trimmedSenderId(senderId) {
290
+ return senderId()?.trim() || undefined;
291
+ }
292
+ function readyPrivateHello(nodeId) {
293
+ return { ok: true, nodeId };
294
+ }
295
+ function isNodeSecret(value) {
296
+ return /^[a-f0-9]{64}$/i.test(value);
297
+ }
63
298
  function firstEnv(env, ...keys) {
64
299
  for (const key of keys) {
65
300
  const value = env[key]?.trim();