@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,332 +0,0 @@
1
- /**
2
- * Tlon Story Format - Rich text converter
3
- *
4
- * Converts markdown-like text to Tlon's story format.
5
- */
6
-
7
- // Inline content types
8
- type StoryInline =
9
- | string
10
- | { bold: StoryInline[] }
11
- | { italics: StoryInline[] }
12
- | { strike: StoryInline[] }
13
- | { blockquote: StoryInline[] }
14
- | { "inline-code": string }
15
- | { code: string }
16
- | { ship: string }
17
- | { link: { href: string; content: string } }
18
- | { break: null }
19
- | { tag: string };
20
-
21
- // Block content types
22
- type StoryBlock =
23
- | { header: { tag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; content: StoryInline[] } }
24
- | { code: { code: string; lang: string } }
25
- | { image: { src: string; height: number; width: number; alt: string } }
26
- | { rule: null }
27
- | { listing: StoryListing };
28
-
29
- type StoryListing =
30
- | {
31
- list: {
32
- type: "ordered" | "unordered" | "tasklist";
33
- items: StoryListing[];
34
- contents: StoryInline[];
35
- };
36
- }
37
- | { item: StoryInline[] };
38
-
39
- // A verse is either a block or inline content
40
- type StoryVerse = { block: StoryBlock } | { inline: StoryInline[] };
41
-
42
- // A story is a list of verses
43
- export type Story = StoryVerse[];
44
-
45
- /**
46
- * Parse inline markdown formatting (bold, italic, code, links, mentions)
47
- */
48
- function parseInlineMarkdown(text: string): StoryInline[] {
49
- const result: StoryInline[] = [];
50
- let remaining = text;
51
-
52
- while (remaining.length > 0) {
53
- // Ship mentions: ~sampel-palnet
54
- const shipMatch = remaining.match(/^(~[a-z][-a-z0-9]*)/);
55
- if (shipMatch) {
56
- result.push({ ship: shipMatch[1] });
57
- remaining = remaining.slice(shipMatch[0].length);
58
- continue;
59
- }
60
-
61
- // Bold: **text** or __text__
62
- const boldMatch = remaining.match(/^\*\*(.+?)\*\*|^__(.+?)__/);
63
- if (boldMatch) {
64
- const content = boldMatch[1] || boldMatch[2];
65
- result.push({ bold: parseInlineMarkdown(content) });
66
- remaining = remaining.slice(boldMatch[0].length);
67
- continue;
68
- }
69
-
70
- // Italics: *text* or _text_ (but not inside words for _)
71
- const italicsMatch = remaining.match(/^\*([^*]+?)\*|^_([^_]+?)_(?![a-zA-Z0-9])/);
72
- if (italicsMatch) {
73
- const content = italicsMatch[1] || italicsMatch[2];
74
- result.push({ italics: parseInlineMarkdown(content) });
75
- remaining = remaining.slice(italicsMatch[0].length);
76
- continue;
77
- }
78
-
79
- // Strikethrough: ~~text~~
80
- const strikeMatch = remaining.match(/^~~(.+?)~~/);
81
- if (strikeMatch) {
82
- result.push({ strike: parseInlineMarkdown(strikeMatch[1]) });
83
- remaining = remaining.slice(strikeMatch[0].length);
84
- continue;
85
- }
86
-
87
- // Inline code: `code`
88
- const codeMatch = remaining.match(/^`([^`]+)`/);
89
- if (codeMatch) {
90
- result.push({ "inline-code": codeMatch[1] });
91
- remaining = remaining.slice(codeMatch[0].length);
92
- continue;
93
- }
94
-
95
- // Links: [text](url)
96
- const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/);
97
- if (linkMatch) {
98
- result.push({ link: { href: linkMatch[2], content: linkMatch[1] } });
99
- remaining = remaining.slice(linkMatch[0].length);
100
- continue;
101
- }
102
-
103
- // Markdown images: ![alt](url)
104
- const imageMatch = remaining.match(/^!\[([^\]]*)\]\(([^)]+)\)/);
105
- if (imageMatch) {
106
- // Return a special marker that will be hoisted to a block
107
- result.push({
108
- __image: { src: imageMatch[2], alt: imageMatch[1] },
109
- } as unknown as StoryInline);
110
- remaining = remaining.slice(imageMatch[0].length);
111
- continue;
112
- }
113
-
114
- // Plain URL detection
115
- const urlMatch = remaining.match(/^(https?:\/\/[^\s<>"\]]+)/);
116
- if (urlMatch) {
117
- result.push({ link: { href: urlMatch[1], content: urlMatch[1] } });
118
- remaining = remaining.slice(urlMatch[0].length);
119
- continue;
120
- }
121
-
122
- // Hashtags: #tag - disabled, chat UI doesn't render them
123
- // const tagMatch = remaining.match(/^#([a-zA-Z][a-zA-Z0-9_-]*)/);
124
- // if (tagMatch) {
125
- // result.push({ tag: tagMatch[1] });
126
- // remaining = remaining.slice(tagMatch[0].length);
127
- // continue;
128
- // }
129
-
130
- // Plain text: consume until next special character or URL start
131
- // Exclude : and / to allow URL detection to work (stops before https://)
132
- const plainMatch = remaining.match(/^[^*_`~[#~\n:/]+/);
133
- if (plainMatch) {
134
- result.push(plainMatch[0]);
135
- remaining = remaining.slice(plainMatch[0].length);
136
- continue;
137
- }
138
-
139
- // Single special char that didn't match a pattern
140
- result.push(remaining[0]);
141
- remaining = remaining.slice(1);
142
- }
143
-
144
- // Merge adjacent strings
145
- return mergeAdjacentStrings(result);
146
- }
147
-
148
- /**
149
- * Merge adjacent string elements in an inline array
150
- */
151
- function mergeAdjacentStrings(inlines: StoryInline[]): StoryInline[] {
152
- const result: StoryInline[] = [];
153
- for (const item of inlines) {
154
- if (typeof item === "string" && typeof result[result.length - 1] === "string") {
155
- result[result.length - 1] = (result[result.length - 1] as string) + item;
156
- } else {
157
- result.push(item);
158
- }
159
- }
160
- return result;
161
- }
162
-
163
- /**
164
- * Create an image block
165
- */
166
- export function createImageBlock(
167
- src: string,
168
- alt: string = "",
169
- height: number = 0,
170
- width: number = 0,
171
- ): StoryVerse {
172
- return {
173
- block: {
174
- image: { src, height, width, alt },
175
- },
176
- };
177
- }
178
-
179
- /**
180
- * Check if URL looks like an image
181
- */
182
- export function isImageUrl(url: string): boolean {
183
- const imageExtensions = /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?.*)?$/i;
184
- return imageExtensions.test(url);
185
- }
186
-
187
- /**
188
- * Process inlines and extract any image markers into blocks
189
- */
190
- function processInlinesForImages(inlines: StoryInline[]): {
191
- inlines: StoryInline[];
192
- imageBlocks: StoryVerse[];
193
- } {
194
- const cleanInlines: StoryInline[] = [];
195
- const imageBlocks: StoryVerse[] = [];
196
-
197
- for (const inline of inlines) {
198
- if (typeof inline === "object" && "__image" in inline) {
199
- const img = (inline as unknown as { __image: { src: string; alt: string } })["__image"];
200
- imageBlocks.push(createImageBlock(img.src, img.alt));
201
- } else {
202
- cleanInlines.push(inline);
203
- }
204
- }
205
-
206
- return { inlines: cleanInlines, imageBlocks };
207
- }
208
-
209
- /**
210
- * Convert markdown text to Tlon story format
211
- */
212
- export function markdownToStory(markdown: string): Story {
213
- const story: Story = [];
214
- const lines = markdown.split("\n");
215
- let i = 0;
216
-
217
- while (i < lines.length) {
218
- const line = lines[i];
219
-
220
- // Code block: ```lang\ncode\n```
221
- if (line.startsWith("```")) {
222
- const lang = line.slice(3).trim() || "plaintext";
223
- const codeLines: string[] = [];
224
- i++;
225
- while (i < lines.length && !lines[i].startsWith("```")) {
226
- codeLines.push(lines[i]);
227
- i++;
228
- }
229
- story.push({
230
- block: {
231
- code: {
232
- code: codeLines.join("\n"),
233
- lang,
234
- },
235
- },
236
- });
237
- i++; // skip closing ```
238
- continue;
239
- }
240
-
241
- // Headers: # H1, ## H2, etc.
242
- const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
243
- if (headerMatch) {
244
- const level = headerMatch[1].length as 1 | 2 | 3 | 4 | 5 | 6;
245
- const tag = `h${level}` as const;
246
- story.push({
247
- block: {
248
- header: {
249
- tag,
250
- content: parseInlineMarkdown(headerMatch[2]),
251
- },
252
- },
253
- });
254
- i++;
255
- continue;
256
- }
257
-
258
- // Horizontal rule: --- or ***
259
- if (/^(-{3,}|\*{3,})$/.test(line.trim())) {
260
- story.push({ block: { rule: null } });
261
- i++;
262
- continue;
263
- }
264
-
265
- // Blockquote: > text
266
- if (line.startsWith("> ")) {
267
- const quoteLines: string[] = [];
268
- while (i < lines.length && lines[i].startsWith("> ")) {
269
- quoteLines.push(lines[i].slice(2));
270
- i++;
271
- }
272
- const quoteText = quoteLines.join("\n");
273
- story.push({
274
- inline: [{ blockquote: parseInlineMarkdown(quoteText) }],
275
- });
276
- continue;
277
- }
278
-
279
- // Empty line - skip
280
- if (line.trim() === "") {
281
- i++;
282
- continue;
283
- }
284
-
285
- // Regular paragraph - collect consecutive non-empty lines
286
- const paragraphLines: string[] = [];
287
- while (
288
- i < lines.length &&
289
- lines[i].trim() !== "" &&
290
- !lines[i].startsWith("#") &&
291
- !lines[i].startsWith("```") &&
292
- !lines[i].startsWith("> ") &&
293
- !/^(-{3,}|\*{3,})$/.test(lines[i].trim())
294
- ) {
295
- paragraphLines.push(lines[i]);
296
- i++;
297
- }
298
-
299
- if (paragraphLines.length > 0) {
300
- const paragraphText = paragraphLines.join("\n");
301
- // Convert newlines within paragraph to break elements
302
- const inlines = parseInlineMarkdown(paragraphText);
303
- // Replace \n in strings with break elements
304
- const withBreaks: StoryInline[] = [];
305
- for (const inline of inlines) {
306
- if (typeof inline === "string" && inline.includes("\n")) {
307
- const parts = inline.split("\n");
308
- for (let j = 0; j < parts.length; j++) {
309
- if (parts[j]) {
310
- withBreaks.push(parts[j]);
311
- }
312
- if (j < parts.length - 1) {
313
- withBreaks.push({ break: null });
314
- }
315
- }
316
- } else {
317
- withBreaks.push(inline);
318
- }
319
- }
320
-
321
- // Extract any images from inlines and add as separate blocks
322
- const { inlines: cleanInlines, imageBlocks } = processInlinesForImages(withBreaks);
323
-
324
- if (cleanInlines.length > 0) {
325
- story.push({ inline: cleanInlines });
326
- }
327
- story.push(...imageBlocks);
328
- }
329
- }
330
-
331
- return story;
332
- }
@@ -1,155 +0,0 @@
1
- import { fetchWithSsrFGuard } from "klaw/plugin-sdk/ssrf-runtime";
2
- import { describe, expect, it, vi, beforeEach } from "vitest";
3
- import { uploadFile } from "../tlon-api.js";
4
- import { uploadImageFromUrl } from "./upload.js";
5
-
6
- vi.mock("klaw/plugin-sdk/ssrf-runtime", () => ({
7
- fetchWithSsrFGuard: vi.fn(),
8
- }));
9
-
10
- vi.mock("../tlon-api.js", () => ({
11
- uploadFile: vi.fn(),
12
- }));
13
-
14
- const mockFetch = vi.mocked(fetchWithSsrFGuard);
15
- const mockUploadFile = vi.mocked(uploadFile);
16
-
17
- type FetchMock = typeof mockFetch;
18
-
19
- function mockSuccessfulFetch(params: {
20
- mockFetch: FetchMock;
21
- blob: Blob;
22
- finalUrl: string;
23
- contentType: string;
24
- }) {
25
- params.mockFetch.mockResolvedValue({
26
- response: {
27
- ok: true,
28
- headers: new Headers({ "content-type": params.contentType }),
29
- blob: () => Promise.resolve(params.blob),
30
- } as unknown as Response,
31
- finalUrl: params.finalUrl,
32
- release: vi.fn().mockResolvedValue(undefined),
33
- });
34
- }
35
-
36
- async function setupSuccessfulUpload(params?: {
37
- sourceUrl?: string;
38
- contentType?: string;
39
- uploadedUrl?: string;
40
- }) {
41
- const sourceUrl = params?.sourceUrl ?? "https://example.com/image.png";
42
- const contentType = params?.contentType ?? "image/png";
43
- const mockBlob = new Blob(["fake-image"], { type: contentType });
44
- mockSuccessfulFetch({
45
- mockFetch,
46
- blob: mockBlob,
47
- finalUrl: sourceUrl,
48
- contentType,
49
- });
50
- if (params?.uploadedUrl) {
51
- mockUploadFile.mockResolvedValue({ url: params.uploadedUrl });
52
- }
53
- return { mockBlob };
54
- }
55
-
56
- function requireUploadParams(): { blob?: Blob; contentType?: string; fileName?: string } {
57
- const [call] = mockUploadFile.mock.calls;
58
- if (!call) {
59
- throw new Error("expected Tlon uploadFile call");
60
- }
61
- const [uploadParams] = call;
62
- if (!uploadParams || typeof uploadParams !== "object" || Array.isArray(uploadParams)) {
63
- throw new Error("expected Tlon uploadFile params");
64
- }
65
- return uploadParams as { blob?: Blob; contentType?: string; fileName?: string };
66
- }
67
-
68
- describe("uploadImageFromUrl", () => {
69
- beforeEach(() => {
70
- vi.clearAllMocks();
71
- });
72
-
73
- it("fetches image and calls uploadFile, returns uploaded URL", async () => {
74
- const { mockBlob } = await setupSuccessfulUpload({
75
- uploadedUrl: "https://memex.tlon.network/uploaded.png",
76
- });
77
-
78
- const result = await uploadImageFromUrl("https://example.com/image.png");
79
-
80
- expect(result).toBe("https://memex.tlon.network/uploaded.png");
81
- expect(mockUploadFile).toHaveBeenCalledTimes(1);
82
- const uploadParams = requireUploadParams();
83
- expect(uploadParams.blob).toBe(mockBlob);
84
- expect(uploadParams.contentType).toBe("image/png");
85
- });
86
-
87
- it("returns original URL if fetch fails", async () => {
88
- mockFetch.mockResolvedValue({
89
- response: {
90
- ok: false,
91
- status: 404,
92
- } as unknown as Response,
93
- finalUrl: "https://example.com/image.png",
94
- release: vi.fn().mockResolvedValue(undefined),
95
- });
96
-
97
- const result = await uploadImageFromUrl("https://example.com/image.png");
98
-
99
- expect(result).toBe("https://example.com/image.png");
100
- });
101
-
102
- it("returns original URL if upload fails", async () => {
103
- await setupSuccessfulUpload();
104
- mockUploadFile.mockRejectedValue(new Error("Upload failed"));
105
-
106
- const result = await uploadImageFromUrl("https://example.com/image.png");
107
-
108
- expect(result).toBe("https://example.com/image.png");
109
- });
110
-
111
- it("rejects non-http(s) URLs", async () => {
112
- const result = await uploadImageFromUrl("file:///etc/passwd");
113
- expect(result).toBe("file:///etc/passwd");
114
-
115
- const result2 = await uploadImageFromUrl("ftp://example.com/image.png");
116
- expect(result2).toBe("ftp://example.com/image.png");
117
- });
118
-
119
- it("handles invalid URLs gracefully", async () => {
120
- const result = await uploadImageFromUrl("not-a-valid-url");
121
- expect(result).toBe("not-a-valid-url");
122
- });
123
-
124
- it("extracts filename from URL path", async () => {
125
- const mockBlob = new Blob(["fake-image"], { type: "image/jpeg" });
126
- mockSuccessfulFetch({
127
- mockFetch,
128
- blob: mockBlob,
129
- finalUrl: "https://example.com/path/to/my-image.jpg",
130
- contentType: "image/jpeg",
131
- });
132
-
133
- mockUploadFile.mockResolvedValue({ url: "https://memex.tlon.network/uploaded.jpg" });
134
-
135
- await uploadImageFromUrl("https://example.com/path/to/my-image.jpg");
136
-
137
- expect(requireUploadParams().fileName).toBe("my-image.jpg");
138
- });
139
-
140
- it("uses default filename when URL has no path", async () => {
141
- const mockBlob = new Blob(["fake-image"], { type: "image/png" });
142
- mockSuccessfulFetch({
143
- mockFetch,
144
- blob: mockBlob,
145
- finalUrl: "https://example.com/",
146
- contentType: "image/png",
147
- });
148
-
149
- mockUploadFile.mockResolvedValue({ url: "https://memex.tlon.network/uploaded.png" });
150
-
151
- await uploadImageFromUrl("https://example.com/");
152
-
153
- expect(requireUploadParams().fileName).toMatch(/^upload-\d+\.png$/);
154
- });
155
- });
@@ -1,60 +0,0 @@
1
- /**
2
- * Upload an image from a URL to Tlon storage.
3
- */
4
- import { fetchWithSsrFGuard } from "klaw/plugin-sdk/ssrf-runtime";
5
- import { uploadFile } from "../tlon-api.js";
6
- import { getDefaultSsrFPolicy } from "./context.js";
7
-
8
- /**
9
- * Fetch an image from a URL and upload it to Tlon storage.
10
- * Returns the uploaded URL, or falls back to the original URL on error.
11
- *
12
- * Note: configureClient must be called before using this function.
13
- */
14
- export async function uploadImageFromUrl(imageUrl: string): Promise<string> {
15
- try {
16
- // Validate URL is http/https before fetching
17
- const url = new URL(imageUrl);
18
- if (url.protocol !== "http:" && url.protocol !== "https:") {
19
- console.warn(`[tlon] Rejected non-http(s) URL: ${imageUrl}`);
20
- return imageUrl;
21
- }
22
-
23
- // Fetch the image with SSRF protection
24
- // Use fetchWithSsrFGuard directly (not urbitFetch) to preserve the full URL path
25
- const { response, release } = await fetchWithSsrFGuard({
26
- url: imageUrl,
27
- init: { method: "GET" },
28
- policy: getDefaultSsrFPolicy(),
29
- auditContext: "tlon-upload-image",
30
- });
31
-
32
- try {
33
- if (!response.ok) {
34
- console.warn(`[tlon] Failed to fetch image from ${imageUrl}: ${response.status}`);
35
- return imageUrl;
36
- }
37
-
38
- const contentType = response.headers.get("content-type") || "image/png";
39
- const blob = await response.blob();
40
-
41
- // Extract filename from URL or use a default
42
- const urlPath = new URL(imageUrl).pathname;
43
- const fileName = urlPath.split("/").pop() || `upload-${Date.now()}.png`;
44
-
45
- // Upload to Tlon storage
46
- const result = await uploadFile({
47
- blob,
48
- fileName,
49
- contentType,
50
- });
51
-
52
- return result.url;
53
- } finally {
54
- await release();
55
- }
56
- } catch (err) {
57
- console.warn(`[tlon] Failed to upload image, using original URL: ${String(err)}`);
58
- return imageUrl;
59
- }
60
- }
package/test-api.ts DELETED
@@ -1 +0,0 @@
1
- export { tlonPlugin } from "./src/channel.js";
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "extends": "../tsconfig.package-boundary.base.json",
3
- "compilerOptions": {
4
- "rootDir": "."
5
- },
6
- "include": ["./*.ts", "./src/**/*.ts"],
7
- "exclude": [
8
- "./**/*.test.ts",
9
- "./dist/**",
10
- "./node_modules/**",
11
- "./src/test-support/**",
12
- "./src/**/*test-helpers.ts",
13
- "./src/**/*test-harness.ts",
14
- "./src/**/*test-support.ts"
15
- ]
16
- }