@abraca/resend 2.16.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/src/server.ts ADDED
@@ -0,0 +1,322 @@
1
+ /**
2
+ * AbracadabraResendServer — connection lifecycle for the Resend bridge.
3
+ *
4
+ * Trimmed-down equivalent of @abraca/mcp's server: Ed25519 identity, login/
5
+ * register, single-space binding, child-provider cache with idle eviction,
6
+ * `ensureConnected` heal on dropped sockets / expired JWTs. No awareness,
7
+ * no chat, no tool wiring — this is a daemon, not an agent.
8
+ */
9
+ import type { DocumentMeta, ServerInfo } from "@abraca/dabra";
10
+ import {
11
+ AbracadabraClient,
12
+ AbracadabraProvider,
13
+ Kind,
14
+ SERVER_ROOT_ID,
15
+ WebSocketStatus,
16
+ } from "@abraca/dabra";
17
+ import * as Y from "yjs";
18
+ import { loadOrCreateKeypair, signChallenge } from "./crypto.ts";
19
+ import { waitForSync } from "./utils.ts";
20
+
21
+ export interface ResendServerConfig {
22
+ url: string;
23
+ agentName?: string;
24
+ keyFile?: string;
25
+ inviteCode?: string;
26
+ /** Bind to this space id. When omitted, the first visible space is used. */
27
+ spaceId?: string;
28
+ }
29
+
30
+ interface SpaceConnection {
31
+ doc: Y.Doc;
32
+ provider: AbracadabraProvider;
33
+ docId: string;
34
+ }
35
+
36
+ interface CachedProvider {
37
+ provider: AbracadabraProvider;
38
+ lastAccessed: number;
39
+ }
40
+
41
+ const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
42
+
43
+ export class AbracadabraResendServer {
44
+ readonly config: ResendServerConfig;
45
+ readonly client: AbracadabraClient;
46
+ private _serverInfo: ServerInfo | null = null;
47
+ private _spaces: DocumentMeta[] = [];
48
+ private _connection: SpaceConnection | null = null;
49
+ private childCache = new Map<string, CachedProvider>();
50
+ private evictionTimer: ReturnType<typeof setInterval> | null = null;
51
+ private _userId: string | null = null;
52
+ private _signFn: ((challenge: string) => Promise<string>) | null = null;
53
+ private _reconnecting: Promise<void> | null = null;
54
+
55
+ constructor(config: ResendServerConfig) {
56
+ this.config = config;
57
+ this.client = new AbracadabraClient({
58
+ url: config.url,
59
+ persistAuth: false,
60
+ });
61
+ }
62
+
63
+ get agentName(): string {
64
+ return this.config.agentName || "Resend Bridge";
65
+ }
66
+
67
+ get serverInfo(): ServerInfo | null {
68
+ return this._serverInfo;
69
+ }
70
+
71
+ get spaceDocId(): string | null {
72
+ return this._connection?.docId ?? null;
73
+ }
74
+
75
+ get rootDocument(): Y.Doc | null {
76
+ return this._connection?.doc ?? null;
77
+ }
78
+
79
+ get rootProvider(): AbracadabraProvider | null {
80
+ return this._connection?.provider ?? null;
81
+ }
82
+
83
+ get userId(): string | null {
84
+ return this._userId;
85
+ }
86
+
87
+ get spaces(): DocumentMeta[] {
88
+ return this._spaces;
89
+ }
90
+
91
+ /** Authenticate, discover spaces, connect to the configured (or first) space. */
92
+ async connect(): Promise<void> {
93
+ const keypair = await loadOrCreateKeypair(this.config.keyFile);
94
+ this._userId = keypair.publicKeyB64;
95
+ const signFn = (challenge: string) =>
96
+ Promise.resolve(signChallenge(challenge, keypair.privateKey));
97
+ this._signFn = signFn;
98
+
99
+ try {
100
+ await this.client.loginWithKey(keypair.publicKeyB64, signFn);
101
+ } catch (err: any) {
102
+ const status = err?.status ?? err?.response?.status;
103
+ const msg = String(err?.message ?? "").toLowerCase();
104
+ const notRegistered =
105
+ status === 404 ||
106
+ status === 422 ||
107
+ (status === 401 &&
108
+ /not registered|user not found|no such user/.test(msg));
109
+ if (!notRegistered) throw err;
110
+ console.error(
111
+ "[abracadabra-resend] Key not registered, creating new account...",
112
+ );
113
+ await this.client.registerWithKey({
114
+ publicKey: keypair.publicKeyB64,
115
+ username: this.agentName.replace(/\s+/g, "-").toLowerCase(),
116
+ displayName: this.agentName,
117
+ deviceName: "Resend Bridge",
118
+ inviteCode: this.config.inviteCode,
119
+ });
120
+ await this.client.loginWithKey(keypair.publicKeyB64, signFn);
121
+ }
122
+ console.error(
123
+ `[abracadabra-resend] Authenticated as ${this.agentName} (pubkey=${keypair.publicKeyB64})`,
124
+ );
125
+
126
+ this._serverInfo = await this.client.serverInfo();
127
+
128
+ const roots = await this.client.listChildren();
129
+ this._spaces = roots.filter((d) => d.kind === Kind.Space);
130
+
131
+ let targetId = this.config.spaceId;
132
+ if (targetId) {
133
+ const found =
134
+ this._spaces.find((s) => s.id === targetId) ??
135
+ roots.find((d) => d.id === targetId);
136
+ if (!found) {
137
+ throw new Error(
138
+ `Configured ABRA_SPACE_ID=${targetId} not found among server roots`,
139
+ );
140
+ }
141
+ } else {
142
+ targetId = this._spaces[0]?.id ?? roots[0]?.id;
143
+ if (!targetId) {
144
+ throw new Error(
145
+ `No entry point found: server has no top-level documents under ${SERVER_ROOT_ID}.`,
146
+ );
147
+ }
148
+ }
149
+
150
+ console.error(`[abracadabra-resend] Binding to space ${targetId}`);
151
+ await this._connectToSpace(targetId);
152
+ console.error("[abracadabra-resend] Space doc synced");
153
+
154
+ this.evictionTimer = setInterval(() => this.evictIdle(), 60_000);
155
+ }
156
+
157
+ private async _connectToSpace(docId: string): Promise<SpaceConnection> {
158
+ if (!this.client.isTokenValid() && this._signFn && this._userId) {
159
+ console.error("[abracadabra-resend] JWT expired, re-authenticating...");
160
+ await this.client.loginWithKey(this._userId, this._signFn);
161
+ }
162
+
163
+ const doc = new Y.Doc({ guid: docId });
164
+ const provider = new AbracadabraProvider({
165
+ name: docId,
166
+ document: doc,
167
+ client: this.client,
168
+ disableOfflineStore: true,
169
+ subdocLoading: "lazy",
170
+ });
171
+
172
+ await waitForSync(provider);
173
+
174
+ provider.awareness?.setLocalStateField("user", {
175
+ name: this.agentName,
176
+ color: "hsl(170, 70%, 45%)",
177
+ publicKey: this._userId,
178
+ isAgent: true,
179
+ });
180
+
181
+ const conn: SpaceConnection = { doc, provider, docId };
182
+ this._connection = conn;
183
+ return conn;
184
+ }
185
+
186
+ private _wsConnected(provider: AbracadabraProvider): boolean {
187
+ return provider.connectionStatus === WebSocketStatus.Connected;
188
+ }
189
+
190
+ /**
191
+ * Heal a dropped socket / expired JWT before tool ops. De-duped across
192
+ * concurrent callers; best-effort (never throws — a failed heal falls
193
+ * through to the caller's normal error handling).
194
+ */
195
+ async ensureConnected(): Promise<void> {
196
+ if (this._reconnecting) return this._reconnecting;
197
+ this._reconnecting = (async () => {
198
+ try {
199
+ if (!this.client.isTokenValid() && this._signFn && this._userId) {
200
+ try {
201
+ await this.client.loginWithKey(this._userId, this._signFn);
202
+ } catch (e) {
203
+ console.error("[abracadabra-resend] Re-auth during heal failed:", e);
204
+ }
205
+ }
206
+ const conn = this._connection;
207
+ if (!conn) return;
208
+ if (this._wsConnected(conn.provider)) return;
209
+ try {
210
+ await waitForSync(conn.provider, 6000);
211
+ } catch {
212
+ /* fall through to rebuild */
213
+ }
214
+ if (this._wsConnected(conn.provider)) return;
215
+
216
+ console.error(
217
+ "[abracadabra-resend] Active connection dead — rebuilding…",
218
+ );
219
+ const docId = conn.docId;
220
+ try {
221
+ conn.provider.destroy();
222
+ } catch {
223
+ /* already gone */
224
+ }
225
+ for (const [, cached] of this.childCache) {
226
+ try {
227
+ cached.provider.destroy();
228
+ } catch {
229
+ /* already gone */
230
+ }
231
+ }
232
+ this.childCache.clear();
233
+ try {
234
+ await this._connectToSpace(docId);
235
+ console.error("[abracadabra-resend] Space provider rebuilt + synced");
236
+ } catch (e) {
237
+ console.error("[abracadabra-resend] Connection rebuild failed:", e);
238
+ }
239
+ } finally {
240
+ this._reconnecting = null;
241
+ }
242
+ })();
243
+ return this._reconnecting;
244
+ }
245
+
246
+ getTreeMap(): Y.Map<any> | null {
247
+ return this._connection?.doc.getMap("doc-tree") ?? null;
248
+ }
249
+
250
+ async getChildProvider(docId: string): Promise<AbracadabraProvider> {
251
+ await this.ensureConnected();
252
+
253
+ const cached = this.childCache.get(docId);
254
+ if (
255
+ cached &&
256
+ cached.provider.connectionStatus !== WebSocketStatus.Disconnected
257
+ ) {
258
+ cached.lastAccessed = Date.now();
259
+ return cached.provider;
260
+ }
261
+ if (cached) {
262
+ try {
263
+ cached.provider.destroy();
264
+ } catch {
265
+ /* already gone */
266
+ }
267
+ this.childCache.delete(docId);
268
+ }
269
+
270
+ const root = this._connection?.provider;
271
+ if (!root) throw new Error("Not connected. Call connect() first.");
272
+
273
+ if (!this.client.isTokenValid() && this._signFn && this._userId) {
274
+ await this.client.loginWithKey(this._userId, this._signFn);
275
+ }
276
+
277
+ const childProvider = await root.loadChild(docId);
278
+ await waitForSync(childProvider);
279
+
280
+ this.childCache.set(docId, {
281
+ provider: childProvider,
282
+ lastAccessed: Date.now(),
283
+ });
284
+
285
+ return childProvider;
286
+ }
287
+
288
+ private evictIdle(): void {
289
+ const now = Date.now();
290
+ for (const [docId, cached] of this.childCache) {
291
+ if (now - cached.lastAccessed > IDLE_TIMEOUT_MS) {
292
+ cached.provider.destroy();
293
+ this.childCache.delete(docId);
294
+ console.error(`[abracadabra-resend] Evicted idle provider: ${docId}`);
295
+ }
296
+ }
297
+ }
298
+
299
+ async destroy(): Promise<void> {
300
+ if (this.evictionTimer) {
301
+ clearInterval(this.evictionTimer);
302
+ this.evictionTimer = null;
303
+ }
304
+ for (const [, cached] of this.childCache) {
305
+ try {
306
+ cached.provider.destroy();
307
+ } catch {
308
+ /* already gone */
309
+ }
310
+ }
311
+ this.childCache.clear();
312
+ if (this._connection) {
313
+ try {
314
+ this._connection.provider.destroy();
315
+ } catch {
316
+ /* already gone */
317
+ }
318
+ this._connection = null;
319
+ }
320
+ console.error("[abracadabra-resend] Shutdown complete");
321
+ }
322
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Wait for a provider's `synced` event with a timeout.
3
+ *
4
+ * The `isSynced` short-circuit is load-bearing: providers that already synced
5
+ * (e.g. cached child providers returned from `loadChild`) won't re-emit
6
+ * `synced`, so without the short-circuit every later op on that doc times out.
7
+ */
8
+ export function waitForSync(
9
+ provider: {
10
+ isSynced?: boolean;
11
+ on(event: string, cb: () => void): void;
12
+ off(event: string, cb: () => void): void;
13
+ },
14
+ timeoutMs = 15000,
15
+ ): Promise<void> {
16
+ if (provider.isSynced) return Promise.resolve();
17
+
18
+ return new Promise<void>((resolve, reject) => {
19
+ const timer = setTimeout(() => {
20
+ provider.off("synced", handler);
21
+ reject(new Error(`Sync timed out after ${timeoutMs}ms`));
22
+ }, timeoutMs);
23
+
24
+ function handler() {
25
+ clearTimeout(timer);
26
+ provider.off("synced", handler);
27
+ resolve();
28
+ }
29
+
30
+ provider.on("synced", handler);
31
+ });
32
+ }