@kodelyth/tlon 2026.5.42 → 2026.6.2
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/klaw.plugin.json +203 -3
- package/package.json +19 -6
- package/api.ts +0 -16
- package/channel-plugin-api.ts +0 -1
- package/doctor-contract-api.ts +0 -1
- package/index.ts +0 -16
- package/runtime-api.ts +0 -17
- package/setup-api.ts +0 -2
- package/setup-entry.ts +0 -9
- package/src/account-fields.ts +0 -31
- package/src/channel.message-adapter.test.ts +0 -145
- package/src/channel.runtime.ts +0 -259
- package/src/channel.ts +0 -192
- package/src/config-schema.ts +0 -54
- package/src/core.test.ts +0 -298
- package/src/doctor-contract.ts +0 -9
- package/src/doctor.test.ts +0 -46
- package/src/doctor.ts +0 -10
- package/src/logger-runtime.ts +0 -1
- package/src/monitor/approval-runtime.ts +0 -363
- package/src/monitor/approval.test.ts +0 -33
- package/src/monitor/approval.ts +0 -283
- package/src/monitor/authorization.ts +0 -30
- package/src/monitor/cites.ts +0 -54
- package/src/monitor/discovery.ts +0 -68
- package/src/monitor/history.ts +0 -226
- package/src/monitor/index.ts +0 -1523
- package/src/monitor/media.test.ts +0 -80
- package/src/monitor/media.ts +0 -156
- package/src/monitor/processed-messages.test.ts +0 -58
- package/src/monitor/processed-messages.ts +0 -89
- package/src/monitor/settings-helpers.test.ts +0 -113
- package/src/monitor/settings-helpers.ts +0 -158
- package/src/monitor/utils.ts +0 -402
- package/src/runtime.ts +0 -9
- package/src/security.test.ts +0 -658
- package/src/session-route.ts +0 -40
- package/src/settings.ts +0 -391
- package/src/setup-core.ts +0 -231
- package/src/setup-surface.ts +0 -99
- package/src/targets.ts +0 -102
- package/src/tlon-api.test.ts +0 -572
- package/src/tlon-api.ts +0 -389
- package/src/types.ts +0 -160
- package/src/urbit/auth.ssrf.test.ts +0 -45
- package/src/urbit/auth.ts +0 -48
- package/src/urbit/base-url.test.ts +0 -48
- package/src/urbit/base-url.ts +0 -61
- package/src/urbit/channel-ops.test.ts +0 -36
- package/src/urbit/channel-ops.ts +0 -149
- package/src/urbit/context.ts +0 -50
- package/src/urbit/errors.ts +0 -51
- package/src/urbit/fetch.ts +0 -38
- package/src/urbit/foreigns.ts +0 -49
- package/src/urbit/send.test.ts +0 -83
- package/src/urbit/send.ts +0 -228
- package/src/urbit/sse-client.test.ts +0 -234
- package/src/urbit/sse-client.ts +0 -492
- package/src/urbit/story.ts +0 -332
- package/src/urbit/upload.test.ts +0 -155
- package/src/urbit/upload.ts +0 -60
- package/test-api.ts +0 -1
- package/tsconfig.json +0 -16
package/src/urbit/sse-client.ts
DELETED
|
@@ -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
|
-
}
|