@relaymessenger/openclaw-plugin 0.3.3 → 0.4.0-staging.0
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/LICENSE +1 -1
- package/README.md +159 -124
- package/contracts/relay-sdk-0.3.0-staging.4.registry.json +58 -0
- package/contracts/relay-v1.lock.json +77 -0
- package/dist/index.js +2 -2
- package/dist/setup-entry.js +1 -2
- package/dist/src/accounts.js +63 -34
- package/dist/src/channel.js +144 -498
- package/dist/src/dispatch.js +257 -0
- package/dist/src/full-sync.js +24 -0
- package/dist/src/gateway.js +171 -0
- package/dist/src/inbound.js +54 -80
- package/dist/src/ingress.js +64 -0
- package/dist/src/outbound.js +48 -109
- package/dist/src/runtime.js +2 -3
- package/dist/src/state.js +492 -0
- package/dist/src/types.js +1 -3
- package/index.ts +1 -2
- package/openclaw.plugin.json +15 -18
- package/package.json +113 -40
- package/setup-entry.ts +0 -2
- package/src/accounts.ts +95 -51
- package/src/channel.ts +271 -611
- package/src/dispatch.ts +324 -0
- package/src/full-sync.ts +47 -0
- package/src/gateway.ts +216 -0
- package/src/inbound.ts +71 -111
- package/src/ingress.ts +123 -0
- package/src/outbound.ts +70 -142
- package/src/runtime.ts +4 -4
- package/src/state.ts +609 -0
- package/src/types.ts +51 -148
- package/dist/src/account-lock.js +0 -91
- package/dist/src/client.js +0 -229
- package/dist/src/cursor-store.js +0 -136
- package/dist/src/inbound-dedupe.js +0 -175
- package/dist/src/lifecycle.js +0 -35
- package/dist/src/poll-loop.js +0 -125
- package/dist/src/responding.js +0 -13
- package/dist/src/security.js +0 -26
- package/dist/src/state-files.js +0 -167
- package/src/account-lock.ts +0 -108
- package/src/client.ts +0 -330
- package/src/cursor-store.ts +0 -186
- package/src/inbound-dedupe.ts +0 -241
- package/src/lifecycle.ts +0 -42
- package/src/poll-loop.ts +0 -161
- package/src/responding.ts +0 -21
- package/src/security.ts +0 -36
- package/src/state-files.ts +0 -212
package/src/state-files.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { jsonStore } from "@openclaw/fs-safe/store";
|
|
2
|
-
import type { JsonStore } from "@openclaw/fs-safe/store";
|
|
3
|
-
import { withFileLock } from "@openclaw/fs-safe/file-lock";
|
|
4
|
-
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
5
|
-
import { chmodSync, lstatSync, mkdirSync } from "node:fs";
|
|
6
|
-
import { hostname } from "node:os";
|
|
7
|
-
import { basename, join } from "node:path";
|
|
8
|
-
|
|
9
|
-
const RELAY_STATE_DOCUMENT_VERSION = 1;
|
|
10
|
-
const RELAY_STATE_LOCK_VERSION = 1;
|
|
11
|
-
const RELAY_STATE_LOCK_TIMEOUT_MS = 30_000;
|
|
12
|
-
|
|
13
|
-
type RelayStateLockOwner = {
|
|
14
|
-
version: typeof RELAY_STATE_LOCK_VERSION;
|
|
15
|
-
kind: "relay-state";
|
|
16
|
-
pid: number;
|
|
17
|
-
host: string;
|
|
18
|
-
createdAt: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type RelayStateDocument<T> = {
|
|
22
|
-
version: typeof RELAY_STATE_DOCUMENT_VERSION;
|
|
23
|
-
entries: Record<string, T>;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export function emptyRelayStateDocument<T>(): RelayStateDocument<T> {
|
|
27
|
-
return { version: RELAY_STATE_DOCUMENT_VERSION, entries: {} };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
31
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function assertRelayStateDocument<T>(
|
|
35
|
-
value: unknown,
|
|
36
|
-
label: string,
|
|
37
|
-
validateEntry: (key: string, value: unknown) => value is T,
|
|
38
|
-
): asserts value is RelayStateDocument<T> {
|
|
39
|
-
if (
|
|
40
|
-
!isRecord(value) ||
|
|
41
|
-
value.version !== RELAY_STATE_DOCUMENT_VERSION ||
|
|
42
|
-
!isRecord(value.entries)
|
|
43
|
-
) {
|
|
44
|
-
throw new Error(`relay ${label} state is corrupt`);
|
|
45
|
-
}
|
|
46
|
-
for (const [key, entry] of Object.entries(value.entries)) {
|
|
47
|
-
if (!validateEntry(key, entry)) {
|
|
48
|
-
throw new Error(`relay ${label} state is corrupt`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function isRelayStateLockOwner(value: unknown): value is RelayStateLockOwner {
|
|
54
|
-
if (!isRecord(value)) return false;
|
|
55
|
-
return (
|
|
56
|
-
value.version === RELAY_STATE_LOCK_VERSION &&
|
|
57
|
-
value.kind === "relay-state" &&
|
|
58
|
-
Number.isSafeInteger(value.pid) &&
|
|
59
|
-
(value.pid as number) > 0 &&
|
|
60
|
-
typeof value.host === "string" &&
|
|
61
|
-
value.host.length > 0 &&
|
|
62
|
-
typeof value.createdAt === "string" &&
|
|
63
|
-
Number.isFinite(Date.parse(value.createdAt))
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function localProcessIsLive(pid: number): boolean {
|
|
68
|
-
try {
|
|
69
|
-
process.kill(pid, 0);
|
|
70
|
-
return true;
|
|
71
|
-
} catch (error) {
|
|
72
|
-
// EPERM proves the process exists but is owned by another user. Unknown
|
|
73
|
-
// failures also fail closed; only ESRCH proves this host no longer has it.
|
|
74
|
-
return !(error instanceof Error && "code" in error && error.code === "ESRCH");
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Windows opens the sidecar lock with an openat-style
|
|
80
|
-
* `O_CREAT | O_EXCL` beneath a parent handle. While a just-released lock file
|
|
81
|
-
* is still delete-pending, that create returns `ACCESS_DENIED` instead of the
|
|
82
|
-
* `already exists` fs-safe retries on, so contention escapes the acquire loop
|
|
83
|
-
* as a hard `EACCES`. Retry those on Windows only, bounded by the caller's lock
|
|
84
|
-
* timeout: a genuine permission failure simply reproduces until the deadline
|
|
85
|
-
* and then surfaces unchanged.
|
|
86
|
-
*/
|
|
87
|
-
function isWindowsLockAcquisitionContention(error: unknown): boolean {
|
|
88
|
-
if (process.platform !== "win32") return false;
|
|
89
|
-
const code = (error as NodeJS.ErrnoException | null)?.code;
|
|
90
|
-
return code === "EACCES" || code === "EPERM";
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function lockRetryDelayMs(attempt: number, remainingMs: number): number {
|
|
94
|
-
const backoff = Math.min(25 * 2 ** attempt, 250);
|
|
95
|
-
return Math.max(1, Math.min(backoff * (0.5 + Math.random() / 2), remainingMs));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function canRecoverRelayStateLock(value: unknown): boolean {
|
|
99
|
-
return (
|
|
100
|
-
isRelayStateLockOwner(value) &&
|
|
101
|
-
value.host === hostname() &&
|
|
102
|
-
!localProcessIsLive(value.pid)
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function ensurePrivateStateDirectory(path: string): void {
|
|
107
|
-
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
108
|
-
const stat = lstatSync(path);
|
|
109
|
-
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
110
|
-
throw new Error(`relay state path is not a private directory: ${path}`);
|
|
111
|
-
}
|
|
112
|
-
try {
|
|
113
|
-
chmodSync(path, 0o700);
|
|
114
|
-
} catch {
|
|
115
|
-
// POSIX modes are not fully implemented on Windows. fs-safe's private
|
|
116
|
-
// write path still owns the platform-specific file guarantees.
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Relay owns these files rather than requesting OpenClaw's privileged host
|
|
122
|
-
* SQLite. jsonStore gives every mutation a private atomic replacement. An
|
|
123
|
-
* fs-safe sidecar lock serializes cross-process mutations and is recovered
|
|
124
|
-
* only when its valid Relay owner names this host and its PID is provably dead.
|
|
125
|
-
*/
|
|
126
|
-
export function openRelayStateDocument<T>(params: {
|
|
127
|
-
fileName: string;
|
|
128
|
-
env?: NodeJS.ProcessEnv;
|
|
129
|
-
lockTimeoutMs?: number;
|
|
130
|
-
}): JsonStore<RelayStateDocument<T>> {
|
|
131
|
-
if (!params.fileName || basename(params.fileName) !== params.fileName) {
|
|
132
|
-
throw new Error("relay state fileName must be one file name");
|
|
133
|
-
}
|
|
134
|
-
const lockTimeoutMs = params.lockTimeoutMs ?? RELAY_STATE_LOCK_TIMEOUT_MS;
|
|
135
|
-
if (!Number.isSafeInteger(lockTimeoutMs) || lockTimeoutMs < 1) {
|
|
136
|
-
throw new Error("relay state lockTimeoutMs must be a positive safe integer");
|
|
137
|
-
}
|
|
138
|
-
const stateRoot = resolveStateDir(params.env ?? process.env);
|
|
139
|
-
const relayRoot = join(stateRoot, "relay");
|
|
140
|
-
const relayStateRoot = join(relayRoot, "state");
|
|
141
|
-
ensurePrivateStateDirectory(relayRoot);
|
|
142
|
-
ensurePrivateStateDirectory(relayStateRoot);
|
|
143
|
-
const store = jsonStore<RelayStateDocument<T>>({
|
|
144
|
-
filePath: join(relayStateRoot, params.fileName),
|
|
145
|
-
dirMode: 0o700,
|
|
146
|
-
mode: 0o600,
|
|
147
|
-
});
|
|
148
|
-
const withMutationLock = async <R>(run: () => Promise<R>): Promise<R> => {
|
|
149
|
-
const deadline = Date.now() + lockTimeoutMs;
|
|
150
|
-
for (let attempt = 0; ; attempt += 1) {
|
|
151
|
-
// Only acquisition is retried. Once the mutation itself has started it has
|
|
152
|
-
// observed state under the lock, so replaying it could double-apply.
|
|
153
|
-
let mutationStarted = false;
|
|
154
|
-
try {
|
|
155
|
-
return await withFileLock(
|
|
156
|
-
store.filePath,
|
|
157
|
-
{
|
|
158
|
-
managerKey: `relay-state:${store.filePath}`,
|
|
159
|
-
staleMs: RELAY_STATE_LOCK_TIMEOUT_MS,
|
|
160
|
-
timeoutMs: Math.max(1, deadline - Date.now()),
|
|
161
|
-
staleRecovery: "remove-if-unchanged",
|
|
162
|
-
retry: {
|
|
163
|
-
retries: 300,
|
|
164
|
-
minTimeout: 25,
|
|
165
|
-
maxTimeout: 250,
|
|
166
|
-
randomize: true,
|
|
167
|
-
},
|
|
168
|
-
payload: (): RelayStateLockOwner => ({
|
|
169
|
-
version: RELAY_STATE_LOCK_VERSION,
|
|
170
|
-
kind: "relay-state",
|
|
171
|
-
pid: process.pid,
|
|
172
|
-
host: hostname(),
|
|
173
|
-
createdAt: new Date().toISOString(),
|
|
174
|
-
}),
|
|
175
|
-
shouldReclaim: ({ payload }) => canRecoverRelayStateLock(payload),
|
|
176
|
-
shouldRemoveStaleLock: ({ payload }) => canRecoverRelayStateLock(payload),
|
|
177
|
-
},
|
|
178
|
-
async () => {
|
|
179
|
-
mutationStarted = true;
|
|
180
|
-
return await run();
|
|
181
|
-
},
|
|
182
|
-
);
|
|
183
|
-
} catch (error) {
|
|
184
|
-
const remaining = deadline - Date.now();
|
|
185
|
-
if (
|
|
186
|
-
mutationStarted ||
|
|
187
|
-
remaining <= 0 ||
|
|
188
|
-
!isWindowsLockAcquisitionContention(error)
|
|
189
|
-
) {
|
|
190
|
-
throw error;
|
|
191
|
-
}
|
|
192
|
-
await new Promise((resolve) =>
|
|
193
|
-
setTimeout(resolve, lockRetryDelayMs(attempt, remaining)),
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
filePath: store.filePath,
|
|
201
|
-
read: store.read,
|
|
202
|
-
readOr: store.readOr,
|
|
203
|
-
readRequired: store.readRequired,
|
|
204
|
-
write: async (value) => {
|
|
205
|
-
await withMutationLock(async () => await store.write(value));
|
|
206
|
-
},
|
|
207
|
-
update: async (run) =>
|
|
208
|
-
await withMutationLock(async () => await store.update(run)),
|
|
209
|
-
updateOr: async (fallback, run) =>
|
|
210
|
-
await withMutationLock(async () => await store.updateOr(fallback, run)),
|
|
211
|
-
};
|
|
212
|
-
}
|