r2-relay-channel 0.3.5

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/LICENSE +21 -0
  2. package/README.md +309 -0
  3. package/dist/channel.d.ts +16 -0
  4. package/dist/channel.js +271 -0
  5. package/dist/checkpoint-store.d.ts +4 -0
  6. package/dist/checkpoint-store.js +16 -0
  7. package/dist/commands.d.ts +2 -0
  8. package/dist/commands.js +54 -0
  9. package/dist/config.d.ts +76 -0
  10. package/dist/config.js +123 -0
  11. package/dist/conversation-targets.d.ts +32 -0
  12. package/dist/conversation-targets.js +120 -0
  13. package/dist/inbound.d.ts +38 -0
  14. package/dist/inbound.js +440 -0
  15. package/dist/index.d.ts +4 -0
  16. package/dist/index.js +26 -0
  17. package/dist/lifecycle.d.ts +64 -0
  18. package/dist/lifecycle.js +296 -0
  19. package/dist/media.d.ts +30 -0
  20. package/dist/media.js +361 -0
  21. package/dist/message-adapter.d.ts +27 -0
  22. package/dist/message-adapter.js +53 -0
  23. package/dist/openclaw.plugin.json +41 -0
  24. package/dist/outbound.d.ts +28 -0
  25. package/dist/outbound.js +141 -0
  26. package/dist/protocol.d.ts +2 -0
  27. package/dist/protocol.js +1 -0
  28. package/dist/r2-relay.d.ts +1 -0
  29. package/dist/r2-relay.js +1 -0
  30. package/dist/r2client.d.ts +33 -0
  31. package/dist/r2client.js +137 -0
  32. package/dist/relay-core/checkpoint.d.ts +31 -0
  33. package/dist/relay-core/checkpoint.js +95 -0
  34. package/dist/relay-core/errors.d.ts +9 -0
  35. package/dist/relay-core/errors.js +18 -0
  36. package/dist/relay-core/index.d.ts +6 -0
  37. package/dist/relay-core/index.js +6 -0
  38. package/dist/relay-core/keyspace.d.ts +24 -0
  39. package/dist/relay-core/keyspace.js +60 -0
  40. package/dist/relay-core/object-store.d.ts +20 -0
  41. package/dist/relay-core/object-store.js +1 -0
  42. package/dist/relay-core/transport.d.ts +48 -0
  43. package/dist/relay-core/transport.js +326 -0
  44. package/dist/relay-core/types.d.ts +220 -0
  45. package/dist/relay-core/types.js +1 -0
  46. package/dist/runtime.d.ts +9 -0
  47. package/dist/runtime.js +54 -0
  48. package/dist/service.d.ts +51 -0
  49. package/dist/service.js +186 -0
  50. package/dist/service.test.d.ts +1 -0
  51. package/dist/service.test.js +27 -0
  52. package/dist/session-publication.d.ts +7 -0
  53. package/dist/session-publication.js +365 -0
  54. package/dist/setup-entry.d.ts +6 -0
  55. package/dist/setup-entry.js +49 -0
  56. package/dist/setup.d.ts +3 -0
  57. package/dist/setup.js +291 -0
  58. package/dist/target.d.ts +6 -0
  59. package/dist/target.js +53 -0
  60. package/dist/webhook.d.ts +18 -0
  61. package/dist/webhook.js +225 -0
  62. package/openclaw.plugin.json +41 -0
  63. package/package.json +62 -0
