@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
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
readRemoteMediaBuffer,
|
|
3
|
-
MAX_IMAGE_BYTES,
|
|
4
|
-
saveRemoteMedia,
|
|
5
|
-
} from "klaw/plugin-sdk/media-runtime";
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
-
import { downloadMedia, extractImageBlocks } from "./media.js";
|
|
8
|
-
|
|
9
|
-
vi.mock("klaw/plugin-sdk/media-runtime", () => ({
|
|
10
|
-
MAX_IMAGE_BYTES: 6 * 1024 * 1024,
|
|
11
|
-
readRemoteMediaBuffer: vi.fn(),
|
|
12
|
-
saveRemoteMedia: vi.fn(),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
const readRemoteMediaBufferMock = vi.mocked(readRemoteMediaBuffer);
|
|
16
|
-
const saveRemoteMediaMock = vi.mocked(saveRemoteMedia);
|
|
17
|
-
|
|
18
|
-
describe("tlon monitor media", () => {
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
vi.clearAllMocks();
|
|
21
|
-
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
22
|
-
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
vi.restoreAllMocks();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("caps extracted images at eight per message", () => {
|
|
30
|
-
const content = Array.from({ length: 10 }, (_, index) => ({
|
|
31
|
-
block: { image: { src: `https://example.com/${index}.png`, alt: `image-${index}` } },
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
const images = extractImageBlocks(content);
|
|
35
|
-
|
|
36
|
-
expect(images).toHaveLength(8);
|
|
37
|
-
expect(images.map((image) => image.url)).toEqual(
|
|
38
|
-
Array.from({ length: 8 }, (_, index) => `https://example.com/${index}.png`),
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("stores fetched media through the shared inbound media store with the image cap", async () => {
|
|
43
|
-
saveRemoteMediaMock.mockResolvedValue({
|
|
44
|
-
id: "photo---uuid.png",
|
|
45
|
-
path: "/tmp/klaw/media/inbound/photo---uuid.png",
|
|
46
|
-
size: "image-data".length,
|
|
47
|
-
contentType: "image/png",
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const result = await downloadMedia("https://example.com/photo.png");
|
|
51
|
-
|
|
52
|
-
expect(readRemoteMediaBufferMock).not.toHaveBeenCalled();
|
|
53
|
-
expect(saveRemoteMediaMock).toHaveBeenCalledTimes(1);
|
|
54
|
-
expect(saveRemoteMediaMock).toHaveBeenCalledWith({
|
|
55
|
-
url: "https://example.com/photo.png",
|
|
56
|
-
maxBytes: MAX_IMAGE_BYTES,
|
|
57
|
-
readIdleTimeoutMs: 30_000,
|
|
58
|
-
ssrfPolicy: undefined,
|
|
59
|
-
requestInit: { method: "GET" },
|
|
60
|
-
});
|
|
61
|
-
expect(result).toEqual({
|
|
62
|
-
localPath: "/tmp/klaw/media/inbound/photo---uuid.png",
|
|
63
|
-
contentType: "image/png",
|
|
64
|
-
originalUrl: "https://example.com/photo.png",
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("returns null when the fetch exceeds the image cap", async () => {
|
|
69
|
-
saveRemoteMediaMock.mockRejectedValue(
|
|
70
|
-
new Error(
|
|
71
|
-
`Failed to fetch media from https://example.com/photo.png: payload exceeds maxBytes ${MAX_IMAGE_BYTES}`,
|
|
72
|
-
),
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
const result = await downloadMedia("https://example.com/photo.png");
|
|
76
|
-
|
|
77
|
-
expect(result).toBeNull();
|
|
78
|
-
expect(readRemoteMediaBufferMock).not.toHaveBeenCalled();
|
|
79
|
-
});
|
|
80
|
-
});
|
package/src/monitor/media.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { formatErrorMessage } from "klaw/plugin-sdk/error-runtime";
|
|
5
|
-
import { extensionForMime } from "klaw/plugin-sdk/media-mime";
|
|
6
|
-
import {
|
|
7
|
-
readRemoteMediaBuffer,
|
|
8
|
-
MAX_IMAGE_BYTES,
|
|
9
|
-
saveRemoteMedia,
|
|
10
|
-
} from "klaw/plugin-sdk/media-runtime";
|
|
11
|
-
import { normalizeLowercaseStringOrEmpty } from "klaw/plugin-sdk/string-coerce-runtime";
|
|
12
|
-
import { getDefaultSsrFPolicy } from "../urbit/context.js";
|
|
13
|
-
|
|
14
|
-
const MAX_IMAGES_PER_MESSAGE = 8;
|
|
15
|
-
const TLON_MEDIA_DOWNLOAD_IDLE_TIMEOUT_MS = 30_000;
|
|
16
|
-
|
|
17
|
-
interface ExtractedImage {
|
|
18
|
-
url: string;
|
|
19
|
-
alt?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface DownloadedMedia {
|
|
23
|
-
localPath: string;
|
|
24
|
-
contentType: string;
|
|
25
|
-
originalUrl: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Extract image blocks from Tlon message content.
|
|
30
|
-
* Returns array of image URLs found in the message.
|
|
31
|
-
*/
|
|
32
|
-
export function extractImageBlocks(content: unknown): ExtractedImage[] {
|
|
33
|
-
if (!content || !Array.isArray(content)) {
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const images: ExtractedImage[] = [];
|
|
38
|
-
|
|
39
|
-
for (const verse of content) {
|
|
40
|
-
if (verse?.block?.image?.src) {
|
|
41
|
-
images.push({
|
|
42
|
-
url: verse.block.image.src,
|
|
43
|
-
alt: verse.block.image.alt,
|
|
44
|
-
});
|
|
45
|
-
if (images.length >= MAX_IMAGES_PER_MESSAGE) {
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return images;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Download a media file from URL to local storage.
|
|
56
|
-
* Returns the local path where the file was saved.
|
|
57
|
-
*/
|
|
58
|
-
export async function downloadMedia(
|
|
59
|
-
url: string,
|
|
60
|
-
mediaDir?: string,
|
|
61
|
-
): Promise<DownloadedMedia | null> {
|
|
62
|
-
try {
|
|
63
|
-
// Validate URL is http/https before fetching
|
|
64
|
-
const parsedUrl = new URL(url);
|
|
65
|
-
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
66
|
-
console.warn(`[tlon-media] Rejected non-http(s) URL: ${url}`);
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const fetchOptions = {
|
|
71
|
-
url,
|
|
72
|
-
maxBytes: MAX_IMAGE_BYTES,
|
|
73
|
-
readIdleTimeoutMs: TLON_MEDIA_DOWNLOAD_IDLE_TIMEOUT_MS,
|
|
74
|
-
ssrfPolicy: getDefaultSsrFPolicy(),
|
|
75
|
-
requestInit: { method: "GET" },
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
if (!mediaDir) {
|
|
79
|
-
const saved = await saveRemoteMedia(fetchOptions);
|
|
80
|
-
return {
|
|
81
|
-
localPath: saved.path,
|
|
82
|
-
contentType: saved.contentType ?? "application/octet-stream",
|
|
83
|
-
originalUrl: url,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const fetched = await readRemoteMediaBuffer(fetchOptions);
|
|
88
|
-
await mkdir(mediaDir, { recursive: true });
|
|
89
|
-
const ext =
|
|
90
|
-
getExtensionFromFileName(fetched.fileName) ||
|
|
91
|
-
getExtensionFromContentType(fetched.contentType ?? "") ||
|
|
92
|
-
getExtensionFromUrl(url) ||
|
|
93
|
-
"bin";
|
|
94
|
-
const localPath = path.join(mediaDir, `${randomUUID()}.${ext}`);
|
|
95
|
-
await writeFile(localPath, fetched.buffer);
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
localPath,
|
|
99
|
-
contentType: fetched.contentType ?? "application/octet-stream",
|
|
100
|
-
originalUrl: url,
|
|
101
|
-
};
|
|
102
|
-
} catch (error: unknown) {
|
|
103
|
-
console.error(`[tlon-media] Error downloading ${url}: ${formatErrorMessage(error)}`);
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function getExtensionFromFileName(fileName?: string): string | null {
|
|
109
|
-
if (!fileName) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
const ext = path.extname(fileName).replace(/^\./, "");
|
|
113
|
-
return ext || null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function getExtensionFromContentType(contentType: string): string | null {
|
|
117
|
-
return extensionForMime(contentType)?.replace(/^\./u, "") ?? null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function getExtensionFromUrl(url: string): string | null {
|
|
121
|
-
try {
|
|
122
|
-
const pathname = new URL(url).pathname;
|
|
123
|
-
const match = pathname.match(/\.([a-z0-9]+)$/i);
|
|
124
|
-
return match ? normalizeLowercaseStringOrEmpty(match[1]) : null;
|
|
125
|
-
} catch {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Download all images from a message and return attachment metadata.
|
|
132
|
-
* Format matches Klaw's expected attachment structure.
|
|
133
|
-
*/
|
|
134
|
-
export async function downloadMessageImages(
|
|
135
|
-
content: unknown,
|
|
136
|
-
mediaDir?: string,
|
|
137
|
-
): Promise<Array<{ path: string; contentType: string }>> {
|
|
138
|
-
const images = extractImageBlocks(content);
|
|
139
|
-
if (images.length === 0) {
|
|
140
|
-
return [];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const attachments: Array<{ path: string; contentType: string }> = [];
|
|
144
|
-
|
|
145
|
-
for (const image of images) {
|
|
146
|
-
const downloaded = await downloadMedia(image.url, mediaDir);
|
|
147
|
-
if (downloaded) {
|
|
148
|
-
attachments.push({
|
|
149
|
-
path: downloaded.localPath,
|
|
150
|
-
contentType: downloaded.contentType,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return attachments;
|
|
156
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
createProcessedMessageTracker,
|
|
4
|
-
runWithProcessedMessageClaim,
|
|
5
|
-
} from "./processed-messages.js";
|
|
6
|
-
|
|
7
|
-
describe("createProcessedMessageTracker", () => {
|
|
8
|
-
it("dedupes and evicts oldest entries", () => {
|
|
9
|
-
const tracker = createProcessedMessageTracker(3);
|
|
10
|
-
|
|
11
|
-
expect(tracker.mark("a")).toBe(true);
|
|
12
|
-
expect(tracker.mark("a")).toBe(false);
|
|
13
|
-
expect(tracker.has("a")).toBe(true);
|
|
14
|
-
|
|
15
|
-
tracker.mark("b");
|
|
16
|
-
tracker.mark("c");
|
|
17
|
-
expect(tracker.size()).toBe(3);
|
|
18
|
-
|
|
19
|
-
tracker.mark("d");
|
|
20
|
-
expect(tracker.size()).toBe(3);
|
|
21
|
-
expect(tracker.has("a")).toBe(false);
|
|
22
|
-
expect(tracker.has("b")).toBe(true);
|
|
23
|
-
expect(tracker.has("c")).toBe(true);
|
|
24
|
-
expect(tracker.has("d")).toBe(true);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("releases failed claims so retries can run again", async () => {
|
|
28
|
-
const tracker = createProcessedMessageTracker();
|
|
29
|
-
|
|
30
|
-
await expect(
|
|
31
|
-
runWithProcessedMessageClaim({
|
|
32
|
-
tracker,
|
|
33
|
-
id: "evt-1",
|
|
34
|
-
task: async () => {
|
|
35
|
-
throw new Error("boom");
|
|
36
|
-
},
|
|
37
|
-
}),
|
|
38
|
-
).rejects.toThrow("boom");
|
|
39
|
-
|
|
40
|
-
expect(tracker.has("evt-1")).toBe(false);
|
|
41
|
-
expect(tracker.claim("evt-1")).toEqual({ kind: "claimed" });
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("keeps successful claims deduped", async () => {
|
|
45
|
-
const tracker = createProcessedMessageTracker();
|
|
46
|
-
|
|
47
|
-
await expect(
|
|
48
|
-
runWithProcessedMessageClaim({
|
|
49
|
-
tracker,
|
|
50
|
-
id: "evt-2",
|
|
51
|
-
task: async () => undefined,
|
|
52
|
-
}),
|
|
53
|
-
).resolves.toEqual({ kind: "processed", value: undefined });
|
|
54
|
-
|
|
55
|
-
expect(tracker.has("evt-2")).toBe(true);
|
|
56
|
-
expect(tracker.claim("evt-2")).toEqual({ kind: "duplicate" });
|
|
57
|
-
});
|
|
58
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { createDedupeCache } from "../../runtime-api.js";
|
|
2
|
-
|
|
3
|
-
type ProcessedMessageTracker = {
|
|
4
|
-
claim: (id?: string | null) => { kind: "claimed" } | { kind: "duplicate" };
|
|
5
|
-
commit: (id?: string | null) => void;
|
|
6
|
-
release: (id?: string | null) => void;
|
|
7
|
-
mark: (id?: string | null) => boolean;
|
|
8
|
-
has: (id?: string | null) => boolean;
|
|
9
|
-
size: () => number;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export function createProcessedMessageTracker(limit = 2000): ProcessedMessageTracker {
|
|
13
|
-
const dedupe = createDedupeCache({ ttlMs: 0, maxSize: limit });
|
|
14
|
-
const inFlight = new Set<string>();
|
|
15
|
-
|
|
16
|
-
const claim = (id?: string | null) => {
|
|
17
|
-
const trimmed = id?.trim();
|
|
18
|
-
if (!trimmed) {
|
|
19
|
-
return { kind: "claimed" } as const;
|
|
20
|
-
}
|
|
21
|
-
if (inFlight.has(trimmed) || dedupe.peek(trimmed)) {
|
|
22
|
-
return { kind: "duplicate" } as const;
|
|
23
|
-
}
|
|
24
|
-
inFlight.add(trimmed);
|
|
25
|
-
return { kind: "claimed" } as const;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const commit = (id?: string | null) => {
|
|
29
|
-
const trimmed = id?.trim();
|
|
30
|
-
if (!trimmed) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
inFlight.delete(trimmed);
|
|
34
|
-
dedupe.check(trimmed);
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const release = (id?: string | null) => {
|
|
38
|
-
const trimmed = id?.trim();
|
|
39
|
-
if (!trimmed) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
inFlight.delete(trimmed);
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const mark = (id?: string | null) => {
|
|
46
|
-
const claimed = claim(id);
|
|
47
|
-
if (claimed.kind === "duplicate") {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
commit(id);
|
|
51
|
-
return true;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const has = (id?: string | null) => {
|
|
55
|
-
const trimmed = id?.trim();
|
|
56
|
-
if (!trimmed) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
return dedupe.peek(trimmed);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
claim,
|
|
64
|
-
commit,
|
|
65
|
-
release,
|
|
66
|
-
mark,
|
|
67
|
-
has,
|
|
68
|
-
size: () => dedupe.size(),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function runWithProcessedMessageClaim<T>(params: {
|
|
73
|
-
tracker: ProcessedMessageTracker;
|
|
74
|
-
id?: string | null;
|
|
75
|
-
task: () => Promise<T>;
|
|
76
|
-
}): Promise<{ kind: "processed"; value: T } | { kind: "duplicate" }> {
|
|
77
|
-
const claim = params.tracker.claim(params.id);
|
|
78
|
-
if (claim.kind === "duplicate") {
|
|
79
|
-
return claim;
|
|
80
|
-
}
|
|
81
|
-
try {
|
|
82
|
-
const value = await params.task();
|
|
83
|
-
params.tracker.commit(params.id);
|
|
84
|
-
return { kind: "processed", value };
|
|
85
|
-
} catch (error) {
|
|
86
|
-
params.tracker.release(params.id);
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import type { TlonResolvedAccount } from "../types.js";
|
|
3
|
-
import {
|
|
4
|
-
applyTlonSettingsOverrides,
|
|
5
|
-
buildTlonSettingsMigrations,
|
|
6
|
-
shouldMigrateTlonSetting,
|
|
7
|
-
} from "./settings-helpers.js";
|
|
8
|
-
|
|
9
|
-
const baseAccount: TlonResolvedAccount = {
|
|
10
|
-
accountId: "default",
|
|
11
|
-
name: "Tlon",
|
|
12
|
-
enabled: true,
|
|
13
|
-
configured: true,
|
|
14
|
-
ship: "~sampel-palnet",
|
|
15
|
-
url: "https://example.com",
|
|
16
|
-
code: "lidlut-tabwed-pillex-ridrup",
|
|
17
|
-
dangerouslyAllowPrivateNetwork: false,
|
|
18
|
-
groupChannels: ["chat/~host/general"],
|
|
19
|
-
dmAllowlist: ["~zod"],
|
|
20
|
-
groupInviteAllowlist: ["~bus"],
|
|
21
|
-
autoDiscoverChannels: true,
|
|
22
|
-
showModelSignature: false,
|
|
23
|
-
autoAcceptDmInvites: true,
|
|
24
|
-
autoAcceptGroupInvites: true,
|
|
25
|
-
defaultAuthorizedShips: ["~nec"],
|
|
26
|
-
ownerShip: "~marzod",
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function allowlistMigrationDecisions(currentSettings: Record<string, unknown>) {
|
|
30
|
-
const allowlistKeys = new Set(["dmAllowlist", "groupInviteAllowlist", "defaultAuthorizedShips"]);
|
|
31
|
-
return Object.fromEntries(
|
|
32
|
-
buildTlonSettingsMigrations(baseAccount, currentSettings)
|
|
33
|
-
.filter((migration) => allowlistKeys.has(migration.key))
|
|
34
|
-
.map((migration) => [
|
|
35
|
-
migration.key,
|
|
36
|
-
shouldMigrateTlonSetting(migration.fileValue, migration.settingsValue),
|
|
37
|
-
]),
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
describe("shouldMigrateTlonSetting", () => {
|
|
42
|
-
it("does not rehydrate explicit empty-array revocations during startup migration", () => {
|
|
43
|
-
const decisions = allowlistMigrationDecisions({
|
|
44
|
-
dmAllowlist: [],
|
|
45
|
-
groupInviteAllowlist: [],
|
|
46
|
-
defaultAuthorizedShips: [],
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
expect(decisions).toEqual({
|
|
50
|
-
dmAllowlist: false,
|
|
51
|
-
groupInviteAllowlist: false,
|
|
52
|
-
defaultAuthorizedShips: false,
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("still seeds file-config allowlists on first run when settings are missing", () => {
|
|
57
|
-
const decisions = allowlistMigrationDecisions({});
|
|
58
|
-
|
|
59
|
-
expect(decisions).toEqual({
|
|
60
|
-
dmAllowlist: true,
|
|
61
|
-
groupInviteAllowlist: true,
|
|
62
|
-
defaultAuthorizedShips: true,
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe("applyTlonSettingsOverrides", () => {
|
|
68
|
-
it("treats explicit empty settings allowlists as authoritative deny-all", () => {
|
|
69
|
-
const result = applyTlonSettingsOverrides({
|
|
70
|
-
account: baseAccount,
|
|
71
|
-
currentSettings: {
|
|
72
|
-
dmAllowlist: [],
|
|
73
|
-
groupInviteAllowlist: [],
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
expect(result.effectiveDmAllowlist).toStrictEqual([]);
|
|
78
|
-
expect(result.effectiveGroupInviteAllowlist).toStrictEqual([]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("falls back to file config when settings fields are removed", () => {
|
|
82
|
-
const result = applyTlonSettingsOverrides({
|
|
83
|
-
account: baseAccount,
|
|
84
|
-
currentSettings: {},
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
expect(result.effectiveDmAllowlist).toEqual(baseAccount.dmAllowlist);
|
|
88
|
-
expect(result.effectiveGroupInviteAllowlist).toEqual(baseAccount.groupInviteAllowlist);
|
|
89
|
-
expect(result.effectiveAutoDiscoverChannels).toBe(baseAccount.autoDiscoverChannels);
|
|
90
|
-
expect(result.effectiveOwnerShip).toBe(baseAccount.ownerShip);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("keeps other explicit settings overrides authoritative", () => {
|
|
94
|
-
const result = applyTlonSettingsOverrides({
|
|
95
|
-
account: baseAccount,
|
|
96
|
-
currentSettings: {
|
|
97
|
-
autoDiscoverChannels: false,
|
|
98
|
-
autoAcceptDmInvites: false,
|
|
99
|
-
autoAcceptGroupInvites: false,
|
|
100
|
-
showModelSig: true,
|
|
101
|
-
ownerShip: "~nec",
|
|
102
|
-
pendingApprovals: [],
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
expect(result.effectiveAutoDiscoverChannels).toBe(false);
|
|
107
|
-
expect(result.effectiveAutoAcceptDmInvites).toBe(false);
|
|
108
|
-
expect(result.effectiveAutoAcceptGroupInvites).toBe(false);
|
|
109
|
-
expect(result.effectiveShowModelSig).toBe(true);
|
|
110
|
-
expect(result.effectiveOwnerShip).toBe("~nec");
|
|
111
|
-
expect(result.pendingApprovals).toStrictEqual([]);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import type { PendingApproval, TlonSettingsStore } from "../settings.js";
|
|
2
|
-
import { normalizeShip } from "../targets.js";
|
|
3
|
-
import type { TlonResolvedAccount } from "../types.js";
|
|
4
|
-
|
|
5
|
-
type TlonMonitorSettingsState = {
|
|
6
|
-
effectiveDmAllowlist: string[];
|
|
7
|
-
effectiveShowModelSig: boolean;
|
|
8
|
-
effectiveAutoAcceptDmInvites: boolean;
|
|
9
|
-
effectiveAutoAcceptGroupInvites: boolean;
|
|
10
|
-
effectiveGroupInviteAllowlist: string[];
|
|
11
|
-
effectiveAutoDiscoverChannels: boolean;
|
|
12
|
-
effectiveOwnerShip: string | null;
|
|
13
|
-
pendingApprovals: PendingApproval[];
|
|
14
|
-
currentSettings: TlonSettingsStore;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export function buildTlonSettingsMigrations(
|
|
18
|
-
account: TlonResolvedAccount,
|
|
19
|
-
currentSettings: TlonSettingsStore,
|
|
20
|
-
): Array<{ key: string; fileValue: unknown; settingsValue: unknown }> {
|
|
21
|
-
return [
|
|
22
|
-
{
|
|
23
|
-
key: "dmAllowlist",
|
|
24
|
-
fileValue: account.dmAllowlist,
|
|
25
|
-
settingsValue: currentSettings.dmAllowlist,
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
key: "groupInviteAllowlist",
|
|
29
|
-
fileValue: account.groupInviteAllowlist,
|
|
30
|
-
settingsValue: currentSettings.groupInviteAllowlist,
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
key: "groupChannels",
|
|
34
|
-
fileValue: account.groupChannels,
|
|
35
|
-
settingsValue: currentSettings.groupChannels,
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
key: "defaultAuthorizedShips",
|
|
39
|
-
fileValue: account.defaultAuthorizedShips,
|
|
40
|
-
settingsValue: currentSettings.defaultAuthorizedShips,
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
key: "autoDiscoverChannels",
|
|
44
|
-
fileValue: account.autoDiscoverChannels,
|
|
45
|
-
settingsValue: currentSettings.autoDiscoverChannels,
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
key: "autoAcceptDmInvites",
|
|
49
|
-
fileValue: account.autoAcceptDmInvites,
|
|
50
|
-
settingsValue: currentSettings.autoAcceptDmInvites,
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
key: "autoAcceptGroupInvites",
|
|
54
|
-
fileValue: account.autoAcceptGroupInvites,
|
|
55
|
-
settingsValue: currentSettings.autoAcceptGroupInvites,
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
key: "showModelSig",
|
|
59
|
-
fileValue: account.showModelSignature,
|
|
60
|
-
settingsValue: currentSettings.showModelSig,
|
|
61
|
-
},
|
|
62
|
-
];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function shouldMigrateTlonSetting(fileValue: unknown, settingsValue: unknown): boolean {
|
|
66
|
-
const hasFileValue = Array.isArray(fileValue) ? fileValue.length > 0 : fileValue != null;
|
|
67
|
-
const hasSettingsValue = settingsValue != null;
|
|
68
|
-
return hasFileValue && !hasSettingsValue;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function applyTlonSettingsOverrides(params: {
|
|
72
|
-
account: TlonResolvedAccount;
|
|
73
|
-
currentSettings: TlonSettingsStore;
|
|
74
|
-
log?: (message: string) => void;
|
|
75
|
-
}): TlonMonitorSettingsState {
|
|
76
|
-
let effectiveDmAllowlist = params.account.dmAllowlist;
|
|
77
|
-
let effectiveShowModelSig = params.account.showModelSignature ?? false;
|
|
78
|
-
let effectiveAutoAcceptDmInvites = params.account.autoAcceptDmInvites ?? false;
|
|
79
|
-
let effectiveAutoAcceptGroupInvites = params.account.autoAcceptGroupInvites ?? false;
|
|
80
|
-
let effectiveGroupInviteAllowlist = params.account.groupInviteAllowlist;
|
|
81
|
-
let effectiveAutoDiscoverChannels = params.account.autoDiscoverChannels ?? false;
|
|
82
|
-
let effectiveOwnerShip = params.account.ownerShip
|
|
83
|
-
? normalizeShip(params.account.ownerShip)
|
|
84
|
-
: null;
|
|
85
|
-
let pendingApprovals: PendingApproval[] = [];
|
|
86
|
-
|
|
87
|
-
if (params.currentSettings.defaultAuthorizedShips?.length) {
|
|
88
|
-
params.log?.(
|
|
89
|
-
`[tlon] Using defaultAuthorizedShips from settings store: ${params.currentSettings.defaultAuthorizedShips.join(", ")}`,
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
if (params.currentSettings.autoDiscoverChannels !== undefined) {
|
|
93
|
-
effectiveAutoDiscoverChannels = params.currentSettings.autoDiscoverChannels;
|
|
94
|
-
params.log?.(
|
|
95
|
-
`[tlon] Using autoDiscoverChannels from settings store: ${effectiveAutoDiscoverChannels}`,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
if (params.currentSettings.dmAllowlist !== undefined) {
|
|
99
|
-
effectiveDmAllowlist = params.currentSettings.dmAllowlist;
|
|
100
|
-
params.log?.(
|
|
101
|
-
`[tlon] Using dmAllowlist from settings store: ${effectiveDmAllowlist.join(", ")}`,
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
if (params.currentSettings.showModelSig !== undefined) {
|
|
105
|
-
effectiveShowModelSig = params.currentSettings.showModelSig;
|
|
106
|
-
}
|
|
107
|
-
if (params.currentSettings.autoAcceptDmInvites !== undefined) {
|
|
108
|
-
effectiveAutoAcceptDmInvites = params.currentSettings.autoAcceptDmInvites;
|
|
109
|
-
params.log?.(
|
|
110
|
-
`[tlon] Using autoAcceptDmInvites from settings store: ${effectiveAutoAcceptDmInvites}`,
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
if (params.currentSettings.autoAcceptGroupInvites !== undefined) {
|
|
114
|
-
effectiveAutoAcceptGroupInvites = params.currentSettings.autoAcceptGroupInvites;
|
|
115
|
-
params.log?.(
|
|
116
|
-
`[tlon] Using autoAcceptGroupInvites from settings store: ${effectiveAutoAcceptGroupInvites}`,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
if (params.currentSettings.groupInviteAllowlist !== undefined) {
|
|
120
|
-
effectiveGroupInviteAllowlist = params.currentSettings.groupInviteAllowlist;
|
|
121
|
-
params.log?.(
|
|
122
|
-
`[tlon] Using groupInviteAllowlist from settings store: ${effectiveGroupInviteAllowlist.join(", ")}`,
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
if (params.currentSettings.ownerShip) {
|
|
126
|
-
effectiveOwnerShip = normalizeShip(params.currentSettings.ownerShip);
|
|
127
|
-
params.log?.(`[tlon] Using ownerShip from settings store: ${effectiveOwnerShip}`);
|
|
128
|
-
}
|
|
129
|
-
if (params.currentSettings.pendingApprovals?.length) {
|
|
130
|
-
pendingApprovals = params.currentSettings.pendingApprovals;
|
|
131
|
-
params.log?.(`[tlon] Loaded ${pendingApprovals.length} pending approval(s) from settings`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
effectiveDmAllowlist,
|
|
136
|
-
effectiveShowModelSig,
|
|
137
|
-
effectiveAutoAcceptDmInvites,
|
|
138
|
-
effectiveAutoAcceptGroupInvites,
|
|
139
|
-
effectiveGroupInviteAllowlist,
|
|
140
|
-
effectiveAutoDiscoverChannels,
|
|
141
|
-
effectiveOwnerShip,
|
|
142
|
-
pendingApprovals,
|
|
143
|
-
currentSettings: params.currentSettings,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function mergeUniqueStrings(base: string[], next?: string[]): string[] {
|
|
148
|
-
if (!next?.length) {
|
|
149
|
-
return [...base];
|
|
150
|
-
}
|
|
151
|
-
const merged = [...base];
|
|
152
|
-
for (const value of next) {
|
|
153
|
-
if (!merged.includes(value)) {
|
|
154
|
-
merged.push(value);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return merged;
|
|
158
|
-
}
|