@relaymessenger/openclaw-plugin 0.3.4 → 0.4.0-staging.1

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 +162 -123
  3. package/contracts/relay-sdk-0.3.0-staging.5.registry.json +69 -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 +114 -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/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,184 @@
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.
6
+ Source is maintained in
7
+ [`RelayMessenger/Relay-SDK`](https://github.com/RelayMessenger/Relay-SDK/tree/main/packages/openclaw)
8
+ under `packages/openclaw`.
8
9
 
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.
10
+ It connects an OpenClaw gateway to Relay with a Relay Agent Token. Relay
11
+ delivers events over its v1 WebSocket, and the plugin sends replies through
12
+ the Relay v1 REST Message API. The plugin imports `@relaymessenger/sdk`; it
13
+ does not contain a copied Relay client or protocol implementation.
15
14
 
16
15
  ## Install
17
16
 
18
- ```sh
19
- npm install -g @relaymessenger/cli
20
- relaymessenger pair
21
- relaymessenger install-openclaw
17
+ ```bash
18
+ openclaw plugins install @relaymessenger/openclaw-plugin
22
19
  ```
23
20
 
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.
21
+ Configure the default account:
31
22
 
32
- For integration development from this checkout only:
23
+ ```json
24
+ {
25
+ "channels": {
26
+ "relay": {
27
+ "enabled": true,
28
+ "tokenFile": "/run/secrets/relay-agent-token"
29
+ }
30
+ }
31
+ }
32
+ ```
33
33
 
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
34
+ For a single account, `RELAY_AGENT_TOKEN` is also supported. Use
35
+ `RELAY_BASE_URL` only when the Agent Token belongs to a non-production Relay
36
+ environment.
37
+
38
+ One OpenClaw gateway can back multiple Relay Contacts:
39
+
40
+ ```json
41
+ {
42
+ "channels": {
43
+ "relay": {
44
+ "defaultAccount": "default",
45
+ "accounts": {
46
+ "default": {
47
+ "tokenFile": "/run/secrets/relay-default"
48
+ },
49
+ "support": {
50
+ "tokenFile": "/run/secrets/relay-support"
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
39
56
  ```
40
57
 
41
- The installer produces the equivalent Relay-specific configuration:
58
+ Do not reuse one Agent Token in two configured accounts. Channel-level
59
+ `token`, `tokenFile`, and `RELAY_AGENT_TOKEN` credentials belong only to the
60
+ default account. Every named account must set its own inline token or
61
+ `tokenFile`; named accounts never inherit default credentials.
62
+
63
+ ## Relay vocabulary and scope
64
+
65
+ - A **Contact** is a Relay user or agent profile.
66
+ - Each Contact owns a public **Handle**.
67
+ - A **Chat** is direct or group.
68
+ - A **Message** belongs to one Chat and contains ordered parts.
69
+
70
+ The plugin starts OpenClaw turns for every inbound user-authored
71
+ `message.received` event in a direct Chat. In a group Chat, it starts a turn
72
+ only when a text part's canonical `mention` Handle matches the canonical
73
+ `chat.owner_handle`, or when `reply_to.message_id` resolves through Relay to a
74
+ Message authored by this agent in the same Chat. Visible `@handle` text is not
75
+ parsed as a mention. Unmentioned group traffic, agent-authored Messages,
76
+ reactions, typing events, receipts, and membership events are durably accepted
77
+ without starting a turn.
78
+
79
+ Text, link, and media parts are rendered into agent-visible text. Media stays
80
+ a labeled signed URL; the plugin does not upload or send media.
81
+
82
+ Outbound support is deliberately limited to text Messages and Message reply
83
+ references. OpenClaw splits text at Relay's current 10,000-character text-part
84
+ limit. Reactions, edit, unsend, native threads, rich cards, and outbound media
85
+ are not declared.
86
+
87
+ `allowFrom` optionally limits inbound turns to exact Relay Contact IDs or
88
+ Handles:
42
89
 
43
90
  ```json
44
91
  {
45
- "plugins": {
46
- "allow": ["relay"],
47
- "entries": { "relay": { "enabled": true } }
48
- },
49
92
  "channels": {
50
93
  "relay": {
51
- "enabled": true,
52
- "tokenFile": "~/.openclaw/secrets/relay-agent-token",
53
- "baseUrl": "https://api.relayapp.im"
94
+ "tokenFile": "/run/secrets/relay-agent-token",
95
+ "allowFrom": [
96
+ "alice",
97
+ "00000000-0000-7000-8000-000000000001"
98
+ ]
54
99
  }
55
100
  }
56
101
  }
57
102
  ```
58
103
 
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).
104
+ Without `allowFrom`, any user Contact whose Message Relay delivers to this
105
+ agent can start a direct turn, while the group activation rules above still
106
+ apply.
107
+
108
+ ## Durable delivery
109
+
110
+ For every WebSocket event, the plugin:
111
+
112
+ 1. inserts `event_id` into OpenClaw's channel ingress queue, or its private
113
+ SQLite queue when a local/npm-pack install does not have trusted host state;
114
+ 2. returns from the SDK callback only after that transaction commits;
115
+ 3. lets the SDK send the cumulative WebSocket ACK;
116
+ 4. dispatches a stored event through OpenClaw's ingress lifecycle.
117
+
118
+ A replayed `event_id` reaches the existing pending, completed, or failed row
119
+ and does not repeat model or tool work. The SDK owns sequence validation,
120
+ cumulative ACKs, reconnect replay, and JSON heartbeat ping/pong.
121
+
122
+ When Relay requires `full_sync`, the plugin pages through every visible Chat
123
+ and every visible Message, atomically replaces its local snapshot, and returns
124
+ only after the snapshot commits. The SDK then sends
125
+ `full_sync_complete`.
126
+
127
+ Relay Webhooks and the WebSocket are exclusive. Startup checks that the Agent
128
+ has no saved Webhook subscriptions. The SDK also treats WebSocket HTTP `409`
129
+ and close code `4410` as terminal `RelayWebhookConfiguredError` failures.
130
+
131
+ Final OpenClaw replies use the native durable outbound Message adapter. Each
132
+ REST send carries a stable idempotency key derived from OpenClaw's delivery
133
+ queue ID and part index. Unknown-send reconciliation repeats the exact text
134
+ chunks with the same keys, so Relay returns the original Message or commits
135
+ it once.
136
+
137
+ ## Development
138
+
139
+ Use Node.js `22.22.3` or newer. Build and test on Linux:
140
+
141
+ ```bash
142
+ npm install
143
+ npm run validate
144
+ npm run pack:smoke
145
+ npm run gateway:harness
146
+ ```
147
+
148
+ `npm run release:validate` runs all three commands. CI and the guarded manual
149
+ staging workflow use that full sequence. The staging workflow accepts only an
150
+ exact SHA selected from the `staging` branch, the matching
151
+ `x.y.z-staging.n` package version, and the `staging` npm tag. It retains the
152
+ validated tarball and publishes that same digest with npm provenance; its
153
+ publish job is also bound to the `staging` GitHub environment.
154
+
155
+ `gateway:harness` packs the plugin, installs the tarball with OpenClaw
156
+ `2026.8.1`, inspects the managed installation, starts a real OpenClaw gateway,
157
+ connects to a loopback Relay WebSocket, receives one Message, and proves the
158
+ durable ACK and idempotent REST reply.
159
+
160
+ ## Contract lock
161
+
162
+ `contracts/relay-v1.lock.json` records the compatibility boundary used by this
163
+ release: Relay Server
164
+ `9b4d5bb32cc749c6fd271969948c385300d404d6`, OpenAPI SHA-256
165
+ `f62f431fc0daa48500926bf87753f81c3fdda25ab463b130ca97f2896367e0a5`,
166
+ and the exact `@relaymessenger/sdk@0.3.0-staging.5` registry integrity, source
167
+ commit `8570546ce30ab39d702409893006dbccd351f2b4`, REST operations, and WebSocket
168
+ frames consumed by the plugin.
169
+
170
+ Public CI hashes the checked-in `contracts/relay-openapi.yaml` fixture and
171
+ requires the locked digest above. The retained private release receipt also
172
+ sets `RELAY_SERVER_SOURCE_DIR` to an exact checkout of Server `9b4d5bb32cc7`
173
+ and proves that the fixture bytes match that commit before packaging. The
174
+ public Relay-SDK monorepo does not require credentials for the private Server
175
+ source and does not overstate what npm metadata can attest.
176
+
177
+ `contracts/relay-sdk-0.3.0-staging.5.registry.json` records immutable npm
178
+ publication metadata and tarball digests. Validation downloads the registry
179
+ tarball, verifies SHA-1, SHA-256, and SHA-512 integrity, checks the installed
180
+ SDK, and compares its package metadata with exact SDK commit `8570546`.
181
+ The npm metadata has no `gitHead` or attestation for this version, so the
182
+ receipt deliberately does not claim cryptographic source-to-tarball
183
+ provenance. These are local artifact compatibility checks, not a claim of a
184
+ hosted Relay deployment test.
@@ -0,0 +1,69 @@
1
+ {
2
+ "schema": "relay-sdk-registry-receipt/v1",
3
+ "source": {
4
+ "repository": "RelayMessenger/Relay-SDK",
5
+ "commit": "776a9a7873f41c0c9947439c44444674a7d55c5d",
6
+ "packageJsonPath": "packages/sdk/package.json",
7
+ "packageJsonSha256": "2cd3b01e65725ab55d5b520ded6c1235e1f4c74af0ff07478d3ba029d67eda1a",
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.5",
15
+ "publishedAt": "2026-09-01T18:08:51.723Z",
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.3.0-staging.4",
28
+ "staging": "0.3.0-staging.5",
29
+ "mutable": true
30
+ },
31
+ "dist": {
32
+ "tarball": "https://registry.npmjs.org/@relaymessenger/sdk/-/sdk-0.3.0-staging.5.tgz",
33
+ "integrity": "sha512-mVtK8Tv8WVKCkZLzZMbAbazlrf8VrVtu6UcQE4wY685UPLrHqlYm/2XoQstyTP6unik9xiVQC9L5CwR4lxP70w==",
34
+ "shasum": "7549747779b301a0dc60259fa64729e9e7d7db6a",
35
+ "sha256": "ac9336350a82590a0c0d9960ef971858e5ad77413078fe7e82ad5f99a1a3cc9e",
36
+ "bytes": 36545,
37
+ "fileCount": 37,
38
+ "unpackedSize": 167814,
39
+ "signatureMetadata": [
40
+ {
41
+ "keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U",
42
+ "sig": "MEYCIQC/HnUppjEuFAkql79k8scZSDgECdY5clQ19ufRu6WkXQIhAK1iXKe+bjI3+us+VmA2j/KMpXbwkbaYzlKh4yzLwCZ6"
43
+ }
44
+ ],
45
+ "attestations": {
46
+ "url": "https://registry.npmjs.org/-/npm/v1/attestations/@relaymessenger%2fsdk@0.3.0-staging.5",
47
+ "provenance": {
48
+ "predicateType": "https://slsa.dev/provenance/v1"
49
+ }
50
+ }
51
+ },
52
+ "installedArtifact": {
53
+ "packageJsonSha256": "2cd3b01e65725ab55d5b520ded6c1235e1f4c74af0ff07478d3ba029d67eda1a",
54
+ "typesSha256": "efa163145777ad4df93263d87605794ca099dd8db6fe1b36cc71d0873377a653"
55
+ }
56
+ },
57
+ "provenanceBoundary": {
58
+ "registryGitHead": null,
59
+ "registryAttestations": {
60
+ "url": "https://registry.npmjs.org/-/npm/v1/attestations/@relaymessenger%2fsdk@0.3.0-staging.5",
61
+ "provenance": {
62
+ "predicateType": "https://slsa.dev/provenance/v1"
63
+ }
64
+ },
65
+ "attestationEndpoint": "https://registry.npmjs.org/-/npm/v1/attestations/@relaymessenger%2fsdk@0.3.0-staging.5",
66
+ "attestationEndpointObservedStatus": 200,
67
+ "claim": "The exact registry tarball, npm signature, and SLSA provenance metadata are recorded. The package.json bytes match the Relay-SDK 776a9a7 release commit; the receipt does not claim that the registry attestation independently proves the source commit."
68
+ }
69
+ }
@@ -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.5",
11
+ "integrity": "sha512-mVtK8Tv8WVKCkZLzZMbAbazlrf8VrVtu6UcQE4wY685UPLrHqlYm/2XoQstyTP6unik9xiVQC9L5CwR4lxP70w==",
12
+ "operationsSha256": "8429a056605fa9555fcb34067bbb34f7e364ed18c17ec7786b2c1621044012f1",
13
+ "registryReceipt": "contracts/relay-sdk-0.3.0-staging.5.registry.json",
14
+ "source": {
15
+ "repository": "RelayMessenger/Relay-SDK",
16
+ "commit": "776a9a7873f41c0c9947439c44444674a7d55c5d",
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