@wakeshift/openclaw 1.4.37 → 1.4.39

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/dist/plugin.js CHANGED
@@ -32,7 +32,7 @@ import { acceptable, BeaconError, beaconIn, findBeacon, jwkKey, mcpServer, mcpTo
32
32
  import { Conversation, Ledger } from "./deliveries.js";
33
33
  import { agentOf } from "./hooks.js";
34
34
  import { linkKey, PINS_UNREADABLE } from "./store.js";
35
- import { Transport } from "./transport.js";
35
+ import { checkStreamLink, Transport } from "./transport.js";
36
36
  import { CLIENT_HEADER, clientHeader } from "./version.js";
37
37
  /** The connected note's start (harnesses.Connected): agents look for it. */
38
38
  export const CONNECTED = "Wakeshift: connected";
@@ -86,16 +86,7 @@ export function stripBeacon(v) {
86
86
  * ws for http), as the plugin trusts only links from the pinned Wakeshift.
87
87
  */
88
88
  export function linkOnIssuer(link, issuer) {
89
- let l, i;
90
- try {
91
- l = new URL(link);
92
- i = new URL(issuer);
93
- }
94
- catch {
95
- return false;
96
- }
97
- const scheme = { "wss:": "https:", "https:": "https:", "ws:": "http:", "http:": "http:" };
98
- return !!l.host && scheme[l.protocol] !== undefined && scheme[l.protocol] === i.protocol && l.host.toLowerCase() === i.host.toLowerCase();
89
+ return checkStreamLink(link, issuer) === undefined;
99
90
  }
100
91
  /** How often delivery records are pruned while the gateway runs. */
101
92
  export const PRUNE_EVERY = 3600 * 1000;
@@ -300,8 +291,11 @@ export class Wakeshift {
300
291
  return NOTE_TAKEN; // the link it had stays
301
292
  return noteFailed(e.message);
302
293
  }
303
- if (!linkOnIssuer(link, this.o.issuer))
304
- return noteFailed("Wakeshift gave a stream link on another host");
294
+ const refused = checkStreamLink(link, this.o.issuer);
295
+ if (refused) {
296
+ this.o.host.log.warn(`wakeshift (${sessionKey}): ${refused}`);
297
+ return noteFailed(refused);
298
+ }
305
299
  // A new stream ID: what the link it replaces brought isn't this one's to acknowledge.
306
300
  const saved = { conversation, sessionKey, agentId: call.agentId ?? "", link, issuer: this.o.issuer, app: claims.app, stream: randomBytes(8).toString("hex") };
307
301
  this.o.store.saveLink(saved);
@@ -478,6 +472,7 @@ export class Wakeshift {
478
472
  t = new Transport({
479
473
  ...this.o.stream,
480
474
  link: l.link,
475
+ issuer: this.o.issuer,
481
476
  fetch: this.f,
482
477
  log,
483
478
  receive: (ds) => c.receive(ds, stream),
package/dist/transport.js CHANGED
@@ -12,6 +12,7 @@
12
12
  // including another 403 or 404 (a router during a deploy, a firewall),
13
13
  // reconnects with backoff. A connection that ends at once without a
14
14
  // delivery counts as a failure for the backoff.
15
+ import { isIP } from "node:net";
15
16
  import { CLIENT_HEADER, clientHeader } from "./version.js";
16
17
  /** The longest SSE line, and the largest poll answer, read; a longer line is skipped. */
17
18
  export const MAX_LINE = 1 << 20;
@@ -31,6 +32,59 @@ export function parseLink(link) {
31
32
  return undefined;
32
33
  return { base: `${scheme}//${u.host}`, token };
33
34
  }
35
+ /** localhost, or a loopback IP address (127.0.0.0/8, ::1), as Hermes's plugin reads it (ipaddress). */
36
+ function loopback(hostname) {
37
+ const h = hostname.replace(/^\[|\]$/g, "");
38
+ if (h === "localhost")
39
+ return true;
40
+ if (isIP(h) === 4)
41
+ return h.startsWith("127.");
42
+ return isIP(h) === 6 && /^(0{0,4}:){7}0{0,3}1$|^::1$/.test(h);
43
+ }
44
+ /**
45
+ * Undefined if a stream link may be dialed: wss or https on the issuer's
46
+ * host exactly (its port included), or ws or http on it where the issuer
47
+ * is itself http on loopback (development and tests). Otherwise why not,
48
+ * naming only the link's scheme and host (never its token or a user name):
49
+ * another host, a lookalike or subdomain, userinfo, another port, an IP
50
+ * literal for a named issuer, ws or http to an https issuer.
51
+ */
52
+ export function checkStreamLink(link, issuer) {
53
+ let l;
54
+ try {
55
+ l = new URL(link);
56
+ }
57
+ catch {
58
+ return "refused a stream link: it isn't a stream link";
59
+ }
60
+ const scheme = l.protocol.replace(/:$/, "");
61
+ if (!l.host)
62
+ return "refused a stream link: it isn't a stream link";
63
+ if (l.username || l.password)
64
+ return `refused a stream link to ${scheme}://…@${l.host}: it carries a user name or password`;
65
+ const where = `${scheme}://${l.host}`;
66
+ const secure = { wss: true, https: true, ws: false, http: false }[scheme];
67
+ if (secure === undefined)
68
+ return `refused a stream link to ${where}: it isn't wss or https`;
69
+ let i;
70
+ try {
71
+ i = new URL(issuer);
72
+ }
73
+ catch {
74
+ return `refused a stream link to ${where}: no Wakeshift is trusted here`;
75
+ }
76
+ if (!i.host || i.username || i.password)
77
+ return `refused a stream link to ${where}: no Wakeshift is trusted here`;
78
+ if (l.host.toLowerCase() === i.host.toLowerCase()) {
79
+ if (i.protocol === "https:" && secure)
80
+ return undefined;
81
+ if (i.protocol === "http:" && !secure && loopback(i.hostname))
82
+ return undefined;
83
+ }
84
+ return `refused a stream link to ${where}: Wakeshift's stream links go only to ${i.protocol}//${i.host}`;
85
+ }
86
+ class Refused extends Error {
87
+ }
34
88
  export function sameOrigin(a, b) {
35
89
  const x = parseLink(a), y = parseLink(b);
36
90
  return !!x && !!y && x.base === y.base;
@@ -126,6 +180,11 @@ export class Transport {
126
180
  async ack(ids) {
127
181
  if (ids.length === 0 || ids.length > 100)
128
182
  throw new Error("ack: 1 to 100 IDs");
183
+ const why = checkStreamLink(this.link, this.o.issuer);
184
+ if (why) {
185
+ this.log(why);
186
+ return 401;
187
+ }
129
188
  const p = parseLink(this.link);
130
189
  if (!p)
131
190
  return 401;
@@ -187,6 +246,13 @@ export class Transport {
187
246
  catch (e) {
188
247
  if (this.stopped)
189
248
  break;
249
+ if (e instanceof Refused) {
250
+ // Never dialed: its token goes nowhere. The link is kept, in case
251
+ // its Wakeshift is pinned again.
252
+ this.log(e.message);
253
+ result = { why: "refused" };
254
+ break;
255
+ }
190
256
  if (e instanceof Gone) {
191
257
  result = { why: "gone", reason: e.reason };
192
258
  break;
@@ -221,6 +287,9 @@ export class Transport {
221
287
  this.resolveDone(result);
222
288
  }
223
289
  parts() {
290
+ const why = checkStreamLink(this.link, this.o.issuer);
291
+ if (why)
292
+ throw new Refused(why);
224
293
  const p = parseLink(this.link);
225
294
  if (!p)
226
295
  throw new Gone("not a stream link");
@@ -246,7 +315,7 @@ export class Transport {
246
315
  if (typeof next !== "string" || !next)
247
316
  return;
248
317
  const p = parseLink(next);
249
- if (!p || !sameOrigin(next, this.link))
318
+ if (!p || !sameOrigin(next, this.link) || checkStreamLink(next, this.o.issuer))
250
319
  return;
251
320
  const cur = new URL(this.link);
252
321
  const link = `${cur.protocol}//${cur.host}/v1/stream?link=${encodeURIComponent(p.token)}`;
package/dist/version.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // ask the user to update. "dev" here; `npm run pack` stamps the build's
4
4
  // version into dist/version.js (scripts/stamp-version.mjs, from
5
5
  // WAKESHIFT_VERSION).
6
- export const VERSION = "2026.11.08.2132+16d665f";
6
+ export const VERSION = "2026.11.09.1849+33b8864";
7
7
  export const CLIENT_HEADER = "X-Wakeshift-Connector";
8
8
  /** X-Wakeshift-Connector's value: wakeshift-openclaw/<version> openclaw. */
9
9
  export function clientHeader(version = VERSION) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wakeshift/openclaw",
3
- "version": "1.4.37",
3
+ "version": "1.4.39",
4
4
  "description": "Wakeshift for OpenClaw: wakes an OpenClaw session when an app it watches through Wakeshift sends something.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {