@kodelyth/tlon 2026.5.42 → 2026.6.1

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 (63) hide show
  1. package/klaw.plugin.json +203 -3
  2. package/package.json +17 -4
  3. package/api.ts +0 -16
  4. package/channel-plugin-api.ts +0 -1
  5. package/doctor-contract-api.ts +0 -1
  6. package/index.ts +0 -16
  7. package/runtime-api.ts +0 -17
  8. package/setup-api.ts +0 -2
  9. package/setup-entry.ts +0 -9
  10. package/src/account-fields.ts +0 -31
  11. package/src/channel.message-adapter.test.ts +0 -145
  12. package/src/channel.runtime.ts +0 -259
  13. package/src/channel.ts +0 -192
  14. package/src/config-schema.ts +0 -54
  15. package/src/core.test.ts +0 -298
  16. package/src/doctor-contract.ts +0 -9
  17. package/src/doctor.test.ts +0 -46
  18. package/src/doctor.ts +0 -10
  19. package/src/logger-runtime.ts +0 -1
  20. package/src/monitor/approval-runtime.ts +0 -363
  21. package/src/monitor/approval.test.ts +0 -33
  22. package/src/monitor/approval.ts +0 -283
  23. package/src/monitor/authorization.ts +0 -30
  24. package/src/monitor/cites.ts +0 -54
  25. package/src/monitor/discovery.ts +0 -68
  26. package/src/monitor/history.ts +0 -226
  27. package/src/monitor/index.ts +0 -1523
  28. package/src/monitor/media.test.ts +0 -80
  29. package/src/monitor/media.ts +0 -156
  30. package/src/monitor/processed-messages.test.ts +0 -58
  31. package/src/monitor/processed-messages.ts +0 -89
  32. package/src/monitor/settings-helpers.test.ts +0 -113
  33. package/src/monitor/settings-helpers.ts +0 -158
  34. package/src/monitor/utils.ts +0 -402
  35. package/src/runtime.ts +0 -9
  36. package/src/security.test.ts +0 -658
  37. package/src/session-route.ts +0 -40
  38. package/src/settings.ts +0 -391
  39. package/src/setup-core.ts +0 -231
  40. package/src/setup-surface.ts +0 -99
  41. package/src/targets.ts +0 -102
  42. package/src/tlon-api.test.ts +0 -572
  43. package/src/tlon-api.ts +0 -389
  44. package/src/types.ts +0 -160
  45. package/src/urbit/auth.ssrf.test.ts +0 -45
  46. package/src/urbit/auth.ts +0 -48
  47. package/src/urbit/base-url.test.ts +0 -48
  48. package/src/urbit/base-url.ts +0 -61
  49. package/src/urbit/channel-ops.test.ts +0 -36
  50. package/src/urbit/channel-ops.ts +0 -149
  51. package/src/urbit/context.ts +0 -50
  52. package/src/urbit/errors.ts +0 -51
  53. package/src/urbit/fetch.ts +0 -38
  54. package/src/urbit/foreigns.ts +0 -49
  55. package/src/urbit/send.test.ts +0 -83
  56. package/src/urbit/send.ts +0 -228
  57. package/src/urbit/sse-client.test.ts +0 -234
  58. package/src/urbit/sse-client.ts +0 -492
  59. package/src/urbit/story.ts +0 -332
  60. package/src/urbit/upload.test.ts +0 -155
  61. package/src/urbit/upload.ts +0 -60
  62. package/test-api.ts +0 -1
  63. package/tsconfig.json +0 -16
