@relaymessenger/openclaw-plugin 0.3.4 → 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.
Files changed (61) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +159 -124
  3. package/contracts/relay-sdk-0.3.0-staging.4.registry.json +58 -0
  4. package/contracts/relay-v1.lock.json +77 -0
  5. package/dist/index.js +2 -2
  6. package/dist/setup-entry.js +1 -2
  7. package/dist/src/accounts.js +63 -34
  8. package/dist/src/channel.js +144 -533
  9. package/dist/src/dispatch.js +257 -0
  10. package/dist/src/full-sync.js +24 -0
  11. package/dist/src/gateway.js +171 -0
  12. package/dist/src/inbound.js +54 -85
  13. package/dist/src/ingress.js +64 -0
  14. package/dist/src/outbound.js +48 -111
  15. package/dist/src/runtime.js +2 -3
  16. package/dist/src/state.js +492 -0
  17. package/dist/src/types.js +1 -3
  18. package/index.ts +1 -2
  19. package/openclaw.plugin.json +15 -18
  20. package/package.json +113 -40
  21. package/setup-entry.ts +0 -2
  22. package/src/accounts.ts +95 -51
  23. package/src/channel.ts +271 -646
  24. package/src/dispatch.ts +324 -0
  25. package/src/full-sync.ts +47 -0
  26. package/src/gateway.ts +216 -0
  27. package/src/inbound.ts +71 -122
  28. package/src/ingress.ts +123 -0
  29. package/src/outbound.ts +70 -149
  30. package/src/runtime.ts +4 -4
  31. package/src/state.ts +609 -0
  32. package/src/types.ts +51 -162
  33. package/dist/src/account-lock.js +0 -91
  34. package/dist/src/client.js +0 -13
  35. package/dist/src/cursor-store.js +0 -136
  36. package/dist/src/inbound-dedupe.js +0 -175
  37. package/dist/src/invocations.js +0 -47
  38. package/dist/src/lifecycle.js +0 -35
  39. package/dist/src/poll-loop.js +0 -137
  40. package/dist/src/responding.js +0 -36
  41. package/dist/src/security.js +0 -26
  42. package/dist/src/state-files.js +0 -243
  43. package/dist/src/vendor/relay-sdk/client.js +0 -163
  44. package/dist/src/vendor/relay-sdk/errors.js +0 -45
  45. package/dist/src/vendor/relay-sdk/types.js +0 -2
  46. package/dist/src/vendor/relay-sdk/url.js +0 -39
  47. package/src/account-lock.ts +0 -108
  48. package/src/client.ts +0 -51
  49. package/src/cursor-store.ts +0 -186
  50. package/src/inbound-dedupe.ts +0 -241
  51. package/src/invocations.ts +0 -58
  52. package/src/lifecycle.ts +0 -42
  53. package/src/poll-loop.ts +0 -173
  54. package/src/responding.ts +0 -52
  55. package/src/security.ts +0 -36
  56. package/src/state-files.ts +0 -298
  57. package/src/vendor/relay-sdk/README.md +0 -28
  58. package/src/vendor/relay-sdk/client.ts +0 -293
  59. package/src/vendor/relay-sdk/errors.ts +0 -61
  60. package/src/vendor/relay-sdk/types.ts +0 -82
  61. package/src/vendor/relay-sdk/url.ts +0 -43
package/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- // Relay channel plugin entrypoint registers the OpenClaw integration.
2
1
  import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
3
2
  import { relayChannelPlugin } from "./src/channel.js";
4
3
  import { setRelayRuntime } from "./src/runtime.js";
@@ -6,7 +5,7 @@ import { setRelayRuntime } from "./src/runtime.js";
6
5
  export default defineChannelPluginEntry({
7
6
  id: "relay",
8
7
  name: "Relay",
9
- description: "Relay channel plugin. Text your OpenClaw like a friend.",
8
+ description: "Native Relay channel plugin for OpenClaw.",
10
9
  plugin: relayChannelPlugin,
11
10
  setRuntime: setRelayRuntime,
12
11
  });
