@kodelyth/tlon 2026.5.39 → 2026.5.42
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/README.md +5 -0
- package/api.ts +16 -0
- package/channel-plugin-api.ts +1 -0
- package/dist/api.js +4 -0
- package/dist/channel-Bvzym9ez.js +236 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-CDY2BdfM.js +3626 -0
- package/dist/doctor-contract-Ip6FcHDH.js +7 -0
- package/dist/doctor-contract-api.js +2 -0
- package/dist/index.js +18 -0
- package/dist/runtime-BmSb9A-q.js +8 -0
- package/dist/runtime-api-Dq8wkBC_.js +4 -0
- package/dist/runtime-api.js +2 -0
- package/dist/setup-api.js +3 -0
- package/dist/setup-core-CF3ryHqs.js +387 -0
- package/dist/setup-entry.js +11 -0
- package/dist/setup-surface-BM5_V_XL.js +74 -0
- package/dist/test-api.js +2 -0
- package/doctor-contract-api.ts +1 -0
- package/index.ts +16 -0
- package/klaw.plugin.json +3 -203
- package/package.json +4 -4
- package/runtime-api.ts +17 -0
- package/setup-api.ts +2 -0
- package/setup-entry.ts +9 -0
- package/src/account-fields.ts +31 -0
- package/src/channel.message-adapter.test.ts +145 -0
- package/src/channel.runtime.ts +259 -0
- package/src/channel.ts +192 -0
- package/src/config-schema.ts +54 -0
- package/src/core.test.ts +298 -0
- package/src/doctor-contract.ts +9 -0
- package/src/doctor.test.ts +46 -0
- package/src/doctor.ts +10 -0
- package/src/logger-runtime.ts +1 -0
- package/src/monitor/approval-runtime.ts +363 -0
- package/src/monitor/approval.test.ts +33 -0
- package/src/monitor/approval.ts +283 -0
- package/src/monitor/authorization.ts +30 -0
- package/src/monitor/cites.ts +54 -0
- package/src/monitor/discovery.ts +68 -0
- package/src/monitor/history.ts +226 -0
- package/src/monitor/index.ts +1523 -0
- package/src/monitor/media.test.ts +80 -0
- package/src/monitor/media.ts +156 -0
- package/src/monitor/processed-messages.test.ts +58 -0
- package/src/monitor/processed-messages.ts +89 -0
- package/src/monitor/settings-helpers.test.ts +113 -0
- package/src/monitor/settings-helpers.ts +158 -0
- package/src/monitor/utils.ts +402 -0
- package/src/runtime.ts +9 -0
- package/src/security.test.ts +658 -0
- package/src/session-route.ts +40 -0
- package/src/settings.ts +391 -0
- package/src/setup-core.ts +231 -0
- package/src/setup-surface.ts +99 -0
- package/src/targets.ts +102 -0
- package/src/tlon-api.test.ts +572 -0
- package/src/tlon-api.ts +389 -0
- package/src/types.ts +160 -0
- package/src/urbit/auth.ssrf.test.ts +45 -0
- package/src/urbit/auth.ts +48 -0
- package/src/urbit/base-url.test.ts +48 -0
- package/src/urbit/base-url.ts +61 -0
- package/src/urbit/channel-ops.test.ts +36 -0
- package/src/urbit/channel-ops.ts +149 -0
- package/src/urbit/context.ts +50 -0
- package/src/urbit/errors.ts +51 -0
- package/src/urbit/fetch.ts +38 -0
- package/src/urbit/foreigns.ts +49 -0
- package/src/urbit/send.test.ts +83 -0
- package/src/urbit/send.ts +228 -0
- package/src/urbit/sse-client.test.ts +234 -0
- package/src/urbit/sse-client.ts +492 -0
- package/src/urbit/story.ts +332 -0
- package/src/urbit/upload.test.ts +155 -0
- package/src/urbit/upload.ts +60 -0
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/api.js +0 -7
- package/bundled-skills/@tloncorp/tlon-skill/SKILL.md +0 -501
- package/bundled-skills/@tloncorp/tlon-skill/bin/tlon.js +0 -7
- package/bundled-skills/@tloncorp/tlon-skill/package.json +0 -40
- package/bundled-skills/@tloncorp/tlon-skill/scripts/postinstall.js +0 -7
- package/channel-plugin-api.js +0 -7
- package/doctor-contract-api.js +0 -7
- package/index.js +0 -7
- package/runtime-api.js +0 -7
- package/setup-api.js +0 -7
- package/setup-entry.js +0 -7
- package/test-api.js +0 -7
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import type { ChannelAccountSnapshot } from "klaw/plugin-sdk/channel-contract";
|
|
3
|
+
import type { ChannelOutboundAdapter } from "klaw/plugin-sdk/channel-send-result";
|
|
4
|
+
import type { KlawConfig } from "klaw/plugin-sdk/config-contracts";
|
|
5
|
+
import type { ChannelPlugin } from "klaw/plugin-sdk/core";
|
|
6
|
+
import { monitorTlonProvider } from "./monitor/index.js";
|
|
7
|
+
import { tlonSetupWizard } from "./setup-surface.js";
|
|
8
|
+
import {
|
|
9
|
+
formatTargetHint,
|
|
10
|
+
normalizeShip,
|
|
11
|
+
parseTlonTarget,
|
|
12
|
+
resolveTlonOutboundTarget,
|
|
13
|
+
} from "./targets.js";
|
|
14
|
+
import { configureClient } from "./tlon-api.js";
|
|
15
|
+
import { resolveTlonAccount } from "./types.js";
|
|
16
|
+
import { authenticate } from "./urbit/auth.js";
|
|
17
|
+
import { ssrfPolicyFromDangerouslyAllowPrivateNetwork } from "./urbit/context.js";
|
|
18
|
+
import { urbitFetch } from "./urbit/fetch.js";
|
|
19
|
+
import {
|
|
20
|
+
buildMediaStory,
|
|
21
|
+
sendDm,
|
|
22
|
+
sendDmWithStory,
|
|
23
|
+
sendGroupMessage,
|
|
24
|
+
sendGroupMessageWithStory,
|
|
25
|
+
} from "./urbit/send.js";
|
|
26
|
+
import { uploadImageFromUrl } from "./urbit/upload.js";
|
|
27
|
+
|
|
28
|
+
type ResolvedTlonAccount = ReturnType<typeof resolveTlonAccount>;
|
|
29
|
+
type ConfiguredTlonAccount = ResolvedTlonAccount & {
|
|
30
|
+
ship: string;
|
|
31
|
+
url: string;
|
|
32
|
+
code: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
async function createHttpPokeApi(params: {
|
|
36
|
+
url: string;
|
|
37
|
+
code: string;
|
|
38
|
+
ship: string;
|
|
39
|
+
dangerouslyAllowPrivateNetwork?: boolean;
|
|
40
|
+
}) {
|
|
41
|
+
const ssrfPolicy = ssrfPolicyFromDangerouslyAllowPrivateNetwork(
|
|
42
|
+
params.dangerouslyAllowPrivateNetwork,
|
|
43
|
+
);
|
|
44
|
+
const cookie = await authenticate(params.url, params.code, { ssrfPolicy });
|
|
45
|
+
const channelId = `${Math.floor(Date.now() / 1000)}-${crypto.randomUUID()}`;
|
|
46
|
+
const channelPath = `/~/channel/${channelId}`;
|
|
47
|
+
const shipName = params.ship.replace(/^~/, "");
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
poke: async (pokeParams: { app: string; mark: string; json: unknown }) => {
|
|
51
|
+
const pokeId = Date.now();
|
|
52
|
+
const pokeData = {
|
|
53
|
+
id: pokeId,
|
|
54
|
+
action: "poke",
|
|
55
|
+
ship: shipName,
|
|
56
|
+
app: pokeParams.app,
|
|
57
|
+
mark: pokeParams.mark,
|
|
58
|
+
json: pokeParams.json,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const { response, release } = await urbitFetch({
|
|
62
|
+
baseUrl: params.url,
|
|
63
|
+
path: channelPath,
|
|
64
|
+
init: {
|
|
65
|
+
method: "PUT",
|
|
66
|
+
headers: {
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
Cookie: cookie.split(";")[0],
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify([pokeData]),
|
|
71
|
+
},
|
|
72
|
+
ssrfPolicy,
|
|
73
|
+
auditContext: "tlon-poke",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
if (!response.ok && response.status !== 204) {
|
|
78
|
+
const errorText = await response.text();
|
|
79
|
+
throw new Error(`Poke failed: ${response.status} - ${errorText}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return pokeId;
|
|
83
|
+
} finally {
|
|
84
|
+
await release();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
delete: async () => {
|
|
88
|
+
// No-op for HTTP-only client
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveOutboundContext(params: {
|
|
94
|
+
cfg: KlawConfig;
|
|
95
|
+
accountId?: string | null;
|
|
96
|
+
to: string;
|
|
97
|
+
}) {
|
|
98
|
+
const account = resolveTlonAccount(params.cfg, params.accountId ?? undefined);
|
|
99
|
+
if (!account.configured || !account.ship || !account.url || !account.code) {
|
|
100
|
+
throw new Error("Tlon account not configured");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parsed = parseTlonTarget(params.to);
|
|
104
|
+
if (!parsed) {
|
|
105
|
+
throw new Error(`Invalid Tlon target. Use ${formatTargetHint()}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { account: account as ConfiguredTlonAccount, parsed };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveReplyId(replyToId?: string | null, threadId?: string | number | null) {
|
|
112
|
+
return (replyToId ?? threadId) ? String(replyToId ?? threadId) : undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function withHttpPokeAccountApi<T>(
|
|
116
|
+
account: ConfiguredTlonAccount,
|
|
117
|
+
run: (api: Awaited<ReturnType<typeof createHttpPokeApi>>) => Promise<T>,
|
|
118
|
+
) {
|
|
119
|
+
const api = await createHttpPokeApi({
|
|
120
|
+
url: account.url,
|
|
121
|
+
ship: account.ship,
|
|
122
|
+
code: account.code,
|
|
123
|
+
dangerouslyAllowPrivateNetwork: account.dangerouslyAllowPrivateNetwork ?? undefined,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
return await run(api);
|
|
128
|
+
} finally {
|
|
129
|
+
try {
|
|
130
|
+
await api.delete();
|
|
131
|
+
} catch {
|
|
132
|
+
// ignore cleanup errors
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const tlonRuntimeOutbound: ChannelOutboundAdapter = {
|
|
138
|
+
deliveryMode: "direct",
|
|
139
|
+
textChunkLimit: 10000,
|
|
140
|
+
resolveTarget: ({ to }) => resolveTlonOutboundTarget(to),
|
|
141
|
+
deliveryCapabilities: {
|
|
142
|
+
durableFinal: {
|
|
143
|
+
text: true,
|
|
144
|
+
media: true,
|
|
145
|
+
replyTo: true,
|
|
146
|
+
thread: true,
|
|
147
|
+
messageSendingHooks: true,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => {
|
|
151
|
+
const { account, parsed } = resolveOutboundContext({ cfg, accountId, to });
|
|
152
|
+
return withHttpPokeAccountApi(account, async (api) => {
|
|
153
|
+
const fromShip = normalizeShip(account.ship);
|
|
154
|
+
if (parsed.kind === "dm") {
|
|
155
|
+
return await sendDm({
|
|
156
|
+
api,
|
|
157
|
+
fromShip,
|
|
158
|
+
toShip: parsed.ship,
|
|
159
|
+
text,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return await sendGroupMessage({
|
|
163
|
+
api,
|
|
164
|
+
fromShip,
|
|
165
|
+
hostShip: parsed.hostShip,
|
|
166
|
+
channelName: parsed.channelName,
|
|
167
|
+
text,
|
|
168
|
+
replyToId: resolveReplyId(replyToId, threadId),
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId, threadId }) => {
|
|
173
|
+
const { account, parsed } = resolveOutboundContext({ cfg, accountId, to });
|
|
174
|
+
|
|
175
|
+
configureClient({
|
|
176
|
+
shipUrl: account.url,
|
|
177
|
+
shipName: account.ship.replace(/^~/, ""),
|
|
178
|
+
verbose: false,
|
|
179
|
+
getCode: async () => account.code,
|
|
180
|
+
dangerouslyAllowPrivateNetwork: account.dangerouslyAllowPrivateNetwork ?? undefined,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const uploadedUrl = mediaUrl ? await uploadImageFromUrl(mediaUrl) : undefined;
|
|
184
|
+
return withHttpPokeAccountApi(account, async (api) => {
|
|
185
|
+
const fromShip = normalizeShip(account.ship);
|
|
186
|
+
const story = buildMediaStory(text, uploadedUrl);
|
|
187
|
+
|
|
188
|
+
if (parsed.kind === "dm") {
|
|
189
|
+
return await sendDmWithStory({
|
|
190
|
+
api,
|
|
191
|
+
fromShip,
|
|
192
|
+
toShip: parsed.ship,
|
|
193
|
+
story,
|
|
194
|
+
kind: "media",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return await sendGroupMessageWithStory({
|
|
198
|
+
api,
|
|
199
|
+
fromShip,
|
|
200
|
+
hostShip: parsed.hostShip,
|
|
201
|
+
channelName: parsed.channelName,
|
|
202
|
+
story,
|
|
203
|
+
replyToId: resolveReplyId(replyToId, threadId),
|
|
204
|
+
kind: "media",
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export async function probeTlonAccount(account: ConfiguredTlonAccount) {
|
|
211
|
+
try {
|
|
212
|
+
const ssrfPolicy = ssrfPolicyFromDangerouslyAllowPrivateNetwork(
|
|
213
|
+
account.dangerouslyAllowPrivateNetwork,
|
|
214
|
+
);
|
|
215
|
+
const cookie = await authenticate(account.url, account.code, { ssrfPolicy });
|
|
216
|
+
const { response, release } = await urbitFetch({
|
|
217
|
+
baseUrl: account.url,
|
|
218
|
+
path: "/~/name",
|
|
219
|
+
init: {
|
|
220
|
+
method: "GET",
|
|
221
|
+
headers: { Cookie: cookie },
|
|
222
|
+
},
|
|
223
|
+
ssrfPolicy,
|
|
224
|
+
timeoutMs: 30_000,
|
|
225
|
+
auditContext: "tlon-probe-account",
|
|
226
|
+
});
|
|
227
|
+
try {
|
|
228
|
+
if (!response.ok) {
|
|
229
|
+
return { ok: false, error: `Name request failed: ${response.status}` };
|
|
230
|
+
}
|
|
231
|
+
return { ok: true };
|
|
232
|
+
} finally {
|
|
233
|
+
await release();
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return { ok: false, error: (error as { message?: string })?.message ?? String(error) };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function startTlonGatewayAccount(
|
|
241
|
+
ctx: Parameters<
|
|
242
|
+
NonNullable<NonNullable<ChannelPlugin<ResolvedTlonAccount>["gateway"]>["startAccount"]>
|
|
243
|
+
>[0],
|
|
244
|
+
) {
|
|
245
|
+
const account = ctx.account;
|
|
246
|
+
ctx.setStatus({
|
|
247
|
+
accountId: account.accountId,
|
|
248
|
+
ship: account.ship,
|
|
249
|
+
url: account.url,
|
|
250
|
+
} as ChannelAccountSnapshot);
|
|
251
|
+
ctx.log?.info(`[${account.accountId}] starting Tlon provider for ${account.ship ?? "tlon"}`);
|
|
252
|
+
return monitorTlonProvider({
|
|
253
|
+
runtime: ctx.runtime,
|
|
254
|
+
abortSignal: ctx.abortSignal,
|
|
255
|
+
accountId: account.accountId,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export { tlonSetupWizard };
|
package/src/channel.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describeAccountSnapshot } from "klaw/plugin-sdk/account-helpers";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID } from "klaw/plugin-sdk/account-id";
|
|
3
|
+
import { createHybridChannelConfigAdapter } from "klaw/plugin-sdk/channel-config-helpers";
|
|
4
|
+
import { createChatChannelPlugin, type ChannelPlugin } from "klaw/plugin-sdk/channel-core";
|
|
5
|
+
import { createChannelMessageAdapterFromOutbound } from "klaw/plugin-sdk/channel-message";
|
|
6
|
+
import type { ChannelOutboundAdapter } from "klaw/plugin-sdk/channel-send-result";
|
|
7
|
+
import { createLazyRuntimeModule } from "klaw/plugin-sdk/lazy-runtime";
|
|
8
|
+
import { createRuntimeOutboundDelegates } from "klaw/plugin-sdk/outbound-runtime";
|
|
9
|
+
import {
|
|
10
|
+
createComputedAccountStatusAdapter,
|
|
11
|
+
createDefaultChannelRuntimeState,
|
|
12
|
+
} from "klaw/plugin-sdk/status-helpers";
|
|
13
|
+
import { tlonChannelConfigSchema } from "./config-schema.js";
|
|
14
|
+
import { tlonDoctor } from "./doctor.js";
|
|
15
|
+
import { resolveTlonOutboundSessionRoute } from "./session-route.js";
|
|
16
|
+
import { createTlonSetupWizardBase, tlonSetupAdapter } from "./setup-core.js";
|
|
17
|
+
import {
|
|
18
|
+
formatTargetHint,
|
|
19
|
+
normalizeShip,
|
|
20
|
+
parseTlonTarget,
|
|
21
|
+
resolveTlonOutboundTarget,
|
|
22
|
+
} from "./targets.js";
|
|
23
|
+
import { listTlonAccountIds, resolveTlonAccount } from "./types.js";
|
|
24
|
+
|
|
25
|
+
const TLON_CHANNEL_ID = "tlon" as const;
|
|
26
|
+
|
|
27
|
+
const loadTlonChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js"));
|
|
28
|
+
|
|
29
|
+
const tlonSetupWizardProxy = createTlonSetupWizardBase({
|
|
30
|
+
resolveConfigured: async ({ cfg, accountId }) =>
|
|
31
|
+
await (
|
|
32
|
+
await loadTlonChannelRuntime()
|
|
33
|
+
).tlonSetupWizard.status.resolveConfigured({
|
|
34
|
+
cfg,
|
|
35
|
+
accountId,
|
|
36
|
+
}),
|
|
37
|
+
resolveStatusLines: async ({ cfg, accountId, configured }) =>
|
|
38
|
+
(await (
|
|
39
|
+
await loadTlonChannelRuntime()
|
|
40
|
+
).tlonSetupWizard.status.resolveStatusLines?.({
|
|
41
|
+
cfg,
|
|
42
|
+
accountId,
|
|
43
|
+
configured,
|
|
44
|
+
})) ?? [],
|
|
45
|
+
finalize: async (params) =>
|
|
46
|
+
await (
|
|
47
|
+
await loadTlonChannelRuntime()
|
|
48
|
+
).tlonSetupWizard.finalize!(params),
|
|
49
|
+
}) satisfies NonNullable<ChannelPlugin["setupWizard"]>;
|
|
50
|
+
|
|
51
|
+
const tlonConfigAdapter = createHybridChannelConfigAdapter({
|
|
52
|
+
sectionKey: TLON_CHANNEL_ID,
|
|
53
|
+
listAccountIds: listTlonAccountIds,
|
|
54
|
+
resolveAccount: resolveTlonAccount,
|
|
55
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
56
|
+
clearBaseFields: ["ship", "code", "url", "name"],
|
|
57
|
+
preserveSectionOnDefaultDelete: true,
|
|
58
|
+
resolveAllowFrom: (account) => account.dmAllowlist,
|
|
59
|
+
formatAllowFrom: (allowFrom) =>
|
|
60
|
+
allowFrom.map((entry) => normalizeShip(String(entry))).filter(Boolean),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const tlonChannelOutbound: ChannelOutboundAdapter = {
|
|
64
|
+
deliveryMode: "direct",
|
|
65
|
+
textChunkLimit: 10000,
|
|
66
|
+
resolveTarget: ({ to }) => resolveTlonOutboundTarget(to),
|
|
67
|
+
deliveryCapabilities: {
|
|
68
|
+
durableFinal: {
|
|
69
|
+
text: true,
|
|
70
|
+
media: true,
|
|
71
|
+
replyTo: true,
|
|
72
|
+
thread: true,
|
|
73
|
+
messageSendingHooks: true,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
...createRuntimeOutboundDelegates({
|
|
77
|
+
getRuntime: loadTlonChannelRuntime,
|
|
78
|
+
sendText: { resolve: (runtime) => runtime.tlonRuntimeOutbound.sendText },
|
|
79
|
+
sendMedia: { resolve: (runtime) => runtime.tlonRuntimeOutbound.sendMedia },
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const tlonMessageAdapter = createChannelMessageAdapterFromOutbound({
|
|
84
|
+
id: TLON_CHANNEL_ID,
|
|
85
|
+
outbound: tlonChannelOutbound,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const tlonPlugin = createChatChannelPlugin({
|
|
89
|
+
base: {
|
|
90
|
+
id: TLON_CHANNEL_ID,
|
|
91
|
+
meta: {
|
|
92
|
+
id: TLON_CHANNEL_ID,
|
|
93
|
+
label: "Tlon",
|
|
94
|
+
selectionLabel: "Tlon (Urbit)",
|
|
95
|
+
docsPath: "/channels/tlon",
|
|
96
|
+
docsLabel: "tlon",
|
|
97
|
+
blurb: "Decentralized messaging on Urbit",
|
|
98
|
+
aliases: ["urbit"],
|
|
99
|
+
order: 90,
|
|
100
|
+
},
|
|
101
|
+
capabilities: {
|
|
102
|
+
chatTypes: ["direct", "group", "thread"],
|
|
103
|
+
media: true,
|
|
104
|
+
reply: true,
|
|
105
|
+
threads: true,
|
|
106
|
+
},
|
|
107
|
+
setup: tlonSetupAdapter,
|
|
108
|
+
setupWizard: tlonSetupWizardProxy,
|
|
109
|
+
reload: { configPrefixes: ["channels.tlon"] },
|
|
110
|
+
configSchema: tlonChannelConfigSchema,
|
|
111
|
+
config: {
|
|
112
|
+
...tlonConfigAdapter,
|
|
113
|
+
isConfigured: (account) => account.configured,
|
|
114
|
+
describeAccount: (account) =>
|
|
115
|
+
describeAccountSnapshot({
|
|
116
|
+
account,
|
|
117
|
+
configured: account.configured,
|
|
118
|
+
extra: {
|
|
119
|
+
ship: account.ship,
|
|
120
|
+
url: account.url,
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
},
|
|
124
|
+
doctor: tlonDoctor,
|
|
125
|
+
messaging: {
|
|
126
|
+
targetPrefixes: ["tlon"],
|
|
127
|
+
normalizeTarget: (target) => {
|
|
128
|
+
const parsed = parseTlonTarget(target);
|
|
129
|
+
if (!parsed) {
|
|
130
|
+
return target.trim();
|
|
131
|
+
}
|
|
132
|
+
if (parsed.kind === "dm") {
|
|
133
|
+
return parsed.ship;
|
|
134
|
+
}
|
|
135
|
+
return parsed.nest;
|
|
136
|
+
},
|
|
137
|
+
targetResolver: {
|
|
138
|
+
looksLikeId: (target) => Boolean(parseTlonTarget(target)),
|
|
139
|
+
hint: formatTargetHint(),
|
|
140
|
+
},
|
|
141
|
+
resolveOutboundSessionRoute: (params) => resolveTlonOutboundSessionRoute(params),
|
|
142
|
+
},
|
|
143
|
+
message: tlonMessageAdapter,
|
|
144
|
+
status: createComputedAccountStatusAdapter<ReturnType<typeof resolveTlonAccount>>({
|
|
145
|
+
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
|
|
146
|
+
collectStatusIssues: (accounts) => {
|
|
147
|
+
return accounts.flatMap((account) => {
|
|
148
|
+
if (!account.configured) {
|
|
149
|
+
return [
|
|
150
|
+
{
|
|
151
|
+
channel: TLON_CHANNEL_ID,
|
|
152
|
+
accountId: account.accountId,
|
|
153
|
+
kind: "config",
|
|
154
|
+
message: "Account not configured (missing ship, code, or url)",
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
return [];
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
buildChannelSummary: ({ snapshot }) => {
|
|
162
|
+
const s = snapshot as { configured?: boolean; ship?: string; url?: string };
|
|
163
|
+
return {
|
|
164
|
+
configured: s.configured ?? false,
|
|
165
|
+
ship: s.ship ?? null,
|
|
166
|
+
url: s.url ?? null,
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
probeAccount: async ({ account }) => {
|
|
170
|
+
if (!account.configured || !account.ship || !account.url || !account.code) {
|
|
171
|
+
return { ok: false, error: "Not configured" };
|
|
172
|
+
}
|
|
173
|
+
return await (await loadTlonChannelRuntime()).probeTlonAccount(account as never);
|
|
174
|
+
},
|
|
175
|
+
resolveAccountSnapshot: ({ account }) => ({
|
|
176
|
+
accountId: account.accountId,
|
|
177
|
+
name: account.name ?? undefined,
|
|
178
|
+
enabled: account.enabled,
|
|
179
|
+
configured: account.configured,
|
|
180
|
+
extra: {
|
|
181
|
+
ship: account.ship,
|
|
182
|
+
url: account.url,
|
|
183
|
+
},
|
|
184
|
+
}),
|
|
185
|
+
}),
|
|
186
|
+
gateway: {
|
|
187
|
+
startAccount: async (ctx) =>
|
|
188
|
+
await (await loadTlonChannelRuntime()).startTlonGatewayAccount(ctx),
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
outbound: tlonChannelOutbound,
|
|
192
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { buildChannelConfigSchema } from "klaw/plugin-sdk/channel-config-schema";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
const ShipSchema = z.string().min(1);
|
|
5
|
+
const ChannelNestSchema = z.string().min(1);
|
|
6
|
+
|
|
7
|
+
const TlonChannelRuleSchema = z.object({
|
|
8
|
+
mode: z.enum(["restricted", "open"]).optional(),
|
|
9
|
+
allowedShips: z.array(ShipSchema).optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const TlonAuthorizationSchema = z.object({
|
|
13
|
+
channelRules: z.record(z.string(), TlonChannelRuleSchema).optional(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const TlonNetworkSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
dangerouslyAllowPrivateNetwork: z.boolean().optional(),
|
|
19
|
+
})
|
|
20
|
+
.strict()
|
|
21
|
+
.optional();
|
|
22
|
+
|
|
23
|
+
const tlonCommonConfigFields = {
|
|
24
|
+
name: z.string().optional(),
|
|
25
|
+
enabled: z.boolean().optional(),
|
|
26
|
+
ship: ShipSchema.optional(),
|
|
27
|
+
url: z.string().optional(),
|
|
28
|
+
code: z.string().optional(),
|
|
29
|
+
network: TlonNetworkSchema,
|
|
30
|
+
groupChannels: z.array(ChannelNestSchema).optional(),
|
|
31
|
+
dmAllowlist: z.array(ShipSchema).optional(),
|
|
32
|
+
groupInviteAllowlist: z.array(ShipSchema).optional(),
|
|
33
|
+
autoDiscoverChannels: z.boolean().optional(),
|
|
34
|
+
showModelSignature: z.boolean().optional(),
|
|
35
|
+
responsePrefix: z.string().optional(),
|
|
36
|
+
// Auto-accept settings
|
|
37
|
+
autoAcceptDmInvites: z.boolean().optional(), // Auto-accept DMs from ships in dmAllowlist
|
|
38
|
+
autoAcceptGroupInvites: z.boolean().optional(), // Auto-accept all group invites
|
|
39
|
+
// Owner ship for approval system
|
|
40
|
+
ownerShip: ShipSchema.optional(), // Ship that receives approval requests and can approve/deny
|
|
41
|
+
} satisfies z.ZodRawShape;
|
|
42
|
+
|
|
43
|
+
const TlonAccountSchema = z.object({
|
|
44
|
+
...tlonCommonConfigFields,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const TlonConfigSchema = z.object({
|
|
48
|
+
...tlonCommonConfigFields,
|
|
49
|
+
authorization: TlonAuthorizationSchema.optional(),
|
|
50
|
+
defaultAuthorizedShips: z.array(ShipSchema).optional(),
|
|
51
|
+
accounts: z.record(z.string(), TlonAccountSchema).optional(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const tlonChannelConfigSchema = buildChannelConfigSchema(TlonConfigSchema);
|