@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
package/src/urbit/send.ts DELETED
@@ -1,228 +0,0 @@
1
- import { scot, da } from "@urbit/aura";
2
- import {
3
- createMessageReceiptFromOutboundResults,
4
- type MessageReceiptPartKind,
5
- } from "klaw/plugin-sdk/channel-message";
6
- import { markdownToStory, createImageBlock, isImageUrl, type Story } from "./story.js";
7
-
8
- export type TlonPokeApi = {
9
- poke: (params: { app: string; mark: string; json: unknown }) => Promise<unknown>;
10
- };
11
-
12
- type SendTextParams = {
13
- api: TlonPokeApi;
14
- fromShip: string;
15
- toShip: string;
16
- text: string;
17
- };
18
-
19
- type SendStoryParams = {
20
- api: TlonPokeApi;
21
- fromShip: string;
22
- toShip: string;
23
- story: Story;
24
- kind?: MessageReceiptPartKind;
25
- };
26
-
27
- function createTlonSendReceipt(params: {
28
- messageId: string;
29
- conversationId: string;
30
- kind: MessageReceiptPartKind;
31
- }) {
32
- return createMessageReceiptFromOutboundResults({
33
- results: [
34
- {
35
- channel: "tlon",
36
- messageId: params.messageId,
37
- conversationId: params.conversationId,
38
- },
39
- ],
40
- threadId: params.conversationId,
41
- kind: params.kind,
42
- });
43
- }
44
-
45
- export async function sendDm({ api, fromShip, toShip, text }: SendTextParams) {
46
- const story: Story = markdownToStory(text);
47
- return sendDmWithStory({ api, fromShip, toShip, story, kind: "text" });
48
- }
49
-
50
- export async function sendDmWithStory({
51
- api,
52
- fromShip,
53
- toShip,
54
- story,
55
- kind = "unknown",
56
- }: SendStoryParams) {
57
- const sentAt = Date.now();
58
- const idUd = scot("ud", da.fromUnix(sentAt));
59
- const id = `${fromShip}/${idUd}`;
60
-
61
- const delta = {
62
- add: {
63
- memo: {
64
- content: story,
65
- author: fromShip,
66
- sent: sentAt,
67
- },
68
- kind: null,
69
- time: null,
70
- },
71
- };
72
-
73
- const action = {
74
- ship: toShip,
75
- diff: { id, delta },
76
- };
77
-
78
- await api.poke({
79
- app: "chat",
80
- mark: "chat-dm-action",
81
- json: action,
82
- });
83
-
84
- return {
85
- channel: "tlon",
86
- messageId: id,
87
- receipt: createTlonSendReceipt({ messageId: id, conversationId: toShip, kind }),
88
- };
89
- }
90
-
91
- type SendGroupParams = {
92
- api: TlonPokeApi;
93
- fromShip: string;
94
- hostShip: string;
95
- channelName: string;
96
- text: string;
97
- replyToId?: string | null;
98
- };
99
-
100
- type SendGroupStoryParams = {
101
- api: TlonPokeApi;
102
- fromShip: string;
103
- hostShip: string;
104
- channelName: string;
105
- story: Story;
106
- replyToId?: string | null;
107
- kind?: MessageReceiptPartKind;
108
- };
109
-
110
- export async function sendGroupMessage({
111
- api,
112
- fromShip,
113
- hostShip,
114
- channelName,
115
- text,
116
- replyToId,
117
- }: SendGroupParams) {
118
- const story: Story = markdownToStory(text);
119
- return sendGroupMessageWithStory({
120
- api,
121
- fromShip,
122
- hostShip,
123
- channelName,
124
- story,
125
- replyToId,
126
- kind: "text",
127
- });
128
- }
129
-
130
- export async function sendGroupMessageWithStory({
131
- api,
132
- fromShip,
133
- hostShip,
134
- channelName,
135
- story,
136
- replyToId,
137
- kind = "unknown",
138
- }: SendGroupStoryParams) {
139
- const sentAt = Date.now();
140
-
141
- // Format reply ID as @ud (with dots) - required for Tlon to recognize thread replies
142
- let formattedReplyId = replyToId;
143
- if (replyToId && /^\d+$/.test(replyToId)) {
144
- try {
145
- // scot('ud', n) formats a number as @ud with dots
146
- formattedReplyId = scot("ud", BigInt(replyToId));
147
- } catch {
148
- // Fall back to raw ID if formatting fails
149
- }
150
- }
151
-
152
- const action = {
153
- channel: {
154
- nest: `chat/${hostShip}/${channelName}`,
155
- action: formattedReplyId
156
- ? {
157
- // Thread reply - needs post wrapper around reply action
158
- // ReplyActionAdd takes Memo: {content, author, sent} - no kind/blob/meta
159
- post: {
160
- reply: {
161
- id: formattedReplyId,
162
- action: {
163
- add: {
164
- content: story,
165
- author: fromShip,
166
- sent: sentAt,
167
- },
168
- },
169
- },
170
- },
171
- }
172
- : {
173
- // Regular post
174
- post: {
175
- add: {
176
- content: story,
177
- author: fromShip,
178
- sent: sentAt,
179
- kind: "/chat",
180
- blob: null,
181
- meta: null,
182
- },
183
- },
184
- },
185
- },
186
- };
187
-
188
- await api.poke({
189
- app: "channels",
190
- mark: "channel-action-1",
191
- json: action,
192
- });
193
-
194
- const messageId = `${fromShip}/${sentAt}`;
195
- return {
196
- channel: "tlon",
197
- messageId,
198
- receipt: createTlonSendReceipt({
199
- messageId,
200
- conversationId: `${hostShip}/${channelName}`,
201
- kind,
202
- }),
203
- };
204
- }
205
-
206
- /**
207
- * Build a story with text and optional media (image)
208
- */
209
- export function buildMediaStory(text: string | undefined, mediaUrl: string | undefined): Story {
210
- const story: Story = [];
211
- const cleanText = text?.trim() ?? "";
212
- const cleanUrl = mediaUrl?.trim() ?? "";
213
-
214
- // Add text content if present
215
- if (cleanText) {
216
- story.push(...markdownToStory(cleanText));
217
- }
218
-
219
- // Add image block if URL looks like an image
220
- if (cleanUrl && isImageUrl(cleanUrl)) {
221
- story.push(createImageBlock(cleanUrl, ""));
222
- } else if (cleanUrl) {
223
- // For non-image URLs, add as a link
224
- story.push({ inline: [{ link: { href: cleanUrl, content: cleanUrl } }] });
225
- }
226
-
227
- return story.length > 0 ? story : [{ inline: [""] }];
228
- }
@@ -1,234 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { urbitFetch } from "./fetch.js";
3
- import { UrbitSSEClient } from "./sse-client.js";
4
-
5
- // Mock urbitFetch to avoid real network calls
6
- vi.mock("./fetch.js", () => ({
7
- urbitFetch: vi.fn(),
8
- }));
9
-
10
- // Mock channel-ops to avoid real channel operations
11
- vi.mock("./channel-ops.js", () => ({
12
- ensureUrbitChannelOpen: vi.fn().mockResolvedValue(undefined),
13
- pokeUrbitChannel: vi.fn().mockResolvedValue(undefined),
14
- scryUrbitPath: vi.fn().mockResolvedValue({}),
15
- }));
16
-
17
- function requireFirstMockCall(calls: readonly unknown[][], label: string): unknown[] {
18
- const call = calls.at(0);
19
- if (!call) {
20
- throw new Error(`Expected ${label} call`);
21
- }
22
- return call;
23
- }
24
-
25
- describe("UrbitSSEClient", () => {
26
- beforeEach(() => {
27
- vi.clearAllMocks();
28
- });
29
-
30
- afterEach(() => {
31
- vi.restoreAllMocks();
32
- });
33
-
34
- describe("subscribe", () => {
35
- it("sends subscriptions added after connect", async () => {
36
- const mockUrbitFetch = vi.mocked(urbitFetch);
37
- mockUrbitFetch.mockResolvedValue({
38
- response: { ok: true, status: 200 } as unknown as Response,
39
- finalUrl: "https://example.com",
40
- release: vi.fn().mockResolvedValue(undefined),
41
- });
42
-
43
- const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
44
- // Simulate connected state
45
- (client as { isConnected: boolean }).isConnected = true;
46
-
47
- await client.subscribe({
48
- app: "chat",
49
- path: "/dm/~zod",
50
- event: () => {},
51
- });
52
-
53
- expect(mockUrbitFetch).toHaveBeenCalledTimes(1);
54
- const callArgs = requireFirstMockCall(mockUrbitFetch.mock.calls, "urbit fetch")[0] as
55
- | Parameters<typeof urbitFetch>[0]
56
- | undefined;
57
- if (!callArgs) {
58
- throw new Error("Expected urbit fetch arguments");
59
- }
60
- expect(callArgs.path).toContain("/~/channel/");
61
- expect(callArgs.init?.method).toBe("PUT");
62
-
63
- const body = JSON.parse(callArgs.init?.body as string);
64
- expect(body).toHaveLength(1);
65
- expect(body[0]).toEqual({
66
- id: 1,
67
- action: "subscribe",
68
- ship: "example",
69
- app: "chat",
70
- path: "/dm/~zod",
71
- });
72
- });
73
-
74
- it("queues subscriptions before connect", async () => {
75
- const mockUrbitFetch = vi.mocked(urbitFetch);
76
-
77
- const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
78
- // Not connected yet
79
-
80
- await client.subscribe({
81
- app: "chat",
82
- path: "/dm/~zod",
83
- event: () => {},
84
- });
85
-
86
- // Should not call urbitFetch since not connected
87
- expect(mockUrbitFetch).not.toHaveBeenCalled();
88
- // But subscription should be queued
89
- expect(client.subscriptions).toHaveLength(1);
90
- expect(client.subscriptions[0]).toEqual({
91
- id: 1,
92
- action: "subscribe",
93
- ship: "example",
94
- app: "chat",
95
- path: "/dm/~zod",
96
- });
97
- });
98
- });
99
-
100
- describe("updateCookie", () => {
101
- it("normalizes cookie when updating", () => {
102
- const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
103
-
104
- // Cookie with extra parts that should be stripped
105
- client.updateCookie("urbauth-~zod=456; Path=/; HttpOnly");
106
-
107
- expect(client.cookie).toBe("urbauth-~zod=456");
108
- });
109
-
110
- it("handles simple cookie values", () => {
111
- const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
112
-
113
- client.updateCookie("urbauth-~zod=newvalue");
114
-
115
- expect(client.cookie).toBe("urbauth-~zod=newvalue");
116
- });
117
- });
118
-
119
- describe("reconnection", () => {
120
- it("has autoReconnect enabled by default", () => {
121
- const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
122
- expect(client.autoReconnect).toBe(true);
123
- });
124
-
125
- it("can disable autoReconnect via options", () => {
126
- const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123", {
127
- autoReconnect: false,
128
- });
129
- expect(client.autoReconnect).toBe(false);
130
- });
131
-
132
- it("stores onReconnect callback", () => {
133
- const onReconnect = vi.fn();
134
- const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123", {
135
- onReconnect,
136
- });
137
- expect(client.onReconnect).toBe(onReconnect);
138
- });
139
-
140
- it("resets reconnect attempts on successful connect", async () => {
141
- const mockUrbitFetch = vi.mocked(urbitFetch);
142
-
143
- // Mock a response that returns a readable stream
144
- const mockStream = new ReadableStream({
145
- start(controller) {
146
- controller.close();
147
- },
148
- });
149
-
150
- mockUrbitFetch.mockResolvedValue({
151
- response: {
152
- ok: true,
153
- status: 200,
154
- body: mockStream,
155
- } as unknown as Response,
156
- finalUrl: "https://example.com",
157
- release: vi.fn().mockResolvedValue(undefined),
158
- });
159
-
160
- const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123", {
161
- autoReconnect: false, // Disable to prevent reconnect loop
162
- });
163
- client.reconnectAttempts = 5;
164
-
165
- await client.connect();
166
-
167
- expect(client.reconnectAttempts).toBe(0);
168
- });
169
- });
170
-
171
- describe("event acking", () => {
172
- it("logs malformed SSE JSON with an owned parser error", () => {
173
- const logger = { error: vi.fn() };
174
- const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123", {
175
- logger,
176
- });
177
-
178
- client.processEvent("id: 1\ndata: {not json");
179
-
180
- expect(logger.error).toHaveBeenCalledWith(
181
- "Error parsing SSE event: Error: Tlon Urbit SSE event was malformed JSON",
182
- );
183
- });
184
-
185
- it("tracks lastHeardEventId and ackThreshold", () => {
186
- const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
187
-
188
- // Access private properties for testing
189
- const lastHeardEventId = (client as unknown as { lastHeardEventId: number }).lastHeardEventId;
190
- const ackThreshold = (client as unknown as { ackThreshold: number }).ackThreshold;
191
-
192
- expect(lastHeardEventId).toBe(-1);
193
- expect(ackThreshold).toBeGreaterThan(0);
194
- });
195
- });
196
-
197
- describe("constructor", () => {
198
- it("generates unique channel ID", () => {
199
- const client1 = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
200
- const client2 = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
201
-
202
- expect(client1.channelId).not.toBe(client2.channelId);
203
- });
204
-
205
- it("normalizes cookie in constructor", () => {
206
- const client = new UrbitSSEClient(
207
- "https://example.com",
208
- "urbauth-~zod=123; Path=/; HttpOnly",
209
- );
210
-
211
- expect(client.cookie).toBe("urbauth-~zod=123");
212
- });
213
-
214
- it("sets default reconnection parameters", () => {
215
- const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123");
216
-
217
- expect(client.maxReconnectAttempts).toBe(10);
218
- expect(client.reconnectDelay).toBe(1000);
219
- expect(client.maxReconnectDelay).toBe(30000);
220
- });
221
-
222
- it("allows overriding reconnection parameters", () => {
223
- const client = new UrbitSSEClient("https://example.com", "urbauth-~zod=123", {
224
- maxReconnectAttempts: 5,
225
- reconnectDelay: 500,
226
- maxReconnectDelay: 10000,
227
- });
228
-
229
- expect(client.maxReconnectAttempts).toBe(5);
230
- expect(client.reconnectDelay).toBe(500);
231
- expect(client.maxReconnectDelay).toBe(10000);
232
- });
233
- });
234
- });