@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.
Files changed (50) 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 -498
  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 -80
  13. package/dist/src/ingress.js +64 -0
  14. package/dist/src/outbound.js +48 -109
  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 -611
  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 -111
  28. package/src/ingress.ts +123 -0
  29. package/src/outbound.ts +70 -142
  30. package/src/runtime.ts +4 -4
  31. package/src/state.ts +609 -0
  32. package/src/types.ts +51 -148
  33. package/dist/src/account-lock.js +0 -91
  34. package/dist/src/client.js +0 -229
  35. package/dist/src/cursor-store.js +0 -136
  36. package/dist/src/inbound-dedupe.js +0 -175
  37. package/dist/src/lifecycle.js +0 -35
  38. package/dist/src/poll-loop.js +0 -125
  39. package/dist/src/responding.js +0 -13
  40. package/dist/src/security.js +0 -26
  41. package/dist/src/state-files.js +0 -167
  42. package/src/account-lock.ts +0 -108
  43. package/src/client.ts +0 -330
  44. package/src/cursor-store.ts +0 -186
  45. package/src/inbound-dedupe.ts +0 -241
  46. package/src/lifecycle.ts +0 -42
  47. package/src/poll-loop.ts +0 -161
  48. package/src/responding.ts +0 -21
  49. package/src/security.ts +0 -36
  50. package/src/state-files.ts +0 -212
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Companion Inc.
3
+ Copyright (c) 2026 Relay Messenger
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,145 +1,180 @@
1
- # Relay channel plugin for OpenClaw
1
+ # Relay for OpenClaw
2
2
 
3
- Backs a Relay contact with an OpenClaw agent: install the plugin, point it at
4
- an owner-only Agent Token file, and your OpenClaw appears in Relay as a contact
5
- you text like a friend.
3
+ `@relaymessenger/openclaw-plugin` is the native Relay channel for OpenClaw
4
+ `2026.8.1`.
6
5
 
7
- Requires `openclaw >= 2026.7.1-2`, which the stable channel satisfies today.
8
-
9
- Cores from `2026.7.2-beta.5` onward tell the channel which part of a delivery
10
- each chunk is, and the plugin keys its idempotent sends on that. Older cores do
11
- not, so on those the plugin keys each chunk by a digest of the chunk's own
12
- text. Both keep a retry replaying the same message instead of posting a second
13
- one. The one thing an older core cannot do is separate two byte-identical
14
- chunks of a single reply, which arrive as one message.
6
+ It connects an OpenClaw gateway to Relay with a Relay Agent Token. Relay
7
+ delivers events over its v1 WebSocket, and the plugin sends replies through
8
+ the Relay v1 REST Message API. The plugin imports `@relaymessenger/sdk`; it
9
+ does not contain a copied Relay client or protocol implementation.
15
10
 
16
11
  ## Install
17
12
 
18
- ```sh
19
- npm install -g @relaymessenger/cli
20
- relaymessenger pair
21
- relaymessenger install-openclaw
13
+ ```bash
14
+ openclaw plugins install @relaymessenger/openclaw-plugin
15
+ ```
16
+
17
+ Configure the default account:
18
+
19
+ ```json
20
+ {
21
+ "channels": {
22
+ "relay": {
23
+ "enabled": true,
24
+ "tokenFile": "/run/secrets/relay-agent-token"
25
+ }
26
+ }
27
+ }
22
28
  ```
23
29
 
24
- The installer uses the OpenClaw archive bundled in the installed `relaymessenger`
25
- package, persists that archive in the paired account's private Relay runtime,
26
- and invokes OpenClaw's managed `npm-pack:` installer on the stable copy so
27
- declared runtime dependencies are installed with the plugin. It surgically
28
- adds Relay to `~/.openclaw/openclaw.json`, preserves unrelated configuration,
29
- writes the token to an owner-only file, and never prints it. It refuses to
30
- replace a different configured Relay identity.
30
+ For a single account, `RELAY_AGENT_TOKEN` is also supported. Use
31
+ `RELAY_BASE_URL` only when the Agent Token belongs to a non-production Relay
32
+ environment.
31
33
 
32
- For integration development from this checkout only:
34
+ One OpenClaw gateway can back multiple Relay Contacts:
33
35
 
34
- ```sh
35
- cd integrations/openclaw
36
- npm install
37
- npm pack
38
- openclaw plugins install npm-pack:./relaymessenger-openclaw-plugin-0.1.0.tgz --force
36
+ ```json
37
+ {
38
+ "channels": {
39
+ "relay": {
40
+ "defaultAccount": "default",
41
+ "accounts": {
42
+ "default": {
43
+ "tokenFile": "/run/secrets/relay-default"
44
+ },
45
+ "support": {
46
+ "tokenFile": "/run/secrets/relay-support"
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
39
52
  ```