@@ -1,22 +1,17 @@
1
1
  {
2
2
  "id": "relay",
3
3
  "name": "Relay",
4
- "description": "Relay channel plugin. Connects an OpenClaw agent to a Relay contact.",
4
+ "description": "Native Relay channel plugin for OpenClaw.",
5
5
  "activation": {
6
6
  "onStartup": false
7
7
  },
8
8
  "channels": [
9
9
  "relay"
10
10
  ],
11
- "configSchema": {
12
- "type": "object",
13
- "additionalProperties": false,
14
- "properties": {}
15
- },
16
11
  "channelConfigs": {
17
12
  "relay": {
18
13
  "label": "Relay",
19
- "description": "Back a Relay contact with this OpenClaw agent.",
14
+ "description": "Connect one or more Relay Contacts to OpenClaw.",
20
15
  "schema": {
21
16
  "type": "object",
22
17
  "additionalProperties": false,
@@ -42,11 +37,6 @@
42
37
  "type": "string"
43
38
  }
44
39
  },
45
- "pollTimeoutSeconds": {
46
- "type": "number",
47
- "minimum": 1,
48
- "maximum": 30
49
- },
50
40
  "defaultAccount": {
51
41
  "type": "string"
52
42
  },
@@ -76,11 +66,6 @@
76
66
  "items": {
77
67
  "type": "string"
78
68
  }
79
- },
80
- "pollTimeoutSeconds": {
81
- "type": "number",
82
- "minimum": 1,
83
- "maximum": 30
84
69
  }
85
70
  }
86
71
  }
@@ -92,10 +77,22 @@
92
77
  "label": "Agent Token",
93
78
  "sensitive": true
94
79
  },
80
+ "tokenFile": {
81
+ "label": "Agent Token file",
82
+ "sensitive": true
83
+ },
95
84
  "baseUrl": {
96
- "label": "Relay API base URL"
85
+ "label": "Relay API origin"
86
+ },
87
+ "allowFrom": {
88
+ "label": "Allowed Contact IDs or Handles"
97
89
  }
98
90
  }
99
91
  }
92
+ },
93
+ "configSchema": {
94
+ "type": "object",
95
+ "additionalProperties": false,
96
+ "properties": {}
100
97
  }
101
98
  }
