@actagent/tlon 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/README.md +5 -0
- package/actagent.plugin.json +15 -0
- package/api.ts +17 -0
- package/channel-plugin-api.ts +2 -0
- package/doctor-contract-api.ts +2 -0
- package/index.ts +17 -0
- package/npm-shrinkwrap.json +861 -0
- package/package.json +87 -0
- package/runtime-api.ts +17 -0
- package/setup-api.ts +3 -0
- package/setup-entry.ts +10 -0
- package/src/account-fields.ts +32 -0
- package/src/channel.message-adapter.test.ts +147 -0
- package/src/channel.runtime.ts +260 -0
- package/src/channel.ts +193 -0
- package/src/config-schema.ts +55 -0
- package/src/core.test.ts +299 -0
- package/src/doctor-contract.ts +10 -0
- package/src/doctor.test.ts +47 -0
- package/src/doctor.ts +11 -0
- package/src/logger-runtime.ts +2 -0
- package/src/monitor/approval-runtime.ts +364 -0
- package/src/monitor/approval.test.ts +34 -0
- package/src/monitor/approval.ts +283 -0
- package/src/monitor/authorization.ts +31 -0
- package/src/monitor/cites.ts +55 -0
- package/src/monitor/discovery.ts +69 -0
- package/src/monitor/history.ts +227 -0
- package/src/monitor/index.ts +1531 -0
- package/src/monitor/media.test.ts +81 -0
- package/src/monitor/media.ts +157 -0
- package/src/monitor/processed-messages.test.ts +59 -0
- package/src/monitor/processed-messages.ts +90 -0
- package/src/monitor/settings-helpers.test.ts +114 -0
- package/src/monitor/settings-helpers.ts +151 -0
- package/src/monitor/utils.ts +403 -0
- package/src/runtime.ts +10 -0
- package/src/security.test.ts +654 -0
- package/src/session-route.ts +41 -0
- package/src/settings.ts +391 -0
- package/src/setup-core.ts +232 -0
- package/src/setup-surface.ts +98 -0
- package/src/targets.ts +103 -0
- package/src/tlon-api.test.ts +573 -0
- package/src/tlon-api.ts +390 -0
- package/src/types.ts +161 -0
- package/src/urbit/auth.ssrf.test.ts +46 -0
- package/src/urbit/auth.ts +49 -0
- package/src/urbit/base-url.test.ts +49 -0
- package/src/urbit/base-url.ts +62 -0
- package/src/urbit/channel-ops.test.ts +37 -0
- package/src/urbit/channel-ops.ts +150 -0
- package/src/urbit/context.ts +51 -0
- package/src/urbit/errors.ts +52 -0
- package/src/urbit/fetch.ts +43 -0
- package/src/urbit/foreigns.ts +49 -0
- package/src/urbit/send.test.ts +84 -0
- package/src/urbit/send.ts +229 -0
- package/src/urbit/sse-client.test.ts +262 -0
- package/src/urbit/sse-client.ts +507 -0
- package/src/urbit/story.ts +327 -0
- package/src/urbit/upload.test.ts +156 -0
- package/src/urbit/upload.ts +60 -0
- package/test-api.ts +2 -0
- package/tsconfig.json +16 -0
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@actagent/tlon",
|
|
3
|
+
"version": "2026.6.2",
|
|
4
|
+
"description": "ACTAgent Tlon/Urbit channel plugin for chat workflows.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/actagent/actagent"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@aws-sdk/client-s3": "3.1056.0",
|
|
12
|
+
"@aws-sdk/s3-request-presigner": "3.1056.0",
|
|
13
|
+
"@tloncorp/tlon-skill": "0.4.0",
|
|
14
|
+
"@urbit/aura": "3.0.0",
|
|
15
|
+
"zod": "4.4.3"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@actagent/plugin-sdk": "workspace:*",
|
|
19
|
+
"actagent": "workspace:*"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"actagent": "workspace:*"
|
|
23
|
+
},
|
|
24
|
+
"peerDependenciesMeta": {
|
|
25
|
+
"actagent": {
|
|
26
|
+
"optional": true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"actagent": {
|
|
30
|
+
"extensions": [
|
|
31
|
+
"./index.ts"
|
|
32
|
+
],
|
|
33
|
+
"setupEntry": "./setup-entry.ts",
|
|
34
|
+
"channel": {
|
|
35
|
+
"id": "tlon",
|
|
36
|
+
"label": "Tlon",
|
|
37
|
+
"selectionLabel": "Tlon (Urbit)",
|
|
38
|
+
"docsPath": "/channels/tlon",
|
|
39
|
+
"docsLabel": "tlon",
|
|
40
|
+
"blurb": "decentralized messaging on Urbit; install the plugin to enable.",
|
|
41
|
+
"order": 90,
|
|
42
|
+
"quickstartAllowFrom": true,
|
|
43
|
+
"cliAddOptions": [
|
|
44
|
+
{
|
|
45
|
+
"flags": "--ship <ship>",
|
|
46
|
+
"description": "Tlon ship name (~sampel-palnet)"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"flags": "--code <code>",
|
|
50
|
+
"description": "Tlon login code"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"flags": "--group-channels <list>",
|
|
54
|
+
"description": "Tlon group channels (comma-separated)"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"flags": "--dm-allowlist <list>",
|
|
58
|
+
"description": "Tlon DM allowlist (comma-separated ships)"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"flags": "--auto-discover-channels",
|
|
62
|
+
"description": "Tlon auto-discover group channels"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"flags": "--no-auto-discover-channels",
|
|
66
|
+
"description": "Disable Tlon auto-discovery"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"install": {
|
|
71
|
+
"npmSpec": "@actagent/tlon",
|
|
72
|
+
"defaultChoice": "npm",
|
|
73
|
+
"minHostVersion": ">=2026.4.10"
|
|
74
|
+
},
|
|
75
|
+
"compat": {
|
|
76
|
+
"pluginApi": ">=2026.6.2"
|
|
77
|
+
},
|
|
78
|
+
"build": {
|
|
79
|
+
"actagentVersion": "2026.6.2"
|
|
80
|
+
},
|
|
81
|
+
"release": {
|
|
82
|
+
"bundleRuntimeDependencies": false,
|
|
83
|
+
"publishToACTAgentHub": true,
|
|
84
|
+
"publishToNpm": true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
package/runtime-api.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Private runtime barrel for the bundled Tlon extension.
|
|
2
|
+
// Keep this barrel thin and aligned with the local extension surface.
|
|
3
|
+
|
|
4
|
+
export type { ReplyPayload } from "actagent/plugin-sdk/reply-runtime";
|
|
5
|
+
export type { ACTAgentConfig } from "actagent/plugin-sdk/config-contracts";
|
|
6
|
+
export type { RuntimeEnv } from "actagent/plugin-sdk/runtime";
|
|
7
|
+
export { createDedupeCache } from "actagent/plugin-sdk/core";
|
|
8
|
+
export { createLoggerBackedRuntime } from "./src/logger-runtime.js";
|
|
9
|
+
export {
|
|
10
|
+
fetchWithSsrFGuard,
|
|
11
|
+
isBlockedHostnameOrIp,
|
|
12
|
+
ssrfPolicyFromAllowPrivateNetwork,
|
|
13
|
+
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
|
|
14
|
+
type LookupFn,
|
|
15
|
+
type SsrFPolicy,
|
|
16
|
+
} from "actagent/plugin-sdk/ssrf-runtime";
|
|
17
|
+
export { SsrFBlockedError } from "actagent/plugin-sdk/ssrf-runtime";
|
package/setup-api.ts
ADDED
package/setup-entry.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Tlon plugin module implements setup entry behavior.
|
|
2
|
+
import { defineBundledChannelSetupEntry } from "actagent/plugin-sdk/channel-entry-contract";
|
|
3
|
+
|
|
4
|
+
export default defineBundledChannelSetupEntry({
|
|
5
|
+
importMetaUrl: import.meta.url,
|
|
6
|
+
plugin: {
|
|
7
|
+
specifier: "./api.js",
|
|
8
|
+
exportName: "tlonPlugin",
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Tlon plugin module implements account fields behavior.
|
|
2
|
+
export type TlonAccountFieldsInput = {
|
|
3
|
+
ship?: string;
|
|
4
|
+
url?: string;
|
|
5
|
+
code?: string;
|
|
6
|
+
dangerouslyAllowPrivateNetwork?: boolean;
|
|
7
|
+
groupChannels?: string[];
|
|
8
|
+
dmAllowlist?: string[];
|
|
9
|
+
autoDiscoverChannels?: boolean;
|
|
10
|
+
ownerShip?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function buildTlonAccountFields(input: TlonAccountFieldsInput) {
|
|
14
|
+
return {
|
|
15
|
+
...(input.ship ? { ship: input.ship } : {}),
|
|
16
|
+
...(input.url ? { url: input.url } : {}),
|
|
17
|
+
...(input.code ? { code: input.code } : {}),
|
|
18
|
+
...(typeof input.dangerouslyAllowPrivateNetwork === "boolean"
|
|
19
|
+
? {
|
|
20
|
+
network: {
|
|
21
|
+
dangerouslyAllowPrivateNetwork: input.dangerouslyAllowPrivateNetwork,
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
: {}),
|
|
25
|
+
...(input.groupChannels ? { groupChannels: input.groupChannels } : {}),
|
|
26
|
+
...(input.dmAllowlist ? { dmAllowlist: input.dmAllowlist } : {}),
|
|
27
|
+
...(typeof input.autoDiscoverChannels === "boolean"
|
|
28
|
+
? { autoDiscoverChannels: input.autoDiscoverChannels }
|
|
29
|
+
: {}),
|
|
30
|
+
...(input.ownerShip ? { ownerShip: input.ownerShip } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Tlon tests cover channel.message adapter plugin behavior.
|
|
2
|
+
import { verifyChannelMessageAdapterCapabilityProofs } from "actagent/plugin-sdk/channel-outbound";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { ACTAgentConfig } from "../runtime-api.js";
|
|
5
|
+
|
|
6
|
+
const mocks = vi.hoisted(() => ({
|
|
7
|
+
sendText: vi.fn(),
|
|
8
|
+
sendMedia: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("./channel.runtime.js", () => ({
|
|
12
|
+
tlonRuntimeOutbound: {
|
|
13
|
+
sendText: mocks.sendText,
|
|
14
|
+
sendMedia: mocks.sendMedia,
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { tlonPlugin } from "./channel.js";
|
|
19
|
+
|
|
20
|
+
const cfg = {
|
|
21
|
+
channels: {
|
|
22
|
+
tlon: {
|
|
23
|
+
ship: "~zod",
|
|
24
|
+
url: "https://zod.example",
|
|
25
|
+
code: "lidlut-tabwed-pillex-ridrup",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
} as ACTAgentConfig;
|
|
29
|
+
|
|
30
|
+
describe("tlon channel message adapter", () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
mocks.sendText.mockReset();
|
|
33
|
+
mocks.sendMedia.mockReset();
|
|
34
|
+
mocks.sendText.mockResolvedValue({
|
|
35
|
+
channel: "tlon",
|
|
36
|
+
messageId: "~zod/1700000000000",
|
|
37
|
+
conversationId: "~nec/general",
|
|
38
|
+
});
|
|
39
|
+
mocks.sendMedia.mockResolvedValue({
|
|
40
|
+
channel: "tlon",
|
|
41
|
+
messageId: "~zod/1700000000001",
|
|
42
|
+
conversationId: "~nec/general",
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("backs declared durable-final capabilities with outbound send proofs", async () => {
|
|
47
|
+
const adapter = tlonPlugin.message;
|
|
48
|
+
if (!adapter?.send?.text || !adapter.send.media) {
|
|
49
|
+
throw new Error("expected tlon channel message adapter with text and media senders");
|
|
50
|
+
}
|
|
51
|
+
const sendText = adapter.send.text;
|
|
52
|
+
const sendMedia = adapter.send.media;
|
|
53
|
+
|
|
54
|
+
const proveText = async () => {
|
|
55
|
+
mocks.sendText.mockClear();
|
|
56
|
+
const result = await sendText({
|
|
57
|
+
cfg,
|
|
58
|
+
to: "chat/~nec/general",
|
|
59
|
+
text: "hello",
|
|
60
|
+
accountId: "default",
|
|
61
|
+
});
|
|
62
|
+
expect(mocks.sendText).toHaveBeenLastCalledWith({
|
|
63
|
+
cfg,
|
|
64
|
+
to: "chat/~nec/general",
|
|
65
|
+
text: "hello",
|
|
66
|
+
accountId: "default",
|
|
67
|
+
replyToId: undefined,
|
|
68
|
+
threadId: undefined,
|
|
69
|
+
});
|
|
70
|
+
expect(result.receipt.platformMessageIds).toEqual(["~zod/1700000000000"]);
|
|
71
|
+
expect(result.receipt.parts[0]?.kind).toBe("text");
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const proveMedia = async () => {
|
|
75
|
+
mocks.sendMedia.mockClear();
|
|
76
|
+
const result = await sendMedia({
|
|
77
|
+
cfg,
|
|
78
|
+
to: "chat/~nec/general",
|
|
79
|
+
text: "image",
|
|
80
|
+
mediaUrl: "https://example.com/image.png",
|
|
81
|
+
accountId: "default",
|
|
82
|
+
});
|
|
83
|
+
expect(mocks.sendMedia).toHaveBeenLastCalledWith({
|
|
84
|
+
cfg,
|
|
85
|
+
to: "chat/~nec/general",
|
|
86
|
+
text: "image",
|
|
87
|
+
mediaUrl: "https://example.com/image.png",
|
|
88
|
+
accountId: "default",
|
|
89
|
+
replyToId: undefined,
|
|
90
|
+
threadId: undefined,
|
|
91
|
+
});
|
|
92
|
+
expect(result.receipt.platformMessageIds).toEqual(["~zod/1700000000001"]);
|
|
93
|
+
expect(result.receipt.parts[0]?.kind).toBe("media");
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const proveReplyThread = async () => {
|
|
97
|
+
mocks.sendText.mockClear();
|
|
98
|
+
const result = await sendText({
|
|
99
|
+
cfg,
|
|
100
|
+
to: "chat/~nec/general",
|
|
101
|
+
text: "threaded",
|
|
102
|
+
accountId: "default",
|
|
103
|
+
replyToId: "1700000000000",
|
|
104
|
+
threadId: "1700000000000",
|
|
105
|
+
});
|
|
106
|
+
expect(mocks.sendText).toHaveBeenLastCalledWith({
|
|
107
|
+
cfg,
|
|
108
|
+
to: "chat/~nec/general",
|
|
109
|
+
text: "threaded",
|
|
110
|
+
accountId: "default",
|
|
111
|
+
replyToId: "1700000000000",
|
|
112
|
+
threadId: "1700000000000",
|
|
113
|
+
});
|
|
114
|
+
expect(result.receipt.replyToId).toBe("1700000000000");
|
|
115
|
+
expect(result.receipt.threadId).toBe("1700000000000");
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const proofs = await verifyChannelMessageAdapterCapabilityProofs({
|
|
119
|
+
adapterName: "tlonMessageAdapter",
|
|
120
|
+
adapter,
|
|
121
|
+
proofs: {
|
|
122
|
+
text: proveText,
|
|
123
|
+
media: proveMedia,
|
|
124
|
+
replyTo: proveReplyThread,
|
|
125
|
+
thread: proveReplyThread,
|
|
126
|
+
messageSendingHooks: () => {
|
|
127
|
+
expect(sendText).toBeTypeOf("function");
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
expect(proofs).toStrictEqual([
|
|
132
|
+
{ capability: "text", status: "verified" },
|
|
133
|
+
{ capability: "media", status: "verified" },
|
|
134
|
+
{ capability: "poll", status: "not_declared" },
|
|
135
|
+
{ capability: "payload", status: "not_declared" },
|
|
136
|
+
{ capability: "silent", status: "not_declared" },
|
|
137
|
+
{ capability: "replyTo", status: "verified" },
|
|
138
|
+
{ capability: "thread", status: "verified" },
|
|
139
|
+
{ capability: "nativeQuote", status: "not_declared" },
|
|
140
|
+
{ capability: "messageSendingHooks", status: "verified" },
|
|
141
|
+
{ capability: "batch", status: "not_declared" },
|
|
142
|
+
{ capability: "reconcileUnknownSend", status: "not_declared" },
|
|
143
|
+
{ capability: "afterSendSuccess", status: "not_declared" },
|
|
144
|
+
{ capability: "afterCommit", status: "not_declared" },
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Tlon plugin module implements channel behavior.
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import type { ChannelAccountSnapshot } from "actagent/plugin-sdk/channel-contract";
|
|
4
|
+
import type { ChannelOutboundAdapter } from "actagent/plugin-sdk/channel-send-result";
|
|
5
|
+
import type { ACTAgentConfig } from "actagent/plugin-sdk/config-contracts";
|
|
6
|
+
import type { ChannelPlugin } from "actagent/plugin-sdk/core";
|
|
7
|
+
import { monitorTlonProvider } from "./monitor/index.js";
|
|
8
|
+
import { tlonSetupWizard } from "./setup-surface.js";
|
|
9
|
+
import {
|
|
10
|
+
formatTargetHint,
|
|
11
|
+
normalizeShip,
|
|
12
|
+
parseTlonTarget,
|
|
13
|
+
resolveTlonOutboundTarget,
|
|
14
|
+
} from "./targets.js";
|
|
15
|
+
import { configureClient } from "./tlon-api.js";
|
|
16
|
+
import { resolveTlonAccount } from "./types.js";
|
|
17
|
+
import { authenticate } from "./urbit/auth.js";
|
|
18
|
+
import { ssrfPolicyFromDangerouslyAllowPrivateNetwork } from "./urbit/context.js";
|
|
19
|
+
import { urbitFetch } from "./urbit/fetch.js";
|
|
20
|
+
import {
|
|
21
|
+
buildMediaStory,
|
|
22
|
+
sendDm,
|
|
23
|
+
sendDmWithStory,
|
|
24
|
+
sendGroupMessage,
|
|
25
|
+
sendGroupMessageWithStory,
|
|
26
|
+
} from "./urbit/send.js";
|
|
27
|
+
import { uploadImageFromUrl } from "./urbit/upload.js";
|
|
28
|
+
|
|
29
|
+
type ResolvedTlonAccount = ReturnType<typeof resolveTlonAccount>;
|
|
30
|
+
type ConfiguredTlonAccount = ResolvedTlonAccount & {
|
|
31
|
+
ship: string;
|
|
32
|
+
url: string;
|
|
33
|
+
code: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
async function createHttpPokeApi(params: {
|
|
37
|
+
url: string;
|
|
38
|
+
code: string;
|
|
39
|
+
ship: string;
|
|
40
|
+
dangerouslyAllowPrivateNetwork?: boolean;
|
|
41
|
+
}) {
|
|
42
|
+
const ssrfPolicy = ssrfPolicyFromDangerouslyAllowPrivateNetwork(
|
|
43
|
+
params.dangerouslyAllowPrivateNetwork,
|
|
44
|
+
);
|
|
45
|
+
const cookie = await authenticate(params.url, params.code, { ssrfPolicy });
|
|
46
|
+
const channelId = `${Math.floor(Date.now() / 1000)}-${crypto.randomUUID()}`;
|
|
47
|
+
const channelPath = `/~/channel/${channelId}`;
|
|
48
|
+
const shipName = params.ship.replace(/^~/, "");
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
poke: async (pokeParams: { app: string; mark: string; json: unknown }) => {
|
|
52
|
+
const pokeId = Date.now();
|
|
53
|
+
const pokeData = {
|
|
54
|
+
id: pokeId,
|
|
55
|
+
action: "poke",
|
|
56
|
+
ship: shipName,
|
|
57
|
+
app: pokeParams.app,
|
|
58
|
+
mark: pokeParams.mark,
|
|
59
|
+
json: pokeParams.json,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const { response, release } = await urbitFetch({
|
|
63
|
+
baseUrl: params.url,
|
|
64
|
+
path: channelPath,
|
|
65
|
+
init: {
|
|
66
|
+
method: "PUT",
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
Cookie: cookie.split(";")[0],
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify([pokeData]),
|
|
72
|
+
},
|
|
73
|
+
ssrfPolicy,
|
|
74
|
+
auditContext: "tlon-poke",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
if (!response.ok && response.status !== 204) {
|
|
79
|
+
const errorText = await response.text();
|
|
80
|
+
throw new Error(`Poke failed: ${response.status} - ${errorText}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return pokeId;
|
|
84
|
+
} finally {
|
|
85
|
+
await release();
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
delete: async () => {
|
|
89
|
+
// No-op for HTTP-only client
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveOutboundContext(params: {
|
|
95
|
+
cfg: ACTAgentConfig;
|
|
96
|
+
accountId?: string | null;
|
|
97
|
+
to: string;
|
|
98
|
+
}) {
|
|
99
|
+
const account = resolveTlonAccount(params.cfg, params.accountId ?? undefined);
|
|
100
|
+
if (!account.configured || !account.ship || !account.url || !account.code) {
|
|
101
|
+
throw new Error("Tlon account not configured");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const parsed = parseTlonTarget(params.to);
|
|
105
|
+
if (!parsed) {
|
|
106
|
+
throw new Error(`Invalid Tlon target. Use ${formatTargetHint()}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { account: account as ConfiguredTlonAccount, parsed };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveReplyId(replyToId?: string | null, threadId?: string | number | null) {
|
|
113
|
+
return (replyToId ?? threadId) ? String(replyToId ?? threadId) : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function withHttpPokeAccountApi<T>(
|
|
117
|
+
account: ConfiguredTlonAccount,
|
|
118
|
+
run: (api: Awaited<ReturnType<typeof createHttpPokeApi>>) => Promise<T>,
|
|
119
|
+
) {
|
|
120
|
+
const api = await createHttpPokeApi({
|
|
121
|
+
url: account.url,
|
|
122
|
+
ship: account.ship,
|
|
123
|
+
code: account.code,
|
|
124
|
+
dangerouslyAllowPrivateNetwork: account.dangerouslyAllowPrivateNetwork ?? undefined,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
return await run(api);
|
|
129
|
+
} finally {
|
|
130
|
+
try {
|
|
131
|
+
await api.delete();
|
|
132
|
+
} catch {
|
|
133
|
+
// ignore cleanup errors
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const tlonRuntimeOutbound: ChannelOutboundAdapter = {
|
|
139
|
+
deliveryMode: "direct",
|
|
140
|
+
textChunkLimit: 10000,
|
|
141
|
+
resolveTarget: ({ to }) => resolveTlonOutboundTarget(to),
|
|
142
|
+
deliveryCapabilities: {
|
|
143
|
+
durableFinal: {
|
|
144
|
+
text: true,
|
|
145
|
+
media: true,
|
|
146
|
+
replyTo: true,
|
|
147
|
+
thread: true,
|
|
148
|
+
messageSendingHooks: true,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => {
|
|
152
|
+
const { account, parsed } = resolveOutboundContext({ cfg, accountId, to });
|
|
153
|
+
return withHttpPokeAccountApi(account, async (api) => {
|
|
154
|
+
const fromShip = normalizeShip(account.ship);
|
|
155
|
+
if (parsed.kind === "dm") {
|
|
156
|
+
return await sendDm({
|
|
157
|
+
api,
|
|
158
|
+
fromShip,
|
|
159
|
+
toShip: parsed.ship,
|
|
160
|
+
text,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return await sendGroupMessage({
|
|
164
|
+
api,
|
|
165
|
+
fromShip,
|
|
166
|
+
hostShip: parsed.hostShip,
|
|
167
|
+
channelName: parsed.channelName,
|
|
168
|
+
text,
|
|
169
|
+
replyToId: resolveReplyId(replyToId, threadId),
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId, threadId }) => {
|
|
174
|
+
const { account, parsed } = resolveOutboundContext({ cfg, accountId, to });
|
|
175
|
+
|
|
176
|
+
configureClient({
|
|
177
|
+
shipUrl: account.url,
|
|
178
|
+
shipName: account.ship.replace(/^~/, ""),
|
|
179
|
+
verbose: false,
|
|
180
|
+
getCode: async () => account.code,
|
|
181
|
+
dangerouslyAllowPrivateNetwork: account.dangerouslyAllowPrivateNetwork ?? undefined,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const uploadedUrl = mediaUrl ? await uploadImageFromUrl(mediaUrl) : undefined;
|
|
185
|
+
return withHttpPokeAccountApi(account, async (api) => {
|
|
186
|
+
const fromShip = normalizeShip(account.ship);
|
|
187
|
+
const story = buildMediaStory(text, uploadedUrl);
|
|
188
|
+
|
|
189
|
+
if (parsed.kind === "dm") {
|
|
190
|
+
return await sendDmWithStory({
|
|
191
|
+
api,
|
|
192
|
+
fromShip,
|
|
193
|
+
toShip: parsed.ship,
|
|
194
|
+
story,
|
|
195
|
+
kind: "media",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return await sendGroupMessageWithStory({
|
|
199
|
+
api,
|
|
200
|
+
fromShip,
|
|
201
|
+
hostShip: parsed.hostShip,
|
|
202
|
+
channelName: parsed.channelName,
|
|
203
|
+
story,
|
|
204
|
+
replyToId: resolveReplyId(replyToId, threadId),
|
|
205
|
+
kind: "media",
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export async function probeTlonAccount(account: ConfiguredTlonAccount) {
|
|
212
|
+
try {
|
|
213
|
+
const ssrfPolicy = ssrfPolicyFromDangerouslyAllowPrivateNetwork(
|
|
214
|
+
account.dangerouslyAllowPrivateNetwork,
|
|
215
|
+
);
|
|
216
|
+
const cookie = await authenticate(account.url, account.code, { ssrfPolicy });
|
|
217
|
+
const { response, release } = await urbitFetch({
|
|
218
|
+
baseUrl: account.url,
|
|
219
|
+
path: "/~/name",
|
|
220
|
+
init: {
|
|
221
|
+
method: "GET",
|
|
222
|
+
headers: { Cookie: cookie },
|
|
223
|
+
},
|
|
224
|
+
ssrfPolicy,
|
|
225
|
+
timeoutMs: 30_000,
|
|
226
|
+
auditContext: "tlon-probe-account",
|
|
227
|
+
});
|
|
228
|
+
try {
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
return { ok: false, error: `Name request failed: ${response.status}` };
|
|
231
|
+
}
|
|
232
|
+
return { ok: true };
|
|
233
|
+
} finally {
|
|
234
|
+
await release();
|
|
235
|
+
}
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return { ok: false, error: (error as { message?: string })?.message ?? String(error) };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function startTlonGatewayAccount(
|
|
242
|
+
ctx: Parameters<
|
|
243
|
+
NonNullable<NonNullable<ChannelPlugin<ResolvedTlonAccount>["gateway"]>["startAccount"]>
|
|
244
|
+
>[0],
|
|
245
|
+
) {
|
|
246
|
+
const account = ctx.account;
|
|
247
|
+
ctx.setStatus({
|
|
248
|
+
accountId: account.accountId,
|
|
249
|
+
ship: account.ship,
|
|
250
|
+
url: account.url,
|
|
251
|
+
} as ChannelAccountSnapshot);
|
|
252
|
+
ctx.log?.info(`[${account.accountId}] starting Tlon provider for ${account.ship ?? "tlon"}`);
|
|
253
|
+
return monitorTlonProvider({
|
|
254
|
+
runtime: ctx.runtime,
|
|
255
|
+
abortSignal: ctx.abortSignal,
|
|
256
|
+
accountId: account.accountId,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export { tlonSetupWizard };
|