40
53
 
41
- The installer produces the equivalent Relay-specific configuration:
54
+ Do not reuse one Agent Token in two configured accounts. Channel-level
55
+ `token`, `tokenFile`, and `RELAY_AGENT_TOKEN` credentials belong only to the
56
+ default account. Every named account must set its own inline token or
57
+ `tokenFile`; named accounts never inherit default credentials.
58
+
59
+ ## Relay vocabulary and scope
60
+
61
+ - A **Contact** is a Relay user or agent profile.
62
+ - Each Contact owns a public **Handle**.
63
+ - A **Chat** is direct or group.
64
+ - A **Message** belongs to one Chat and contains ordered parts.
65
+
66
+ The plugin starts OpenClaw turns for every inbound user-authored
67
+ `message.received` event in a direct Chat. In a group Chat, it starts a turn
68
+ only when a text part's canonical `mention` Handle matches the canonical
69
+ `chat.owner_handle`, or when `reply_to.message_id` resolves through Relay to a
70
+ Message authored by this agent in the same Chat. Visible `@handle` text is not
71
+ parsed as a mention. Unmentioned group traffic, agent-authored Messages,
72
+ reactions, typing events, receipts, and membership events are durably accepted
73
+ without starting a turn.
74
+
75
+ Text, link, and media parts are rendered into agent-visible text. Media stays
76
+ a labeled signed URL; the plugin does not upload or send media.
77
+
78
+ Outbound support is deliberately limited to text Messages and Message reply
79
+ references. OpenClaw splits text at Relay's current 10,000-character text-part
80
+ limit. Reactions, edit, unsend, native threads, rich cards, and outbound media
81
+ are not declared.
82
+
83
+ `allowFrom` optionally limits inbound turns to exact Relay Contact IDs or
84
+ Handles:
42
85
 
43
86
  ```json
44
87
  {
45
- "plugins": {
46
- "allow": ["relay"],
47
- "entries": { "relay": { "enabled": true } }
48
- },
49
88
  "channels": {
50
89
  "relay": {
51
- "enabled": true,
52
- "tokenFile": "~/.openclaw/secrets/relay-agent-token",
53
- "baseUrl": "https://api.relayapp.im"
90
+ "tokenFile": "/run/secrets/relay-agent-token",
91
+ "allowFrom": [
92
+ "alice",
93
+ "00000000-0000-7000-8000-000000000001"
94
+ ]
54
95
  }
55
96
  }
56
97
  }
57
98
  ```
58
99
 