package/package.json CHANGED
@@ -1,72 +1,79 @@
1
1
  {
2
2
  "name": "@relaymessenger/openclaw-plugin",
3
- "version": "0.3.4",
4
- "description": "Relay channel plugin for OpenClaw. Text your OpenClaw like a friend.",
3
+ "version": "0.4.0-staging.0",
4
+ "description": "Native Relay channel plugin for OpenClaw",
5
5
  "license": "MIT",
6
- "publishConfig": {
7
- "access": "public"
8
- },
9
- "keywords": [
10
- "openclaw",
11
- "relay",
12
- "messaging",
13
- "agent"
14
- ],
6
+ "type": "module",
15
7
  "engines": {
16
- "node": ">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0"
8
+ "node": ">=22.22.3"
17
9
  },
18
- "repository": {
19
- "type": "git",
20
- "url": "git+https://github.com/relaymessenger/Relay-SDK.git",
21
- "directory": "integrations/openclaw"
10
+ "exports": {
11
+ ".": "./dist/index.js",
12
+ "./setup": "./dist/setup-entry.js"
22
13
  },
23
- "type": "module",
24
14
  "files": [
15
+ "LICENSE",
25
16
  "README.md",
17
+ "contracts/relay-v1.lock.json",
18
+ "contracts/relay-sdk-0.3.0-staging.4.registry.json",
19
+ "openclaw.plugin.json",
26
20
  "index.ts",
27
21
  "setup-entry.ts",
28
- "openclaw.plugin.json",
29
22
  "src/**/*.ts",
30
23
  "!src/**/*.test.ts",
31
- "dist/**/*.js"
24
+ "dist/**/*.js",
25
+ "dist/**/*.d.ts"
32
26
  ],
33
27
  "scripts": {
34
- "build": "tsc -p tsconfig.build.json",
35
- "check": "tsc --noEmit",
28
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
29
+ "build": "npm run clean && tsc -p tsconfig.build.json",
30
+ "check": "tsc -p tsconfig.json --noEmit",
36
31
  "test": "vitest run",
32
+ "validate": "npm run check && npm run build && npm test && npm run contract:verify && npm run contract:test",
33
+ "release:validate": "node scripts/release-validate.mjs",
34
+ "contract:verify": "node scripts/verify-contract-provenance.mjs",
35
+ "contract:test": "node --test test/*.test.mjs",
37
36
  "pack:smoke": "node scripts/pack-smoke.mjs",
38
37
  "gateway:harness": "node scripts/gateway-harness.mjs",
39
38
  "prepack": "npm run build"
40
39
  },
41
40
  "dependencies": {
42
- "@openclaw/fs-safe": "0.5.6"
41
+ "@relaymessenger/sdk": "0.3.0-staging.4"
43
42
  },
44
43
  "devDependencies": {
45
- "@types/node": "^26.2.0",
46
- "openclaw": "2026.7.2-beta.7",
44
+ "@types/node": "^26.0.0",
45
+ "openclaw": "2026.8.1",
47
46
  "typescript": "^7.0.2",
48
47
  "vitest": "^4.1.10"
49
48
  },
50
49
  "peerDependencies": {
51
- "openclaw": ">=2026.7.1-2"
50
+ "openclaw": ">=2026.8.1 <2026.9.0"
52
51
  },
53
52
  "peerDependenciesMeta": {
54
53
  "openclaw": {
55
54
  "optional": true
56
55
  }
57
56
  },
57
+ "publishConfig": {
58
+ "access": "public",
59
+ "tag": "staging",
60
+ "provenance": true
61
+ },
62
+ "repository": {
63
+ "type": "git",
64
+ "url": "git+https://github.com/RelayMessenger/Relay-OpenClaw.git"
65
+ },
66
+ "homepage": "https://docs.relayapp.im/integrations/openclaw",
67
+ "bugs": {
68
+ "url": "https://github.com/RelayMessenger/Relay-OpenClaw/issues"
69
+ },
70
+ "keywords": [
71
+ "openclaw",
72
+ "relay",
73
+ "messaging",
74
+ "channel"
75
+ ],
58
76
  "openclaw": {
59
- "compat": {
60
- "pluginApi": ">=2026.7.1-2"
61
- },
62
- "install": {
63
- "localPath": ".",
64
- "defaultChoice": "local",
65
- "minHostVersion": ">=2026.7.1-2"
66
- },
67
- "build": {
68
- "openclawVersion": "2026.7.2-beta.7"
69
- },
70
77
  "extensions": [
71
78
  "./index.ts"
72
79
  ],
@@ -86,12 +93,78 @@
86
93
  },
87
94
  "label": "Relay",
88
95
  "selectionLabel": "Relay",
89
- "detailLabel": "Relay",
96
+ "detailLabel": "Relay Messenger",
90
97
  "docsPath": "https://docs.relayapp.im/integrations/openclaw",
91
- "docsLabel": "documentation",
92
- "blurb": "Text your OpenClaw like a friend.",
98
+ "docsLabel": "Relay OpenClaw",
99
+ "blurb": "Message your OpenClaw through Relay.",
93
100
  "systemImage": "message",
94
- "markdownCapable": false
101
+ "markdownCapable": false,
102
+ "order": 70,
103
+ "commands": {
104
+ "nativeCommandsAutoEnabled": false,
105
+ "nativeSkillsAutoEnabled": false
106
+ },
107
+ "setup": {
108
+ "fields": [
109
+ {
110
+ "key": "token",
111
+ "kind": "string",
112
+ "sensitive": true,
113
+ "cli": {
114
+ "flags": "--token <token>",
115
+ "description": "Relay Agent Token"
116
+ }
117
+ },
118
+ {
119
+ "key": "tokenFile",
120
+ "kind": "string",
121
+ "sensitive": true,
122
+ "cli": {
123
+ "flags": "--token-file <path>",
124
+ "description": "File containing a Relay Agent Token"
125
+ }
126
+ },
127
+ {
128
+ "key": "baseUrl",
129
+ "kind": "string",
130
+ "cli": {
131
+ "flags": "--base-url <url>",
132
+ "description": "Relay API origin"
133
+ }
134
+ },
135
+ {
136
+ "key": "allowFrom",
137
+ "kind": "string-list",
138
+ "cli": {
139
+ "flags": "--allow-from <contacts>",
140
+ "description": "Allowed Relay Contact IDs or Handles"
141
+ }
142
+ },
143
+ {
144
+ "key": "useEnv",
145
+ "kind": "boolean",
146
+ "cli": {
147
+ "flags": "--use-env",
148
+ "description": "Use RELAY_AGENT_TOKEN"
149
+ },
150
+ "envVars": [
151
+ "RELAY_AGENT_TOKEN"
152
+ ]
153
+ }
154
+ ]
155
+ }
156
+ },
157
+ "install": {
158
+ "npmSpec": "@relaymessenger/openclaw-plugin",
159
+ "defaultChoice": "npm",
160
+ "minHostVersion": ">=2026.8.1",
161
+ "allowInvalidConfigRecovery": true
162
+ },
163
+ "compat": {
164
+ "pluginApi": ">=2026.8.1 <2026.9.0"
165
+ },
166
+ "build": {
167
+ "openclawVersion": "2026.8.1"
95
168
  }
96
169
  }
97
170
  }
package/setup-entry.ts CHANGED
@@ -1,5 +1,3 @@
1
- // Lightweight setup entry: loaded instead of the full entry while the channel
2
- // is disabled/unconfigured, so status/config surfaces avoid runtime imports.
3
1
  import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
