refactor: dedupe tlon helpers

This commit is contained in:
Peter Steinberger
2026-04-21 00:13:36 +01:00
parent f700ad32a8
commit 897a7b794f
6 changed files with 80 additions and 110 deletions

View File

@@ -27,6 +27,25 @@ export type TlonHistoryEntry = {
id?: string;
};
function createHistoryEntryFromMemo(params: {
memo?: Record<string, unknown> | null;
seal?: Record<string, unknown> | null;
fallbackId?: unknown;
}): TlonHistoryEntry {
const { memo, seal, fallbackId } = params;
return {
author: typeof memo?.author === "string" ? memo.author : "unknown",
content: extractMessageText(memo?.content || []),
timestamp: typeof memo?.sent === "number" ? memo.sent : Date.now(),
id:
typeof seal?.id === "string"
? seal.id
: typeof fallbackId === "string"
? fallbackId
: undefined,
};
}
const messageCache = new Map<string, TlonHistoryEntry[]>();
const MAX_CACHED_MESSAGES = 100;
@@ -166,17 +185,7 @@ export async function fetchThreadHistory(
const memo = asRecord(itemRecord?.memo) ?? asRecord(replySet?.memo) ?? itemRecord;
const seal = asRecord(itemRecord?.seal) ?? asRecord(replySet?.seal);
return {
author: typeof memo?.author === "string" ? memo.author : "unknown",
content: extractMessageText(memo?.content || []),
timestamp: typeof memo?.sent === "number" ? memo.sent : Date.now(),
id:
typeof seal?.id === "string"
? seal.id
: typeof itemRecord?.id === "string"
? itemRecord.id
: undefined,
} as TlonHistoryEntry;
return createHistoryEntryFromMemo({ memo, seal, fallbackId: itemRecord?.id });
})
.filter((msg) => msg.content);
@@ -202,12 +211,7 @@ export async function fetchThreadHistory(
const replyRecord = asRecord(reply);
const memo = asRecord(replyRecord?.memo);
const seal = asRecord(replyRecord?.seal);
return {
author: typeof memo?.author === "string" ? memo.author : "unknown",
content: extractMessageText(memo?.content || []),
timestamp: typeof memo?.sent === "number" ? memo.sent : Date.now(),
id: typeof seal?.id === "string" ? seal.id : undefined,
};
return createHistoryEntryFromMemo({ memo, seal });
})
.filter((msg: TlonHistoryEntry) => msg.content);

View File

