@chenacc/agentbridge 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +44 -0
- package/LICENSE +21 -0
- package/README.md +297 -2
- package/bin/agentbridge.js +7 -0
- package/docs/RELEASING.md +47 -0
- package/package.json +65 -4
- package/public/adapter-capabilities.js +29 -0
- package/public/app.js +2211 -0
- package/public/background.jpg +0 -0
- package/public/command-id.js +16 -0
- package/public/highlight.js +125 -0
- package/public/index.html +339 -0
- package/public/markdown.js +291 -0
- package/public/styles.css +1073 -0
- package/public/transcript.js +368 -0
- package/public/workspace-navigation.js +241 -0
- package/src/adapter-registry.js +23 -0
- package/src/adapters/capabilities.js +36 -0
- package/src/adapters/claude.js +493 -0
- package/src/adapters/codex.js +529 -0
- package/src/adapters/deepseek-harness.js +413 -0
- package/src/adapters/dsh-desktop.js +322 -0
- package/src/adapters/echo.js +57 -0
- package/src/adapters/process-utils.js +125 -0
- package/src/adapters/unavailable.js +30 -0
- package/src/adapters/zcode-native.js +40 -0
- package/src/attachments.js +187 -0
- package/src/cc-switch-models.js +86 -0
- package/src/cli.js +199 -0
- package/src/config.js +127 -0
- package/src/conversations/conversation-catalog.js +283 -0
- package/src/dsh-desktop-channels.js +460 -0
- package/src/event-store.js +330 -0
- package/src/notifier.js +266 -0
- package/src/pairing.js +167 -0
- package/src/server.js +957 -0
- package/src/session-manager.js +1355 -0
- package/src/workspaces/workspace-catalog.js +196 -0
package/src/pairing.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
function digest(value) {
|
|
6
|
+
return createHash("sha256").update(value).digest();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class PairingManager {
|
|
10
|
+
#pairings = new Map();
|
|
11
|
+
#sessions = new Map();
|
|
12
|
+
#storePath;
|
|
13
|
+
|
|
14
|
+
constructor({ pairingTtlMs, sessionTtlMs, now = () => Date.now(), storePath = null, accessKey = null }) {
|
|
15
|
+
this.pairingTtlMs = pairingTtlMs;
|
|
16
|
+
this.sessionTtlMs = sessionTtlMs;
|
|
17
|
+
this.now = now;
|
|
18
|
+
this.accessKey = accessKey || null;
|
|
19
|
+
this.#storePath = storePath;
|
|
20
|
+
this.#load();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
createPairing(origin, { targetSessionId = null } = {}) {
|
|
24
|
+
const id = randomBytes(24).toString("base64url");
|
|
25
|
+
const expiresAt = this.now() + this.pairingTtlMs;
|
|
26
|
+
let url = `${origin}/pair?code=${encodeURIComponent(id)}`;
|
|
27
|
+
if (this.accessKey) url += `&key=${encodeURIComponent(this.accessKey)}`;
|
|
28
|
+
const pairing = {
|
|
29
|
+
id,
|
|
30
|
+
expiresAt,
|
|
31
|
+
state: "pending",
|
|
32
|
+
targetSessionId,
|
|
33
|
+
url,
|
|
34
|
+
};
|
|
35
|
+
this.#pairings.set(id, pairing);
|
|
36
|
+
return { ...pairing };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getPendingPairing(id) {
|
|
40
|
+
const pairing = this.#pairings.get(id);
|
|
41
|
+
if (!pairing || pairing.state !== "pending" || pairing.expiresAt <= this.now()) {
|
|
42
|
+
throw Object.assign(new Error("Pairing code is invalid, expired, or already used"), { statusCode: 410 });
|
|
43
|
+
}
|
|
44
|
+
return { ...pairing };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
exchange(id, deviceName = "WeChat device") {
|
|
48
|
+
const pairing = this.getPendingPairing(id);
|
|
49
|
+
const storedPairing = this.#pairings.get(id);
|
|
50
|
+
storedPairing.state = "used";
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...this.createSession(deviceName, { lastSessionId: pairing.targetSessionId }),
|
|
54
|
+
targetSessionId: pairing.targetSessionId,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
createSession(deviceName, { lastSessionId = null } = {}) {
|
|
59
|
+
const token = randomBytes(32).toString("base64url");
|
|
60
|
+
const id = randomUUID();
|
|
61
|
+
const createdAt = this.now();
|
|
62
|
+
const expiresAt = createdAt + this.sessionTtlMs;
|
|
63
|
+
this.#sessions.set(id, { id, digest: digest(token), deviceName, createdAt, expiresAt, lastSessionId, revoked: false });
|
|
64
|
+
this.#persist();
|
|
65
|
+
return { id, token, deviceName, expiresAt, lastSessionId };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
authenticate(token) {
|
|
69
|
+
if (!token) return null;
|
|
70
|
+
const candidate = digest(token);
|
|
71
|
+
for (const session of this.#sessions.values()) {
|
|
72
|
+
if (!session.revoked && session.expiresAt > this.now() && timingSafeEqual(candidate, session.digest)) {
|
|
73
|
+
return {
|
|
74
|
+
id: session.id,
|
|
75
|
+
deviceName: session.deviceName,
|
|
76
|
+
expiresAt: session.expiresAt,
|
|
77
|
+
lastSessionId: session.lastSessionId ?? null,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
revoke(id) {
|
|
85
|
+
const session = this.#sessions.get(id);
|
|
86
|
+
if (!session || session.revoked) return false;
|
|
87
|
+
session.revoked = true;
|
|
88
|
+
this.#persist();
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
listDevices() {
|
|
93
|
+
const now = this.now();
|
|
94
|
+
return [...this.#sessions.values()]
|
|
95
|
+
.filter((session) => !session.revoked && session.expiresAt > now)
|
|
96
|
+
.map((session) => ({
|
|
97
|
+
id: session.id,
|
|
98
|
+
deviceName: session.deviceName,
|
|
99
|
+
createdAt: session.createdAt ?? null,
|
|
100
|
+
expiresAt: session.expiresAt,
|
|
101
|
+
lastSessionId: session.lastSessionId ?? null,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setLastSession(id, lastSessionId) {
|
|
106
|
+
const session = this.#sessions.get(id);
|
|
107
|
+
if (!session || session.revoked || session.expiresAt <= this.now()) return false;
|
|
108
|
+
session.lastSessionId = lastSessionId;
|
|
109
|
+
this.#persist();
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Only digests are stored, never raw tokens. Revoked and expired sessions are
|
|
114
|
+
// dropped from the file entirely: absence from disk means "not authenticated".
|
|
115
|
+
#persist() {
|
|
116
|
+
if (!this.#storePath) return;
|
|
117
|
+
const now = this.now();
|
|
118
|
+
const sessions = [...this.#sessions.values()]
|
|
119
|
+
.filter((session) => !session.revoked && session.expiresAt > now)
|
|
120
|
+
.map((session) => ({
|
|
121
|
+
id: session.id,
|
|
122
|
+
digest: session.digest.toString("hex"),
|
|
123
|
+
deviceName: session.deviceName,
|
|
124
|
+
createdAt: session.createdAt ?? null,
|
|
125
|
+
expiresAt: session.expiresAt,
|
|
126
|
+
lastSessionId: session.lastSessionId ?? null,
|
|
127
|
+
}));
|
|
128
|
+
try {
|
|
129
|
+
mkdirSync(path.dirname(this.#storePath), { recursive: true });
|
|
130
|
+
const tempPath = `${this.#storePath}.tmp`;
|
|
131
|
+
writeFileSync(tempPath, JSON.stringify({ version: 1, sessions }), "utf8");
|
|
132
|
+
renameSync(tempPath, this.#storePath);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(`Failed to persist pairing sessions: ${error.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#load() {
|
|
139
|
+
if (!this.#storePath) return;
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(readFileSync(this.#storePath, "utf8"));
|
|
143
|
+
} catch {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (!parsed || !Array.isArray(parsed.sessions)) return;
|
|
147
|
+
const now = this.now();
|
|
148
|
+
for (const record of parsed.sessions) {
|
|
149
|
+
try {
|
|
150
|
+
if (!record || record.revoked || !(record.expiresAt > now) || typeof record.id !== "string") continue;
|
|
151
|
+
const digestBuffer = Buffer.from(String(record.digest), "hex");
|
|
152
|
+
if (digestBuffer.length !== 32) continue;
|
|
153
|
+
this.#sessions.set(record.id, {
|
|
154
|
+
id: record.id,
|
|
155
|
+
digest: digestBuffer,
|
|
156
|
+
deviceName: String(record.deviceName ?? "Paired device"),
|
|
157
|
+
createdAt: record.createdAt ?? null,
|
|
158
|
+
expiresAt: record.expiresAt,
|
|
159
|
+
lastSessionId: record.lastSessionId ?? null,
|
|
160
|
+
revoked: false,
|
|
161
|
+
});
|
|
162
|
+
} catch {
|
|
163
|
+
// Skip malformed records rather than refusing to start.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|