4
2
  import { relayChannelPlugin } from "./src/channel.js";
5
3
 
package/src/accounts.ts CHANGED
@@ -1,98 +1,142 @@
1
- // Multi-account resolution: channels.relay.accounts.<id> with a
2
- // default-account fallback, so one OpenClaw can back several Relay contacts
3
- // (one Agent Token each). Env vars cover the single-account quickstart.
4
1
  import {
5
2
  createAccountListHelpers,
6
3
  resolveMergedAccountConfig,
7
4
  } from "openclaw/plugin-sdk/account-helpers";
8
- import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
5
+ import {
6
+ DEFAULT_ACCOUNT_ID,
7
+ normalizeAccountId,
8
+ } from "openclaw/plugin-sdk/account-id";
9
9
  import { tryReadSecretFileSync } from "openclaw/plugin-sdk/channel-core";
10
- import { DEFAULT_RELAY_BASE_URL, normalizeRelayBaseUrl } from "./client.js";
11
- import type { RelayAccountConfig, RelayCoreConfig, ResolvedRelayAccount } from "./types.js";
10
+ import type {
11
+ RelayAccountConfig,
12
+ RelayCoreConfig,
13
+ ResolvedRelayAccount,
14
+ } from "./types.js";
12
15
 
13
16
  export const RELAY_TOKEN_ENV_VAR = "RELAY_AGENT_TOKEN";
14
17
  export const RELAY_BASE_URL_ENV_VAR = "RELAY_BASE_URL";
18
+ export const DEFAULT_RELAY_BASE_URL = "https://api.relayapp.im";
15
19
 
