pikiloom 0.4.26 → 0.4.28
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/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DtjuI8NU.js} +1 -1
- package/dashboard/dist/assets/ConnectionModal-BIx6a5o1.js +1 -0
- package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-BINh_7NB.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-CHpd147b.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C4DbGxbG.js} +1 -1
- package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-D_PUi-5O.js} +1 -1
- package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-BahDEMqf.js} +1 -1
- package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-Cp5VXb_2.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-DZVNXptx.js +1 -0
- package/dashboard/dist/assets/SystemTab-CLznS31f.js +1 -0
- package/dashboard/dist/assets/index-CLGovkkw.js +3 -0
- package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
- package/dashboard/dist/assets/index-DcYOI6rL.js +23 -0
- package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C8DCTJ44.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/acp-client.js +0 -59
- package/dist/agent/artifacts.js +1 -38
- package/dist/agent/auto-update.js +2 -78
- package/dist/agent/await-resume.js +0 -20
- package/dist/agent/cli/auth.js +3 -51
- package/dist/agent/cli/catalog.js +0 -3
- package/dist/agent/cli/detector.js +1 -19
- package/dist/agent/cli/index.js +0 -3
- package/dist/agent/cli/registry.js +0 -25
- package/dist/agent/driver.js +0 -14
- package/dist/agent/drivers/claude-tui.js +2 -769
- package/dist/agent/drivers/claude.js +12 -631
- package/dist/agent/drivers/codex.js +11 -162
- package/dist/agent/drivers/gemini.js +6 -114
- package/dist/agent/drivers/hermes.js +0 -128
- package/dist/agent/goal.js +0 -45
- package/dist/agent/handover.js +1 -46
- package/dist/agent/images.js +7 -133
- package/dist/agent/index.js +1 -29
- package/dist/agent/mcp/bridge.js +1 -197
- package/dist/agent/mcp/extensions.js +5 -84
- package/dist/agent/mcp/oauth.js +2 -69
- package/dist/agent/mcp/registry.js +2 -33
- package/dist/agent/mcp/session-server.js +2 -45
- package/dist/agent/mcp/tools/ask-user.js +0 -6
- package/dist/agent/mcp/tools/await-resume.js +0 -15
- package/dist/agent/mcp/tools/goal.js +0 -25
- package/dist/agent/mcp/tools/types.js +0 -5
- package/dist/agent/mcp/tools/workspace.js +0 -23
- package/dist/agent/npm.js +0 -14
- package/dist/agent/session.js +86 -196
- package/dist/agent/skill-installer.js +3 -70
- package/dist/agent/skills.js +0 -43
- package/dist/agent/stream.js +5 -148
- package/dist/agent/types.js +0 -12
- package/dist/agent/utils.js +2 -120
- package/dist/bot/bot.js +3 -468
- package/dist/bot/command-ui.js +0 -45
- package/dist/bot/commands.js +0 -51
- package/dist/bot/headless-bot.js +0 -21
- package/dist/bot/host.js +1 -5
- package/dist/bot/human-loop.js +0 -8
- package/dist/bot/menu.js +0 -3
- package/dist/bot/orchestration.js +0 -3
- package/dist/bot/render-shared.js +0 -63
- package/dist/bot/session-hub.js +14 -92
- package/dist/bot/session-status.js +1 -5
- package/dist/bot/streaming.js +0 -53
- package/dist/browser-profile.js +0 -51
- package/dist/browser-supervisor.js +0 -75
- package/dist/catalog/cli-tools.js +0 -53
- package/dist/catalog/index.js +0 -18
- package/dist/catalog/local-models.js +0 -18
- package/dist/catalog/mcp-servers.js +0 -35
- package/dist/catalog/skill-repos.js +0 -32
- package/dist/channels/base.js +0 -19
- package/dist/channels/dingtalk/bot.js +0 -3
- package/dist/channels/dingtalk/channel.js +0 -33
- package/dist/channels/discord/bot.js +0 -7
- package/dist/channels/discord/channel.js +0 -22
- package/dist/channels/feishu/bot.js +0 -71
- package/dist/channels/feishu/channel.js +0 -78
- package/dist/channels/feishu/markdown.js +0 -5
- package/dist/channels/feishu/render.js +0 -45
- package/dist/channels/health.js +0 -46
- package/dist/channels/slack/bot.js +0 -7
- package/dist/channels/slack/channel.js +1 -27
- package/dist/channels/states.js +0 -3
- package/dist/channels/telegram/bot.js +0 -60
- package/dist/channels/telegram/channel.js +2 -99
- package/dist/channels/telegram/directory.js +0 -3
- package/dist/channels/telegram/live-preview.js +0 -8
- package/dist/channels/telegram/render.js +0 -30
- package/dist/channels/wecom/bot.js +0 -3
- package/dist/channels/wecom/channel.js +0 -58
- package/dist/channels/weixin/api.js +0 -9
- package/dist/channels/weixin/bot.js +0 -88
- package/dist/channels/weixin/channel.js +0 -6
- package/dist/cli/autostart.js +1 -44
- package/dist/cli/channel-supervisor.js +1 -59
- package/dist/cli/channels.js +0 -6
- package/dist/cli/main.js +1 -145
- package/dist/cli/onboarding.js +0 -6
- package/dist/cli/run.js +0 -11
- package/dist/cli/setup-wizard.js +0 -3
- package/dist/core/config/runtime-config.js +0 -38
- package/dist/core/config/user-config.js +0 -63
- package/dist/core/config/validation.js +0 -9
- package/dist/core/constants.js +0 -268
- package/dist/core/git.js +0 -31
- package/dist/core/legacy-compat.js +1 -28
- package/dist/core/logging.js +0 -3
- package/dist/core/platform.js +0 -28
- package/dist/core/process-control.js +0 -8
- package/dist/core/secrets/index.js +0 -17
- package/dist/core/secrets/inline-seal.js +0 -7
- package/dist/core/secrets/ref.js +0 -19
- package/dist/core/secrets/resolver.js +0 -8
- package/dist/core/secrets/store.js +1 -12
- package/dist/core/utils.js +1 -37
- package/dist/core/version.js +0 -3
- package/dist/dashboard/platform.js +1 -35
- package/dist/dashboard/routes/agents.js +2 -71
- package/dist/dashboard/routes/cli.js +0 -27
- package/dist/dashboard/routes/config.js +0 -54
- package/dist/dashboard/routes/extensions.js +0 -132
- package/dist/dashboard/routes/local-models.js +0 -68
- package/dist/dashboard/routes/models.js +0 -46
- package/dist/dashboard/routes/sessions.js +0 -100
- package/dist/dashboard/runtime.js +1 -58
- package/dist/dashboard/server.js +0 -49
- package/dist/dashboard/session-control.js +2 -69
- package/dist/model/catalog.js +1 -17
- package/dist/model/index.js +0 -15
- package/dist/model/injector.js +1 -181
- package/dist/model/provider-models.js +1 -48
- package/dist/model/responses-bridge.js +3 -45
- package/dist/model/store.js +0 -17
- package/dist/model/types.js +0 -12
- package/dist/model/validation.js +1 -18
- package/dist/pikichannel/adapter-pikiloom.js +0 -31
- package/dist/pikichannel/code.js +0 -12
- package/dist/pikichannel/codec.js +0 -14
- package/dist/pikichannel/host.js +4 -33
- package/dist/pikichannel/protocol.js +0 -52
- package/dist/pikichannel/rendezvous-broker.js +5 -26
- package/dist/pikichannel/rendezvous-host.js +3 -16
- package/dist/pikichannel/server.js +6 -55
- package/dist/pikichannel/transport.js +0 -25
- package/dist/pikichannel/transports/webrtc-host.js +0 -18
- package/dist/pikichannel/transports/webrtc-shared.js +4 -34
- package/dist/pikichannel/transports/websocket-host.js +1 -14
- package/dist/pikichannel/turn.js +2 -69
- package/dist/pikichannel/web/sdk.js +14 -71
- package/package.json +1 -1
- package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
- package/dashboard/dist/assets/SessionPanel-_0dLWvEb.js +0 -1
- package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
- package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
- package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
- package/dashboard/dist/assets/index-CyCspgtc.css +0 -1
|
@@ -1,31 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pikichannel/rendezvous-broker.ts — the public signaling broker (NAT traversal).
|
|
3
|
-
*
|
|
4
|
-
* This is the "route, don't relay data" rendezvous from the original design. A
|
|
5
|
-
* pikiloom host that sits behind NAT dials OUT to a broker and registers a
|
|
6
|
-
* NodeID; a client dials the same broker by NodeID. The broker pairs them and
|
|
7
|
-
* relays ONLY signaling envelopes (SDP offer/answer + ICE candidates) — it never
|
|
8
|
-
* sees datachannel data, which flows directly P2P (DTLS-encrypted) once ICE
|
|
9
|
-
* hole-punches through. It is dependency-light (no werift) so any pikiloom can
|
|
10
|
-
* act as a broker for reachable peers, or it can be run standalone.
|
|
11
|
-
*
|
|
12
|
-
* Wire protocol (JSON over the rendezvous WebSocket):
|
|
13
|
-
* host→ {t:'register', nodeId} ← register to receive dials
|
|
14
|
-
* ← {t:'registered', nodeId} | {t:'error', message}
|
|
15
|
-
* client→{t:'dial', nodeId} ← reach a registered host
|
|
16
|
-
* ← {t:'dialed', sessionId} | {t:'error', message}
|
|
17
|
-
* broker→host {t:'open', sessionId} ← a client dialed; prepare answerer
|
|
18
|
-
* peer↔ {t:'signal', sessionId, data} ← relayed to the other end verbatim
|
|
19
|
-
* broker→peer {t:'close', sessionId} ← the other end went away
|
|
20
|
-
*/
|
|
21
1
|
import crypto from 'node:crypto';
|
|
22
2
|
import { WebSocketServer } from 'ws';
|
|
23
3
|
import { writeScopedLog } from '../core/logging.js';
|
|
24
4
|
const rlog = (msg) => writeScopedLog('pikichannel', `[rendezvous] ${msg}`, { level: 'info' });
|
|
25
5
|
export class RendezvousBroker {
|
|
26
6
|
wss = new WebSocketServer({ noServer: true });
|
|
27
|
-
hosts = new Map();
|
|
28
|
-
sessions = new Map();
|
|
7
|
+
hosts = new Map();
|
|
8
|
+
sessions = new Map();
|
|
29
9
|
constructor() {
|
|
30
10
|
this.wss.on('connection', (ws) => this.onConnection(ws));
|
|
31
11
|
}
|
|
@@ -39,12 +19,12 @@ export class RendezvousBroker {
|
|
|
39
19
|
try {
|
|
40
20
|
ws.send(JSON.stringify(msg));
|
|
41
21
|
}
|
|
42
|
-
catch {
|
|
22
|
+
catch { }
|
|
43
23
|
}
|
|
44
24
|
}
|
|
45
25
|
onConnection(ws) {
|
|
46
|
-
let myNodeId = null;
|
|
47
|
-
const mySessions = new Set();
|
|
26
|
+
let myNodeId = null;
|
|
27
|
+
const mySessions = new Set();
|
|
48
28
|
ws.on('message', (raw) => {
|
|
49
29
|
let m;
|
|
50
30
|
try {
|
|
@@ -60,7 +40,6 @@ export class RendezvousBroker {
|
|
|
60
40
|
this.send(ws, { t: 'error', message: 'nodeId required' });
|
|
61
41
|
return;
|
|
62
42
|
}
|
|
63
|
-
// Last registration wins (a host reconnecting replaces its stale socket).
|
|
64
43
|
this.hosts.set(nodeId, ws);
|
|
65
44
|
myNodeId = nodeId;
|
|
66
45
|
this.send(ws, { t: 'registered', nodeId });
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pikichannel/rendezvous-host.ts — the host's OUTBOUND registrar (NAT side).
|
|
3
|
-
*
|
|
4
|
-
* A pikiloom host behind NAT can't be dialed directly, so it dials OUT to a
|
|
5
|
-
* reachable {@link RendezvousBroker} and registers its NodeID. For each client
|
|
6
|
-
* that dials that NodeID, the broker relays signaling and this client drives a
|
|
7
|
-
* shared werift answerer — the resulting datachannel is a normal pikichannel
|
|
8
|
-
* connection, indistinguishable downstream from a direct or local one. The
|
|
9
|
-
* registration auto-reconnects so the host stays reachable.
|
|
10
|
-
*
|
|
11
|
-
* Imports werift (via webrtc-shared), so the server wiring loads it only inside
|
|
12
|
-
* the guarded WebRTC dynamic import.
|
|
13
|
-
*/
|
|
14
1
|
import WebSocket from 'ws';
|
|
15
2
|
import { writeScopedLog } from '../core/logging.js';
|
|
16
3
|
import { createAnswerer } from './transports/webrtc-shared.js';
|
|
@@ -37,7 +24,7 @@ export class RendezvousHostClient {
|
|
|
37
24
|
try {
|
|
38
25
|
ws?.close();
|
|
39
26
|
}
|
|
40
|
-
catch {
|
|
27
|
+
catch { }
|
|
41
28
|
for (const a of this.answerers.values())
|
|
42
29
|
a.close();
|
|
43
30
|
this.answerers.clear();
|
|
@@ -107,7 +94,7 @@ export class RendezvousHostClient {
|
|
|
107
94
|
}
|
|
108
95
|
case 'signal': {
|
|
109
96
|
if (this.adopted.has(m.sessionId))
|
|
110
|
-
return;
|
|
97
|
+
return;
|
|
111
98
|
const a = this.ensureAnswerer(m.sessionId);
|
|
112
99
|
void a.onSignal(m.data);
|
|
113
100
|
return;
|
|
@@ -123,7 +110,7 @@ export class RendezvousHostClient {
|
|
|
123
110
|
log: rlog,
|
|
124
111
|
onConnection: (conn) => {
|
|
125
112
|
this.adopted.add(sessionId);
|
|
126
|
-
this.answerers.delete(sessionId);
|
|
113
|
+
this.answerers.delete(sessionId);
|
|
127
114
|
this.onConnection(conn);
|
|
128
115
|
},
|
|
129
116
|
sendSignal: (data) => {
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pikichannel/server.ts — wires pikichannel into the pikiloom dashboard server.
|
|
3
|
-
*
|
|
4
|
-
* Responsibilities:
|
|
5
|
-
* - build the host over the pikiloom SessionSource,
|
|
6
|
-
* - stand up BOTH transport bindings (WebSocket always; WebRTC via a guarded
|
|
7
|
-
* dynamic import so a missing/broken werift never blocks startup),
|
|
8
|
-
* - register the reference web routes (demo page + browser SDK + status),
|
|
9
|
-
* - expose `attachUpgrade(server)` so the dashboard's HTTP server can route
|
|
10
|
-
* `/pikichannel/*` upgrade requests to the right binding.
|
|
11
|
-
*
|
|
12
|
-
* Both bindings funnel into the SAME `host.handleConnection`, so a session
|
|
13
|
-
* behaves identically regardless of transport — that is the pluggability the
|
|
14
|
-
* two bindings exist to demonstrate.
|
|
15
|
-
*/
|
|
16
1
|
import crypto from 'node:crypto';
|
|
17
2
|
import fs from 'node:fs';
|
|
18
3
|
import path from 'node:path';
|
|
@@ -24,14 +9,12 @@ import { prewarmTurn, turnStatus } from './turn.js';
|
|
|
24
9
|
import { PikiloomSessionSource } from './adapter-pikiloom.js';
|
|
25
10
|
import { WebSocketTransport } from './transports/websocket-host.js';
|
|
26
11
|
import { RendezvousBroker } from './rendezvous-broker.js';
|
|
27
|
-
/** A remote address label is loopback when it is the local host. */
|
|
28
12
|
function isLoopback(remote) {
|
|
29
13
|
if (!remote)
|
|
30
14
|
return false;
|
|
31
15
|
const r = remote.toLowerCase();
|
|
32
16
|
return r.startsWith('127.') || r.includes('127.0.0.1') || r === '::1' || r.startsWith('::1:') || r.startsWith('[::1]') || r.includes('::ffff:127.');
|
|
33
17
|
}
|
|
34
|
-
/** Constant-time token comparison. */
|
|
35
18
|
function tokenMatches(presented, expected) {
|
|
36
19
|
if (!presented || !expected)
|
|
37
20
|
return false;
|
|
@@ -45,13 +28,7 @@ function readWebAsset(name) {
|
|
|
45
28
|
}
|
|
46
29
|
export async function mountPikichannel(app) {
|
|
47
30
|
const log = (msg) => writeScopedLog('pikichannel', msg, { level: 'info' });
|
|
48
|
-
// -- Access token: provision on first run, persist to ~/.pikiloom/setting.json.
|
|
49
|
-
// Loopback peers (the local dashboard / same-machine demo) are exempt;
|
|
50
|
-
// remote peers must present this token in `hello`. `strict` requires the
|
|
51
|
-
// token even from loopback (defense-in-depth / testing). --
|
|
52
31
|
const cfg = loadUserConfig();
|
|
53
|
-
// Env overrides (docker / headless): PIKICHANNEL_TOKEN pins the token without
|
|
54
|
-
// persisting; PIKICHANNEL_STRICT=1 forces token even from loopback.
|
|
55
32
|
const envToken = String(process.env.PIKICHANNEL_TOKEN || '').trim();
|
|
56
33
|
let token = envToken || String(cfg.pikichannelToken || '').trim();
|
|
57
34
|
if (!token) {
|
|
@@ -70,20 +47,16 @@ export async function mountPikichannel(app) {
|
|
|
70
47
|
return true;
|
|
71
48
|
return tokenMatches(presented, token);
|
|
72
49
|
};
|
|
73
|
-
// -- NodeID: the stable address a remote client dials over the rendezvous. --
|
|
74
50
|
let nodeId = String(cfg.pikichannelNodeId || '').trim();
|
|
75
51
|
if (!nodeId) {
|
|
76
52
|
nodeId = crypto.randomBytes(8).toString('hex');
|
|
77
53
|
try {
|
|
78
54
|
updateUserConfig({ pikichannelNodeId: nodeId });
|
|
79
55
|
}
|
|
80
|
-
catch {
|
|
56
|
+
catch { }
|
|
81
57
|
}
|
|
82
58
|
const rendezvousUrl = String(process.env.PIKICHANNEL_RENDEZVOUS || cfg.pikichannelRendezvous || '').trim();
|
|
83
59
|
let currentPublicHost = String(process.env.PIKICHANNEL_PUBLIC_HOST || cfg.pikichannelPublicHost || '').trim();
|
|
84
|
-
// Control-plane tunnel forwarder: replay a tunneled request against the SAME
|
|
85
|
-
// Hono router that serves /api/* locally — management logic stays single-sourced.
|
|
86
|
-
// Text responses ride as utf8; anything else (attachment bytes) as base64.
|
|
87
60
|
const forward = async (req) => {
|
|
88
61
|
const init = { method: req.method || 'GET', headers: req.headers };
|
|
89
62
|
if (req.body != null && req.method && req.method !== 'GET' && req.method !== 'HEAD') {
|
|
@@ -102,10 +75,8 @@ export async function mountPikichannel(app) {
|
|
|
102
75
|
const source = new PikiloomSessionSource(forward);
|
|
103
76
|
const host = new PikichannelHost(source, authenticate, log);
|
|
104
77
|
host.start();
|
|
105
|
-
// -- WebSocket binding (always available) --
|
|
106
78
|
const wsTransport = new WebSocketTransport();
|
|
107
79
|
wsTransport.start((conn) => host.handleConnection(conn));
|
|
108
|
-
// -- WebRTC binding (guarded; degrades gracefully) --
|
|
109
80
|
let rtcTransport = null;
|
|
110
81
|
let webrtcError = null;
|
|
111
82
|
try {
|
|
@@ -119,18 +90,8 @@ export async function mountPikichannel(app) {
|
|
|
119
90
|
webrtcError = err?.message || String(err);
|
|
120
91
|
log(`webrtc transport unavailable: ${webrtcError}`);
|
|
121
92
|
}
|
|
122
|
-
// Pre-mint Cloudflare TURN credentials (if configured) so the first WebRTC
|
|
123
|
-
// connection already has a relay to fall back to. Best-effort, non-blocking;
|
|
124
|
-
// no creds → no-op, and the answerer resolves STUN until/unless minting lands.
|
|
125
93
|
void prewarmTurn();
|
|
126
|
-
// -- Rendezvous broker (NAT traversal): always mounted (no werift dep), so any
|
|
127
|
-
// reachable pikiloom can broker signaling for NAT'd peers. Relays signaling
|
|
128
|
-
// only; data stays P2P. --
|
|
129
94
|
const broker = new RendezvousBroker();
|
|
130
|
-
// -- Rendezvous registrar: when enabled, this host dials OUT to a broker and
|
|
131
|
-
// registers its NodeID so remote clients can reach it through NAT. Can be
|
|
132
|
-
// toggled at RUNTIME (one-click from the dashboard) — no restart. Needs
|
|
133
|
-
// werift, so the host-client class is loaded only when WebRTC is available. --
|
|
134
95
|
let rendezvousHost = null;
|
|
135
96
|
let HostClientCtor = null;
|
|
136
97
|
let currentRendezvous = '';
|
|
@@ -160,14 +121,13 @@ export async function mountPikichannel(app) {
|
|
|
160
121
|
try {
|
|
161
122
|
updateUserConfig({ pikichannelRendezvous: next });
|
|
162
123
|
}
|
|
163
|
-
catch {
|
|
124
|
+
catch { }
|
|
164
125
|
}
|
|
165
126
|
log(`rendezvous ${next ? `enabled url=${next} nodeId=${nodeId}` : 'disabled'}`);
|
|
166
127
|
return { ok: true };
|
|
167
128
|
}
|
|
168
129
|
if (rendezvousUrl)
|
|
169
|
-
await setRendezvous(rendezvousUrl, false);
|
|
170
|
-
// -- Web assets (read once; the demo + SDK double as the OSS reference client) --
|
|
130
|
+
await setRendezvous(rendezvousUrl, false);
|
|
171
131
|
let sdkJs = '';
|
|
172
132
|
let demoHtml = '';
|
|
173
133
|
try {
|
|
@@ -180,9 +140,6 @@ export async function mountPikichannel(app) {
|
|
|
180
140
|
const serveDemo = (c) => { c.header('Cache-Control', 'no-cache'); return c.html(demoHtml || '<h1>pikichannel demo asset missing</h1>'); };
|
|
181
141
|
app.get('/pikichannel', serveDemo);
|
|
182
142
|
app.get('/pikichannel/', serveDemo);
|
|
183
|
-
// QR of a connection code (or any short string) as SVG — reuses the repo's
|
|
184
|
-
// `qrcode` dep so the browser SDK needs no QR library. Dynamic-imported to
|
|
185
|
-
// keep startup lean.
|
|
186
143
|
app.get('/pikichannel/qr', async (c) => {
|
|
187
144
|
const data = String(c.req.query('data') || '').slice(0, 2048);
|
|
188
145
|
if (!data)
|
|
@@ -204,9 +161,6 @@ export async function mountPikichannel(app) {
|
|
|
204
161
|
return c.body(sdkJs || '// pikichannel sdk asset missing');
|
|
205
162
|
});
|
|
206
163
|
app.get('/pikichannel/status', (c) => c.json(statusObj()));
|
|
207
|
-
// -- Pairing: hand the access token to a trusted LOCAL caller only (the
|
|
208
|
-
// dashboard / CLI on this machine). Remote callers get 403 — they must be
|
|
209
|
-
// paired out-of-band (token shown in the local dashboard, scanned as a QR). --
|
|
210
164
|
app.get('/pikichannel/pair', (c) => {
|
|
211
165
|
let remote;
|
|
212
166
|
try {
|
|
@@ -219,9 +173,6 @@ export async function mountPikichannel(app) {
|
|
|
219
173
|
return c.json({ ok: false, error: 'pairing is only available from localhost' }, 403);
|
|
220
174
|
return c.json({ ok: true, token, strict, nodeId, rendezvous: currentRendezvous || null, publicHost: currentPublicHost || null, registered: !!rendezvousHost, hint: 'paste the connection code into a client' });
|
|
221
175
|
});
|
|
222
|
-
// -- Remote-access settings: configure how others reach THIS host — a public
|
|
223
|
-
// address (direct) and/or internet穿透 (rendezvous). Runtime, one-click,
|
|
224
|
-
// localhost-only (it changes who can reach this machine). --
|
|
225
176
|
app.post('/pikichannel/remote', async (c) => {
|
|
226
177
|
let remote;
|
|
227
178
|
try {
|
|
@@ -238,7 +189,7 @@ export async function mountPikichannel(app) {
|
|
|
238
189
|
try {
|
|
239
190
|
updateUserConfig({ pikichannelPublicHost: currentPublicHost });
|
|
240
191
|
}
|
|
241
|
-
catch {
|
|
192
|
+
catch { }
|
|
242
193
|
}
|
|
243
194
|
let r = { ok: true };
|
|
244
195
|
if ('enabled' in body || 'rendezvous' in body) {
|
|
@@ -268,7 +219,7 @@ export async function mountPikichannel(app) {
|
|
|
268
219
|
handleUpgrade(req, socket, head) {
|
|
269
220
|
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
270
221
|
if (!url.pathname.startsWith('/pikichannel/'))
|
|
271
|
-
return false;
|
|
222
|
+
return false;
|
|
272
223
|
if (url.pathname === '/pikichannel/ws') {
|
|
273
224
|
wsTransport.handleUpgrade(req, socket, head);
|
|
274
225
|
return true;
|
|
@@ -281,7 +232,7 @@ export async function mountPikichannel(app) {
|
|
|
281
232
|
broker.handleUpgrade(req, socket, head);
|
|
282
233
|
return true;
|
|
283
234
|
}
|
|
284
|
-
socket.destroy();
|
|
235
|
+
socket.destroy();
|
|
285
236
|
return true;
|
|
286
237
|
},
|
|
287
238
|
status: statusObj,
|
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pikichannel/transport.ts — the L1 transport abstraction (host side).
|
|
3
|
-
*
|
|
4
|
-
* This is the seam that makes the two delivery mechanisms — WebSocket and
|
|
5
|
-
* WebRTC datachannel — interchangeable. A {@link ChannelTransport} accepts peer
|
|
6
|
-
* connections and surfaces each as a {@link ChannelConnection}: a reliable,
|
|
7
|
-
* ordered, bidirectional frame pipe. The host ({@link PikichannelHost}) drives
|
|
8
|
-
* the L2 protocol over a ChannelConnection and is wholly blind to whether the
|
|
9
|
-
* bytes travel over a TCP WebSocket or an SCTP datachannel — flip the binding,
|
|
10
|
-
* the session behaves identically. That blindness IS the comparability the two
|
|
11
|
-
* bindings are meant to demonstrate.
|
|
12
|
-
*
|
|
13
|
-
* Contract every binding must honour:
|
|
14
|
-
* - reliable + ordered delivery of whole frames (no partial frames),
|
|
15
|
-
* - bidirectional,
|
|
16
|
-
* - a single 'close' notification, exactly once,
|
|
17
|
-
* - `send()` after close is a silent no-op (never throws).
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* Minimal base that implements the callback bookkeeping every binding repeats,
|
|
21
|
-
* so concrete connections only push frames in and call `emitClose()` once.
|
|
22
|
-
*/
|
|
23
1
|
export class BaseConnection {
|
|
24
2
|
remote;
|
|
25
3
|
messageCb = null;
|
|
@@ -28,16 +6,13 @@ export class BaseConnection {
|
|
|
28
6
|
onMessage(cb) { this.messageCb = cb; }
|
|
29
7
|
onClose(cb) {
|
|
30
8
|
this.closeCb = cb;
|
|
31
|
-
// If close already happened before the handler was attached, fire now.
|
|
32
9
|
if (this.closed)
|
|
33
10
|
cb();
|
|
34
11
|
}
|
|
35
|
-
/** Concrete bindings call this when a frame arrives. */
|
|
36
12
|
emitMessage(frame) {
|
|
37
13
|
if (this.messageCb)
|
|
38
14
|
this.messageCb(frame);
|
|
39
15
|
}
|
|
40
|
-
/** Concrete bindings call this exactly once when the pipe closes. */
|
|
41
16
|
emitClose() {
|
|
42
17
|
if (this.closed)
|
|
43
18
|
return;
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pikichannel/transports/webrtc-host.ts — the DIRECT WebRTC binding (host).
|
|
3
|
-
*
|
|
4
|
-
* SDP/ICE over a same-origin `/pikichannel/signal` WebSocket, for clients that
|
|
5
|
-
* can already reach the host (localhost / LAN / public IP). The cross-NAT path —
|
|
6
|
-
* where the host dials OUT to a public broker — lives in rendezvous.ts. Both
|
|
7
|
-
* paths share the same werift answerer (webrtc-shared.ts), so a connection from
|
|
8
|
-
* either is identical downstream.
|
|
9
|
-
*
|
|
10
|
-
* werift is imported (transitively) here; the server wiring loads this module via
|
|
11
|
-
* a guarded dynamic import, so a missing/broken werift disables WebRTC while the
|
|
12
|
-
* WebSocket binding and the dashboard keep working.
|
|
13
|
-
*/
|
|
14
1
|
import { WebSocketServer } from 'ws';
|
|
15
2
|
import { writeScopedLog } from '../../core/logging.js';
|
|
16
3
|
import { createAnswerer } from './webrtc-shared.js';
|
|
@@ -23,14 +10,12 @@ export class WebRTCTransport {
|
|
|
23
10
|
this.onConn = onConnection;
|
|
24
11
|
this.wss.on('connection', (ws, req) => this.runSignaling(ws, req));
|
|
25
12
|
}
|
|
26
|
-
/** Server wiring routes a matching signaling upgrade request here. */
|
|
27
13
|
handleUpgrade(req, socket, head) {
|
|
28
14
|
this.wss.handleUpgrade(req, socket, head, (ws) => this.wss.emit('connection', ws, req));
|
|
29
15
|
}
|
|
30
16
|
stop() {
|
|
31
17
|
this.wss.close();
|
|
32
18
|
}
|
|
33
|
-
/** One signaling WebSocket brokers exactly one peer connection. */
|
|
34
19
|
runSignaling(ws, req) {
|
|
35
20
|
const remote = req.socket.remoteAddress ? `${req.socket.remoteAddress}:${req.socket.remotePort}` : undefined;
|
|
36
21
|
const answerer = createAnswerer({
|
|
@@ -50,9 +35,6 @@ export class WebRTCTransport {
|
|
|
50
35
|
}
|
|
51
36
|
void answerer.onSignal(msg);
|
|
52
37
|
});
|
|
53
|
-
// The signaling socket is only needed for the handshake; once the datachannel
|
|
54
|
-
// carries traffic it can close. If it drops before a channel was adopted,
|
|
55
|
-
// tear the half-open peer down.
|
|
56
38
|
const cleanup = () => { if (!answerer.isAdopted())
|
|
57
39
|
answerer.close(); };
|
|
58
40
|
ws.on('close', cleanup);
|
|
@@ -1,29 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pikichannel/transports/webrtc-shared.ts — shared werift answerer + connection.
|
|
3
|
-
*
|
|
4
|
-
* The host is always the WebRTC *answerer* (the browser/SDK creates the offer +
|
|
5
|
-
* datachannel). The same answerer logic drives two signaling paths:
|
|
6
|
-
* - direct (webrtc-host.ts): SDP/ICE over a same-origin `/pikichannel/signal`
|
|
7
|
-
* WebSocket — for clients that can already reach the host.
|
|
8
|
-
* - rendezvous (rendezvous.ts): SDP/ICE relayed through a public broker both
|
|
9
|
-
* peers dial OUTBOUND — the NAT-traversal path.
|
|
10
|
-
*
|
|
11
|
-
* Once the datachannel opens the bytes are pure P2P (DTLS-encrypted); signaling
|
|
12
|
-
* only brokers the handshake. `getIceServers()` is the STUN/TURN config hook —
|
|
13
|
-
* see turn.ts: STUN by default, Cloudflare-minted short-lived TURN (or a manual
|
|
14
|
-
* PIKICHANNEL_ICE_SERVERS override) when configured, for symmetric-NAT / CGNAT
|
|
15
|
-
* relay fallback.
|
|
16
|
-
*/
|
|
17
1
|
import { RTCPeerConnection } from 'werift';
|
|
18
2
|
import { BaseConnection } from '../transport.js';
|
|
19
3
|
import { getCachedIceServers, toWeriftIceServers } from '../turn.js';
|
|
20
4
|
let connCounter = 0;
|
|
21
|
-
/**
|
|
22
|
-
* ICE servers for the werift answerer, reduced to exactly what werift consumes
|
|
23
|
-
* (one STUN + one UDP TURN — see {@link toWeriftIceServers}). The resolution
|
|
24
|
-
* policy lives in turn.ts: a manual PIKICHANNEL_ICE_SERVERS override, else cached
|
|
25
|
-
* Cloudflare-minted short-lived credentials, else plain STUN.
|
|
26
|
-
*/
|
|
27
5
|
export function getIceServers() {
|
|
28
6
|
return toWeriftIceServers(getCachedIceServers());
|
|
29
7
|
}
|
|
@@ -39,7 +17,6 @@ function coerceFrame(data) {
|
|
|
39
17
|
return String(data);
|
|
40
18
|
}
|
|
41
19
|
}
|
|
42
|
-
/** A pikichannel connection riding an SCTP datachannel. */
|
|
43
20
|
export class RtcConnection extends BaseConnection {
|
|
44
21
|
channel;
|
|
45
22
|
pc;
|
|
@@ -63,7 +40,7 @@ export class RtcConnection extends BaseConnection {
|
|
|
63
40
|
try {
|
|
64
41
|
this.channel.send(frame);
|
|
65
42
|
}
|
|
66
|
-
catch {
|
|
43
|
+
catch { }
|
|
67
44
|
}
|
|
68
45
|
}
|
|
69
46
|
isOpen() { return this.channel.readyState === 'open'; }
|
|
@@ -71,21 +48,14 @@ export class RtcConnection extends BaseConnection {
|
|
|
71
48
|
try {
|
|
72
49
|
this.channel.close();
|
|
73
50
|
}
|
|
74
|
-
catch {
|
|
51
|
+
catch { }
|
|
75
52
|
try {
|
|
76
53
|
this.pc.close();
|
|
77
54
|
}
|
|
78
|
-
catch {
|
|
55
|
+
catch { }
|
|
79
56
|
this.emitClose();
|
|
80
57
|
}
|
|
81
58
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Build a werift answerer. The caller wires `sendSignal` to whatever signaling
|
|
84
|
-
* channel it has (a WS, or the rendezvous), feeds inbound signals to `onSignal`,
|
|
85
|
-
* and receives the live {@link ChannelConnection} via `onConnection` once the
|
|
86
|
-
* datachannel opens. Trickle ICE: the answer is sent immediately, candidates
|
|
87
|
-
* follow as they are gathered.
|
|
88
|
-
*/
|
|
89
59
|
export function createAnswerer(opts) {
|
|
90
60
|
const pc = new RTCPeerConnection({ iceServers: getIceServers() });
|
|
91
61
|
let adopted = false;
|
|
@@ -121,6 +91,6 @@ export function createAnswerer(opts) {
|
|
|
121
91
|
close() { try {
|
|
122
92
|
pc.close();
|
|
123
93
|
}
|
|
124
|
-
catch {
|
|
94
|
+
catch { } },
|
|
125
95
|
};
|
|
126
96
|
}
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pikichannel/transports/websocket-host.ts — the WebSocket L1 binding (host).
|
|
3
|
-
*
|
|
4
|
-
* The baseline transport: protocol frames ride a raw WebSocket. Same-machine /
|
|
5
|
-
* LAN clients (the Dashboard case) want exactly this — there is no NAT to
|
|
6
|
-
* traverse, so a TCP WebSocket is the optimal path. It is the control against
|
|
7
|
-
* which the WebRTC binding is compared.
|
|
8
|
-
*
|
|
9
|
-
* The server wiring owns upgrade routing and feeds matching upgrades to
|
|
10
|
-
* `handleUpgrade`; this class only manages the noServer WebSocketServer and
|
|
11
|
-
* wraps each socket as a {@link ChannelConnection}.
|
|
12
|
-
*/
|
|
13
1
|
import { WebSocketServer } from 'ws';
|
|
14
2
|
import { BaseConnection } from '../transport.js';
|
|
15
3
|
const WS_KEEPALIVE_MS = 25_000;
|
|
@@ -48,7 +36,7 @@ class WsConnection extends BaseConnection {
|
|
|
48
36
|
try {
|
|
49
37
|
this.ws.close();
|
|
50
38
|
}
|
|
51
|
-
catch {
|
|
39
|
+
catch { }
|
|
52
40
|
this.emitClose();
|
|
53
41
|
}
|
|
54
42
|
}
|
|
@@ -68,7 +56,6 @@ export class WebSocketTransport {
|
|
|
68
56
|
this.onConn?.(new WsConnection(ws, remote));
|
|
69
57
|
});
|
|
70
58
|
}
|
|
71
|
-
/** Server wiring routes a matching upgrade request here. */
|
|
72
59
|
handleUpgrade(req, socket, head) {
|
|
73
60
|
this.wss.handleUpgrade(req, socket, head, (ws) => this.wss.emit('connection', ws, req));
|
|
74
61
|
}
|
package/dist/pikichannel/turn.js
CHANGED
|
@@ -1,51 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pikichannel/turn.ts — Cloudflare Realtime TURN credential minter (host side).
|
|
3
|
-
*
|
|
4
|
-
* Why host-only is sufficient: the host is the WebRTC *answerer*. A TURN relay
|
|
5
|
-
* candidate it gathers is an allocation ON the TURN server that ANY client can
|
|
6
|
-
* reach OUTBOUND (NAT always allows outbound). So configuring TURN here alone
|
|
7
|
-
* gives relay fallback for EVERY NAT combination — symmetric NAT and CGNAT on
|
|
8
|
-
* either end included — without the browser needing its own TURN credentials.
|
|
9
|
-
* The host trickles its relay candidate to the client; the client just sends to
|
|
10
|
-
* it. (The browser keeps plain STUN; see web/sdk.js.)
|
|
11
|
-
*
|
|
12
|
-
* Security: the long-lived Cloudflare API token NEVER leaves this process. We
|
|
13
|
-
* exchange it for SHORT-LIVED (TTL'd) credentials via Cloudflare's API and feed
|
|
14
|
-
* only those to the peer connection. No secret is ever sent over the wire.
|
|
15
|
-
*
|
|
16
|
-
* Cost: callers must keep `iceTransportPolicy` at its default `'all'` (never
|
|
17
|
-
* force `'relay'`), so direct/STUN paths are always preferred and the relay —
|
|
18
|
-
* the only metered part of Cloudflare Realtime — is a genuine last resort.
|
|
19
|
-
* Credentials are minted once and cached across all connections, refreshed just
|
|
20
|
-
* before expiry, so steady state is zero API calls per connection.
|
|
21
|
-
*
|
|
22
|
-
* Fallback: no Cloudflare config (or any mint failure) → plain STUN, exactly as
|
|
23
|
-
* before. Existing hosts see zero behavior change.
|
|
24
|
-
*
|
|
25
|
-
* See https://developers.cloudflare.com/realtime/turn/generate-credentials/.
|
|
26
|
-
*/
|
|
27
1
|
import { loadUserConfig } from '../core/config/user-config.js';
|
|
28
2
|
import { writeScopedLog } from '../core/logging.js';
|
|
29
3
|
const tlog = (msg) => writeScopedLog('pikichannel', `[turn] ${msg}`, { level: 'info' });
|
|
30
|
-
/** Default credential lifetime (24h). Must exceed a single session's duration. */
|
|
31
4
|
const DEFAULT_TTL = 86_400;
|
|
32
|
-
/** Floor for TTL so a fat-fingered tiny value can't churn the cache. */
|
|
33
5
|
const MIN_TTL = 600;
|
|
34
|
-
/** Re-mint this long before expiry so live connections never use dead creds. */
|
|
35
6
|
const REFRESH_MARGIN_MS = 5 * 60 * 1000;
|
|
36
|
-
/** Abort a hung Cloudflare call so connection setup falls back to STUN promptly. */
|
|
37
7
|
const MINT_TIMEOUT_MS = 8_000;
|
|
38
|
-
/** Cloudflare credentials endpoint (the array-returning `generate-ice-servers`). */
|
|
39
8
|
const CF_ENDPOINT = (keyId) => `https://rtc.live.cloudflare.com/v1/turn/keys/${encodeURIComponent(keyId)}/credentials/generate-ice-servers`;
|
|
40
|
-
/** Free public STUN used when no TURN is configured. Google first keeps the
|
|
41
|
-
* no-config host's behavior identical to before; Cloudflare added for redundancy. */
|
|
42
9
|
const DEFAULT_STUN = [
|
|
43
10
|
{ urls: 'stun:stun.l.google.com:19302' },
|
|
44
11
|
{ urls: 'stun:stun.cloudflare.com:3478' },
|
|
45
12
|
];
|
|
46
13
|
let cache = null;
|
|
47
14
|
let pending = null;
|
|
48
|
-
/** Resolve TURN config from env (highest) then ~/.pikiloom/setting.json. */
|
|
49
15
|
export function resolveTurnConfig() {
|
|
50
16
|
const cfg = loadUserConfig();
|
|
51
17
|
const keyId = String(process.env.PIKICHANNEL_TURN_KEY_ID || cfg.pikichannelTurnKeyId || '').trim();
|
|
@@ -54,8 +20,6 @@ export function resolveTurnConfig() {
|
|
|
54
20
|
const ttl = Math.max(MIN_TTL, Number.parseInt(ttlRaw, 10) || DEFAULT_TTL);
|
|
55
21
|
return { keyId, apiToken, ttl };
|
|
56
22
|
}
|
|
57
|
-
/** A manual `PIKICHANNEL_ICE_SERVERS` JSON override, flattened — or null. This
|
|
58
|
-
* preserves the original escape hatch and takes precedence over minting. */
|
|
59
23
|
function manualIceServers() {
|
|
60
24
|
const raw = process.env.PIKICHANNEL_ICE_SERVERS;
|
|
61
25
|
if (!raw)
|
|
@@ -65,11 +29,9 @@ function manualIceServers() {
|
|
|
65
29
|
if (Array.isArray(v) && v.length)
|
|
66
30
|
return flatten(v);
|
|
67
31
|
}
|
|
68
|
-
catch {
|
|
32
|
+
catch { }
|
|
69
33
|
return null;
|
|
70
34
|
}
|
|
71
|
-
/** Flatten any `{ urls: string | string[], ... }[]` into one entry per URL,
|
|
72
|
-
* carrying username/credential onto each. werift treats `urls` as a string. */
|
|
73
35
|
function flatten(servers) {
|
|
74
36
|
const out = [];
|
|
75
37
|
for (const s of servers) {
|
|
@@ -93,8 +55,6 @@ const flattenCloudflare = flatten;
|
|
|
93
55
|
function cfgKey(c) {
|
|
94
56
|
return `${c.keyId}::${c.ttl}`;
|
|
95
57
|
}
|
|
96
|
-
/** Mint a fresh set of short-lived ICE servers from Cloudflare. Throws on any
|
|
97
|
-
* non-2xx / network error / timeout so the caller can fall back to STUN. */
|
|
98
58
|
async function mintCloudflare(cfg) {
|
|
99
59
|
const ctrl = new AbortController();
|
|
100
60
|
const timer = setTimeout(() => ctrl.abort(), MINT_TIMEOUT_MS);
|
|
@@ -117,9 +77,6 @@ async function mintCloudflare(cfg) {
|
|
|
117
77
|
clearTimeout(timer);
|
|
118
78
|
}
|
|
119
79
|
}
|
|
120
|
-
/** (Re)mint into the module cache. Single-flight: concurrent callers await one
|
|
121
|
-
* in-flight mint. On failure the last-good cache is kept if still for this key;
|
|
122
|
-
* otherwise cleared so the resolver yields STUN. */
|
|
123
80
|
function refresh(cfg) {
|
|
124
81
|
if (pending)
|
|
125
82
|
return pending;
|
|
@@ -141,11 +98,6 @@ function refresh(cfg) {
|
|
|
141
98
|
})();
|
|
142
99
|
return pending;
|
|
143
100
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Mint TURN credentials ahead of the first connection (call once at mount).
|
|
146
|
-
* Best-effort and non-blocking-by-design — awaiting it is optional. No creds /
|
|
147
|
-
* manual override → no-op.
|
|
148
|
-
*/
|
|
149
101
|
export async function prewarmTurn() {
|
|
150
102
|
if (manualIceServers())
|
|
151
103
|
return;
|
|
@@ -154,13 +106,6 @@ export async function prewarmTurn() {
|
|
|
154
106
|
return;
|
|
155
107
|
await refresh(cfg);
|
|
156
108
|
}
|
|
157
|
-
/**
|
|
158
|
-
* The current resolved ICE servers, synchronously, for building a peer
|
|
159
|
-
* connection. Order of precedence: manual override → cached Cloudflare creds →
|
|
160
|
-
* STUN. When the cache is missing/stale it kicks off a background refresh and
|
|
161
|
-
* returns the best value available right now (never blocks, never returns
|
|
162
|
-
* expired creds).
|
|
163
|
-
*/
|
|
164
109
|
export function getCachedIceServers() {
|
|
165
110
|
const manual = manualIceServers();
|
|
166
111
|
if (manual)
|
|
@@ -171,13 +116,11 @@ export function getCachedIceServers() {
|
|
|
171
116
|
const key = cfgKey(cfg);
|
|
172
117
|
const fresh = !!cache && cache.key === key && Date.now() < cache.expiresAt - REFRESH_MARGIN_MS;
|
|
173
118
|
if (!fresh)
|
|
174
|
-
void refresh(cfg);
|
|
175
|
-
// Use cached creds only while genuinely unexpired; otherwise STUN.
|
|
119
|
+
void refresh(cfg);
|
|
176
120
|
if (cache && cache.key === key && Date.now() < cache.expiresAt)
|
|
177
121
|
return cache.servers;
|
|
178
122
|
return DEFAULT_STUN;
|
|
179
123
|
}
|
|
180
|
-
/** Async resolver (mint awaited) — for status, tests, and one-off callers. */
|
|
181
124
|
export async function resolveIceServers() {
|
|
182
125
|
const manual = manualIceServers();
|
|
183
126
|
if (manual)
|
|
@@ -193,14 +136,6 @@ export async function resolveIceServers() {
|
|
|
193
136
|
return cache.servers;
|
|
194
137
|
return DEFAULT_STUN;
|
|
195
138
|
}
|
|
196
|
-
/**
|
|
197
|
-
* Reduce a (flattened) ICE list to what werift's primitive `parseIceServers`
|
|
198
|
-
* actually consumes: it takes the FIRST `stun:` and FIRST `turn:` entry, reads
|
|
199
|
-
* `urls` as a string, and connects to TURN over UDP. So we hand it exactly one
|
|
200
|
-
* STUN and one UDP TURN entry. `turns:` (TLS) is dropped — werift's TURN client
|
|
201
|
-
* speaks only udp/tcp. List order is meaningful: Cloudflare returns :3478/udp
|
|
202
|
-
* first, and DEFAULT_STUN puts Google first, so the right entries win.
|
|
203
|
-
*/
|
|
204
139
|
export function toWeriftIceServers(servers) {
|
|
205
140
|
const stun = servers.find((s) => s.urls.startsWith('stun:'));
|
|
206
141
|
const turn = servers.find((s) => s.urls.startsWith('turn:') && s.urls.includes('transport=udp')) ||
|
|
@@ -212,7 +147,6 @@ export function toWeriftIceServers(servers) {
|
|
|
212
147
|
out.push({ urls: turn.urls, username: turn.username, credential: turn.credential });
|
|
213
148
|
return out.length ? out : servers;
|
|
214
149
|
}
|
|
215
|
-
/** Lightweight status for /pikichannel/status (no secrets). */
|
|
216
150
|
export function turnStatus() {
|
|
217
151
|
if (manualIceServers()) {
|
|
218
152
|
const hasTurn = (manualIceServers() || []).some((s) => s.urls.startsWith('turn:') || s.urls.startsWith('turns:'));
|
|
@@ -228,7 +162,6 @@ export function turnStatus() {
|
|
|
228
162
|
expiresAt: live && cache ? cache.expiresAt : null,
|
|
229
163
|
};
|
|
230
164
|
}
|
|
231
|
-
/** Test-only: drop the module cache + in-flight mint so cases start clean. */
|
|
232
165
|
export function __resetTurnCacheForTest() {
|
|
233
166
|
cache = null;
|
|
234
167
|
pending = null;
|