@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,259 +0,0 @@
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 DELETED
@@ -1,192 +0,0 @@
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
- });
@@ -1,54 +0,0 @@
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);