@@ -26,28 +26,27 @@ const baseAccount: TlonResolvedAccount = {
ownerShip: "~marzod",
};
function allowlistMigrationDecisions(currentSettings: Record<string, unknown>) {
const allowlistKeys = new Set(["dmAllowlist", "groupInviteAllowlist", "defaultAuthorizedShips"]);
return Object.fromEntries(
buildTlonSettingsMigrations(baseAccount, currentSettings)
.filter((migration) => allowlistKeys.has(migration.key))
.map((migration) => [
migration.key,
shouldMigrateTlonSetting(migration.fileValue, migration.settingsValue),
]),
);
}
describe("shouldMigrateTlonSetting", () => {
it("does not rehydrate explicit empty-array revocations during startup migration", () => {
const migrations = buildTlonSettingsMigrations(baseAccount, {
const decisions = allowlistMigrationDecisions({
dmAllowlist: [],
groupInviteAllowlist: [],
defaultAuthorizedShips: [],
});
expect(
Object.fromEntries(
migrations
.filter((migration) =>
["dmAllowlist", "groupInviteAllowlist", "defaultAuthorizedShips"].includes(
migration.key,
),
)
.map((migration) => [
migration.key,
shouldMigrateTlonSetting(migration.fileValue, migration.settingsValue),
]),
),
).toEqual({
expect(decisions).toEqual({
dmAllowlist: false,
groupInviteAllowlist: false,
defaultAuthorizedShips: false,
@@ -55,22 +54,9 @@ describe("shouldMigrateTlonSetting", () => {
});
it("still seeds file-config allowlists on first run when settings are missing", () => {
const migrations = buildTlonSettingsMigrations(baseAccount, {});
const decisions = allowlistMigrationDecisions({});
expect(
Object.fromEntries(
migrations
.filter((migration) =>
["dmAllowlist", "groupInviteAllowlist", "defaultAuthorizedShips"].includes(
migration.key,
),
)
.map((migration) => [
migration.key,
shouldMigrateTlonSetting(migration.fileValue, migration.settingsValue),
]),
),
).toEqual({
expect(decisions).toEqual({
dmAllowlist: true,
groupInviteAllowlist: true,
defaultAuthorizedShips: true,

View File

@@ -14,7 +14,7 @@ import {
normalizeOptionalString,
normalizeStringifiedOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import { buildTlonAccountFields } from "./account-fields.js";
import { buildTlonAccountFields, type TlonAccountFieldsInput } from "./account-fields.js";
import { normalizeShip } from "./targets.js";
import { listTlonAccountIds, resolveTlonAccount, type TlonResolvedAccount } from "./types.js";
import { validateUrbitBaseUrl } from "./urbit/base-url.js";
@@ -23,16 +23,7 @@ function tlonChannelId() {
return "tlon" as const;
}
export type TlonSetupInput = ChannelSetupInput & {
ship?: string;
url?: string;
code?: string;
dangerouslyAllowPrivateNetwork?: boolean;
groupChannels?: string[];
dmAllowlist?: string[];
autoDiscoverChannels?: boolean;
ownerShip?: string;
};
export type TlonSetupInput = ChannelSetupInput & TlonAccountFieldsInput;
function isConfigured(account: TlonResolvedAccount): boolean {
return Boolean(account.ship && account.url && account.code);

View File

@@ -23,6 +23,15 @@ export function parseChannelNest(raw: string): { hostShip: string; channelName:
return { hostShip, channelName };
}
function makeGroupTarget(parsed: { hostShip: string; channelName: string }): TlonTarget {
return {
kind: "group",
nest: `chat/${parsed.hostShip}/${parsed.channelName}`,
hostShip: parsed.hostShip,
channelName: parsed.channelName,
};
}
export function parseTlonTarget(raw?: string | null): TlonTarget | null {
const trimmed = raw?.trim();
if (!trimmed) {
@@ -43,12 +52,7 @@ export function parseTlonTarget(raw?: string | null): TlonTarget | null {
if (!parsed) {
return null;
}
return {
kind: "group",
nest: `chat/${parsed.hostShip}/${parsed.channelName}`,
hostShip: parsed.hostShip,
channelName: parsed.channelName,
};
return makeGroupTarget(parsed);
}
const parts = groupTarget.split("/");
if (parts.length === 2) {
@@ -69,12 +73,7 @@ export function parseTlonTarget(raw?: string | null): TlonTarget | null {
if (!parsed) {
return null;
}
return {
kind: "group",
nest: `chat/${parsed.hostShip}/${parsed.channelName}`,
hostShip: parsed.hostShip,
channelName: parsed.channelName,
};
return makeGroupTarget(parsed);
}
if (SHIP_RE.test(withoutPrefix)) {

View File

@@ -2,12 +2,17 @@ import { describe, expect, it } from "vitest";
import { validateUrbitBaseUrl } from "./base-url.js";
describe("validateUrbitBaseUrl", () => {
it("adds https:// when scheme is missing and strips path/query fragments", () => {
const result = validateUrbitBaseUrl("example.com/foo?bar=baz");
function expectValidBaseUrl(raw: string) {
const result = validateUrbitBaseUrl(raw);
expect(result.ok).toBe(true);
if (!result.ok) {
return;
throw new Error(result.error);
}
return result;
}
it("adds https:// when scheme is missing and strips path/query fragments", () => {
const result = expectValidBaseUrl("example.com/foo?bar=baz");
expect(result.baseUrl).toBe("https://example.com");
expect(result.hostname).toBe("example.com");
});
@@ -31,21 +36,13 @@ describe("validateUrbitBaseUrl", () => {
});
it("normalizes a trailing dot in the hostname for origin construction", () => {
const result = validateUrbitBaseUrl("https://example.com./foo");
expect(result.ok).toBe(true);
if (!result.ok) {
return;
}
const result = expectValidBaseUrl("https://example.com./foo");
expect(result.baseUrl).toBe("https://example.com");
expect(result.hostname).toBe("example.com");
});
it("preserves port in the normalized origin", () => {
const result = validateUrbitBaseUrl("http://example.com:8080/~/login");
expect(result.ok).toBe(true);
if (!result.ok) {
return;
}
const result = expectValidBaseUrl("http://example.com:8080/~/login");
expect(result.baseUrl).toBe("http://example.com:8080");
});
});

View File

@@ -78,6 +78,18 @@ export class UrbitSSEClient {
this.fetchImpl = options.fetchImpl;
}
private channelRequestContext() {
return {
baseUrl: this.url,
cookie: this.cookie,
ship: this.ship,
channelId: this.channelId,
ssrfPolicy: this.ssrfPolicy,
lookupFn: this.lookupFn,
fetchImpl: this.fetchImpl,
};
}
async subscribe(params: {
app: string;
path: string;
@@ -133,21 +145,10 @@ export class UrbitSSEClient {
}
async connect() {
await ensureUrbitChannelOpen(
{
baseUrl: this.url,
cookie: this.cookie,
ship: this.ship,
channelId: this.channelId,
ssrfPolicy: this.ssrfPolicy,
lookupFn: this.lookupFn,
fetchImpl: this.fetchImpl,
},
{
createBody: this.subscriptions,
createAuditContext: "tlon-urbit-channel-create",
},
);
await ensureUrbitChannelOpen(this.channelRequestContext(), {
createBody: this.subscriptions,
createAuditContext: "tlon-urbit-channel-create",
});
await this.openStream();
this.isConnected = true;
@@ -305,18 +306,10 @@ export class UrbitSSEClient {
}
async poke(params: { app: string; mark: string; json: unknown }) {
return await pokeUrbitChannel(
{
baseUrl: this.url,
cookie: this.cookie,
ship: this.ship,
channelId: this.channelId,
ssrfPolicy: this.ssrfPolicy,
lookupFn: this.lookupFn,
fetchImpl: this.fetchImpl,
},
{ ...params, auditContext: "tlon-urbit-poke" },
);
return await pokeUrbitChannel(this.channelRequestContext(), {
...params,
auditContext: "tlon-urbit-poke",
});
}
async scry(path: string) {