16
- const DEFAULT_POLL_TIMEOUT_SECONDS = 30;
17
-
18
- const { listAccountIds, resolveDefaultAccountId } = createAccountListHelpers("relay", {
19
- normalizeAccountId,
20
- implicitDefaultAccount: {
21
- channelKeys: ["token", "tokenFile"],
22
- envVars: [RELAY_TOKEN_ENV_VAR],
20
+ const { listAccountIds, resolveDefaultAccountId } = createAccountListHelpers(
21
+ "relay",
22
+ {
23
+ normalizeAccountId,
24
+ implicitDefaultAccount: {
25
+ channelKeys: ["token", "tokenFile"],
26
+ envVars: [RELAY_TOKEN_ENV_VAR],
27
+ },
23
28
  },
24
- });
29
+ );
25
30
 
26
31
  export {
32
+ DEFAULT_ACCOUNT_ID,
27
33
  listAccountIds as listRelayAccountIds,
28
34
  resolveDefaultAccountId as resolveDefaultRelayAccountId,
29
- DEFAULT_ACCOUNT_ID,
30
35
  };
31
36
 
32
- function resolveMergedRelayAccountConfig(
37
+ function mergedAccountConfig(
33
38
  cfg: RelayCoreConfig,
34
39
  accountId: string,
35
40
  ): RelayAccountConfig {
36
41
  return resolveMergedAccountConfig<RelayAccountConfig>({
37
- channelConfig: cfg.channels?.relay as RelayAccountConfig | undefined,
42
+ channelConfig: cfg.channels?.relay,
38
43
  accounts: cfg.channels?.relay?.accounts,
39
44
  accountId,
40
- omitKeys: ["defaultAccount"],
45
+ // A channel-level token or tokenFile belongs only to the implicit/default
46
+ // account. Named accounts may inherit non-credential defaults, but must
47
+ // opt into their own credential source.
48
+ omitKeys:
49
+ accountId === DEFAULT_ACCOUNT_ID
50
+ ? ["defaultAccount"]
51
+ : ["defaultAccount", "token", "tokenFile"],
41
52
  normalizeAccountId,
42
53
  });
43
54
  }
44
55
 
45
56
  function resolveToken(params: {
46
- merged: RelayAccountConfig;
57
+ config: RelayAccountConfig;
47
58
  accountId: string;
48
59
  env: NodeJS.ProcessEnv;
49
60
  }): string {
50
- const direct = params.merged.token?.trim();
51
- if (direct) {
52
- return direct;
53
- }
54
- const fromFile = params.merged.tokenFile
55
- ? tryReadSecretFileSync(params.merged.tokenFile, "relay tokenFile")?.trim()
61
+ const inline = params.config.token?.trim();
62
+ if (inline) return inline;
63
+
64
+ const fromFile = params.config.tokenFile
65
+ ? tryReadSecretFileSync(params.config.tokenFile, "relay tokenFile")?.trim()
56
66
  : undefined;
57
- if (fromFile) {
58
- return fromFile;
67
+ if (fromFile) return fromFile;
68
+
69
+ return params.accountId === DEFAULT_ACCOUNT_ID
70
+ ? (params.env[RELAY_TOKEN_ENV_VAR]?.trim() ?? "")
71
+ : "";
72
+ }
73
+
74
+ function isLoopbackHostname(hostname: string): boolean {
75
+ return (
76
+ hostname === "localhost" ||
77
+ hostname === "127.0.0.1" ||
78
+ hostname === "::1" ||
79
+ hostname.endsWith(".localhost")
80
+ );
81
+ }
82
+
83
+ export function normalizeRelayBaseUrl(
84
+ value: string | undefined,
85
+ ): string {
86
+ let parsed: URL;
87
+ try {
88
+ parsed = new URL(value?.trim() || DEFAULT_RELAY_BASE_URL);
89
+ } catch {
90
+ throw new Error("relay: baseUrl must be an absolute Relay API origin");
91
+ }
92
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
93
+ throw new Error("relay: baseUrl must not contain credentials, query, or fragment");
59
94
  }
60
- // Env token applies to the default account only, so named accounts cannot
61
- // silently share one token.
62
- if (params.accountId === DEFAULT_ACCOUNT_ID) {
63
- return params.env[RELAY_TOKEN_ENV_VAR]?.trim() ?? "";
95
+ if (parsed.pathname !== "/" && parsed.pathname !== "") {
96
+ throw new Error("relay: baseUrl must be an origin without a path");
64
97
  }
65
- return "";
98
+ if (
99
+ parsed.protocol !== "https:" &&
100
+ !(parsed.protocol === "http:" && isLoopbackHostname(parsed.hostname))
101
+ ) {
102
+ throw new Error("relay: baseUrl must use HTTPS (HTTP is allowed only for loopback tests)");
103
+ }
104
+ return parsed.origin;
105
+ }
106
+
107
+ function normalizeAllowFrom(values: string[] | undefined): string[] {
108
+ return [
109
+ ...new Set(
110
+ (values ?? [])
111
+ .map((value) => value.trim())
112
+ .filter((value) => value.length > 0),
113
+ ),
114
+ ];
66
115
  }
67
116
 
68
117
  export function resolveRelayAccount(params: {
69
118
  cfg: RelayCoreConfig;
70
- accountId?: string | null;
119
+ accountId?: string | null | undefined;
71
120
  env?: NodeJS.ProcessEnv;
72
121
  }): ResolvedRelayAccount {
73
122
  const env = params.env ?? process.env;
74
123
  const accountId = normalizeAccountId(params.accountId);
75
- const merged = resolveMergedRelayAccountConfig(params.cfg, accountId);
124
+ const config = mergedAccountConfig(params.cfg, accountId);
76
125
  const baseEnabled = params.cfg.channels?.relay?.enabled !== false;
77
- const enabled = baseEnabled && merged.enabled !== false;
78
- const token = resolveToken({ merged, accountId, env });
79
- const baseUrl = normalizeRelayBaseUrl(
80
- merged.baseUrl?.trim() ||
81
- (accountId === DEFAULT_ACCOUNT_ID ? env[RELAY_BASE_URL_ENV_VAR]?.trim() : undefined) ||
82
- DEFAULT_RELAY_BASE_URL,
83
- );
84
- const pollTimeoutSeconds = Math.min(
85
- Math.max(merged.pollTimeoutSeconds ?? DEFAULT_POLL_TIMEOUT_SECONDS, 1),
86
- 30,
87
- );
126
+ const token = resolveToken({ config, accountId, env });
127
+ const configuredBaseUrl =
128
+ config.baseUrl?.trim() ||
129
+ (accountId === DEFAULT_ACCOUNT_ID
130
+ ? env[RELAY_BASE_URL_ENV_VAR]?.trim()
131
+ : undefined);
88
132
  return {
89
133
  accountId,
90
- enabled,
91
- configured: Boolean(token),
92
- ...(merged.name?.trim() ? { name: merged.name.trim() } : {}),
134
+ ...(config.name?.trim() ? { name: config.name.trim() } : {}),
135
+ enabled: baseEnabled && config.enabled !== false,
136
+ configured: token.length > 0,
93
137
  token,
94
- baseUrl,
95
- pollTimeoutSeconds,
96
- config: merged,
138
+ baseUrl: normalizeRelayBaseUrl(configuredBaseUrl),
139
+ allowFrom: normalizeAllowFrom(config.allowFrom),
140
+ config,
97
141
  };
98
142
  }