@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,61 +0,0 @@
1
- import { isBlockedHostnameOrIp } from "klaw/plugin-sdk/ssrf-runtime";
2
-
3
- type UrbitBaseUrlValidation =
4
- | { ok: true; baseUrl: string; hostname: string }
5
- | { ok: false; error: string };
6
-
7
- function hasScheme(value: string): boolean {
8
- return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value);
9
- }
10
-
11
- export function normalizeUrbitHostname(hostname: string | undefined): string {
12
- return (hostname ?? "").trim().toLowerCase().replace(/\.$/, "");
13
- }
14
-
15
- export function validateUrbitBaseUrl(raw: string): UrbitBaseUrlValidation {
16
- const trimmed = raw.trim();
17
- if (!trimmed) {
18
- return { ok: false, error: "Required" };
19
- }
20
-
21
- const candidate = hasScheme(trimmed) ? trimmed : `https://${trimmed}`;
22
-
23
- let parsed: URL;
24
- try {
25
- parsed = new URL(candidate);
26
- } catch {
27
- return { ok: false, error: "Invalid URL" };
28
- }
29
-
30
- if (!["http:", "https:"].includes(parsed.protocol)) {
31
- return { ok: false, error: "URL must use http:// or https://" };
32
- }
33
-
34
- if (parsed.username || parsed.password) {
35
- return { ok: false, error: "URL must not include credentials" };
36
- }
37
-
38
- const hostname = normalizeUrbitHostname(parsed.hostname);
39
- if (!hostname) {
40
- return { ok: false, error: "Invalid hostname" };
41
- }
42
-
43
- // Normalize to origin so callers can't smuggle paths/query fragments into the base URL,
44
- // and strip a trailing dot from the hostname (DNS root label).
45
- const isIpv6 = hostname.includes(":");
46
- const host = parsed.port
47
- ? `${isIpv6 ? `[${hostname}]` : hostname}:${parsed.port}`
48
- : isIpv6
49
- ? `[${hostname}]`
50
- : hostname;
51
-
52
- return { ok: true, baseUrl: `${parsed.protocol}//${host}`, hostname };
53
- }
54
-
55
- export function isBlockedUrbitHostname(hostname: string): boolean {
56
- const normalized = normalizeUrbitHostname(hostname);
57
- if (!normalized) {
58
- return false;
59
- }
60
- return isBlockedHostnameOrIp(normalized);
61
- }
@@ -1,36 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { scryUrbitPath } from "./channel-ops.js";
3
- import { urbitFetch } from "./fetch.js";
4
-
5
- vi.mock("./fetch.js", () => ({
6
- urbitFetch: vi.fn(),
7
- }));
8
-
9
- describe("Urbit channel operations", () => {
10
- beforeEach(() => {
11
- vi.mocked(urbitFetch).mockReset();
12
- });
13
-
14
- it("wraps malformed scry response JSON", async () => {
15
- const release = vi.fn().mockResolvedValue(undefined);
16
- vi.mocked(urbitFetch).mockResolvedValue({
17
- response: new Response("{not json", {
18
- status: 200,
19
- headers: { "content-type": "application/json" },
20
- }),
21
- finalUrl: "https://example.com/~/scry/chat/inbox.json",
22
- release,
23
- });
24
-
25
- await expect(
26
- scryUrbitPath(
27
- {
28
- baseUrl: "https://example.com",
29
- cookie: "urbauth-~zod=123",
30
- },
31
- { path: "/chat/inbox.json", auditContext: "test" },
32
- ),
33
- ).rejects.toThrow("Urbit scry response was malformed JSON for path /chat/inbox.json");
34
- expect(release).toHaveBeenCalledTimes(1);
35
- });
36
- });
@@ -1,149 +0,0 @@
1
- import type { LookupFn, SsrFPolicy } from "klaw/plugin-sdk/ssrf-runtime";
2
- import { UrbitHttpError } from "./errors.js";
3
- import { urbitFetch } from "./fetch.js";
4
-
5
- type UrbitChannelDeps = {
6
- baseUrl: string;
7
- cookie: string;
8
- ship: string;
9
- channelId: string;
10
- ssrfPolicy?: SsrFPolicy;
11
- lookupFn?: LookupFn;
12
- fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
13
- };
14
-
15
- async function putUrbitChannel(
16
- deps: UrbitChannelDeps,
17
- params: { body: unknown; auditContext: string },
18
- ) {
19
- return await urbitFetch({
20
- baseUrl: deps.baseUrl,
21
- path: `/~/channel/${deps.channelId}`,
22
- init: {
23
- method: "PUT",
24
- headers: {
25
- "Content-Type": "application/json",
26
- Cookie: deps.cookie,
27
- },
28
- body: JSON.stringify(params.body),
29
- },
30
- ssrfPolicy: deps.ssrfPolicy,
31
- lookupFn: deps.lookupFn,
32
- fetchImpl: deps.fetchImpl,
33
- timeoutMs: 30_000,
34
- auditContext: params.auditContext,
35
- });
36
- }
37
-
38
- export async function pokeUrbitChannel(
39
- deps: UrbitChannelDeps,
40
- params: { app: string; mark: string; json: unknown; auditContext: string },
41
- ): Promise<number> {
42
- const pokeId = Date.now();
43
- const pokeData = {
44
- id: pokeId,
45
- action: "poke",
46
- ship: deps.ship,
47
- app: params.app,
48
- mark: params.mark,
49
- json: params.json,
50
- };
51
-
52
- const { response, release } = await putUrbitChannel(deps, {
53
- body: [pokeData],
54
- auditContext: params.auditContext,
55
- });
56
-
57
- try {
58
- if (!response.ok && response.status !== 204) {
59
- const errorText = await response.text().catch(() => "");
60
- throw new Error(`Poke failed: ${response.status}${errorText ? ` - ${errorText}` : ""}`);
61
- }
62
- return pokeId;
63
- } finally {
64
- await release();
65
- }
66
- }
67
-
68
- export async function scryUrbitPath(
69
- deps: Pick<UrbitChannelDeps, "baseUrl" | "cookie" | "ssrfPolicy" | "lookupFn" | "fetchImpl">,
70
- params: { path: string; auditContext: string },
71
- ): Promise<unknown> {
72
- const scryPath = `/~/scry${params.path}`;
73
- const { response, release } = await urbitFetch({
74
- baseUrl: deps.baseUrl,
75
- path: scryPath,
76
- init: {
77
- method: "GET",
78
- headers: { Cookie: deps.cookie },
79
- },
80
- ssrfPolicy: deps.ssrfPolicy,
81
- lookupFn: deps.lookupFn,
82
- fetchImpl: deps.fetchImpl,
83
- timeoutMs: 30_000,
84
- auditContext: params.auditContext,
85
- });
86
-
87
- try {
88
- if (!response.ok) {
89
- throw new Error(`Scry failed: ${response.status} for path ${params.path}`);
90
- }
91
- try {
92
- return await response.json();
93
- } catch (cause) {
94
- throw new Error(`Urbit scry response was malformed JSON for path ${params.path}`, { cause });
95
- }
96
- } finally {
97
- await release();
98
- }
99
- }
100
-
101
- async function createUrbitChannel(
102
- deps: UrbitChannelDeps,
103
- params: { body: unknown; auditContext: string },
104
- ): Promise<void> {
105
- const { response, release } = await putUrbitChannel(deps, params);
106
-
107
- try {
108
- if (!response.ok && response.status !== 204) {
109
- throw new UrbitHttpError({ operation: "Channel creation", status: response.status });
110
- }
111
- } finally {
112
- await release();
113
- }
114
- }
115
-
116
- async function wakeUrbitChannel(deps: UrbitChannelDeps): Promise<void> {
117
- const { response, release } = await putUrbitChannel(deps, {
118
- body: [
119
- {
120
- id: Date.now(),
121
- action: "poke",
122
- ship: deps.ship,
123
- app: "hood",
124
- mark: "helm-hi",
125
- json: "Opening API channel",
126
- },
127
- ],
128
- auditContext: "tlon-urbit-channel-wake",
129
- });
130
-
131
- try {
132
- if (!response.ok && response.status !== 204) {
133
- throw new UrbitHttpError({ operation: "Channel activation", status: response.status });
134
- }
135
- } finally {
136
- await release();
137
- }
138
- }
139
-
140
- export async function ensureUrbitChannelOpen(
141
- deps: UrbitChannelDeps,
142
- params: { createBody: unknown; createAuditContext: string },
143
- ): Promise<void> {
144
- await createUrbitChannel(deps, {
145
- body: params.createBody,
146
- auditContext: params.createAuditContext,
147
- });
148
- await wakeUrbitChannel(deps);
149
- }
@@ -1,50 +0,0 @@
1
- export { ssrfPolicyFromDangerouslyAllowPrivateNetwork } from "klaw/plugin-sdk/ssrf-runtime";
2
- import { normalizeUrbitHostname, validateUrbitBaseUrl } from "./base-url.js";
3
- import { UrbitUrlError } from "./errors.js";
4
-
5
- type UrbitContext = {
6
- baseUrl: string;
7
- hostname: string;
8
- ship: string;
9
- };
10
-
11
- function resolveShipFromHostname(hostname: string): string {
12
- const trimmed = normalizeUrbitHostname(hostname);
13
- if (!trimmed) {
14
- return "";
15
- }
16
- if (trimmed.includes(".")) {
17
- return trimmed.split(".")[0] ?? trimmed;
18
- }
19
- return trimmed;
20
- }
21
-
22
- function normalizeUrbitShip(ship: string | undefined, hostname: string): string {
23
- const raw = ship?.replace(/^~/, "") ?? resolveShipFromHostname(hostname);
24
- return raw.trim();
25
- }
26
-
27
- export function normalizeUrbitCookie(cookie: string): string {
28
- return cookie.split(";")[0] ?? cookie;
29
- }
30
-
31
- export function getUrbitContext(url: string, ship?: string): UrbitContext {
32
- const validated = validateUrbitBaseUrl(url);
33
- if (!validated.ok) {
34
- throw new UrbitUrlError(validated.error);
35
- }
36
- return {
37
- baseUrl: validated.baseUrl,
38
- hostname: validated.hostname,
39
- ship: normalizeUrbitShip(ship, validated.hostname),
40
- };
41
- }
42
-
43
- /**
44
- * Get the default SSRF policy for image uploads.
45
- * Uses a restrictive policy that blocks private networks by default.
46
- */
47
- export function getDefaultSsrFPolicy(): undefined {
48
- // Default: block private networks for image uploads (safer default)
49
- return undefined;
50
- }
@@ -1,51 +0,0 @@
1
- type UrbitErrorCode =
2
- | "invalid_url"
3
- | "http_error"
4
- | "auth_failed"
5
- | "missing_cookie"
6
- | "channel_not_open";
7
-
8
- class UrbitError extends Error {
9
- readonly code: UrbitErrorCode;
10
-
11
- constructor(code: UrbitErrorCode, message: string, options?: { cause?: unknown }) {
12
- super(message, options);
13
- this.name = "UrbitError";
14
- this.code = code;
15
- }
16
- }
17
-
18
- export class UrbitUrlError extends UrbitError {
19
- constructor(message: string, options?: { cause?: unknown }) {
20
- super("invalid_url", message, options);
21
- this.name = "UrbitUrlError";
22
- }
23
- }
24
-
25
- export class UrbitHttpError extends UrbitError {
26
- readonly status: number;
27
- readonly operation: string;
28
- readonly bodyText?: string;
29
-
30
- constructor(params: { operation: string; status: number; bodyText?: string; cause?: unknown }) {
31
- const suffix = params.bodyText ? ` - ${params.bodyText}` : "";
32
- super("http_error", `${params.operation} failed: ${params.status}${suffix}`, {
33
- cause: params.cause,
34
- });
35
- this.name = "UrbitHttpError";
36
- this.status = params.status;
37
- this.operation = params.operation;
38
- this.bodyText = params.bodyText;
39
- }
40
- }
41
-
42
- export class UrbitAuthError extends UrbitError {
43
- constructor(
44
- code: "auth_failed" | "missing_cookie",
45
- message: string,
46
- options?: { cause?: unknown },
47
- ) {
48
- super(code, message, options);
49
- this.name = "UrbitAuthError";
50
- }
51
- }
@@ -1,38 +0,0 @@
1
- import { fetchWithSsrFGuard, type LookupFn, type SsrFPolicy } from "klaw/plugin-sdk/ssrf-runtime";
2
- import { validateUrbitBaseUrl } from "./base-url.js";
3
- import { UrbitUrlError } from "./errors.js";
4
-
5
- type UrbitFetchOptions = {
6
- baseUrl: string;
7
- path: string;
8
- init?: RequestInit;
9
- ssrfPolicy?: SsrFPolicy;
10
- lookupFn?: LookupFn;
11
- fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
12
- timeoutMs?: number;
13
- maxRedirects?: number;
14
- signal?: AbortSignal;
15
- auditContext?: string;
16
- pinDns?: boolean;
17
- };
18
-
19
- export async function urbitFetch(params: UrbitFetchOptions) {
20
- const validated = validateUrbitBaseUrl(params.baseUrl);
21
- if (!validated.ok) {
22
- throw new UrbitUrlError(validated.error);
23
- }
24
-
25
- const url = new URL(params.path, validated.baseUrl).toString();
26
- return await fetchWithSsrFGuard({
27
- url,
28
- fetchImpl: params.fetchImpl,
29
- init: params.init,
30
- timeoutMs: params.timeoutMs,
31
- maxRedirects: params.maxRedirects,
32
- signal: params.signal,
33
- policy: params.ssrfPolicy,
34
- lookupFn: params.lookupFn,
35
- auditContext: params.auditContext,
36
- pinDns: params.pinDns,
37
- });
38
- }
@@ -1,49 +0,0 @@
1
- /**
2
- * Types for Urbit groups foreigns (group invites)
3
- * Based on packages/shared/src/urbit/groups.ts from homestead
4
- */
5
-
6
- interface GroupPreviewV7 {
7
- meta: {
8
- title: string;
9
- description: string;
10
- image: string;
11
- cover: string;
12
- };
13
- "channel-count": number;
14
- "member-count": number;
15
- admissions: {
16
- privacy: "public" | "private" | "secret";
17
- };
18
- }
19
-
20
- interface ForeignInvite {
21
- flag: string; // group flag e.g. "~host/group-name"
22
- time: number; // timestamp
23
- from: string; // ship that sent invite
24
- token: string | null;
25
- note: string | null;
26
- preview: GroupPreviewV7;
27
- valid: boolean; // tracks if invite has been revoked
28
- }
29
-
30
- type Lookup = "preview" | "done" | "error";
31
- type Progress = "ask" | "join" | "watch" | "done" | "error";
32
-
33
- interface Foreign {
34
- invites: ForeignInvite[];
35
- lookup: Lookup | null;
36
- preview: GroupPreviewV7 | null;
37
- progress: Progress | null;
38
- token: string | null;
39
- }
40
-
41
- export interface Foreigns {
42
- [flag: string]: Foreign;
43
- }
44
-
45
- // DM invite structure from chat /v3 firehose
46
- export interface DmInvite {
47
- ship: string;
48
- // Additional fields may be present
49
- }
@@ -1,83 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
-
3
- vi.mock("@urbit/aura", () => ({
4
- scot: vi.fn(() => "mocked-ud"),
5
- da: {
6
- fromUnix: vi.fn(() => 123n),
7
- },
8
- }));
9
-
10
- describe("sendDm", () => {
11
- afterEach(() => {
12
- vi.restoreAllMocks();
13
- });
14
-
15
- it("uses aura v3 helpers for the DM id", async () => {
16
- const { sendDm } = await import("./send.js");
17
- const aura = await import("@urbit/aura");
18
- const scot = vi.mocked(aura.scot);
19
- const fromUnix = vi.mocked(aura.da.fromUnix);
20
-
21
- const sentAt = 1_700_000_000_000;
22
- vi.spyOn(Date, "now").mockReturnValue(sentAt);
23
-
24
- const poke = vi.fn(async () => ({}));
25
-
26
- const result = await sendDm({
27
- api: { poke },
28
- fromShip: "~zod",
29
- toShip: "~nec",
30
- text: "hi",
31
- });
32
-
33
- expect(fromUnix).toHaveBeenCalledWith(sentAt);
34
- expect(scot).toHaveBeenCalledWith("ud", 123n);
35
- expect(poke).toHaveBeenCalledTimes(1);
36
- expect(result.messageId).toBe("~zod/mocked-ud");
37
- expect(result.receipt.primaryPlatformMessageId).toBe("~zod/mocked-ud");
38
- });
39
-
40
- it("passes numeric group reply ids through aura formatting", async () => {
41
- const { sendGroupMessage } = await import("./send.js");
42
- const aura = await import("@urbit/aura");
43
- const scot = vi.mocked(aura.scot);
44
- scot.mockReturnValueOnce("~2024.1.1");
45
- vi.spyOn(Date, "now").mockReturnValue(1_700_000_000_000);
46
- const poke = vi.fn(async () => ({}));
47
-
48
- const result = await sendGroupMessage({
49
- api: { poke },
50
- fromShip: "~zod",
51
- hostShip: "~nec",
52
- channelName: "general",
53
- text: "threaded",
54
- replyToId: "1700000000000",
55
- });
56
-
57
- expect(scot).toHaveBeenCalledWith("ud", 1_700_000_000_000n);
58
- expect(poke).toHaveBeenCalledWith({
59
- app: "channels",
60
- mark: "channel-action-1",
61
- json: {
62
- channel: {
63
- nest: "chat/~nec/general",
64
- action: {
65
- post: {
66
- reply: {
67
- id: "~2024.1.1",
68
- action: {
69
- add: {
70
- content: [{ inline: ["threaded"] }],
71
- author: "~zod",
72
- sent: 1_700_000_000_000,
73
- },
74
- },
75
- },
76
- },
77
- },
78
- },
79
- },
80
- });
81
- expect(result.receipt.threadId).toBe("~nec/general");
82
- });
83
- });