@@ -1,492 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { Readable } from "node:stream";
3
- import type { LookupFn, SsrFPolicy } from "klaw/plugin-sdk/ssrf-runtime";
4
- import { ensureUrbitChannelOpen, pokeUrbitChannel, scryUrbitPath } from "./channel-ops.js";
5
- import { getUrbitContext, normalizeUrbitCookie } from "./context.js";
6
- import { urbitFetch } from "./fetch.js";
7
-
8
- type UrbitSseLogger = {
9
- log?: (message: string) => void;
10
- error?: (message: string) => void;
11
- };
12
-
13
- type UrbitSseOptions = {
14
- ship?: string;
15
- ssrfPolicy?: SsrFPolicy;
16
- lookupFn?: LookupFn;
17
- fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
18
- onReconnect?: (client: UrbitSSEClient) => Promise<void> | void;
19
- autoReconnect?: boolean;
20
- maxReconnectAttempts?: number;
21
- reconnectDelay?: number;
22
- maxReconnectDelay?: number;
23
- logger?: UrbitSseLogger;
24
- };
25
-
26
- function parseUrbitSsePayload(data: string): { id?: number; json?: unknown; response?: string } {
27
- try {
28
- return JSON.parse(data) as { id?: number; json?: unknown; response?: string };
29
- } catch (cause) {
30
- throw new Error("Tlon Urbit SSE event was malformed JSON", { cause });
31
- }
32
- }
33
-
34
- export class UrbitSSEClient {
35
- url: string;
36
- cookie: string;
37
- ship: string;
38
- channelId: string;
39
- channelUrl: string;
40
- subscriptions: Array<{
41
- id: number;
42
- action: "subscribe";
43
- ship: string;
44
- app: string;
45
- path: string;
46
- }> = [];
47
- eventHandlers = new Map<
48
- number,
49
- { event?: (data: unknown) => void; err?: (error: unknown) => void; quit?: () => void }
50
- >();
51
- aborted = false;
52
- streamController: AbortController | null = null;
53
- onReconnect: UrbitSseOptions["onReconnect"] | null;
54
- autoReconnect: boolean;
55
- reconnectAttempts = 0;
56
- maxReconnectAttempts: number;
57
- reconnectDelay: number;
58
- maxReconnectDelay: number;
59
- isConnected = false;
60
- logger: UrbitSseLogger;
61
- ssrfPolicy?: SsrFPolicy;
62
- lookupFn?: LookupFn;
63
- fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
64
- streamRelease: (() => Promise<void>) | null = null;
65
-
66
- // Event ack tracking - must ack every ~50 events to keep channel healthy
67
- private lastHeardEventId = -1;
68
- private lastAcknowledgedEventId = -1;
69
- private readonly ackThreshold = 20;
70
-
71
- constructor(url: string, cookie: string, options: UrbitSseOptions = {}) {
72
- const ctx = getUrbitContext(url, options.ship);
73
- this.url = ctx.baseUrl;
74
- this.cookie = normalizeUrbitCookie(cookie);
75
- this.ship = ctx.ship;
76
- this.channelId = `${Math.floor(Date.now() / 1000)}-${randomUUID()}`;
77
- this.channelUrl = new URL(`/~/channel/${this.channelId}`, this.url).toString();
78
- this.onReconnect = options.onReconnect ?? null;
79
- this.autoReconnect = options.autoReconnect !== false;
80
- this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
81
- this.reconnectDelay = options.reconnectDelay ?? 1000;
82
- this.maxReconnectDelay = options.maxReconnectDelay ?? 30000;
83
- this.logger = options.logger ?? {};
84
- this.ssrfPolicy = options.ssrfPolicy;
85
- this.lookupFn = options.lookupFn;
86
- this.fetchImpl = options.fetchImpl;
87
- }
88
-
89
- private channelRequestContext() {
90
- return {
91
- baseUrl: this.url,
92
- cookie: this.cookie,
93
- ship: this.ship,
94
- channelId: this.channelId,
95
- ssrfPolicy: this.ssrfPolicy,
96
- lookupFn: this.lookupFn,
97
- fetchImpl: this.fetchImpl,
98
- };
99
- }
100
-
101
- async subscribe(params: {
102
- app: string;
103
- path: string;
104
- event?: (data: unknown) => void;
105
- err?: (error: unknown) => void;
106
- quit?: () => void;
107
- }) {
108
- const subId = this.subscriptions.length + 1;
109
- const subscription = {
110
- id: subId,
111
- action: "subscribe",
112
- ship: this.ship,
113
- app: params.app,
114
- path: params.path,
115
- } as const;
116
-
117
- this.subscriptions.push(subscription);
118
- this.eventHandlers.set(subId, { event: params.event, err: params.err, quit: params.quit });
119
-
120
- if (this.isConnected) {
121
- try {
122
- await this.sendSubscription(subscription);
123
- } catch (error) {
124
- const handler = this.eventHandlers.get(subId);
125
- handler?.err?.(error);
126
- }
127
- }
128
- return subId;
129
- }
130
-
131
- private async sendSubscription(subscription: {
132
- id: number;
133
- action: "subscribe";
134
- ship: string;
135
- app: string;
136
- path: string;
137
- }) {
138
- const { response, release } = await this.putChannelPayload([subscription], {
139
- timeoutMs: 30_000,
140
- auditContext: "tlon-urbit-subscribe",
141
- });
142
-
143
- try {
144
- if (!response.ok && response.status !== 204) {
145
- const errorText = await response.text().catch(() => "");
146
- throw new Error(
147
- `Subscribe failed: ${response.status}${errorText ? ` - ${errorText}` : ""}`,
148
- );
149
- }
150
- } finally {
151
- await release();
152
- }
153
- }
154
-
155
- async connect() {
156
- await ensureUrbitChannelOpen(this.channelRequestContext(), {
157
- createBody: this.subscriptions,
158
- createAuditContext: "tlon-urbit-channel-create",
159
- });
160
-
161
- await this.openStream();
162
- this.isConnected = true;
163
- this.reconnectAttempts = 0;
164
- }
165
-
166
- async openStream() {
167
- // Use AbortController with manual timeout so we only abort during initial connection,
168
- // not after the SSE stream is established and actively streaming.
169
- const controller = new AbortController();
170
- const timeoutId = setTimeout(() => controller.abort(), 60_000);
171
-
172
- this.streamController = controller;
173
-
174
- const { response, release } = await urbitFetch({
175
- baseUrl: this.url,
176
- path: `/~/channel/${this.channelId}`,
177
- init: {
178
- method: "GET",
179
- headers: {
180
- Accept: "text/event-stream",
181
- Cookie: this.cookie,
182
- },
183
- },
184
- ssrfPolicy: this.ssrfPolicy,
185
- lookupFn: this.lookupFn,
186
- fetchImpl: this.fetchImpl,
187
- signal: controller.signal,
188
- auditContext: "tlon-urbit-sse-stream",
189
- });
190
-
191
- this.streamRelease = release;
192
-
193
- // Clear timeout once connection established (headers received).
194
- clearTimeout(timeoutId);
195
-
196
- if (!response.ok) {
197
- await release();
198
- this.streamRelease = null;
199
- throw new Error(`Stream connection failed: ${response.status}`);
200
- }
201
-
202
- this.processStream(response.body).catch((error) => {
203
- if (!this.aborted) {
204
- this.logger.error?.(`Stream error: ${String(error)}`);
205
- for (const { err } of this.eventHandlers.values()) {
206
- if (err) {
207
- err(error);
208
- }
209
- }
210
- }
211
- });
212
- }
213
-
214
- async processStream(body: unknown) {
215
- if (!body) {
216
- return;
217
- }
218
- // Bridge DOM fetch stream types to Node's stream/web declaration on newer TS/node combos.
219
- const stream =
220
- body instanceof ReadableStream
221
- ? Readable.fromWeb(body as never)
222
- : (body as NodeJS.ReadableStream);
223
- let buffer = "";
224
-
225
- try {
226
- for await (const chunk of stream) {
227
- if (this.aborted) {
228
- break;
229
- }
230
- buffer += chunk.toString();
231
- let eventEnd;
232
- while ((eventEnd = buffer.indexOf("\n\n")) !== -1) {
233
- const eventData = buffer.slice(0, eventEnd);
234
- buffer = buffer.slice(eventEnd + 2);
235
- this.processEvent(eventData);
236
- }
237
- }
238
- } finally {
239
- if (this.streamRelease) {
240
- const release = this.streamRelease;
241
- this.streamRelease = null;
242
- await release();
243
- }
244
- this.streamController = null;
245
- if (!this.aborted && this.autoReconnect) {
246
- this.isConnected = false;
247
- this.logger.log?.("[SSE] Stream ended, attempting reconnection...");
248
- await this.attemptReconnect();
249
- }
250
- }
251
- }
252
-
253
- processEvent(eventData: string) {
254
- const lines = eventData.split("\n");
255
- let data: string | null = null;
256
- let eventId: number | null = null;
257
-
258
- for (const line of lines) {
259
- if (line.startsWith("id: ")) {
260
- eventId = Number.parseInt(line.slice(4), 10);
261
- }
262
- if (line.startsWith("data: ")) {
263
- data = line.slice(6);
264
- }
265
- }
266
-
267
- if (!data) {
268
- return;
269
- }
270
-
271
- // Track event ID and send ack if needed
272
- if (eventId !== null && !Number.isNaN(eventId)) {
273
- if (eventId > this.lastHeardEventId) {
274
- this.lastHeardEventId = eventId;
275
- if (eventId - this.lastAcknowledgedEventId > this.ackThreshold) {
276
- this.logger.log?.(
277
- `[SSE] Acking event ${eventId} (last acked: ${this.lastAcknowledgedEventId})`,
278
- );
279
- this.ack(eventId).catch((err) => {
280
- this.logger.error?.(`Failed to ack event ${eventId}: ${String(err)}`);
281
- });
282
- }
283
- }
284
- }
285
-
286
- try {
287
- const parsed = parseUrbitSsePayload(data);
288
-
289
- if (parsed.response === "quit") {
290
- if (parsed.id) {
291
- const handlers = this.eventHandlers.get(parsed.id);
292
- if (handlers?.quit) {
293
- handlers.quit();
294
- }
295
- }
296
- return;
297
- }
298
-
299
- if (parsed.id && this.eventHandlers.has(parsed.id)) {
300
- const { event } = this.eventHandlers.get(parsed.id) ?? {};
301
- if (event && parsed.json) {
302
- event(parsed.json);
303
- }
304
- } else if (parsed.json) {
305
- for (const { event } of this.eventHandlers.values()) {
306
- if (event) {
307
- event(parsed.json);
308
- }
309
- }
310
- }
311
- } catch (error) {
312
- this.logger.error?.(`Error parsing SSE event: ${String(error)}`);
313
- }
314
- }
315
-
316
- async poke(params: { app: string; mark: string; json: unknown }) {
317
- return await pokeUrbitChannel(this.channelRequestContext(), {
318
- ...params,
319
- auditContext: "tlon-urbit-poke",
320
- });
321
- }
322
-
323
- async scry(path: string) {
324
- return await scryUrbitPath(
325
- {
326
- baseUrl: this.url,
327
- cookie: this.cookie,
328
- ssrfPolicy: this.ssrfPolicy,
329
- lookupFn: this.lookupFn,
330
- fetchImpl: this.fetchImpl,
331
- },
332
- { path, auditContext: "tlon-urbit-scry" },
333
- );
334
- }
335
-
336
- /**
337
- * Update the cookie used for authentication.
338
- * Call this when re-authenticating after session expiry.
339
- */
340
- updateCookie(newCookie: string): void {
341
- this.cookie = normalizeUrbitCookie(newCookie);
342
- }
343
-
344
- private async ack(eventId: number): Promise<void> {
345
- this.lastAcknowledgedEventId = eventId;
346
-
347
- const ackData = {
348
- id: Date.now(),
349
- action: "ack",
350
- "event-id": eventId,
351
- };
352
-
353
- const { response, release } = await this.putChannelPayload([ackData], {
354
- timeoutMs: 10_000,
355
- auditContext: "tlon-urbit-ack",
356
- });
357
-
358
- try {
359
- if (!response.ok) {
360
- throw new Error(`Ack failed with status ${response.status}`);
361
- }
362
- } finally {
363
- await release();
364
- }
365
- }
366
-
367
- async attemptReconnect() {
368
- if (this.aborted || !this.autoReconnect) {
369
- this.logger.log?.("[SSE] Reconnection aborted or disabled");
370
- return;
371
- }
372
-
373
- // If we've hit max attempts, wait longer then reset and keep trying
374
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
375
- this.logger.log?.(
376
- `[SSE] Max reconnection attempts (${this.maxReconnectAttempts}) reached. Waiting 10s before resetting...`,
377
- );
378
- // Wait 10 seconds before resetting and trying again
379
- const extendedBackoff = 10000; // 10 seconds
380
- await new Promise((resolve) => setTimeout(resolve, extendedBackoff));
381
- this.reconnectAttempts = 0; // Reset counter to continue trying
382
- this.logger.log?.("[SSE] Reconnection attempts reset, resuming reconnection...");
383
- }
384
-
385
- this.reconnectAttempts += 1;
386
- const delay = Math.min(
387
- this.reconnectDelay * 2 ** (this.reconnectAttempts - 1),
388
- this.maxReconnectDelay,
389
- );
390
-
391
- this.logger.log?.(
392
- `[SSE] Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms...`,
393
- );
394
-
395
- await new Promise((resolve) => setTimeout(resolve, delay));
396
-
397
- try {
398
- this.channelId = `${Math.floor(Date.now() / 1000)}-${randomUUID()}`;
399
- this.channelUrl = new URL(`/~/channel/${this.channelId}`, this.url).toString();
400
-
401
- if (this.onReconnect) {
402
- await this.onReconnect(this);
403
- }
404
-
405
- await this.connect();
406
- this.logger.log?.("[SSE] Reconnection successful!");
407
- } catch (error) {
408
- this.logger.error?.(`[SSE] Reconnection failed: ${String(error)}`);
409
- await this.attemptReconnect();
410
- }
411
- }
412
-
413
- async close() {
414
- this.aborted = true;
415
- this.isConnected = false;
416
- this.streamController?.abort();
417
-
418
- try {
419
- const unsubscribes = this.subscriptions.map((sub) => ({
420
- id: sub.id,
421
- action: "unsubscribe",
422
- subscription: sub.id,
423
- }));
424
-
425
- {
426
- const { response, release } = await this.putChannelPayload(unsubscribes, {
427
- timeoutMs: 30_000,
428
- auditContext: "tlon-urbit-unsubscribe",
429
- });
430
- try {
431
- void response.body?.cancel();
432
- } finally {
433
- await release();
434
- }
435
- }
436
-
437
- {
438
- const { response, release } = await urbitFetch({
439
- baseUrl: this.url,
440
- path: `/~/channel/${this.channelId}`,
441
- init: {
442
- method: "DELETE",
443
- headers: {
444
- Cookie: this.cookie,
445
- },
446
- },
447
- ssrfPolicy: this.ssrfPolicy,
448
- lookupFn: this.lookupFn,
449
- fetchImpl: this.fetchImpl,
450
- timeoutMs: 30_000,
451
- auditContext: "tlon-urbit-channel-close",
452
- });
453
- try {
454
- void response.body?.cancel();
455
- } finally {
456
- await release();
457
- }
458
- }
459
- } catch (error) {
460
- this.logger.error?.(`Error closing channel: ${String(error)}`);
461
- }
462
-
463
- if (this.streamRelease) {
464
- const release = this.streamRelease;
465
- this.streamRelease = null;
466
- await release();
467
- }
468
- }
469
-
470
- private async putChannelPayload(
471
- payload: unknown,
472
- params: { timeoutMs: number; auditContext: string },
473
- ) {
474
- return await urbitFetch({
475
- baseUrl: this.url,
476
- path: `/~/channel/${this.channelId}`,
477
- init: {
478
- method: "PUT",
479
- headers: {
480
- "Content-Type": "application/json",
481
- Cookie: this.cookie,
482
- },
483
- body: JSON.stringify(payload),
484
- },
485
- ssrfPolicy: this.ssrfPolicy,
486
- lookupFn: this.lookupFn,
487
- fetchImpl: this.fetchImpl,
488
- timeoutMs: params.timeoutMs,
489
- auditContext: params.auditContext,
490
- });
491
- }
492
- }