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,27 @@
1
+ import type { MessageReceipt } from "openclaw/plugin-sdk/channel-message";
2
+ export declare const r2RelayMessageAdapter: {
3
+ readonly id: "r2-relay-channel";
4
+ readonly durableFinal: {
5
+ readonly capabilities: {
6
+ readonly text: true;
7
+ readonly media: true;
8
+ readonly payload: true;
9
+ };
10
+ };
11
+ readonly send: {
12
+ readonly text: ({ cfg, to, text, accountId }: import("openclaw/plugin-sdk/channel-message").ChannelMessageSendTextContext<import("openclaw/plugin-sdk/core").OpenClawConfig>) => Promise<{
13
+ receipt: MessageReceipt;
14
+ messageId: string;
15
+ }>;
16
+ readonly media: ({ cfg, to, text, mediaUrl, accountId, mediaAccess, mediaLocalRoots, mediaReadFile }: import("openclaw/plugin-sdk/channel-message").ChannelMessageSendMediaContext<import("openclaw/plugin-sdk/core").OpenClawConfig>) => Promise<{
17
+ receipt: MessageReceipt;
18
+ messageId: string;
19
+ }>;
20
+ readonly payload: ({ cfg, to, payload, accountId, mediaAccess, mediaLocalRoots, mediaReadFile }: import("openclaw/plugin-sdk/channel-message").ChannelMessageSendPayloadContext<import("openclaw/plugin-sdk/core").OpenClawConfig>) => Promise<{
21
+ receipt: MessageReceipt;
22
+ messageId: string;
23
+ }>;
24
+ };
25
+ } & {
26
+ receive: {};
27
+ };
@@ -0,0 +1,53 @@
1
+ import { defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-message";
2
+ import { sendRelayPayloadMessage } from "./outbound.js";
3
+ function asMessageResult(result) {
4
+ const sentAt = result.timestamp ?? Date.now();
5
+ const receipt = {
6
+ primaryPlatformMessageId: result.messageId,
7
+ platformMessageIds: [result.messageId],
8
+ parts: [{
9
+ platformMessageId: result.messageId,
10
+ kind: "text",
11
+ index: 0,
12
+ raw: result,
13
+ }],
14
+ sentAt,
15
+ raw: [result],
16
+ };
17
+ return { receipt, messageId: result.messageId };
18
+ }
19
+ export const r2RelayMessageAdapter = defineChannelMessageAdapter({
20
+ id: "r2-relay-channel",
21
+ durableFinal: {
22
+ capabilities: {
23
+ text: true,
24
+ media: true,
25
+ payload: true,
26
+ },
27
+ },
28
+ send: {
29
+ text: async ({ cfg, to, text, accountId }) => asMessageResult(await sendRelayPayloadMessage({
30
+ cfg: cfg,
31
+ to,
32
+ payload: { text },
33
+ accountId,
34
+ source: "message.text",
35
+ })),
36
+ media: async ({ cfg, to, text, mediaUrl, accountId, mediaAccess, mediaLocalRoots, mediaReadFile }) => asMessageResult(await sendRelayPayloadMessage({
37
+ cfg: cfg,
38
+ to,
39
+ payload: { text, mediaUrl },
40
+ accountId,
41
+ source: "message.media",
42
+ meta: { mediaAccess, mediaLocalRoots, mediaReadFile },
43
+ })),
44
+ payload: async ({ cfg, to, payload, accountId, mediaAccess, mediaLocalRoots, mediaReadFile }) => asMessageResult(await sendRelayPayloadMessage({
45
+ cfg: cfg,
46
+ to,
47
+ payload,
48
+ accountId,
49
+ source: "message.payload",
50
+ meta: { mediaAccess, mediaLocalRoots, mediaReadFile },
51
+ })),
52
+ },
53
+ });
@@ -0,0 +1,41 @@
1
+ {
2
+ "id": "r2-relay-channel",
3
+ "kind": "channel",
4
+ "name": "R2 Relay Channel",
5
+ "description": "Cloudflare R2 relay channel plugin for OpenClaw by AgentDeck App.",
6
+ "channels": ["r2-relay-channel"],
7
+ "channelConfigs": {
8
+ "r2-relay-channel": {
9
+ "label": "R2 Relay",
10
+ "description": "Cloudflare R2 relay channel for direct text messaging.",
11
+ "schema": {
12
+ "type": "object",
13
+ "additionalProperties": false,
14
+ "properties": {
15
+ "enabled": { "type": "boolean" },
16
+ "configFile": { "type": "string" }
17
+ }
18
+ },
19
+ "uiHints": {
20
+ "configFile": {
21
+ "label": "Config file",
22
+ "placeholder": "/path/to/r2relay.config.json"
23
+ }
24
+ }
25
+ }
26
+ },
27
+ "configSchema": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "properties": {
31
+ "r2-relay-channel": {
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "properties": {
35
+ "enabled": { "type": "boolean" },
36
+ "configFile": { "type": "string" }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,28 @@
1
+ import type { OutboundMediaAccess } from "openclaw/plugin-sdk/media-runtime";
2
+ import type { RelayRoute } from "./protocol.js";
3
+ import { type RelayPollLog } from "./lifecycle.js";
4
+ export declare function sendRelayPayloadMessage(params: {
5
+ cfg: Record<string, unknown>;
6
+ to: string;
7
+ payload: {
8
+ text?: string | null;
9
+ mediaUrl?: string | null;
10
+ mediaUrls?: string[] | null;
11
+ channelData?: Record<string, unknown> | null;
12
+ };
13
+ accountId?: string | null;
14
+ log?: RelayPollLog;
15
+ source?: string;
16
+ meta?: {
17
+ route?: RelayRoute | null;
18
+ workspaceDir?: string | null;
19
+ mediaAccess?: OutboundMediaAccess;
20
+ mediaLocalRoots?: readonly string[];
21
+ mediaReadFile?: (filePath: string) => Promise<Buffer>;
22
+ };
23
+ }): Promise<{
24
+ channel: string;
25
+ to: string;
26
+ messageId: string;
27
+ timestamp: number;
28
+ }>;
@@ -0,0 +1,141 @@
1
+ import { getOrCreateService, emitRelayDebug, touchRuntime } from "./lifecycle.js";
2
+ import { parseRelayTarget } from "./target.js";
3
+ import { buildRelayAttachments, describeLocalMediaReference, resolveRelayPayloadMediaUrls, } from "./media.js";
4
+ import { resolveR2RelayAccount } from "./config.js";
5
+ import { schedulePublishedIdentityRefresh } from "./session-publication.js";
6
+ export async function sendRelayPayloadMessage(params) {
7
+ const account = resolveR2RelayAccount({ cfg: params.cfg, accountId: params.accountId });
8
+ const service = getOrCreateService(account);
9
+ const target = parseRelayTarget(params.to);
10
+ const text = params.payload.text ?? "";
11
+ const mediaUrls = resolveRelayPayloadMediaUrls(params.payload);
12
+ const route = params.meta?.route ?? {
13
+ agent_id: agentIdFromConversationId(target.conversationId ?? "main") ?? "main",
14
+ conversation_id: target.conversationId ?? null,
15
+ instance_id: null,
16
+ };
17
+ emitRelayDebug(params.log, `[${account.accountId}] outbound relay send starting`, {
18
+ source: params.source ?? "unknown",
19
+ to: params.to,
20
+ peer: target.peer,
21
+ route,
22
+ mediaCount: mediaUrls.length,
23
+ hasText: Boolean(text.trim()),
24
+ });
25
+ const attachmentBuild = await buildRelayAttachments({
26
+ account,
27
+ service,
28
+ targetPeer: target.peer,
29
+ mediaUrls,
30
+ workspaceDir: params.meta?.workspaceDir ?? null,
31
+ mediaAccess: params.meta?.mediaAccess,
32
+ mediaLocalRoots: params.meta?.mediaLocalRoots,
33
+ mediaReadFile: params.meta?.mediaReadFile,
34
+ log: params.log,
35
+ source: params.source,
36
+ });
37
+ const attachments = attachmentBuild.attachments;
38
+ emitRelayDebug(params.log, `[${account.accountId}] sending outbound relay message`, {
39
+ source: params.source ?? "unknown",
40
+ attachmentCount: attachments.length,
41
+ hasText: Boolean(text.trim()),
42
+ targetPeer: target.peer,
43
+ route,
44
+ });
45
+ if (mediaUrls.length > 0 && attachments.length === 0) {
46
+ const detail = attachmentBuild.failures.length > 0
47
+ ? attachmentBuild.failures.join(" | ")
48
+ : "no attachment objects were created";
49
+ throw new Error(`Failed to prepare outbound relay attachments: ${detail}`);
50
+ }
51
+ const fallbackText = text.trim() || (mediaUrls.length > 0 && attachments.length === 0
52
+ ? mediaUrls.map((url) => `[Attachment unavailable: ${describeLocalMediaReference(url)}]`).join("\n")
53
+ : text);
54
+ const result = await service.sendMessage(target.peer, {
55
+ route,
56
+ content: relayContentFromPayload(fallbackText, attachments, params.payload.channelData),
57
+ });
58
+ emitRelayDebug(params.log, `[${account.accountId}] outbound relay message sent`, {
59
+ source: params.source ?? "unknown",
60
+ messageId: result.messageId,
61
+ attachmentCount: attachments.length,
62
+ });
63
+ touchRuntime(account.accountId, {
64
+ lastOutboundAt: Date.now(),
65
+ lastError: null,
66
+ });
67
+ schedulePublishedIdentityRefresh(service, account);
68
+ return {
69
+ channel: "r2-relay-channel",
70
+ to: params.to.trim(),
71
+ messageId: result.messageId,
72
+ timestamp: Date.now(),
73
+ };
74
+ }
75
+ function relayContentFromPayload(text, attachments, channelData) {
76
+ const execApproval = readExecApprovalChannelData(channelData);
77
+ if (execApproval?.state === "pending") {
78
+ return {
79
+ type: "approval_request",
80
+ approval_id: execApproval.approvalId,
81
+ approval_kind: execApproval.approvalKind === "plugin" ? "tool" : execApproval.approvalKind,
82
+ title: firstNonEmptyLine(text) ?? "Approval required",
83
+ body: text.trim() || null,
84
+ allowed_decisions: execApproval.allowedDecisions.length > 0
85
+ ? execApproval.allowedDecisions
86
+ : ["allow-once", "allow-always", "deny"],
87
+ metadata: {
88
+ openclaw: execApproval.raw,
89
+ },
90
+ };
91
+ }
92
+ return {
93
+ type: "text",
94
+ text,
95
+ attachments: attachments.length > 0 ? attachments : undefined,
96
+ };
97
+ }
98
+ function readExecApprovalChannelData(channelData) {
99
+ const raw = channelData?.execApproval;
100
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
101
+ return null;
102
+ }
103
+ const record = raw;
104
+ const approvalId = typeof record.approvalId === "string" ? record.approvalId.trim() : "";
105
+ if (!approvalId) {
106
+ return null;
107
+ }
108
+ const approvalKind = normalizeApprovalKind(record.approvalKind);
109
+ const allowedDecisions = Array.isArray(record.allowedDecisions)
110
+ ? record.allowedDecisions.filter((value) => typeof value === "string" && value.trim().length > 0)
111
+ : [];
112
+ return {
113
+ approvalId,
114
+ approvalKind,
115
+ allowedDecisions,
116
+ state: typeof record.state === "string" ? record.state : null,
117
+ raw: record,
118
+ };
119
+ }
120
+ function normalizeApprovalKind(value) {
121
+ if (value === "exec" || value === "tool" || value === "custom" || value === "plugin") {
122
+ return value;
123
+ }
124
+ return "exec";
125
+ }
126
+ function firstNonEmptyLine(value) {
127
+ for (const line of value.split(/\r?\n/)) {
128
+ const trimmed = line.trim();
129
+ if (trimmed) {
130
+ return trimmed;
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+ function agentIdFromConversationId(conversationId) {
136
+ const parts = conversationId.split(":");
137
+ if (parts.length >= 2 && parts[0] === "agent" && parts[1]?.trim()) {
138
+ return parts[1].trim();
139
+ }
140
+ return null;
141
+ }
@@ -0,0 +1,2 @@
1
+ export { MAX_MS, R2Relay, extractTimestampFromRelayKey, padRevTs, sanitizeFragment, type AttachmentKind, type AttachmentRef, type AgentDescriptor, type ConversationDescriptor, type HeadDoc, type IdentityDoc, type InboxBatch, type InboxMessage, type RelayContent, type RelayMessage, type RelayRoute, type RelayCheckpointState, type RelayRetentionConfig, type ServerLimits, type SendMessageOptions, type SendMessageResult, type SweepRuleSummary, } from "./relay-core/index.js";
2
+ export type { RelayKeyspaceOptions as R2RelayOptions } from "./relay-core/index.js";
@@ -0,0 +1 @@
1
+ export { MAX_MS, R2Relay, extractTimestampFromRelayKey, padRevTs, sanitizeFragment, } from "./relay-core/index.js";
@@ -0,0 +1 @@
1
+ export { default } from "./index.js";
@@ -0,0 +1 @@
1
+ export { default } from "./index.js";
@@ -0,0 +1,33 @@
1
+ import { S3Client } from "@aws-sdk/client-s3";
2
+ export interface R2Config {
3
+ endpoint: string;
4
+ bucket: string;
5
+ region?: string;
6
+ accessKeyId?: string;
7
+ secretAccessKey?: string;
8
+ forcePathStyle?: boolean;
9
+ }
10
+ export declare class R2Client {
11
+ s3: S3Client;
12
+ bucket: string;
13
+ constructor(cfg: R2Config);
14
+ getPresignedUrl(key: string, expiresIn: number): Promise<string>;
15
+ putObject(key: string, body: Buffer | string, contentType?: string, tagging?: string, ifMatch?: string, ifNoneMatch?: string): Promise<import("@aws-sdk/client-s3").PutObjectCommandOutput>;
16
+ getObject(key: string): Promise<import("@aws-sdk/client-s3").GetObjectCommandOutput | null>;
17
+ headObject(key: string): Promise<import("@aws-sdk/client-s3").HeadObjectCommandOutput | null>;
18
+ getJsonWithEtag<T>(key: string): Promise<{
19
+ body: T;
20
+ etag: string | null;
21
+ } | null>;
22
+ deleteObject(key: string): Promise<import("@aws-sdk/client-s3").DeleteObjectCommandOutput>;
23
+ listPrefix(prefix: string, maxKeys?: number): Promise<import("@aws-sdk/client-s3")._Object[]>;
24
+ listPrefixPage(prefix: string, continuationToken?: string, maxKeys?: number): Promise<{
25
+ contents: import("@aws-sdk/client-s3")._Object[];
26
+ nextContinuationToken: string | null;
27
+ isTruncated: boolean;
28
+ }>;
29
+ deleteObjects(keys: string[]): Promise<import("@aws-sdk/client-s3").DeleteObjectsCommandOutput | {
30
+ deleted: never[];
31
+ errors: never[];
32
+ }>;
33
+ }
@@ -0,0 +1,137 @@
1
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
2
+ import { DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, } from "@aws-sdk/client-s3";
3
+ export class R2Client {
4
+ s3;
5
+ bucket;
6
+ constructor(cfg) {
7
+ this.bucket = cfg.bucket;
8
+ this.s3 = new S3Client({
9
+ region: cfg.region || "auto",
10
+ endpoint: cfg.endpoint,
11
+ credentials: cfg.accessKeyId
12
+ ? {
13
+ accessKeyId: cfg.accessKeyId,
14
+ secretAccessKey: cfg.secretAccessKey ?? "",
15
+ }
16
+ : undefined,
17
+ forcePathStyle: cfg.forcePathStyle ?? true,
18
+ });
19
+ }
20
+ async getPresignedUrl(key, expiresIn) {
21
+ const command = new GetObjectCommand({ Bucket: this.bucket, Key: key });
22
+ return getSignedUrl(this.s3, command, { expiresIn });
23
+ }
24
+ async putObject(key, body, contentType, tagging, ifMatch, ifNoneMatch) {
25
+ const params = { Bucket: this.bucket, Key: key, Body: body };
26
+ if (contentType)
27
+ params.ContentType = contentType;
28
+ if (tagging)
29
+ params.Tagging = tagging;
30
+ if (ifMatch)
31
+ params.IfMatch = ifMatch;
32
+ if (ifNoneMatch)
33
+ params.IfNoneMatch = ifNoneMatch;
34
+ const cmd = new PutObjectCommand(params);
35
+ try {
36
+ return await this.s3.send(cmd);
37
+ }
38
+ catch (err) {
39
+ const normalized = err;
40
+ const code = normalized?.Code || normalized?.name || normalized?.$metadata?.httpStatusCode;
41
+ if (tagging && (code === "NotImplemented" || normalized?.$metadata?.httpStatusCode === 501)) {
42
+ delete params.Tagging;
43
+ return await this.s3.send(new PutObjectCommand(params));
44
+ }
45
+ if (normalized &&
46
+ (normalized.$metadata?.httpStatusCode === 412 || normalized.Code === "PreconditionFailed")) {
47
+ const preconditionError = new Error("PreconditionFailed");
48
+ preconditionError.code = "PreconditionFailed";
49
+ throw preconditionError;
50
+ }
51
+ throw err;
52
+ }
53
+ }
54
+ async getObject(key) {
55
+ const cmd = new GetObjectCommand({ Bucket: this.bucket, Key: key });
56
+ try {
57
+ return await this.s3.send(cmd);
58
+ }
59
+ catch (err) {
60
+ const normalized = err;
61
+ const code = normalized?.Code || normalized?.name || normalized?.code;
62
+ if (code === "NoSuchKey" ||
63
+ code === "NotFound" ||
64
+ normalized?.$metadata?.httpStatusCode === 404) {
65
+ return null;
66
+ }
67
+ throw err;
68
+ }
69
+ }
70
+ async headObject(key) {
71
+ const cmd = new HeadObjectCommand({ Bucket: this.bucket, Key: key });
72
+ try {
73
+ return await this.s3.send(cmd);
74
+ }
75
+ catch (err) {
76
+ const normalized = err;
77
+ if (normalized?.Code === "NotFound" ||
78
+ normalized?.name === "NotFound" ||
79
+ normalized?.$metadata?.httpStatusCode === 404) {
80
+ return null;
81
+ }
82
+ return null;
83
+ }
84
+ }
85
+ async getJsonWithEtag(key) {
86
+ const res = await this.getObject(key);
87
+ if (!res?.Body) {
88
+ return null;
89
+ }
90
+ const chunks = [];
91
+ for await (const chunk of res.Body) {
92
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
93
+ }
94
+ const text = Buffer.concat(chunks).toString("utf8");
95
+ return {
96
+ body: JSON.parse(text),
97
+ etag: normalizeEtag(res.ETag),
98
+ };
99
+ }
100
+ async deleteObject(key) {
101
+ return await this.s3.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
102
+ }
103
+ async listPrefix(prefix, maxKeys = 100) {
104
+ const res = await this.s3.send(new ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix, MaxKeys: maxKeys }));
105
+ return res.Contents || [];
106
+ }
107
+ async listPrefixPage(prefix, continuationToken, maxKeys = 1000) {
108
+ const res = await this.s3.send(new ListObjectsV2Command({
109
+ Bucket: this.bucket,
110
+ Prefix: prefix,
111
+ MaxKeys: maxKeys,
112
+ ContinuationToken: continuationToken,
113
+ }));
114
+ return {
115
+ contents: res.Contents || [],
116
+ nextContinuationToken: res.NextContinuationToken ?? null,
117
+ isTruncated: Boolean(res.IsTruncated),
118
+ };
119
+ }
120
+ async deleteObjects(keys) {
121
+ if (keys.length === 0) {
122
+ return { deleted: [], errors: [] };
123
+ }
124
+ return await this.s3.send(new DeleteObjectsCommand({
125
+ Bucket: this.bucket,
126
+ Delete: {
127
+ Objects: keys.map((Key) => ({ Key })),
128
+ Quiet: true,
129
+ },
130
+ }));
131
+ }
132
+ }
133
+ function normalizeEtag(etag) {
134
+ if (!etag)
135
+ return null;
136
+ return etag.replace(/^"|"$/g, "") || null;
137
+ }
@@ -0,0 +1,31 @@
1
+ import type { RelayCheckpointState } from "./types.js";
2
+ export declare const DEFAULT_CHECKPOINT_STATE: RelayCheckpointState;
3
+ export declare function defaultCheckpointState(): RelayCheckpointState;
4
+ export declare function normalizeCheckpointState(data?: Partial<RelayCheckpointState> | null): RelayCheckpointState;
5
+ export interface CheckpointStore {
6
+ load(): Promise<RelayCheckpointState> | RelayCheckpointState;
7
+ save(state: RelayCheckpointState): Promise<void> | void;
8
+ }
9
+ export declare class InMemoryCheckpointStore implements CheckpointStore {
10
+ private state;
11
+ constructor(state?: Partial<RelayCheckpointState>);
12
+ load(): RelayCheckpointState;
13
+ save(state: RelayCheckpointState): void;
14
+ }
15
+ export declare class FileCheckpointStore implements CheckpointStore {
16
+ readonly filePath: string;
17
+ constructor(filePath: string);
18
+ load(): Promise<RelayCheckpointState>;
19
+ save(state: RelayCheckpointState): Promise<void>;
20
+ }
21
+ export declare function loadCheckpointStateFromFile(filePath: string): Promise<RelayCheckpointState>;
22
+ export declare function saveCheckpointStateToFile(filePath: string, state: RelayCheckpointState): Promise<void>;
23
+ export declare function rememberMessage(state: RelayCheckpointState, params: {
24
+ msgId?: string | null;
25
+ objectKey?: string | null;
26
+ at?: number | null;
27
+ }): RelayCheckpointState;
28
+ export declare function hasSeenMessage(state: RelayCheckpointState, params: {
29
+ msgId?: string | null;
30
+ objectKey?: string | null;
31
+ }): boolean;
@@ -0,0 +1,95 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ const RECENT_MESSAGE_IDS_LIMIT = 200;
5
+ export const DEFAULT_CHECKPOINT_STATE = {
6
+ lastHeadKey: null,
7
+ recentMessageIds: [],
8
+ recentObjectKeys: [],
9
+ lastPollAt: null,
10
+ lastInboundAt: null,
11
+ };
12
+ export function defaultCheckpointState() {
13
+ return {
14
+ lastHeadKey: DEFAULT_CHECKPOINT_STATE.lastHeadKey,
15
+ recentMessageIds: [...DEFAULT_CHECKPOINT_STATE.recentMessageIds],
16
+ recentObjectKeys: [...DEFAULT_CHECKPOINT_STATE.recentObjectKeys],
17
+ lastPollAt: DEFAULT_CHECKPOINT_STATE.lastPollAt,
18
+ lastInboundAt: DEFAULT_CHECKPOINT_STATE.lastInboundAt,
19
+ };
20
+ }
21
+ export function normalizeCheckpointState(data) {
22
+ const raw = data ?? {};
23
+ return {
24
+ lastHeadKey: typeof raw.lastHeadKey === "string" ? raw.lastHeadKey : null,
25
+ recentMessageIds: Array.isArray(raw.recentMessageIds)
26
+ ? raw.recentMessageIds.filter((value) => typeof value === "string").slice(0, RECENT_MESSAGE_IDS_LIMIT)
27
+ : [],
28
+ recentObjectKeys: Array.isArray(raw.recentObjectKeys)
29
+ ? raw.recentObjectKeys.filter((value) => typeof value === "string").slice(0, RECENT_MESSAGE_IDS_LIMIT)
30
+ : [],
31
+ lastPollAt: typeof raw.lastPollAt === "number" ? raw.lastPollAt : null,
32
+ lastInboundAt: typeof raw.lastInboundAt === "number" ? raw.lastInboundAt : null,
33
+ };
34
+ }
35
+ export class InMemoryCheckpointStore {
36
+ state;
37
+ constructor(state) {
38
+ this.state = normalizeCheckpointState(state);
39
+ }
40
+ load() {
41
+ return normalizeCheckpointState(this.state);
42
+ }
43
+ save(state) {
44
+ this.state = normalizeCheckpointState(state);
45
+ }
46
+ }
47
+ export class FileCheckpointStore {
48
+ filePath;
49
+ constructor(filePath) {
50
+ this.filePath = filePath;
51
+ }
52
+ async load() {
53
+ return loadCheckpointStateFromFile(this.filePath);
54
+ }
55
+ async save(state) {
56
+ await saveCheckpointStateToFile(this.filePath, state);
57
+ }
58
+ }
59
+ export async function loadCheckpointStateFromFile(filePath) {
60
+ try {
61
+ const raw = await fs.readFile(filePath, "utf8");
62
+ return normalizeCheckpointState(JSON.parse(raw));
63
+ }
64
+ catch {
65
+ return defaultCheckpointState();
66
+ }
67
+ }
68
+ export async function saveCheckpointStateToFile(filePath, state) {
69
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
70
+ const normalized = normalizeCheckpointState(state);
71
+ const serialized = JSON.stringify(normalized, null, 2);
72
+ const tempPath = `${filePath}.${createHash("sha1").update(serialized).digest("hex")}.tmp`;
73
+ await fs.writeFile(tempPath, serialized, "utf8");
74
+ await fs.rename(tempPath, filePath);
75
+ }
76
+ export function rememberMessage(state, params) {
77
+ const normalized = normalizeCheckpointState(state);
78
+ const recentMessageIds = params.msgId
79
+ ? [params.msgId, ...normalized.recentMessageIds.filter((value) => value !== params.msgId)].slice(0, RECENT_MESSAGE_IDS_LIMIT)
80
+ : normalized.recentMessageIds;
81
+ const recentObjectKeys = params.objectKey
82
+ ? [params.objectKey, ...normalized.recentObjectKeys.filter((value) => value !== params.objectKey)].slice(0, RECENT_MESSAGE_IDS_LIMIT)
83
+ : normalized.recentObjectKeys;
84
+ return {
85
+ ...normalized,
86
+ recentMessageIds,
87
+ recentObjectKeys,
88
+ lastInboundAt: params.at ?? normalized.lastInboundAt ?? Date.now(),
89
+ };
90
+ }
91
+ export function hasSeenMessage(state, params) {
92
+ const normalized = normalizeCheckpointState(state);
93
+ return Boolean((params.msgId && normalized.recentMessageIds.includes(params.msgId)) ||
94
+ (params.objectKey && normalized.recentObjectKeys.includes(params.objectKey)));
95
+ }
@@ -0,0 +1,9 @@
1
+ export declare class RelayTransportError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class PreconditionFailedError extends RelayTransportError {
5
+ constructor(message?: string);
6
+ }
7
+ export declare class CASRetryExceededError extends RelayTransportError {
8
+ constructor(message?: string);
9
+ }
@@ -0,0 +1,18 @@
1
+ export class RelayTransportError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "RelayTransportError";
5
+ }
6
+ }
7
+ export class PreconditionFailedError extends RelayTransportError {
8
+ constructor(message = "PreconditionFailed") {
9
+ super(message);
10
+ this.name = "PreconditionFailedError";
11
+ }
12
+ }
13
+ export class CASRetryExceededError extends RelayTransportError {
14
+ constructor(message = "Failed to append message after CAS retries") {
15
+ super(message);
16
+ this.name = "CASRetryExceededError";
17
+ }
18
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./errors.js";
3
+ export * from "./object-store.js";
4
+ export * from "./keyspace.js";
5
+ export * from "./checkpoint.js";
6
+ export * from "./transport.js";
@@ -0,0 +1,6 @@
1
+ export * from "./types.js";
2
+ export * from "./errors.js";
3
+ export * from "./object-store.js";
4
+ export * from "./keyspace.js";
5
+ export * from "./checkpoint.js";
6
+ export * from "./transport.js";
@@ -0,0 +1,24 @@
1
+ export declare const MAX_MS = 9999999999999;
2
+ export declare function sanitizeFragment(value: string | null | undefined): string;
3
+ export declare function padRevTs(ts: number): string;
4
+ export declare function extractTimestampFromRelayKey(key: string): number | null;
5
+ export interface RelayKeyspaceOptions {
6
+ bucket?: string;
7
+ idFactory?: () => string;
8
+ nowMsFactory?: () => number;
9
+ }
10
+ export declare class RelayKeyspace {
11
+ readonly bucket: string;
12
+ readonly idFactory: () => string;
13
+ readonly nowMsFactory: () => number;
14
+ constructor(options?: RelayKeyspaceOptions);
15
+ padRevTs(ts: number): string;
16
+ shortUuid(): string;
17
+ makeMsgKey(recipient: string, nowMs?: number, suffix?: string): string;
18
+ makeAttKey(recipient: string, messageId: string, index: number, name?: string, nowMs?: number): string;
19
+ makeHeadKey(recipient: string): string;
20
+ makeIdentityKey(peer: string): string;
21
+ makeIdentifyKey(peer: string): string;
22
+ }
23
+ export declare class R2Relay extends RelayKeyspace {
24
+ }