@netnodeag/kraftwerk 0.71.0 → 0.71.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.
- package/dist/client/relay.js +2 -1
- package/dist/server/relay.d.ts +2 -0
- package/dist/server/relay.js +20 -0
- package/dist/server/ws.js +3 -0
- package/package.json +1 -1
- package/web/dist-remote/sw.js +1 -1
package/dist/client/relay.js
CHANGED
|
@@ -158,8 +158,9 @@ const OPEN = 1;
|
|
|
158
158
|
export async function connectTunnel(opts) {
|
|
159
159
|
const id = await relayId(opts.publicKey);
|
|
160
160
|
const url = `${opts.relayUrl}${opts.relayUrl.includes("?") ? "&" : "?"}id=${encodeURIComponent(id)}`;
|
|
161
|
-
|
|
161
|
+
// The keys first: once the socket exists, its events may fire at any await — the listeners below must already be there.
|
|
162
162
|
const hs = await clientHandshake(opts.publicKey);
|
|
163
|
+
const ws = opts.connect ? opts.connect(url) : new WebSocket(url);
|
|
163
164
|
let channel = null;
|
|
164
165
|
let closedWhy = "";
|
|
165
166
|
let resolveClosed;
|
package/dist/server/relay.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface RelayOptions {
|
|
|
62
62
|
version: string;
|
|
63
63
|
/** Opens the relay socket (default: Node's global WebSocket) — tests pass their own. */
|
|
64
64
|
connect?: (url: string) => WebSocket;
|
|
65
|
+
/** Give up on a connection the relay has not accepted (`ok`) within this long, and try again (default 15 s). */
|
|
66
|
+
handshakeTimeoutMs?: number;
|
|
65
67
|
log?: (line: string) => void;
|
|
66
68
|
}
|
|
67
69
|
/** Hold the daemon's socket to the relay, reconnecting with backoff, and answer every client behind it. */
|
package/dist/server/relay.js
CHANGED
|
@@ -189,6 +189,22 @@ export async function startRelay(opts) {
|
|
|
189
189
|
return schedule();
|
|
190
190
|
}
|
|
191
191
|
ws = socket;
|
|
192
|
+
// A relay that restarts can leave a new socket hanging half-open, neither opening nor
|
|
193
|
+
// closing: without this, the link would wait for it forever.
|
|
194
|
+
const handshake = setTimeout(() => {
|
|
195
|
+
if (ws !== socket || status.state === "connected")
|
|
196
|
+
return;
|
|
197
|
+
ws = null;
|
|
198
|
+
try {
|
|
199
|
+
socket.close();
|
|
200
|
+
}
|
|
201
|
+
catch { }
|
|
202
|
+
status.state = "error";
|
|
203
|
+
status.error = "the relay did not answer";
|
|
204
|
+
log("relay: no answer from the relay — trying again");
|
|
205
|
+
schedule();
|
|
206
|
+
}, opts.handshakeTimeoutMs ?? 15_000);
|
|
207
|
+
handshake.unref?.();
|
|
192
208
|
socket.addEventListener("open", () => {
|
|
193
209
|
lastHeard = Date.now();
|
|
194
210
|
socket.send(JSON.stringify({ t: "auth", id, key: opts.settings.publicKey, secret: opts.settings.secret, version: opts.version }));
|
|
@@ -203,6 +219,7 @@ export async function startRelay(opts) {
|
|
|
203
219
|
return;
|
|
204
220
|
}
|
|
205
221
|
if (msg.t === "ok") {
|
|
222
|
+
clearTimeout(handshake);
|
|
206
223
|
attempt = 0;
|
|
207
224
|
status.state = "connected";
|
|
208
225
|
status.error = undefined;
|
|
@@ -226,6 +243,7 @@ export async function startRelay(opts) {
|
|
|
226
243
|
dropConn(msg.c);
|
|
227
244
|
});
|
|
228
245
|
socket.addEventListener("close", (ev) => {
|
|
246
|
+
clearTimeout(handshake);
|
|
229
247
|
if (ws !== socket)
|
|
230
248
|
return;
|
|
231
249
|
ws = null;
|
|
@@ -233,6 +251,8 @@ export async function startRelay(opts) {
|
|
|
233
251
|
dropConn(c);
|
|
234
252
|
if (stopped)
|
|
235
253
|
return;
|
|
254
|
+
if (status.state === "connected")
|
|
255
|
+
log("relay: connection lost — reconnecting");
|
|
236
256
|
status.state = "error";
|
|
237
257
|
status.error = ev.code === 4401 ? `the relay refused this machine: ${ev.reason || "wrong secret"}` : ev.reason || "the relay connection closed";
|
|
238
258
|
if (ev.code === 4401)
|
package/dist/server/ws.js
CHANGED
|
@@ -9,6 +9,7 @@ const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
|
9
9
|
/** One message (all its fragments) may not exceed this. */
|
|
10
10
|
const MAX_MESSAGE = 4_000_000;
|
|
11
11
|
const PING_MS = 25_000;
|
|
12
|
+
const CLOSE_GRACE_MS = 2_000;
|
|
12
13
|
/** Complete the upgrade, or answer 400 and return null when the request is not a WebSocket handshake. */
|
|
13
14
|
export function acceptWebSocket(req, socket, head) {
|
|
14
15
|
const key = req.headers["sec-websocket-key"];
|
|
@@ -55,6 +56,8 @@ export function acceptWebSocket(req, socket, head) {
|
|
|
55
56
|
payload.writeUInt16BE(code, 0);
|
|
56
57
|
payload.write(reason, 2);
|
|
57
58
|
socket.end(frame(0x8, payload));
|
|
59
|
+
// The peer should close its side now; one that does not (or never saw the frame) must not keep the connection — or us — waiting.
|
|
60
|
+
setTimeout(() => socket.destroy(), CLOSE_GRACE_MS).unref?.();
|
|
58
61
|
finish();
|
|
59
62
|
};
|
|
60
63
|
const parse = () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netnodeag/kraftwerk",
|
|
3
|
-
"version": "0.71.
|
|
3
|
+
"version": "0.71.1",
|
|
4
4
|
"description": "Open-source agentic workspace for teams — where people and AI agents get work done together: agents (Claude Code, Codex, Pi), skills, shared knowledge, repeatable workflows, and deterministic verification",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
package/web/dist-remote/sw.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){var e=()=>globalThis.crypto.subtle,t=new TextEncoder,n=new TextDecoder;function r(e){let t=``;for(let n=0;n<e.length;n+=32768)t+=String.fromCharCode(...e.subarray(n,n+32768));return btoa(t)}function i(e){let t=atob(e),n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);return n}var a=e=>r(e).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``),o=e=>i(e.replace(/-/g,`+`).replace(/_/g,`/`)+`===`.slice((e.length+3)%4)),s=(...e)=>{let t=new Uint8Array(e.reduce((e,t)=>e+t.length,0)),n=0;for(let r of e)t.set(r,n),n+=r.length;return t};async function c(){let t=await e().generateKey({name:`X25519`},!0,[`deriveBits`]);return{privateKey:t.privateKey,publicKey:new Uint8Array(await e().exportKey(`raw`,t.publicKey))}}var l=t=>{if(t.length!==32)throw Error(`invalid public key`);return e().importKey(`raw`,t,{name:`X25519`},!1,[])},u=async(t,n)=>new Uint8Array(await e().deriveBits({name:`X25519`,public:await l(n)},t,256));async function d(t){return a(new Uint8Array(await e().digest(`SHA-256`,t)).subarray(0,16))}async function f(n,r){let i=await e().importKey(`raw`,n,`HKDF`,!1,[`deriveKey`]),a=n=>e().deriveKey({name:`HKDF`,hash:`SHA-256`,salt:r,info:t.encode(`kraftwerk-relay-v1 ${n}`)},i,{name:`AES-GCM`,length:256},!1,[`encrypt`,`decrypt`]);return{toDaemon:await a(`to-daemon`),toClient:await a(`to-client`)}}var p=e=>{let t=new Uint8Array(12);return new DataView(t.buffer).setBigUint64(4,BigInt(e)),t};function m(a,o){let s=0,c=0,l=Promise.resolve(),u=Promise.resolve();return{seal(n){let i=l.then(async()=>{let i=p(s++);return r(new Uint8Array(await e().encrypt({name:`AES-GCM`,iv:i},a,t.encode(n))))});return l=i.catch(()=>{}),i},open(t){let r=u.then(async()=>{let r=p(c++);try{return n.decode(await e().decrypt({name:`AES-GCM`,iv:r},o,i(t)))}catch{throw Error(`relay message failed to decrypt — tampered, replayed or out of order`)}});return u=r.catch(()=>{}),r}}}async function h(e){let t=await c();return{hello:{t:`hello`,v:1,key:r(t.publicKey)},async finish(n){if(n?.t!==`ready`||typeof n.key!=`string`||typeof n.box!=`string`)throw Error(`not a relay handshake answer`);let r=i(n.key),a=await f(s(await u(t.privateKey,e),await u(t.privateKey,r)),s(t.publicKey,e,r)),o=m(a.toDaemon,a.toClient);if(await o.open(n.box).catch(()=>``)!==`ready`)throw Error(`the machine did not prove its key — wrong pairing link, or not the machine you paired with`);return o}}}var g=1;async function _(e){let t=await d(e.publicKey),a=`${e.relayUrl}${e.relayUrl.includes(`?`)?`&`:`?`}id=${encodeURIComponent(t)}`,o=e.connect?e.connect(a):new WebSocket(a),
|
|
1
|
+
(function(){var e=()=>globalThis.crypto.subtle,t=new TextEncoder,n=new TextDecoder;function r(e){let t=``;for(let n=0;n<e.length;n+=32768)t+=String.fromCharCode(...e.subarray(n,n+32768));return btoa(t)}function i(e){let t=atob(e),n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);return n}var a=e=>r(e).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``),o=e=>i(e.replace(/-/g,`+`).replace(/_/g,`/`)+`===`.slice((e.length+3)%4)),s=(...e)=>{let t=new Uint8Array(e.reduce((e,t)=>e+t.length,0)),n=0;for(let r of e)t.set(r,n),n+=r.length;return t};async function c(){let t=await e().generateKey({name:`X25519`},!0,[`deriveBits`]);return{privateKey:t.privateKey,publicKey:new Uint8Array(await e().exportKey(`raw`,t.publicKey))}}var l=t=>{if(t.length!==32)throw Error(`invalid public key`);return e().importKey(`raw`,t,{name:`X25519`},!1,[])},u=async(t,n)=>new Uint8Array(await e().deriveBits({name:`X25519`,public:await l(n)},t,256));async function d(t){return a(new Uint8Array(await e().digest(`SHA-256`,t)).subarray(0,16))}async function f(n,r){let i=await e().importKey(`raw`,n,`HKDF`,!1,[`deriveKey`]),a=n=>e().deriveKey({name:`HKDF`,hash:`SHA-256`,salt:r,info:t.encode(`kraftwerk-relay-v1 ${n}`)},i,{name:`AES-GCM`,length:256},!1,[`encrypt`,`decrypt`]);return{toDaemon:await a(`to-daemon`),toClient:await a(`to-client`)}}var p=e=>{let t=new Uint8Array(12);return new DataView(t.buffer).setBigUint64(4,BigInt(e)),t};function m(a,o){let s=0,c=0,l=Promise.resolve(),u=Promise.resolve();return{seal(n){let i=l.then(async()=>{let i=p(s++);return r(new Uint8Array(await e().encrypt({name:`AES-GCM`,iv:i},a,t.encode(n))))});return l=i.catch(()=>{}),i},open(t){let r=u.then(async()=>{let r=p(c++);try{return n.decode(await e().decrypt({name:`AES-GCM`,iv:r},o,i(t)))}catch{throw Error(`relay message failed to decrypt — tampered, replayed or out of order`)}});return u=r.catch(()=>{}),r}}}async function h(e){let t=await c();return{hello:{t:`hello`,v:1,key:r(t.publicKey)},async finish(n){if(n?.t!==`ready`||typeof n.key!=`string`||typeof n.box!=`string`)throw Error(`not a relay handshake answer`);let r=i(n.key),a=await f(s(await u(t.privateKey,e),await u(t.privateKey,r)),s(t.publicKey,e,r)),o=m(a.toDaemon,a.toClient);if(await o.open(n.box).catch(()=>``)!==`ready`)throw Error(`the machine did not prove its key — wrong pairing link, or not the machine you paired with`);return o}}}var g=1;async function _(e){let t=await d(e.publicKey),a=`${e.relayUrl}${e.relayUrl.includes(`?`)?`&`:`?`}id=${encodeURIComponent(t)}`,o=await h(e.publicKey),s=e.connect?e.connect(a):new WebSocket(a),c=null,l=``,u,f=new Promise(e=>u=e),p=new Map,m=1,_=e=>{if(!l){l=e||`the connection closed`;for(let e of p.values()){e.reject(Error(l));try{e.stream?.error(Error(l))}catch{}}p.clear(),u(l)}},v=new Promise((t,r)=>{let i=setTimeout(()=>{r(Error(`the machine did not answer through the relay`)),s.close()},e.timeoutMs??15e3);s.addEventListener(`open`,()=>s.send(JSON.stringify(o.hello))),s.addEventListener(`close`,e=>{clearTimeout(i);let t=e.reason||(e.code===4404?`the machine is not connected to the relay`:`the relay closed the connection`);r(Error(t)),_(t)}),s.addEventListener(`error`,()=>{});let a=!0,l=Promise.resolve();s.addEventListener(`message`,e=>{let u=typeof e.data==`string`?e.data:n.decode(e.data);l=l.then(async()=>{if(a){a=!1;try{c=await o.finish(JSON.parse(u)),clearTimeout(i),t(c)}catch(e){clearTimeout(i),r(e),s.close(4400,`handshake failed`)}return}if(!c)return;let e;try{e=JSON.parse(await c.open(u))}catch(e){s.close(4400,`bad message`),_(e.message);return}y(e)})})}),y=e=>{let t=p.get(e.id);if(t){if(e.t===`res`){let n=new ReadableStream({start:e=>{t.stream=e},cancel:()=>{p.delete(e.id),b({t:`abort`,id:e.id})}}),r=e.status,i=r===204||r===304||r>=100&&r<200;t.resolve(new Response(i?null:n,{status:r,headers:e.headers}))}else if(e.t===`data`)try{t.stream?.enqueue(i(e.chunk))}catch{}else if(e.t===`end`){p.delete(e.id);try{t.stream?.close()}catch{}}else if(e.t===`fail`){p.delete(e.id);let n=Error(e.error);t.reject(n);try{t.stream?.error(n)}catch{}}}},b=async e=>{c&&s.readyState===g&&s.send(await c.seal(JSON.stringify(e)))};return await v,{fetch:async(e,t)=>{if(l)throw Error(l);let n=new Request(typeof e==`string`&&e.startsWith(`/`)?`http://relay${e}`:e,t),i=new URL(n.url),a={};n.headers.forEach((e,t)=>a[t]=e);let o=n.method===`GET`||n.method===`HEAD`?void 0:new Uint8Array(await n.arrayBuffer()),s=m++;return new Promise((e,t)=>{p.set(s,{resolve:e,reject:t,signal:n.signal}),n.signal?.addEventListener(`abort`,()=>{let e=p.get(s);if(!e)return;p.delete(s);let t=new DOMException(`aborted`,`AbortError`);e.reject(t);try{e.stream?.error(t)}catch{}b({t:`abort`,id:s})}),b({t:`req`,id:s,method:n.method,url:i.pathname+i.search,headers:a,...o?.length?{body:r(o)}:{}}).catch(e=>{p.delete(s),t(e)})})},close:()=>{s.close(1e3,`bye`),_(`closed`)},closed:f}}var v=`kraftwerk-remote`;function y(){return new Promise((e,t)=>{let n=indexedDB.open(v,1);n.onupgradeneeded=()=>{n.result.createObjectStore(`machines`,{keyPath:`key`}),n.result.createObjectStore(`state`)},n.onsuccess=()=>e(n.result),n.onerror=()=>t(n.error)})}async function b(e,t,n){let r=await y();try{return await new Promise((i,a)=>{let o=n(r.transaction(e,t).objectStore(e));o.onsuccess=()=>i(o.result),o.onerror=()=>a(o.error)})}finally{r.close()}}var x=()=>b(`machines`,`readonly`,e=>e.getAll()),S=e=>b(`machines`,`readwrite`,t=>t.put(e)),C=()=>b(`state`,`readonly`,e=>e.get(`current`)),w=e=>b(`state`,`readwrite`,t=>t.put(e,`current`));async function T(){let[e,t]=await Promise.all([C(),x()]);return t.find(t=>t.key===e)??(t.length===1?t[0]:void 0)}var E=e=>`${e.replace(/^http/,`ws`)}/api/relay/client`,D=self;D.addEventListener(`install`,e=>e.waitUntil(D.skipWaiting())),D.addEventListener(`activate`,e=>e.waitUntil(D.clients.claim()));var O=null;function k(e){if(O?.key===e)return O.t;O?.t.then(e=>e.close()).catch(()=>{});let t=_({relayUrl:E(D.location.origin),publicKey:o(e)}),n={key:e,t};return O=n,t.then(e=>void e.closed.then(()=>O===n&&(O=null)),()=>O===n&&(O=null)),t}var A=(e,t,n)=>new Response(`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${t}</title><body style="font:16px system-ui;padding:24px;max-width:36em;margin:auto"><h1 style="font-size:20px">${t}</h1><p>${n}</p><p><a href="/_kw/">Machines</a> · <a href="" onclick="location.reload();return false">Try again</a></p>`,{status:e,headers:{"content-type":`text/html; charset=utf-8`}});async function j(e,t){let n=new URL(e.url),r=new Headers(e.headers);r.set(`authorization`,`Bearer ${t.token}`),r.set(`host`,n.host);let i=await k(t.key),a=e.method===`GET`||e.method===`HEAD`?void 0:await e.arrayBuffer(),o=await i.fetch(n.pathname+n.search,{method:e.method,headers:r,body:a,signal:e.signal});if(e.method===`POST`&&/^(\/w\/[^/]+)?\/api\/pair$/.test(n.pathname)&&o.ok){let{token:e}=await o.clone().json().catch(()=>({}));e&&await S({...t,token:e,pairedAt:new Date().toISOString()})}return o}D.addEventListener(`fetch`,e=>{let t=new URL(e.request.url);t.origin!==D.location.origin||t.pathname.startsWith(`/_kw/`)||e.respondWith((async()=>{if(e.request.mode===`navigate`&&e.request.method!==`GET`)return A(403,`Refused`,`A form from another page tried to act on your machine.`);let t=await T();if(!t)return e.request.mode===`navigate`?Response.redirect(`/_kw/`,302):new Response(JSON.stringify({error:`no machine paired here`}),{status:503});try{return await j(e.request,t)}catch(n){if(n.name===`AbortError`)throw n;let r=n.message;return e.request.mode===`navigate`?A(502,`${t.label} is not reachable`,`${r}. Is the Mac awake, and is remote access on (<code>kraftwerk remote</code>)?`):new Response(JSON.stringify({error:r}),{status:502,headers:{"content-type":`application/json`}})}})())}),D.addEventListener(`message`,e=>{let t=e.data,n=e.ports[0];if(t?.type!==`pair`||!t.key||!t.code||!n)return;let r=t.key;(async()=>{try{O=null;let e=await(await k(r)).fetch(`/api/pair`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({code:t.code,name:t.name||`phone`})}),i=await e.json().catch(()=>({}));if(!e.ok||!i.token)throw Error(i.error??`pairing failed (${e.status})`);await S({key:r,token:i.token,label:t.label||`your machine`,pairedAt:new Date().toISOString()}),await w(r),n.postMessage({ok:!0})}catch(e){n.postMessage({ok:!1,error:e.message})}})()})})();
|