59
- `RELAY_AGENT_TOKEN` / `RELAY_BASE_URL` are honored for the default account
60
- when the config fields are absent. Multiple agents run as named accounts under
61
- `channels.relay.accounts.<id>`, one token each.
62
-
63
- ## Sender security
64
-
65
- Relay agents can be discoverable, so adding an agent as a contact is not an
66
- authorization boundary. The plugin accepts inbound turns only from the
67
- authenticated agent's `owner_user_id` returned by `GET /v1/agents/me` and any
68
- Relay user ids the operator explicitly adds to `allowFrom`. Wildcards are
69
- ignored. If neither an API owner nor an explicit allowlist is available, the
70
- account fails closed and does not start polling.
71
-
72
- ## One consumer per token
73
-
74
- Relay's `GET /v1/events` long poll allows **exactly one consumer per Agent
75
- Token**:
76
-
77
- - A newer poll takes the slot; the older consumer's request ends with
78
- `409 terminated_by_other_consumer`. Running this plugin and another
79
- long-poll consumer (for example `relaymessenger start`, or a second OpenClaw) on
80
- the same token makes them steal the slot from each other forever. Give each
81
- consumer its own agent/token.
82
- - Long polling is **XOR with webhooks**: while a webhook endpoint is enabled
83
- for the agent, `/v1/events` returns `409 conflict` and the channel stops
84
- with a terminal disconnect. Disable or delete the agent's webhooks to poll.
85
-
86
- The plugin also refuses to start a second configured account that resolves to
87
- the same agent id as a running one.
88
-
89
- ## Config reference (per account)
90
-
91
- | Field | Meaning |
92
- | --- | --- |
93
- | `token` / `tokenFile` | Agent Token (or file containing it) |
94
- | `baseUrl` | Relay API origin (default `https://api.relayapp.im`). Remote origins must use HTTPS; HTTP is accepted only for loopback development. Paths, credentials, queries, and fragments are rejected. |
95
- | `allowFrom` | Additional Relay user ids (`usr_…`) allowed alongside the API-pinned owner. These identities may also run control commands. Wildcards are ignored. |
96
- | `pollTimeoutSeconds` | Long-poll hold time, 1–30 (default 30) |
97
-
98
- ## Harness
99
-
100
- Run `npm run gateway:harness` from the repository root for the clean installed-
101
- runtime proof. It packs the plugin, installs it into an isolated `HOME`, starts
102
- a real OpenClaw gateway against `harness/mock-relay-server.mjs`, receives a
103
- Relay event, completes a mock model turn, and verifies the reply reaches
104
- Relay. The release workflow requires this proof.
105
-
106
- ## Delivery and crash semantics
107
-
108
- - Every outbound platform send has a logical-send idempotency key. Durable
109
- queue retries reuse the same key, while intentional identical messages and
110
- identical chunks remain distinct.
111
- - Admission, route/session resolution, envelope building, and context
112
- finalization run before an inbound event is marked attempted. Failures in
113
- that replay-safe preflight release the claim and retry the event. The marker
114
- is committed durably immediately before OpenClaw can dispatch the agent or
115
- its tools; a failure after that boundary does not silently replay possible
116
- tool side effects. The user can resend the message deliberately.
117
- - The long-poll cursor and inbound attempt keys are bound to the canonical
118
- Relay API origin plus Relay agent id, not to a mutable local account label.
119
- Renaming an account therefore retains its cursor. A missing identity starts
120
- at cursor zero; corrupt, mismatched, unreadable, or unwritable state fails
121
- closed instead of replaying retained history. The cursor namespace rejects
122
- new identities at capacity rather than evicting an older cursor. Cursor
123
- state is independent of the bounded 30-day attempt-dedupe horizon. Both
124
- namespaces are private, lock-protected, atomically replaced Relay-owned
125
- files under `$OPENCLAW_STATE_DIR/relay/state` (normally
126
- `~/.openclaw/relay/state`); the plugin never requests trusted-only OpenClaw
127
- host SQLite access.
128
- - Before polling, the plugin takes an atomic per-origin/per-agent filesystem
129
- lock under `~/.openclaw/relay/consumer-locks`. A second OpenClaw process
130
- fails closed; a lock whose recorded PID is dead is recovered on startup.
131
- Shutdown aborts the active long poll and releases both process-local and
132
- filesystem ownership before a replacement starts.
133
- - Every API operation has a deadline (15 seconds for ordinary calls; the
134
- configured long-poll hold plus 15 seconds for event polling). Retrying a
135
- message send reuses its logical delivery idempotency key.
136
-
137
- ## v1 scope
138
-
139
- Direct conversations only. Inbound text renders as-is; inbound media and
140
- voice memos render as a labeled fetchable capability URL (the URL is itself
141
- the authorization, so no Agent Token is needed to fetch the bytes) rather
142
- than the agent seeing the file inline; reactions are observe-only; receipts
143
- (`message.delivered`/`message.read`) never start a turn. Final agent replies
144
- are delivered durably (chunked to Relay's 8 KiB per-part cap,
145
- idempotency-keyed, retry-safe).
100
+ Without `allowFrom`, any user Contact whose Message Relay delivers to this
101
+ agent can start a direct turn, while the group activation rules above still
102
+ apply.
103
+
104
+ ## Durable delivery
105
+
106
+ For every WebSocket event, the plugin:
107
+
108
+ 1. inserts `event_id` into OpenClaw's channel ingress queue, or its private
109
+ SQLite queue when a local/npm-pack install does not have trusted host state;
110
+ 2. returns from the SDK callback only after that transaction commits;
111
+ 3. lets the SDK send the cumulative WebSocket ACK;
112
+ 4. dispatches a stored event through OpenClaw's ingress lifecycle.
113
+
114
+ A replayed `event_id` reaches the existing pending, completed, or failed row
115
+ and does not repeat model or tool work. The SDK owns sequence validation,
116
+ cumulative ACKs, reconnect replay, and JSON heartbeat ping/pong.
117
+
118
+ When Relay requires `full_sync`, the plugin pages through every visible Chat
119
+ and every visible Message, atomically replaces its local snapshot, and returns
120
+ only after the snapshot commits. The SDK then sends
121
+ `full_sync_complete`.
122
+
123
+ Relay Webhooks and the WebSocket are exclusive. Startup checks that the Agent
124
+ has no saved Webhook subscriptions. The SDK also treats WebSocket HTTP `409`
125
+ and close code `4410` as terminal `RelayWebhookConfiguredError` failures.
126
+
127
+ Final OpenClaw replies use the native durable outbound Message adapter. Each
128
+ REST send carries a stable idempotency key derived from OpenClaw's delivery
129
+ queue ID and part index. Unknown-send reconciliation repeats the exact text
130
+ chunks with the same keys, so Relay returns the original Message or commits
131
+ it once.
132
+
133
+ ## Development
134
+
135
+ Use Node.js `22.22.3` or newer. Build and test on Linux:
136
+
137
+ ```bash
138
+ npm install
139
+ npm run validate
140
+ npm run pack:smoke
141
+ npm run gateway:harness
142
+ ```
143
+
144
+ `npm run release:validate` runs all three commands. CI and the guarded manual
145
+ staging workflow use that full sequence. The staging workflow accepts only an
146
+ exact SHA selected from the `staging` branch, the matching
147
+ `x.y.z-staging.n` package version, and the `staging` npm tag. It retains the
148
+ validated tarball and publishes that same digest with npm provenance; its
149
+ publish job is also bound to the `staging` GitHub environment.
150
+
151
+ `gateway:harness` packs the plugin, installs the tarball with OpenClaw
152
+ `2026.8.1`, inspects the managed installation, starts a real OpenClaw gateway,
153
+ connects to a loopback Relay WebSocket, receives one Message, and proves the
154
+ durable ACK and idempotent REST reply.
155
+
156
+ ## Contract lock
157
+
158
+ `contracts/relay-v1.lock.json` records the compatibility boundary used by this
159
+ release: Relay Server
160
+ `9b4d5bb32cc749c6fd271969948c385300d404d6`, OpenAPI SHA-256
161
+ `f62f431fc0daa48500926bf87753f81c3fdda25ab463b130ca97f2896367e0a5`,
162
+ and the exact `@relaymessenger/sdk@0.3.0-staging.4` registry integrity, source
163
+ commit `8570546ce30ab39d702409893006dbccd351f2b4`, REST operations, and WebSocket
164
+ frames consumed by the plugin.
165
+
166
+ Public CI hashes the checked-in `contracts/relay-openapi.yaml` fixture and
167
+ requires the locked digest above. The retained private release receipt also
168
+ sets `RELAY_SERVER_SOURCE_DIR` to an exact checkout of Server `9b4d5bb32cc7`
169
+ and proves that the fixture bytes match that commit before packaging. The
170
+ public repository does not require credentials for the private Server source
171
+ and does not overstate what npm metadata can attest.
172
+
173
+ `contracts/relay-sdk-0.3.0-staging.4.registry.json` records immutable npm
174
+ publication metadata and tarball digests. Validation downloads the registry
175
+ tarball, verifies SHA-1, SHA-256, and SHA-512 integrity, checks the installed
176
+ SDK, and compares its package metadata with exact SDK commit `8570546`.
177
+ The npm metadata has no `gitHead` or attestation for this version, so the
178
+ receipt deliberately does not claim cryptographic source-to-tarball
179
+ provenance. These are local artifact compatibility checks, not a claim of a
180
+ hosted Relay deployment test.
@@ -0,0 +1,58 @@
1
+ {
2
+ "schema": "relay-sdk-registry-receipt/v1",
3
+ "source": {
4
+ "repository": "RelayMessenger/Relay-SDK",
5
+ "commit": "8570546ce30ab39d702409893006dbccd351f2b4",
6
+ "packageJsonPath": "packages/sdk/package.json",
7
+ "packageJsonSha256": "b9d0f5bba2c5ba5eea904ca4ad490795c01b0a67bd5592d7fdbea50a7ed8b29b",
8
+ "contractPath": "contracts/relay-v1-openapi.yaml",
9
+ "contractSha256": "f62f431fc0daa48500926bf87753f81c3fdda25ab463b130ca97f2896367e0a5"
10
+ },
11
+ "registry": {
12
+ "packumentUrl": "https://registry.npmjs.org/@relaymessenger%2fsdk",
13
+ "package": "@relaymessenger/sdk",
14
+ "version": "0.3.0-staging.4",
15
+ "publishedAt": "2026-09-01T03:05:27.371Z",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/RelayMessenger/Relay-SDK.git",
19
+ "directory": "packages/sdk"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "registry": "https://registry.npmjs.org/",
24
+ "tag": "staging"
25
+ },
26
+ "distTagsObserved": {
27
+ "latest": "0.2.0",
28
+ "staging": "0.3.0-staging.4",
29
+ "mutable": true
30
+ },
31
+ "dist": {
32
+ "tarball": "https://registry.npmjs.org/@relaymessenger/sdk/-/sdk-0.3.0-staging.4.tgz",
33
+ "integrity": "sha512-S47nMr+igXPIgLOgkKGUSOU7WpSQZZqsByZhbavUz87C3j0Zevy6jv9IT2t+ZS40TuLqjS9FTPnQ/MpMfLdngQ==",
34
+ "shasum": "fa1356cfb37b8d64683b0734c054c4afbacbfe1d",
35
+ "sha256": "73ffd16c0ed4b07d9ff4b90d7efc5e1bf1b2fb82e833089d097b6757c4e94848",
36
+ "bytes": 36322,
37
+ "fileCount": 37,
38
+ "unpackedSize": 167419,
39
+ "signatureMetadata": [
40
+ {
41
+ "keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U",
42
+ "sig": "MEUCIDyZ96e+A/d85Ay57ET/5PGqltJRBpF5//j9GkhnqE2EAiEAoytprMukWmB1BlSxp5sCENpI8YC1kKwRaNzAvLHjqhY="
43
+ }
44
+ ]
45
+ },
46
+ "installedArtifact": {
47
+ "packageJsonSha256": "b9d0f5bba2c5ba5eea904ca4ad490795c01b0a67bd5592d7fdbea50a7ed8b29b",
48
+ "typesSha256": "efa163145777ad4df93263d87605794ca099dd8db6fe1b36cc71d0873377a653"
49
+ }
50
+ },
51
+ "provenanceBoundary": {
52
+ "registryGitHead": null,
53
+ "registryAttestations": null,
54
+ "attestationEndpoint": "https://registry.npmjs.org/-/npm/v1/attestations/@relaymessenger%2fsdk@0.3.0-staging.4",
55
+ "attestationEndpointObservedStatus": 404,
56
+ "claim": "The exact registry tarball integrity and publication metadata are verified, and its package.json bytes match Relay-SDK commit 8570546ce30ab39d702409893006dbccd351f2b4. npm registry metadata exposes neither gitHead nor an attestation for this version, so this receipt does not claim cryptographic source-to-tarball provenance."
57
+ }
58
+ }
@@ -0,0 +1,77 @@
1
+ {
2
+ "relayServer": {
3
+ "repository": "RelayMessenger/Relay-Server",
4
+ "commit": "9b4d5bb32cc749c6fd271969948c385300d404d6",
5
+ "openapiPath": "contracts/developer/openapi.yaml",
6
+ "sha256": "f62f431fc0daa48500926bf87753f81c3fdda25ab463b130ca97f2896367e0a5"
7
+ },
8
+ "relaySdk": {
9
+ "package": "@relaymessenger/sdk",
10
+ "version": "0.3.0-staging.4",
11
+ "integrity": "sha512-S47nMr+igXPIgLOgkKGUSOU7WpSQZZqsByZhbavUz87C3j0Zevy6jv9IT2t+ZS40TuLqjS9FTPnQ/MpMfLdngQ==",
12
+ "operationsSha256": "8429a056605fa9555fcb34067bbb34f7e364ed18c17ec7786b2c1621044012f1",
13
+ "registryReceipt": "contracts/relay-sdk-0.3.0-staging.4.registry.json",
14
+ "source": {
15
+ "repository": "RelayMessenger/Relay-SDK",
16
+ "commit": "8570546ce30ab39d702409893006dbccd351f2b4",
17
+ "carriedOpenapiSha256": "f62f431fc0daa48500926bf87753f81c3fdda25ab463b130ca97f2896367e0a5"
18
+ },
19
+ "usedOperations": [
20
+ {
21
+ "method": "GET",
22
+ "path": "/v1/chats",
23
+ "operationId": "listChats"
24
+ },
25
+ {
26
+ "method": "GET",
27
+ "path": "/v1/chats/{chatId}/messages",
28
+ "operationId": "getMessages"
29
+ },
30
+ {
31
+ "method": "GET",
32
+ "path": "/v1/messages/{messageId}",
33
+ "operationId": "getMessage"
34
+ },
35
+ {
36
+ "method": "POST",
37
+ "path": "/v1/chats/{chatId}/messages",
38
+ "operationId": "sendMessageToChat"
39
+ },
40
+ {
41
+ "method": "POST",
42
+ "path": "/v1/chats/{chatId}/read",
43
+ "operationId": "markChatAsRead"
44
+ },
45
+ {
46
+ "method": "POST",
47
+ "path": "/v1/chats/{chatId}/typing",
48
+ "operationId": "startTyping"
49
+ },
50
+ {
51
+ "method": "DELETE",
52
+ "path": "/v1/chats/{chatId}/typing",
53
+ "operationId": "stopTyping"
54
+ },
55
+ {
56
+ "method": "GET",
57
+ "path": "/v1/webhook-subscriptions",
58
+ "operationId": "listWebhookSubscriptions"
59
+ }
60
+ ],
61
+ "usedWebSocketFrames": {
62
+ "serverToSdk": [
63
+ "ready",
64
+ "event",
65
+ "full_sync",
66
+ "ping",
67
+ "error",
68
+ "disconnect"
69
+ ],
70
+ "sdkToServer": [
71
+ "ack",
72
+ "full_sync_complete",
73
+ "pong"
74
+ ]
75
+ }
76
+ }
77
+ }
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
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";
5
4
  export default defineChannelPluginEntry({
6
5
  id: "relay",
7
6
  name: "Relay",
8
- description: "Relay channel plugin. Text your OpenClaw like a friend.",
7
+ description: "Native Relay channel plugin for OpenClaw.",
9
8
  plugin: relayChannelPlugin,
10
9
  setRuntime: setRelayRuntime,
11
10
  });
