@evomap/evolver-adapter-public 2.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/antiAbuseTelemetry.d.ts +36 -0
- package/dist/antiAbuseTelemetry.js +267 -0
- package/dist/atp.d.ts +54 -0
- package/dist/atp.js +140 -0
- package/dist/auth/credentialStore.d.ts +8 -0
- package/dist/auth/credentialStore.js +23 -0
- package/dist/auth/keypair.d.ts +42 -0
- package/dist/auth/keypair.js +80 -0
- package/dist/auth/legacyShim.d.ts +43 -0
- package/dist/auth/legacyShim.js +82 -0
- package/dist/auth/machineId.d.ts +20 -0
- package/dist/auth/machineId.js +38 -0
- package/dist/auth/oauthDeviceToken.d.ts +62 -0
- package/dist/auth/oauthDeviceToken.js +83 -0
- package/dist/auth/oauthHttpTransport.d.ts +33 -0
- package/dist/auth/oauthHttpTransport.js +93 -0
- package/dist/connect.d.ts +40 -0
- package/dist/connect.js +38 -0
- package/dist/hubCapability.d.ts +169 -0
- package/dist/hubCapability.js +899 -0
- package/dist/hubFetch.d.ts +116 -0
- package/dist/hubFetch.js +469 -0
- package/dist/hubReuse.d.ts +112 -0
- package/dist/hubReuse.js +292 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/offlinePermit.d.ts +74 -0
- package/dist/offlinePermit.js +309 -0
- package/dist/pricing/modelPrices.d.ts +16 -0
- package/dist/pricing/modelPrices.js +44 -0
- package/dist/wireMap.d.ts +28 -0
- package/dist/wireMap.js +99 -0
- package/package.json +29 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { hub } from '@evomap/evolver-core';
|
|
2
|
+
export declare function isNodeSecret(value: string): boolean;
|
|
3
|
+
export declare function parseNodeSecretVersion(value: unknown): number | undefined;
|
|
4
|
+
export type NodeSecretRotationHandler = (nodeSecret: string, nodeSecretVersion?: number) => void;
|
|
5
|
+
export type NodeSecretVersionHandler = (nodeSecretVersion?: number) => void;
|
|
6
|
+
/**
|
|
7
|
+
* Fired when the hub HTTP-200s an app-level rejection of our cached node_secret
|
|
8
|
+
* (reason ∈ {node_secret_invalid, rotation_requires_current_secret, invalid_secret}).
|
|
9
|
+
* The locally-cached secret has DIVERGED from the hub's record (hub-side reset,
|
|
10
|
+
* restored-from-backup machine, manual unlink). The handler owns clearing the
|
|
11
|
+
* durable copies (store keys + on-disk legacy files) so the next start re-acquires
|
|
12
|
+
* cleanly via an unauthenticated hello. Mirrors v1 a2aProtocol.js divergence recovery.
|
|
13
|
+
*/
|
|
14
|
+
export type NodeSecretDivergenceHandler = () => void;
|
|
15
|
+
/**
|
|
16
|
+
* node_secret 双轨过渡(M6-5, 6 个月). 包旧 64-hex node_secret 成 AuthProvider.
|
|
17
|
+
* **node_secret 走 request body**(gep-a2a 契约, 实测 dev: requireNodeSecret 读 body 非 header), 不轮换.
|
|
18
|
+
*/
|
|
19
|
+
export declare class LegacyAuthShim implements hub.AuthProvider {
|
|
20
|
+
private nodeSecret;
|
|
21
|
+
private readonly onRotate?;
|
|
22
|
+
private readonly onVersionUpdate?;
|
|
23
|
+
private readonly onDiverged?;
|
|
24
|
+
readonly kind: "oauth_device_token";
|
|
25
|
+
private nodeSecretVersion?;
|
|
26
|
+
constructor(nodeSecret: string | undefined, onRotate?: NodeSecretRotationHandler | undefined, initialNodeSecretVersion?: number, onVersionUpdate?: NodeSecretVersionHandler | undefined, onDiverged?: NodeSecretDivergenceHandler | undefined);
|
|
27
|
+
login(): Promise<hub.Credential>;
|
|
28
|
+
authenticate(): Promise<hub.SignedRequest>;
|
|
29
|
+
rotate(): Promise<hub.Credential>;
|
|
30
|
+
revoke(): Promise<void>;
|
|
31
|
+
getNodeSecretVersion(): number | undefined;
|
|
32
|
+
adoptNodeSecret(nodeSecret: string, nodeSecretVersion?: number): void;
|
|
33
|
+
adoptNodeSecretVersion(nodeSecretVersion?: number): void;
|
|
34
|
+
clearNodeSecret(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Hub rejected our cached node_secret as diverged (v1 a2aProtocol.js L1983-2017).
|
|
37
|
+
* Drop the in-memory secret + version so buildHubHeaders/authenticate fall back to
|
|
38
|
+
* unauthenticated on the next /a2a/hello, then hand off to onDiverged for the durable
|
|
39
|
+
* clear (store keys + on-disk legacy files). Does NOT route through onVersionUpdate —
|
|
40
|
+
* the divergence handler performs the full clear, including the version, atomically.
|
|
41
|
+
*/
|
|
42
|
+
notifyNodeSecretDiverged(): void;
|
|
43
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export function isNodeSecret(value) {
|
|
2
|
+
return /^[a-f0-9]{64}$/i.test(value);
|
|
3
|
+
}
|
|
4
|
+
export function parseNodeSecretVersion(value) {
|
|
5
|
+
if (typeof value === 'number' && Number.isInteger(value) && value > 0)
|
|
6
|
+
return value;
|
|
7
|
+
if (typeof value !== 'string')
|
|
8
|
+
return undefined;
|
|
9
|
+
const trimmed = value.trim();
|
|
10
|
+
if (!/^[1-9]\d*$/.test(trimmed))
|
|
11
|
+
return undefined;
|
|
12
|
+
const parsed = Number(trimmed);
|
|
13
|
+
return Number.isSafeInteger(parsed) ? parsed : undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* node_secret 双轨过渡(M6-5, 6 个月). 包旧 64-hex node_secret 成 AuthProvider.
|
|
17
|
+
* **node_secret 走 request body**(gep-a2a 契约, 实测 dev: requireNodeSecret 读 body 非 header), 不轮换.
|
|
18
|
+
*/
|
|
19
|
+
export class LegacyAuthShim {
|
|
20
|
+
nodeSecret;
|
|
21
|
+
onRotate;
|
|
22
|
+
onVersionUpdate;
|
|
23
|
+
onDiverged;
|
|
24
|
+
kind = 'oauth_device_token';
|
|
25
|
+
nodeSecretVersion;
|
|
26
|
+
constructor(nodeSecret, onRotate, initialNodeSecretVersion, onVersionUpdate, onDiverged) {
|
|
27
|
+
this.nodeSecret = nodeSecret;
|
|
28
|
+
this.onRotate = onRotate;
|
|
29
|
+
this.onVersionUpdate = onVersionUpdate;
|
|
30
|
+
this.onDiverged = onDiverged;
|
|
31
|
+
if (!nodeSecret || !isNodeSecret(nodeSecret))
|
|
32
|
+
throw new Error('node_secret 必须是 64-hex');
|
|
33
|
+
this.nodeSecretVersion = parseNodeSecretVersion(initialNodeSecretVersion);
|
|
34
|
+
}
|
|
35
|
+
async login() { return { id: 'legacy-node-secret', kind: 'oauth_device_token', token: this.nodeSecret ?? '' }; }
|
|
36
|
+
async authenticate() {
|
|
37
|
+
return {
|
|
38
|
+
headers: this.nodeSecretVersion !== undefined
|
|
39
|
+
? { 'X-EvoMap-Node-Secret-Version': String(this.nodeSecretVersion) }
|
|
40
|
+
: {},
|
|
41
|
+
// node_secret_version travels via the X-EvoMap-Node-Secret-Version header on every request (above)
|
|
42
|
+
// and is added to the heartbeat body explicitly by PublicHubCapability. It is intentionally NOT
|
|
43
|
+
// injected into generic request bodies — matching v1, which carries it in the body only on heartbeat.
|
|
44
|
+
bodyFields: {
|
|
45
|
+
...(this.nodeSecret ? { node_secret: this.nodeSecret } : {}),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async rotate() { return this.login(); }
|
|
50
|
+
async revoke() { }
|
|
51
|
+
getNodeSecretVersion() {
|
|
52
|
+
return this.nodeSecretVersion;
|
|
53
|
+
}
|
|
54
|
+
adoptNodeSecret(nodeSecret, nodeSecretVersion) {
|
|
55
|
+
if (!isNodeSecret(nodeSecret))
|
|
56
|
+
throw new Error('node_secret 必须是 64-hex');
|
|
57
|
+
this.nodeSecret = nodeSecret;
|
|
58
|
+
this.nodeSecretVersion = parseNodeSecretVersion(nodeSecretVersion);
|
|
59
|
+
this.onRotate?.(nodeSecret, this.nodeSecretVersion);
|
|
60
|
+
}
|
|
61
|
+
adoptNodeSecretVersion(nodeSecretVersion) {
|
|
62
|
+
this.nodeSecretVersion = parseNodeSecretVersion(nodeSecretVersion);
|
|
63
|
+
this.onVersionUpdate?.(this.nodeSecretVersion);
|
|
64
|
+
}
|
|
65
|
+
clearNodeSecret() {
|
|
66
|
+
this.nodeSecret = undefined;
|
|
67
|
+
this.nodeSecretVersion = undefined;
|
|
68
|
+
this.onVersionUpdate?.(undefined);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Hub rejected our cached node_secret as diverged (v1 a2aProtocol.js L1983-2017).
|
|
72
|
+
* Drop the in-memory secret + version so buildHubHeaders/authenticate fall back to
|
|
73
|
+
* unauthenticated on the next /a2a/hello, then hand off to onDiverged for the durable
|
|
74
|
+
* clear (store keys + on-disk legacy files). Does NOT route through onVersionUpdate —
|
|
75
|
+
* the divergence handler performs the full clear, including the version, atomically.
|
|
76
|
+
*/
|
|
77
|
+
notifyNodeSecretDiverged() {
|
|
78
|
+
this.nodeSecret = undefined;
|
|
79
|
+
this.nodeSecretVersion = undefined;
|
|
80
|
+
this.onDiverged?.();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** OS 级 machineId 读取器(按平台). 返回首个非空, 读不到返 undefined. */
|
|
2
|
+
export type OsIdReader = () => string | undefined;
|
|
3
|
+
/** Linux: /etc/machine-id | /var/lib/dbus/machine-id; macOS/Windows 由调用方注入. */
|
|
4
|
+
export declare const DEFAULT_OS_READERS: OsIdReader[];
|
|
5
|
+
export interface MachineIdOptions {
|
|
6
|
+
/** 软兜底 UUID 路径(默认 ~/.evomap/machine-id). */
|
|
7
|
+
softIdPath: string;
|
|
8
|
+
/** OS 读取器(默认 Linux); 测试可注入. */
|
|
9
|
+
osReaders?: OsIdReader[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 机器标识(CEO 决策, 2026-06-06): **优先 OS machineId(/etc/machine-id 等), 读不到回退安装时软 UUID**.
|
|
13
|
+
* 容器/CI 不脆、不依赖 MAC、跨平台一致. 软 UUID 首次生成存 0600.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveMachineId(opts: MachineIdOptions): {
|
|
16
|
+
id: string;
|
|
17
|
+
source: 'os' | 'soft';
|
|
18
|
+
};
|
|
19
|
+
/** device token 绑定用的指纹 = sha256(machineId). 不直接外泄 machineId. */
|
|
20
|
+
export declare function machineFingerprint(machineId: string): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
/** Linux: /etc/machine-id | /var/lib/dbus/machine-id; macOS/Windows 由调用方注入. */
|
|
5
|
+
export const DEFAULT_OS_READERS = [
|
|
6
|
+
() => tryRead('/etc/machine-id'),
|
|
7
|
+
() => tryRead('/var/lib/dbus/machine-id'),
|
|
8
|
+
];
|
|
9
|
+
function tryRead(p) {
|
|
10
|
+
try {
|
|
11
|
+
const v = readFileSync(p, 'utf8').trim();
|
|
12
|
+
return v || undefined;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 机器标识(CEO 决策, 2026-06-06): **优先 OS machineId(/etc/machine-id 等), 读不到回退安装时软 UUID**.
|
|
20
|
+
* 容器/CI 不脆、不依赖 MAC、跨平台一致. 软 UUID 首次生成存 0600.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveMachineId(opts) {
|
|
23
|
+
for (const r of opts.osReaders ?? DEFAULT_OS_READERS) {
|
|
24
|
+
const v = r();
|
|
25
|
+
if (v)
|
|
26
|
+
return { id: v, source: 'os' };
|
|
27
|
+
}
|
|
28
|
+
if (existsSync(opts.softIdPath))
|
|
29
|
+
return { id: readFileSync(opts.softIdPath, 'utf8').trim(), source: 'soft' };
|
|
30
|
+
const fresh = randomUUID();
|
|
31
|
+
mkdirSync(dirname(opts.softIdPath), { recursive: true });
|
|
32
|
+
writeFileSync(opts.softIdPath, fresh, { mode: 0o600 });
|
|
33
|
+
return { id: fresh, source: 'soft' };
|
|
34
|
+
}
|
|
35
|
+
/** device token 绑定用的指纹 = sha256(machineId). 不直接外泄 machineId. */
|
|
36
|
+
export function machineFingerprint(machineId) {
|
|
37
|
+
return createHash('sha256').update(machineId).digest('hex');
|
|
38
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { hub } from '@evomap/evolver-core';
|
|
2
|
+
import { type MachineIdOptions } from './machineId.js';
|
|
3
|
+
export declare const TOKEN_TTL_MS: number;
|
|
4
|
+
export declare const ROTATE_BEFORE_MS: number;
|
|
5
|
+
export interface DeviceCodeResp {
|
|
6
|
+
deviceCode: string;
|
|
7
|
+
userCode: string;
|
|
8
|
+
verificationUri: string;
|
|
9
|
+
intervalMs: number;
|
|
10
|
+
}
|
|
11
|
+
export interface TokenResp {
|
|
12
|
+
token: string;
|
|
13
|
+
expiresInMs?: number;
|
|
14
|
+
refreshToken?: string;
|
|
15
|
+
}
|
|
16
|
+
/** 注入的 OAuth device flow 传输(M6-6 真 HTTP; 测试用桩). refresh 走 refresh_token grant. */
|
|
17
|
+
export interface OAuthTransport {
|
|
18
|
+
requestDeviceCode: (fingerprint: string) => Promise<DeviceCodeResp>;
|
|
19
|
+
pollToken: (deviceCode: string) => Promise<TokenResp | {
|
|
20
|
+
pending: true;
|
|
21
|
+
}>;
|
|
22
|
+
refresh?: (refreshToken: string) => Promise<TokenResp>;
|
|
23
|
+
}
|
|
24
|
+
export interface PublicOAuthOptions {
|
|
25
|
+
credPath: string;
|
|
26
|
+
machine: MachineIdOptions;
|
|
27
|
+
transport: OAuthTransport;
|
|
28
|
+
now?: () => number;
|
|
29
|
+
/** 展示 user code 给用户(默认 console). */
|
|
30
|
+
onUserCode?: (d: DeviceCodeResp) => void;
|
|
31
|
+
/** 轮询间隔等待(默认 setTimeout; 测试可注入 no-op). */
|
|
32
|
+
sleep?: (ms: number) => Promise<void>;
|
|
33
|
+
/** 整个 device flow 的最长等待(默认 15min); 超时抛 device_flow_timeout. */
|
|
34
|
+
maxWaitMs?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* device token 认证(M6-5, 默认路径 evolver login). device code flow(学 gh auth login):
|
|
38
|
+
* 显示 user code → 用户在浏览器授权 → 轮询拿 token. 绑机器指纹(CEO: OS machineId+软兜底).
|
|
39
|
+
* hub access token 短寿(~1h)+ refresh_token 续期; 过期前 ROTATE_BEFORE_MS 自动 rotate.
|
|
40
|
+
*/
|
|
41
|
+
export declare class PublicOAuthProvider implements hub.AuthProvider {
|
|
42
|
+
private readonly opts;
|
|
43
|
+
readonly kind: "oauth_device_token";
|
|
44
|
+
private readonly store;
|
|
45
|
+
private readonly now;
|
|
46
|
+
private readonly sleep;
|
|
47
|
+
private readonly maxWaitMs;
|
|
48
|
+
constructor(opts: PublicOAuthOptions);
|
|
49
|
+
private fingerprint;
|
|
50
|
+
/**
|
|
51
|
+
* Full device-flow login: reuse a still-valid cached token, else request a
|
|
52
|
+
* device code (shown once via onUserCode) and poll until the user approves in
|
|
53
|
+
* the browser, returning + persisting the credential. Drives the poll loop
|
|
54
|
+
* itself (respecting the server interval); a terminal error from the transport
|
|
55
|
+
* (access_denied / expired_token) propagates.
|
|
56
|
+
*/
|
|
57
|
+
login(): Promise<hub.Credential>;
|
|
58
|
+
authenticate(): Promise<hub.SignedRequest>;
|
|
59
|
+
rotate(): Promise<hub.Credential>;
|
|
60
|
+
revoke(): Promise<void>;
|
|
61
|
+
private persist;
|
|
62
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { CredentialStore } from './credentialStore.js';
|
|
2
|
+
import { resolveMachineId, machineFingerprint } from './machineId.js';
|
|
3
|
+
export const TOKEN_TTL_MS = 24 * 60 * 60_000; // fallback only; the hub supplies expiresInMs (~1h)
|
|
4
|
+
export const ROTATE_BEFORE_MS = 5 * 60_000; // refresh 5min before expiry (hub access tokens are short-lived ~1h)
|
|
5
|
+
/**
|
|
6
|
+
* device token 认证(M6-5, 默认路径 evolver login). device code flow(学 gh auth login):
|
|
7
|
+
* 显示 user code → 用户在浏览器授权 → 轮询拿 token. 绑机器指纹(CEO: OS machineId+软兜底).
|
|
8
|
+
* hub access token 短寿(~1h)+ refresh_token 续期; 过期前 ROTATE_BEFORE_MS 自动 rotate.
|
|
9
|
+
*/
|
|
10
|
+
export class PublicOAuthProvider {
|
|
11
|
+
opts;
|
|
12
|
+
kind = 'oauth_device_token';
|
|
13
|
+
store;
|
|
14
|
+
now;
|
|
15
|
+
sleep;
|
|
16
|
+
maxWaitMs;
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
this.opts = opts;
|
|
19
|
+
this.store = new CredentialStore(opts.credPath);
|
|
20
|
+
this.now = opts.now ?? (() => Date.now());
|
|
21
|
+
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
22
|
+
this.maxWaitMs = opts.maxWaitMs ?? 15 * 60_000;
|
|
23
|
+
}
|
|
24
|
+
fingerprint() { return machineFingerprint(resolveMachineId(this.opts.machine).id); }
|
|
25
|
+
/**
|
|
26
|
+
* Full device-flow login: reuse a still-valid cached token, else request a
|
|
27
|
+
* device code (shown once via onUserCode) and poll until the user approves in
|
|
28
|
+
* the browser, returning + persisting the credential. Drives the poll loop
|
|
29
|
+
* itself (respecting the server interval); a terminal error from the transport
|
|
30
|
+
* (access_denied / expired_token) propagates.
|
|
31
|
+
*/
|
|
32
|
+
async login() {
|
|
33
|
+
const cached = this.store.load();
|
|
34
|
+
if (cached && 'token' in cached && (cached.expiresAt ?? 0) - ROTATE_BEFORE_MS > this.now())
|
|
35
|
+
return cached;
|
|
36
|
+
const fp = this.fingerprint();
|
|
37
|
+
const dc = await this.opts.transport.requestDeviceCode(fp);
|
|
38
|
+
(this.opts.onUserCode ?? ((d) => { process.stdout.write(`授权码 ${d.userCode} → ${d.verificationUri}\n`); }))(dc);
|
|
39
|
+
const deadline = this.now() + this.maxWaitMs;
|
|
40
|
+
let intervalMs = dc.intervalMs;
|
|
41
|
+
for (;;) {
|
|
42
|
+
const res = await this.opts.transport.pollToken(dc.deviceCode);
|
|
43
|
+
if (!('pending' in res))
|
|
44
|
+
return this.persist(res, fp);
|
|
45
|
+
if (this.now() >= deadline)
|
|
46
|
+
throw new Error('device_flow_timeout');
|
|
47
|
+
await this.sleep(intervalMs);
|
|
48
|
+
intervalMs += 1000; // gentle back-off; the hub's slow_down also surfaces as pending
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async authenticate() {
|
|
52
|
+
let cred = this.store.load();
|
|
53
|
+
if (!cred || !('token' in cred))
|
|
54
|
+
throw new Error('oauth 未登录, 先 login()');
|
|
55
|
+
if ((cred.expiresAt ?? 0) - ROTATE_BEFORE_MS <= this.now())
|
|
56
|
+
cred = await this.rotate();
|
|
57
|
+
return { headers: { authorization: `Bearer ${cred.token}`, 'x-evomap-device': cred.device ?? '' } };
|
|
58
|
+
}
|
|
59
|
+
async rotate() {
|
|
60
|
+
const cred = this.store.load();
|
|
61
|
+
const fp = this.fingerprint();
|
|
62
|
+
// Prefer a seamless refresh_token rotation; fall back to a fresh device-flow
|
|
63
|
+
// login only when there is no usable refresh token (or no refresh transport).
|
|
64
|
+
if (cred && 'refreshToken' in cred && cred.refreshToken && this.opts.transport.refresh) {
|
|
65
|
+
const res = await this.opts.transport.refresh(cred.refreshToken);
|
|
66
|
+
return this.persist(res, fp, cred.refreshToken);
|
|
67
|
+
}
|
|
68
|
+
return this.login();
|
|
69
|
+
}
|
|
70
|
+
async revoke() { }
|
|
71
|
+
// fallbackRefresh: carry the prior refresh token forward when the hub's refresh
|
|
72
|
+
// response does not rotate it (so a later rotate() still has one to use).
|
|
73
|
+
persist(res, fingerprint, fallbackRefresh) {
|
|
74
|
+
const refreshToken = res.refreshToken ?? fallbackRefresh;
|
|
75
|
+
const cred = {
|
|
76
|
+
id: `oauth-${fingerprint.slice(0, 12)}`, kind: 'oauth_device_token', device: fingerprint,
|
|
77
|
+
token: res.token, expiresAt: this.now() + (res.expiresInMs ?? TOKEN_TTL_MS),
|
|
78
|
+
...(refreshToken ? { refreshToken } : {}),
|
|
79
|
+
};
|
|
80
|
+
this.store.save(cred);
|
|
81
|
+
return cred;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type FetchLike } from '../hubFetch.js';
|
|
2
|
+
import type { OAuthTransport } from './oauthDeviceToken.js';
|
|
3
|
+
/** First-party public client seeded on the hub (scripts/seed-evolver-oauth-client.js). */
|
|
4
|
+
export declare const DEFAULT_CLIENT_ID = "evolver-cli";
|
|
5
|
+
/**
|
|
6
|
+
* Default scopes requested by `evolver login`. `a2a` is the converged
|
|
7
|
+
* replacement for node_secret on the agent surface (Phase 2); the recipe/gene
|
|
8
|
+
* scopes cover the publish path. The hub clamps to the client's allowedScopes,
|
|
9
|
+
* so over-asking is safe.
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEFAULT_SCOPES: string[];
|
|
12
|
+
export interface OAuthHttpTransportOptions {
|
|
13
|
+
/** Hub base URL, e.g. https://evomap.ai (same EVOMAP_HUB_URL the /a2a calls use). */
|
|
14
|
+
hubUrl: string;
|
|
15
|
+
clientId?: string;
|
|
16
|
+
scopes?: string[];
|
|
17
|
+
/** Injected for tests; defaults to the secure global fetch (https guard + forced TLS). */
|
|
18
|
+
fetchFn?: FetchLike;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Real HTTP transport (M6-6) for the RFC 8628 device authorization grant. The
|
|
22
|
+
* device_authorization + token endpoints are PUBLIC (the device_code itself is
|
|
23
|
+
* the credential), so this posts plain JSON and does NOT go through HubFetch's
|
|
24
|
+
* AuthProvider header injection. Egress still runs through the secure FetchLike
|
|
25
|
+
* (https-only guard + forced-TLS dispatcher) — same chokepoint as /a2a.
|
|
26
|
+
*
|
|
27
|
+
* Wire shapes are pinned to the deployed hub (verified on prod): the hub speaks
|
|
28
|
+
* JSON (it mounts express.json() only), returns RFC 8628 fields, and signals
|
|
29
|
+
* polling state via `{ "error": "authorization_pending" | "slow_down" }` (HTTP
|
|
30
|
+
* 400). `slow_down`/`authorization_pending` map to `{ pending: true }`; any other
|
|
31
|
+
* error (access_denied / expired_token / invalid_grant) throws.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createOAuthHttpTransport(opts: OAuthHttpTransportOptions): OAuthTransport;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { assertHubUrlSecure, globalFetchLike } from '../hubFetch.js';
|
|
2
|
+
const DEVICE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
3
|
+
/** First-party public client seeded on the hub (scripts/seed-evolver-oauth-client.js). */
|
|
4
|
+
export const DEFAULT_CLIENT_ID = 'evolver-cli';
|
|
5
|
+
/**
|
|
6
|
+
* Default scopes requested by `evolver login`. `a2a` is the converged
|
|
7
|
+
* replacement for node_secret on the agent surface (Phase 2); the recipe/gene
|
|
8
|
+
* scopes cover the publish path. The hub clamps to the client's allowedScopes,
|
|
9
|
+
* so over-asking is safe.
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_SCOPES = ['a2a', 'recipe:read', 'recipe:write', 'recipe:publish', 'gene:read', 'reuse:query'];
|
|
12
|
+
/**
|
|
13
|
+
* Real HTTP transport (M6-6) for the RFC 8628 device authorization grant. The
|
|
14
|
+
* device_authorization + token endpoints are PUBLIC (the device_code itself is
|
|
15
|
+
* the credential), so this posts plain JSON and does NOT go through HubFetch's
|
|
16
|
+
* AuthProvider header injection. Egress still runs through the secure FetchLike
|
|
17
|
+
* (https-only guard + forced-TLS dispatcher) — same chokepoint as /a2a.
|
|
18
|
+
*
|
|
19
|
+
* Wire shapes are pinned to the deployed hub (verified on prod): the hub speaks
|
|
20
|
+
* JSON (it mounts express.json() only), returns RFC 8628 fields, and signals
|
|
21
|
+
* polling state via `{ "error": "authorization_pending" | "slow_down" }` (HTTP
|
|
22
|
+
* 400). `slow_down`/`authorization_pending` map to `{ pending: true }`; any other
|
|
23
|
+
* error (access_denied / expired_token / invalid_grant) throws.
|
|
24
|
+
*/
|
|
25
|
+
export function createOAuthHttpTransport(opts) {
|
|
26
|
+
const base = opts.hubUrl.replace(/\/$/, '');
|
|
27
|
+
const clientId = opts.clientId ?? DEFAULT_CLIENT_ID;
|
|
28
|
+
const scope = (opts.scopes ?? DEFAULT_SCOPES).join(' ');
|
|
29
|
+
const fetchFn = opts.fetchFn ?? globalFetchLike;
|
|
30
|
+
async function postJson(path, body) {
|
|
31
|
+
const url = `${base}${path}`;
|
|
32
|
+
assertHubUrlSecure(url); // defense in depth; also enforced inside globalFetchLike
|
|
33
|
+
const res = await fetchFn(url, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'content-type': 'application/json' },
|
|
36
|
+
body: JSON.stringify(body),
|
|
37
|
+
});
|
|
38
|
+
const json = (await res.json().catch(() => ({})));
|
|
39
|
+
return { status: res.status, json };
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
async requestDeviceCode(_fingerprint) {
|
|
43
|
+
// The hub binds the device authorization to the client + the approving
|
|
44
|
+
// user, not to a machine fingerprint, so the fingerprint is not sent here;
|
|
45
|
+
// it is bound locally on the resulting credential (x-evomap-device header).
|
|
46
|
+
const { status, json } = await postJson('/oauth/device_authorization', {
|
|
47
|
+
client_id: clientId,
|
|
48
|
+
scope,
|
|
49
|
+
});
|
|
50
|
+
if (status >= 400 || !json.device_code || !json.user_code) {
|
|
51
|
+
throw new Error(`device_authorization failed (HTTP ${status}): ${json.error ?? 'no device_code'}`);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
deviceCode: json.device_code,
|
|
55
|
+
userCode: json.user_code,
|
|
56
|
+
verificationUri: json.verification_uri_complete ?? json.verification_uri ?? '',
|
|
57
|
+
intervalMs: (json.interval ?? 5) * 1000,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
async pollToken(deviceCode) {
|
|
61
|
+
const { status, json } = await postJson('/oauth/token', {
|
|
62
|
+
grant_type: DEVICE_GRANT,
|
|
63
|
+
device_code: deviceCode,
|
|
64
|
+
client_id: clientId,
|
|
65
|
+
});
|
|
66
|
+
if (json.access_token) {
|
|
67
|
+
return tokenResp(json);
|
|
68
|
+
}
|
|
69
|
+
if (json.error === 'authorization_pending' || json.error === 'slow_down') {
|
|
70
|
+
return { pending: true };
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`token poll failed (HTTP ${status}): ${json.error ?? 'unknown'}`);
|
|
73
|
+
},
|
|
74
|
+
async refresh(refreshToken) {
|
|
75
|
+
const { status, json } = await postJson('/oauth/token', {
|
|
76
|
+
grant_type: 'refresh_token',
|
|
77
|
+
refresh_token: refreshToken,
|
|
78
|
+
client_id: clientId,
|
|
79
|
+
});
|
|
80
|
+
if (!json.access_token) {
|
|
81
|
+
throw new Error(`refresh failed (HTTP ${status}): ${json.error ?? 'no access_token'}`);
|
|
82
|
+
}
|
|
83
|
+
return tokenResp(json);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function tokenResp(json) {
|
|
88
|
+
return {
|
|
89
|
+
token: json.access_token,
|
|
90
|
+
...(json.expires_in ? { expiresInMs: json.expires_in * 1000 } : {}),
|
|
91
|
+
...(json.refresh_token ? { refreshToken: json.refresh_token } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { hub } from '@evomap/evolver-core';
|
|
2
|
+
import { PublicHubCapability } from './hubCapability.js';
|
|
3
|
+
import type { AntiAbuseTelemetryOptions } from './antiAbuseTelemetry.js';
|
|
4
|
+
import { type FetchLike } from './hubFetch.js';
|
|
5
|
+
import { type OAuthTransport } from './auth/oauthDeviceToken.js';
|
|
6
|
+
export type AuthMode = 'oauth' | 'keypair' | 'legacy';
|
|
7
|
+
export interface ConnectPublicOptions {
|
|
8
|
+
hubUrl: string;
|
|
9
|
+
authMode: AuthMode;
|
|
10
|
+
senderId: () => string | undefined;
|
|
11
|
+
fetchFn?: FetchLike;
|
|
12
|
+
evomapDir?: string;
|
|
13
|
+
/** legacy 模式: 64-hex node_secret. */
|
|
14
|
+
nodeSecret?: string;
|
|
15
|
+
/** legacy 模式: node_secret 对应的可选 Hub keyring version. */
|
|
16
|
+
nodeSecretVersion?: number;
|
|
17
|
+
/** legacy 模式: hello rotate 返回新 node_secret 后持久化到调用方状态存储. */
|
|
18
|
+
onNodeSecretRotated?: (nodeSecret: string, nodeSecretVersion?: number) => void;
|
|
19
|
+
/** legacy 模式: hello 返回 version 但未轮换 secret 时持久化 version. */
|
|
20
|
+
onNodeSecretVersionUpdated?: (nodeSecretVersion?: number) => void;
|
|
21
|
+
/**
|
|
22
|
+
* legacy 模式: hub HTTP-200 拒绝本地 node_secret(已与 hub 记录漂移)时回调.
|
|
23
|
+
* 实现方负责清除持久副本(store keys + 磁盘 legacy 文件), 下次启动以未认证 hello 重新获取.
|
|
24
|
+
* v1 a2aProtocol.js 的 secret-divergence 自愈.
|
|
25
|
+
*/
|
|
26
|
+
onNodeSecretDiverged?: () => void;
|
|
27
|
+
/** oauth 模式: device flow 传输(缺省=真 HTTP, 此处需注入或由 hubUrl 推导). */
|
|
28
|
+
oauthTransport?: OAuthTransport;
|
|
29
|
+
/** keypair 模式: 注册公钥到 hub. */
|
|
30
|
+
registerPublicKey?: (pem: string) => Promise<{
|
|
31
|
+
credentialId: string;
|
|
32
|
+
}>;
|
|
33
|
+
/** Optional anti-abuse metadata context for heartbeat payloads. */
|
|
34
|
+
antiAbuse?: AntiAbuseTelemetryOptions;
|
|
35
|
+
}
|
|
36
|
+
/** 选址装配公版 hub(M6-7): 按 authMode 组 AuthProvider(M6-5) + PublicHubCapability(M6-6). */
|
|
37
|
+
export declare function connectPublicHub(opts: ConnectPublicOptions): {
|
|
38
|
+
hub: PublicHubCapability;
|
|
39
|
+
auth: hub.AuthProvider;
|
|
40
|
+
};
|
package/dist/connect.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { PublicHubCapability } from './hubCapability.js';
|
|
4
|
+
import { globalFetchLike } from './hubFetch.js';
|
|
5
|
+
import { LegacyAuthShim } from './auth/legacyShim.js';
|
|
6
|
+
import { PublicOAuthProvider } from './auth/oauthDeviceToken.js';
|
|
7
|
+
import { createOAuthHttpTransport } from './auth/oauthHttpTransport.js';
|
|
8
|
+
import { KeypairProvider } from './auth/keypair.js';
|
|
9
|
+
/** 选址装配公版 hub(M6-7): 按 authMode 组 AuthProvider(M6-5) + PublicHubCapability(M6-6). */
|
|
10
|
+
export function connectPublicHub(opts) {
|
|
11
|
+
const dir = opts.evomapDir ?? join(homedir(), '.evomap');
|
|
12
|
+
const fetchFn = opts.fetchFn ?? globalFetchLike;
|
|
13
|
+
let auth;
|
|
14
|
+
switch (opts.authMode) {
|
|
15
|
+
case 'legacy':
|
|
16
|
+
if (!opts.nodeSecret)
|
|
17
|
+
throw new Error('legacy 模式需 nodeSecret');
|
|
18
|
+
auth = new LegacyAuthShim(opts.nodeSecret, opts.onNodeSecretRotated, opts.nodeSecretVersion, opts.onNodeSecretVersionUpdated, opts.onNodeSecretDiverged);
|
|
19
|
+
break;
|
|
20
|
+
case 'keypair':
|
|
21
|
+
if (!opts.registerPublicKey)
|
|
22
|
+
throw new Error('keypair 模式需 registerPublicKey');
|
|
23
|
+
auth = new KeypairProvider({ credPath: join(dir, 'keys', 'keypair.json'), registerPublicKey: opts.registerPublicKey });
|
|
24
|
+
break;
|
|
25
|
+
case 'oauth': {
|
|
26
|
+
// Default to the real HTTP device-flow transport derived from hubUrl;
|
|
27
|
+
// tests/embedders may still inject their own.
|
|
28
|
+
const transport = opts.oauthTransport ?? createOAuthHttpTransport({ hubUrl: opts.hubUrl, fetchFn });
|
|
29
|
+
auth = new PublicOAuthProvider({ credPath: join(dir, 'token.json'), machine: { softIdPath: join(dir, 'machine-id') }, transport });
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
default: {
|
|
33
|
+
const _x = opts.authMode;
|
|
34
|
+
throw new Error(`未知 authMode: ${String(_x)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { hub: new PublicHubCapability({ baseUrl: opts.hubUrl, auth, fetchFn, senderId: opts.senderId, antiAbuse: opts.antiAbuse }), auth };
|
|
38
|
+
}
|