@@ -0,0 +1,54 @@
1
+ import { buildCronTarget, buildWebhookPath, buildWebhookUrl, resolveCommandConversationTarget, } from "./conversation-targets.js";
2
+ export function registerRelayCommands(api) {
3
+ api.registerCommand({
4
+ name: "session-target",
5
+ description: "Show cron and webhook targets for the current R2 Relay conversation.",
6
+ acceptsArgs: false,
7
+ requireAuth: true,
8
+ handler: async (ctx) => await handleRelayCommand(ctx),
9
+ });
10
+ }
11
+ async function handleRelayCommand(ctx) {
12
+ const target = resolveCommandConversationTarget(ctx);
13
+ if (!target) {
14
+ return {
15
+ text: [
16
+ "I couldn't determine the current R2 Relay conversation target yet.",
17
+ "Send at least one message through the relay in this conversation, then run /session-target again.",
18
+ ].join("\n"),
19
+ };
20
+ }
21
+ const cronTo = buildCronTarget(target.peer, target.relayConversationId);
22
+ const sendSnippet = `openclaw message send --channel r2-relay-channel --target '${cronTo}' --message "hello"`;
23
+ const cronSnippet = `--announce --channel r2-relay-channel --to '${cronTo}'`;
24
+ const webhook = buildWebhookUrl(ctx.config, target.peer, target.relayConversationId) ?? buildWebhookPath(target.peer, target.relayConversationId);
25
+ const curlSnippet = [
26
+ `curl -X POST '${webhook}'`,
27
+ " -H 'Authorization: Bearer <cron.webhookToken>'",
28
+ " -H 'Content-Type: application/json'",
29
+ " --data '{\"text\":\"hello from curl\"}'",
30
+ ].join(" \\\n");
31
+ return {
32
+ text: [
33
+ "To send a message to this session directly, use:",
34
+ "```TEXT",
35
+ sendSnippet,
36
+ "```",
37
+ "",
38
+ "To send cron output to this session, use:",
39
+ "```TEXT",
40
+ cronSnippet,
41
+ "```",
42
+ "",
43
+ "To send webhook payloads to this session, use:",
44
+ "```TEXT",
45
+ webhook,
46
+ "```",
47
+ "",
48
+ "To test it locally with curl, use:",
49
+ "```TEXT",
50
+ curlSnippet,
51
+ "```",
52
+ ].join("\n"),
53
+ };
54
+ }
@@ -0,0 +1,76 @@
1
+ import { z } from "zod";
2
+ import type { ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk/core";
3
+ export declare const DEFAULT_POLL_INTERVAL_MS = 5000;
4
+ export declare const DEFAULT_BACKOFF_MAX_MS = 40000;
5
+ export declare const DEFAULT_TTL_DAYS = 7;
6
+ export declare const DEFAULT_IDENTITY_TTL_DAYS = 1;
7
+ export declare const DEFAULT_HEAD_TTL_DAYS = 30;
8
+ export declare const DEFAULT_REGION = "auto";
9
+ export declare const DEFAULT_FORCE_PATH_STYLE = true;
10
+ export declare const CHANNEL_ID = "r2-relay-channel";
11
+ export declare const PLUGIN_ID = "r2-relay-channel";
12
+ export declare const R2RelaySidecarConfigSchema: z.ZodObject<{
13
+ enabled: z.ZodOptional<z.ZodBoolean>;
14
+ endpoint: z.ZodString;
15
+ bucket: z.ZodString;
16
+ accessKeyId: z.ZodString;
17
+ secretAccessKey: z.ZodString;
18
+ serverId: z.ZodOptional<z.ZodString>;
19
+ pollIntervalMs: z.ZodOptional<z.ZodNumber>;
20
+ backoffMaxMs: z.ZodOptional<z.ZodNumber>;
21
+ defaultTtlDays: z.ZodOptional<z.ZodNumber>;
22
+ ttl: z.ZodOptional<z.ZodObject<{
23
+ msg: z.ZodOptional<z.ZodNumber>;
24
+ att: z.ZodOptional<z.ZodNumber>;
25
+ identity: z.ZodOptional<z.ZodNumber>;
26
+ head: z.ZodOptional<z.ZodNumber>;
27
+ }, z.core.$strip>>;
28
+ }, z.core.$strip>;
29
+ export declare const r2RelayChannelConfigSchema: NonNullable<ChannelPlugin["configSchema"]>;
30
+ export interface RelayTtlConfig {
31
+ msg?: number;
32
+ att?: number;
33
+ identity?: number;
34
+ head?: number;
35
+ }
36
+ export interface R2RelayAccountConfig {
37
+ enabled?: boolean;
38
+ configFile?: string;
39
+ endpoint?: string;
40
+ bucket?: string;
41
+ accessKeyId?: string;
42
+ secretAccessKey?: string;
43
+ serverId?: string;
44
+ pollIntervalMs?: number;
45
+ backoffMaxMs?: number;
46
+ defaultTtlDays?: number;
47
+ ttl?: RelayTtlConfig;
48
+ }
49
+ export interface ResolvedR2RelayAccount {
50
+ accountId: string;
51
+ enabled: boolean;
52
+ configured: boolean;
53
+ configFile: string;
54
+ endpoint: string;
55
+ bucket: string;
56
+ accessKeyId: string;
57
+ secretAccessKey: string;
58
+ serverId: string;
59
+ region: string;
60
+ forcePathStyle: boolean;
61
+ pollIntervalMs: number;
62
+ backoffMaxMs: number;
63
+ defaultTtlDays: number;
64
+ ttl: Required<RelayTtlConfig>;
65
+ config: R2RelayAccountConfig;
66
+ }
67
+ export declare function getR2RelayConfig(cfg: OpenClawConfig): R2RelayAccountConfig | undefined;
68
+ export declare function listR2RelayAccountIds(cfg: OpenClawConfig): string[];
69
+ export declare function resolveDefaultServerId(_cfg: OpenClawConfig): string;
70
+ export declare function normalizeServerId(value?: string | null): string | null;
71
+ export declare function resolveRelayConfigFilePath(configFile?: string | null): string;
72
+ export declare function loadR2RelaySidecarConfig(configFile: string): R2RelayAccountConfig;
73
+ export declare function resolveR2RelayAccount(params: {
74
+ cfg: OpenClawConfig;
75
+ accountId?: string | null;
76
+ }): ResolvedR2RelayAccount;
package/dist/config.js ADDED
@@ -0,0 +1,123 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { z } from "zod";
5
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
6
+ import { buildJsonChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
7
+ import { getPersistedRelayServerId } from "./runtime.js";
8
+ export const DEFAULT_POLL_INTERVAL_MS = 5_000;
9
+ export const DEFAULT_BACKOFF_MAX_MS = 40_000;
10
+ export const DEFAULT_TTL_DAYS = 7;
11
+ export const DEFAULT_IDENTITY_TTL_DAYS = 1;
12
+ export const DEFAULT_HEAD_TTL_DAYS = 30;
13
+ export const DEFAULT_REGION = "auto";
14
+ export const DEFAULT_FORCE_PATH_STYLE = true;
15
+ export const CHANNEL_ID = "r2-relay-channel";
16
+ export const PLUGIN_ID = "r2-relay-channel";
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = path.dirname(__filename);
19
+ export const R2RelaySidecarConfigSchema = z.object({
20
+ enabled: z.boolean().optional(),
21
+ endpoint: z.string().url().min(1),
22
+ bucket: z.string().min(1),
23
+ accessKeyId: z.string().min(1),
24
+ secretAccessKey: z.string().min(1),
25
+ serverId: z.string().trim().min(1).optional(),
26
+ pollIntervalMs: z.number().int().positive().optional(),
27
+ backoffMaxMs: z.number().int().positive().optional(),
28
+ defaultTtlDays: z.number().positive().optional(),
29
+ ttl: z.object({
30
+ msg: z.number().positive().optional(),
31
+ att: z.number().positive().optional(),
32
+ identity: z.number().positive().optional(),
33
+ head: z.number().positive().optional(),
34
+ }).optional(),
35
+ });
36
+ export const r2RelayChannelConfigSchema = buildJsonChannelConfigSchema({
37
+ type: "object",
38
+ additionalProperties: false,
39
+ properties: {
40
+ enabled: { type: "boolean" },
41
+ configFile: { type: "string", minLength: 1 },
42
+ },
43
+ });
44
+ export function getR2RelayConfig(cfg) {
45
+ return cfg.channels?.[CHANNEL_ID];
46
+ }
47
+ export function listR2RelayAccountIds(cfg) {
48
+ const relayCfg = getR2RelayConfig(cfg);
49
+ if (!relayCfg) {
50
+ return [];
51
+ }
52
+ return [DEFAULT_ACCOUNT_ID];
53
+ }
54
+ export function resolveDefaultServerId(_cfg) {
55
+ return getPersistedRelayServerId();
56
+ }
57
+ export function normalizeServerId(value) {
58
+ const trimmed = value?.trim();
59
+ if (!trimmed) {
60
+ return null;
61
+ }
62
+ const normalized = trimmed.replace(/\s+/g, "-").replace(/[^a-zA-Z0-9._-]/g, "-");
63
+ const collapsed = normalized.replace(/-+/g, "-").replace(/^[-_.]+|[-_.]+$/g, "");
64
+ return collapsed || null;
65
+ }
66
+ export function resolveRelayConfigFilePath(configFile) {
67
+ const trimmed = configFile?.trim();
68
+ if (trimmed) {
69
+ return path.resolve(trimmed);
70
+ }
71
+ return path.resolve(__dirname, "..", "r2relay.config.json");
72
+ }
73
+ export function loadR2RelaySidecarConfig(configFile) {
74
+ try {
75
+ const raw = fs.readFileSync(configFile, "utf8");
76
+ const parsed = JSON.parse(raw);
77
+ return parsed && typeof parsed === "object" ? parsed : {};
78
+ }
79
+ catch {
80
+ return {};
81
+ }
82
+ }
83
+ export function resolveR2RelayAccount(params) {
84
+ const relayCfg = getR2RelayConfig(params.cfg) ?? {};
85
+ const configFile = resolveRelayConfigFilePath(relayCfg.configFile);
86
+ const rawFileCfg = loadR2RelaySidecarConfig(configFile);
87
+ const parsedFileCfg = R2RelaySidecarConfigSchema.safeParse(rawFileCfg);
88
+ const fileCfg = parsedFileCfg.success ? parsedFileCfg.data : {};
89
+ const mergedCfg = {
90
+ ...fileCfg,
91
+ enabled: relayCfg.enabled ?? fileCfg.enabled,
92
+ configFile,
93
+ };
94
+ const endpoint = mergedCfg.endpoint?.trim() ?? "";
95
+ const bucket = mergedCfg.bucket?.trim() ?? "";
96
+ const accessKeyId = mergedCfg.accessKeyId?.trim() ?? "";
97
+ const secretAccessKey = mergedCfg.secretAccessKey?.trim() ?? "";
98
+ const configured = Boolean(endpoint && bucket && accessKeyId && secretAccessKey);
99
+ const ttl = {
100
+ msg: mergedCfg.ttl?.msg ?? mergedCfg.defaultTtlDays ?? DEFAULT_TTL_DAYS,
101
+ att: mergedCfg.ttl?.att ?? mergedCfg.defaultTtlDays ?? DEFAULT_TTL_DAYS,
102
+ identity: mergedCfg.ttl?.identity ?? DEFAULT_IDENTITY_TTL_DAYS,
103
+ head: mergedCfg.ttl?.head ?? DEFAULT_HEAD_TTL_DAYS,
104
+ };
105
+ return {
106
+ accountId: DEFAULT_ACCOUNT_ID,
107
+ enabled: mergedCfg.enabled !== false,
108
+ configured,
109
+ configFile,
110
+ endpoint,
111
+ bucket,
112
+ accessKeyId,
113
+ secretAccessKey,
114
+ serverId: normalizeServerId(mergedCfg.serverId) ?? resolveDefaultServerId(params.cfg),
115
+ region: DEFAULT_REGION,
116
+ forcePathStyle: DEFAULT_FORCE_PATH_STYLE,
117
+ pollIntervalMs: mergedCfg.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
118
+ backoffMaxMs: mergedCfg.backoffMaxMs ?? DEFAULT_BACKOFF_MAX_MS,
119
+ defaultTtlDays: mergedCfg.defaultTtlDays ?? DEFAULT_TTL_DAYS,
120
+ ttl,
121
+ config: mergedCfg,
122
+ };
123
+ }
@@ -0,0 +1,32 @@
1
+ import type { PluginCommandContext } from "openclaw/plugin-sdk/core";
2
+ export interface ConversationTargetRecord {
3
+ channel: string;
4
+ accountId: string;
5
+ conversationId: string;
6
+ threadId?: string | number | null;
7
+ peer: string;
8
+ relayConversationId: string;
9
+ updatedAt: number;
10
+ }
11
+ export declare function rememberConversationTarget(params: {
12
+ channel: string;
13
+ accountId: string;
14
+ conversationId: string;
15
+ threadId?: string | number | null;
16
+ peer: string;
17
+ relayConversationId: string;
18
+ }): void;
19
+ export declare function resolveCommandConversationTarget(ctx: PluginCommandContext): ConversationTargetRecord | null;
20
+ export declare function buildCronTarget(peer: string, conversationId: string): string;
21
+ export declare function buildWebhookPath(peer: string, conversationId: string): string;
22
+ export declare function buildWebhookUrl(cfg: {
23
+ gateway?: {
24
+ publicBaseUrl?: string | null;
25
+ externalUrl?: string | null;
26
+ bindUrl?: string | null;
27
+ port?: number | null;
28
+ remote?: {
29
+ url?: string | null;
30
+ } | null;
31
+ };
32
+ }, peer: string, conversationId: string): string | null;
@@ -0,0 +1,120 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { resolvePluginStateDir } from "./runtime.js";
4
+ const STORE_FILE = "conversation-targets.json";
5
+ export function rememberConversationTarget(params) {
6
+ const store = loadStore();
7
+ const key = buildStoreKey({
8
+ channel: params.channel,
9
+ accountId: params.accountId,
10
+ conversationId: params.conversationId,
11
+ threadId: params.threadId,
12
+ });
13
+ store[key] = {
14
+ channel: params.channel,
15
+ accountId: params.accountId,
16
+ conversationId: params.conversationId,
17
+ threadId: params.threadId ?? null,
18
+ peer: params.peer,
19
+ relayConversationId: params.relayConversationId,
20
+ updatedAt: Date.now(),
21
+ };
22
+ saveStore(store);
23
+ }
24
+ export function resolveCommandConversationTarget(ctx) {
25
+ const store = loadStore();
26
+ const conversationId = resolveCanonicalCommandConversationId(ctx);
27
+ if (!conversationId) {
28
+ return null;
29
+ }
30
+ const directKey = buildStoreKey({
31
+ channel: ctx.channel,
32
+ accountId: ctx.accountId,
33
+ conversationId,
34
+ threadId: ctx.messageThreadId,
35
+ });
36
+ const fallbackKey = buildStoreKey({
37
+ channel: ctx.channel,
38
+ accountId: ctx.accountId,
39
+ conversationId,
40
+ threadId: null,
41
+ });
42
+ return store[directKey] ?? store[fallbackKey] ?? null;
43
+ }
44
+ export function buildCronTarget(peer, conversationId) {
45
+ return `peer=${peer},conversation=${conversationId}`;
46
+ }
47
+ export function buildWebhookPath(peer, conversationId) {
48
+ return `/r2-relay-channel/webhook/${encodeURIComponent(peer)}/${encodeURIComponent(conversationId)}`;
49
+ }
50
+ export function buildWebhookUrl(cfg, peer, conversationId) {
51
+ const pathPart = buildWebhookPath(peer, conversationId);
52
+ const localBase = resolveLocalGatewayBaseUrl(cfg);
53
+ const base = localBase ||
54
+ cfg.gateway?.publicBaseUrl?.trim() ||
55
+ cfg.gateway?.externalUrl?.trim() ||
56
+ cfg.gateway?.remote?.url?.trim() ||
57
+ cfg.gateway?.bindUrl?.trim() ||
58
+ "";
59
+ if (!base) {
60
+ return null;
61
+ }
62
+ try {
63
+ const normalizedBase = normalizeWebhookBaseUrl(base);
64
+ return new URL(pathPart, ensureTrailingSlash(normalizedBase)).toString();
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ function ensureTrailingSlash(value) {
71
+ return value.endsWith("/") ? value : `${value}/`;
72
+ }
73
+ function resolveLocalGatewayBaseUrl(cfg) {
74
+ const port = cfg.gateway?.port;
75
+ if (typeof port === "number" && Number.isFinite(port) && port > 0) {
76
+ return `http://127.0.0.1:${port}`;
77
+ }
78
+ return null;
79
+ }
80
+ function normalizeWebhookBaseUrl(value) {
81
+ const url = new URL(value);
82
+ if (url.protocol === "ws:") {
83
+ url.protocol = "http:";
84
+ }
85
+ else if (url.protocol === "wss:") {
86
+ url.protocol = "https:";
87
+ }
88
+ return url.toString();
89
+ }
90
+ function resolveCanonicalCommandConversationId(ctx) {
91
+ const from = ctx.from?.trim();
92
+ return from || null;
93
+ }
94
+ function buildStoreKey(params) {
95
+ return [
96
+ params.channel,
97
+ params.accountId?.trim() || "default",
98
+ params.conversationId,
99
+ params.threadId == null ? "-" : String(params.threadId),
100
+ ].join("::");
101
+ }
102
+ function loadStore() {
103
+ const file = resolveStoreFile();
104
+ try {
105
+ const raw = fs.readFileSync(file, "utf8");
106
+ const parsed = JSON.parse(raw);
107
+ return parsed && typeof parsed === "object" ? parsed : {};
108
+ }
109
+ catch {
110
+ return {};
111
+ }
112
+ }
113
+ function saveStore(store) {
114
+ const file = resolveStoreFile();
115
+ fs.mkdirSync(path.dirname(file), { recursive: true });
116
+ fs.writeFileSync(file, `${JSON.stringify(store, null, 2)}\n`, "utf8");
117
+ }
118
+ function resolveStoreFile() {
119
+ return path.join(resolvePluginStateDir(), STORE_FILE);
120
+ }
@@ -0,0 +1,38 @@
1
+ import type { ResolvedR2RelayAccount } from "./config.js";
2
+ import type { AttachmentRef, RelayRoute, ServerLimits } from "./protocol.js";
3
+ import type { Service } from "./service.js";
4
+ import { type RelayPollLog } from "./lifecycle.js";
5
+ export declare function dispatchInboundMessage(params: {
6
+ cfg: Record<string, unknown>;
7
+ account: ResolvedR2RelayAccount;
8
+ service: Service;
9
+ log?: RelayPollLog;
10
+ senderId: string;
11
+ text: string;
12
+ timestamp: number;
13
+ messageId: string;
14
+ route: RelayRoute;
15
+ attachments?: AttachmentRef[];
16
+ }): Promise<void>;
17
+ export declare function formatInboundRelayBody(msg: {
18
+ content?: {
19
+ type?: string | null;
20
+ text?: string | null;
21
+ attachments?: {
22
+ file_name?: string | null;
23
+ kind?: string | null;
24
+ }[] | null;
25
+ emoji?: string | null;
26
+ target_msg_id?: string | null;
27
+ remove?: boolean | null;
28
+ approval_id?: string | null;
29
+ decision?: string | null;
30
+ } | null;
31
+ }): string;
32
+ export declare function classifyInboundRelayMessageFailure(err: unknown): "error";
33
+ export declare function resolvePublishedServerLimits(limits: {
34
+ maxImageBytes: number;
35
+ maxVideoBytes: number;
36
+ maxAudioBytes: number;
37
+ maxDocumentBytes: number;
38
+ }): ServerLimits;