11
+ //# sourceMappingURL=index.js.map
@@ -1,5 +1,4 @@
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
  export default defineSetupPluginEntry(relayChannelPlugin);
4
+ //# sourceMappingURL=setup-entry.js.map
@@ -1,13 +1,9 @@
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 { createAccountListHelpers, resolveMergedAccountConfig, } from "openclaw/plugin-sdk/account-helpers";
5
- import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId, } from "openclaw/plugin-sdk/account-id";
6
3
  import { tryReadSecretFileSync } from "openclaw/plugin-sdk/channel-core";
7
- import { DEFAULT_RELAY_BASE_URL, normalizeRelayBaseUrl } from "./client.js";
8
4
  export const RELAY_TOKEN_ENV_VAR = "RELAY_AGENT_TOKEN";
9
5
  export const RELAY_BASE_URL_ENV_VAR = "RELAY_BASE_URL";
10
- const DEFAULT_POLL_TIMEOUT_SECONDS = 30;
6
+ export const DEFAULT_RELAY_BASE_URL = "https://api.relayapp.im";
11
7
  const { listAccountIds, resolveDefaultAccountId } = createAccountListHelpers("relay", {
12
8
  normalizeAccountId,
13
9
  implicitDefaultAccount: {
@@ -15,53 +11,86 @@ const { listAccountIds, resolveDefaultAccountId } = createAccountListHelpers("re
15
11
  envVars: [RELAY_TOKEN_ENV_VAR],
16
12
  },
17
13
  });
18
- export { listAccountIds as listRelayAccountIds, resolveDefaultAccountId as resolveDefaultRelayAccountId, DEFAULT_ACCOUNT_ID, };
19
- function resolveMergedRelayAccountConfig(cfg, accountId) {
14
+ export { DEFAULT_ACCOUNT_ID, listAccountIds as listRelayAccountIds, resolveDefaultAccountId as resolveDefaultRelayAccountId, };
15
+ function mergedAccountConfig(cfg, accountId) {
20
16
  return resolveMergedAccountConfig({
21
17
  channelConfig: cfg.channels?.relay,
22
18
  accounts: cfg.channels?.relay?.accounts,
23
19
  accountId,
24
- omitKeys: ["defaultAccount"],
20
+ // A channel-level token or tokenFile belongs only to the implicit/default
21
+ // account. Named accounts may inherit non-credential defaults, but must
22
+ // opt into their own credential source.
23
+ omitKeys: accountId === DEFAULT_ACCOUNT_ID
24
+ ? ["defaultAccount"]
25
+ : ["defaultAccount", "token", "tokenFile"],
25
26
  normalizeAccountId,
26
27
  });
27
28
  }
28
29
  function resolveToken(params) {
29
- const direct = params.merged.token?.trim();
30
- if (direct) {
31
- return direct;
32
- }
33
- const fromFile = params.merged.tokenFile
34
- ? tryReadSecretFileSync(params.merged.tokenFile, "relay tokenFile")?.trim()
30
+ const inline = params.config.token?.trim();
31
+ if (inline)
32
+ return inline;
33
+ const fromFile = params.config.tokenFile
34
+ ? tryReadSecretFileSync(params.config.tokenFile, "relay tokenFile")?.trim()
35
35
  : undefined;
36
- if (fromFile) {
36
+ if (fromFile)
37
37
  return fromFile;
38
+ return params.accountId === DEFAULT_ACCOUNT_ID
39
+ ? (params.env[RELAY_TOKEN_ENV_VAR]?.trim() ?? "")
40
+ : "";
41
+ }
42
+ function isLoopbackHostname(hostname) {
43
+ return (hostname === "localhost" ||
44
+ hostname === "127.0.0.1" ||
45
+ hostname === "::1" ||
46
+ hostname.endsWith(".localhost"));
47
+ }
48
+ export function normalizeRelayBaseUrl(value) {
49
+ let parsed;
50
+ try {
51
+ parsed = new URL(value?.trim() || DEFAULT_RELAY_BASE_URL);
52
+ }
53
+ catch {
54
+ throw new Error("relay: baseUrl must be an absolute Relay API origin");
38
55
  }
39
- // Env token applies to the default account only, so named accounts cannot
40
- // silently share one token.
41
- if (params.accountId === DEFAULT_ACCOUNT_ID) {
42
- return params.env[RELAY_TOKEN_ENV_VAR]?.trim() ?? "";
56
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
57
+ throw new Error("relay: baseUrl must not contain credentials, query, or fragment");
43
58
  }
44
- return "";
59
+ if (parsed.pathname !== "/" && parsed.pathname !== "") {
60
+ throw new Error("relay: baseUrl must be an origin without a path");
61
+ }
62
+ if (parsed.protocol !== "https:" &&
63
+ !(parsed.protocol === "http:" && isLoopbackHostname(parsed.hostname))) {
64
+ throw new Error("relay: baseUrl must use HTTPS (HTTP is allowed only for loopback tests)");
65
+ }
66
+ return parsed.origin;
67
+ }
68
+ function normalizeAllowFrom(values) {
69
+ return [
70
+ ...new Set((values ?? [])
71
+ .map((value) => value.trim())
72
+ .filter((value) => value.length > 0)),
73
+ ];
45
74
  }
46
75
  export function resolveRelayAccount(params) {
47
76
  const env = params.env ?? process.env;
48
77
  const accountId = normalizeAccountId(params.accountId);
49
- const merged = resolveMergedRelayAccountConfig(params.cfg, accountId);
78
+ const config = mergedAccountConfig(params.cfg, accountId);
50
79
  const baseEnabled = params.cfg.channels?.relay?.enabled !== false;
51
- const enabled = baseEnabled && merged.enabled !== false;
52
- const token = resolveToken({ merged, accountId, env });
53
- const baseUrl = normalizeRelayBaseUrl(merged.baseUrl?.trim() ||
54
- (accountId === DEFAULT_ACCOUNT_ID ? env[RELAY_BASE_URL_ENV_VAR]?.trim() : undefined) ||
55
- DEFAULT_RELAY_BASE_URL);
56
- const pollTimeoutSeconds = Math.min(Math.max(merged.pollTimeoutSeconds ?? DEFAULT_POLL_TIMEOUT_SECONDS, 1), 30);
80
+ const token = resolveToken({ config, accountId, env });
81
+ const configuredBaseUrl = config.baseUrl?.trim() ||
82
+ (accountId === DEFAULT_ACCOUNT_ID
83
+ ? env[RELAY_BASE_URL_ENV_VAR]?.trim()
84
+ : undefined);
57
85
  return {
58
86
  accountId,
59
- enabled,
60
- configured: Boolean(token),
61
- ...(merged.name?.trim() ? { name: merged.name.trim() } : {}),
87
+ ...(config.name?.trim() ? { name: config.name.trim() } : {}),
88
+ enabled: baseEnabled && config.enabled !== false,
89
+ configured: token.length > 0,
62
90
  token,
63
- baseUrl,
64
- pollTimeoutSeconds,
65
- config: merged,
91
+ baseUrl: normalizeRelayBaseUrl(configuredBaseUrl),
92
+ allowFrom: normalizeAllowFrom(config.allowFrom),
93
+ config,
66
94
  };
67
95
  }
96
+ //# sourceMappingURL=accounts.js.map