pikiloom 0.4.26 → 0.4.27

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 (155) hide show
  1. package/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DIP-y4dl.js} +1 -1
  2. package/dashboard/dist/assets/ConnectionModal-BCfvhkiI.js +1 -0
  3. package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-Dot7ua9-.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-GioMx4S6.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C9_r8jJP.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-M1AP1sgR.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-CCm3UUfJ.js} +1 -1
  8. package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-YubDM16P.js} +1 -1
  9. package/dashboard/dist/assets/{SessionPanel-_0dLWvEb.js → SessionPanel-Cnv9o96n.js} +1 -1
  10. package/dashboard/dist/assets/SystemTab-VgNivIe3.js +1 -0
  11. package/dashboard/dist/assets/index-BY7l20bi.js +23 -0
  12. package/dashboard/dist/assets/index-BlqvzDUL.js +3 -0
  13. package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
  14. package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C0SAInnT.js} +1 -1
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/agent/acp-client.js +0 -59
  17. package/dist/agent/artifacts.js +1 -38
  18. package/dist/agent/auto-update.js +2 -78
  19. package/dist/agent/await-resume.js +0 -20
  20. package/dist/agent/cli/auth.js +3 -51
  21. package/dist/agent/cli/catalog.js +0 -3
  22. package/dist/agent/cli/detector.js +1 -19
  23. package/dist/agent/cli/index.js +0 -3
  24. package/dist/agent/cli/registry.js +0 -25
  25. package/dist/agent/driver.js +0 -14
  26. package/dist/agent/drivers/claude-tui.js +2 -769
  27. package/dist/agent/drivers/claude.js +12 -631
  28. package/dist/agent/drivers/codex.js +11 -162
  29. package/dist/agent/drivers/gemini.js +6 -114
  30. package/dist/agent/drivers/hermes.js +0 -128
  31. package/dist/agent/goal.js +0 -45
  32. package/dist/agent/handover.js +1 -46
  33. package/dist/agent/images.js +7 -133
  34. package/dist/agent/index.js +1 -29
  35. package/dist/agent/mcp/bridge.js +1 -197
  36. package/dist/agent/mcp/extensions.js +5 -84
  37. package/dist/agent/mcp/oauth.js +2 -69
  38. package/dist/agent/mcp/registry.js +2 -33
  39. package/dist/agent/mcp/session-server.js +2 -45
  40. package/dist/agent/mcp/tools/ask-user.js +0 -6
  41. package/dist/agent/mcp/tools/await-resume.js +0 -15
  42. package/dist/agent/mcp/tools/goal.js +0 -25
  43. package/dist/agent/mcp/tools/types.js +0 -5
  44. package/dist/agent/mcp/tools/workspace.js +0 -23
  45. package/dist/agent/npm.js +0 -14
  46. package/dist/agent/session.js +86 -196
  47. package/dist/agent/skill-installer.js +3 -70
  48. package/dist/agent/skills.js +0 -43
  49. package/dist/agent/stream.js +5 -148
  50. package/dist/agent/types.js +0 -12
  51. package/dist/agent/utils.js +2 -120
  52. package/dist/bot/bot.js +3 -468
  53. package/dist/bot/command-ui.js +0 -45
  54. package/dist/bot/commands.js +0 -51
  55. package/dist/bot/headless-bot.js +0 -21
  56. package/dist/bot/host.js +1 -5
  57. package/dist/bot/human-loop.js +0 -8
  58. package/dist/bot/menu.js +0 -3
  59. package/dist/bot/orchestration.js +0 -3
  60. package/dist/bot/render-shared.js +0 -63
  61. package/dist/bot/session-hub.js +14 -92
  62. package/dist/bot/session-status.js +1 -5
  63. package/dist/bot/streaming.js +0 -53
  64. package/dist/browser-profile.js +0 -51
  65. package/dist/browser-supervisor.js +0 -75
  66. package/dist/catalog/cli-tools.js +0 -53
  67. package/dist/catalog/index.js +0 -18
  68. package/dist/catalog/local-models.js +0 -18
  69. package/dist/catalog/mcp-servers.js +0 -35
  70. package/dist/catalog/skill-repos.js +0 -32
  71. package/dist/channels/base.js +0 -19
  72. package/dist/channels/dingtalk/bot.js +0 -3
  73. package/dist/channels/dingtalk/channel.js +0 -33
  74. package/dist/channels/discord/bot.js +0 -7
  75. package/dist/channels/discord/channel.js +0 -22
  76. package/dist/channels/feishu/bot.js +0 -71
  77. package/dist/channels/feishu/channel.js +0 -78
  78. package/dist/channels/feishu/markdown.js +0 -5
  79. package/dist/channels/feishu/render.js +0 -45
  80. package/dist/channels/health.js +0 -46
  81. package/dist/channels/slack/bot.js +0 -7
  82. package/dist/channels/slack/channel.js +1 -27
  83. package/dist/channels/states.js +0 -3
  84. package/dist/channels/telegram/bot.js +0 -60
  85. package/dist/channels/telegram/channel.js +2 -99
  86. package/dist/channels/telegram/directory.js +0 -3
  87. package/dist/channels/telegram/live-preview.js +0 -8
  88. package/dist/channels/telegram/render.js +0 -30
  89. package/dist/channels/wecom/bot.js +0 -3
  90. package/dist/channels/wecom/channel.js +0 -58
  91. package/dist/channels/weixin/api.js +0 -9
  92. package/dist/channels/weixin/bot.js +0 -88
  93. package/dist/channels/weixin/channel.js +0 -6
  94. package/dist/cli/autostart.js +1 -44
  95. package/dist/cli/channel-supervisor.js +1 -59
  96. package/dist/cli/channels.js +0 -6
  97. package/dist/cli/main.js +1 -145
  98. package/dist/cli/onboarding.js +0 -6
  99. package/dist/cli/run.js +0 -11
  100. package/dist/cli/setup-wizard.js +0 -3
  101. package/dist/core/config/runtime-config.js +0 -38
  102. package/dist/core/config/user-config.js +0 -63
  103. package/dist/core/config/validation.js +0 -9
  104. package/dist/core/constants.js +0 -268
  105. package/dist/core/git.js +0 -31
  106. package/dist/core/legacy-compat.js +1 -28
  107. package/dist/core/logging.js +0 -3
  108. package/dist/core/platform.js +0 -28
  109. package/dist/core/process-control.js +0 -8
  110. package/dist/core/secrets/index.js +0 -17
  111. package/dist/core/secrets/inline-seal.js +0 -7
  112. package/dist/core/secrets/ref.js +0 -19
  113. package/dist/core/secrets/resolver.js +0 -8
  114. package/dist/core/secrets/store.js +1 -12
  115. package/dist/core/utils.js +1 -37
  116. package/dist/core/version.js +0 -3
  117. package/dist/dashboard/platform.js +1 -35
  118. package/dist/dashboard/routes/agents.js +2 -71
  119. package/dist/dashboard/routes/cli.js +0 -27
  120. package/dist/dashboard/routes/config.js +0 -54
  121. package/dist/dashboard/routes/extensions.js +0 -132
  122. package/dist/dashboard/routes/local-models.js +0 -68
  123. package/dist/dashboard/routes/models.js +0 -46
  124. package/dist/dashboard/routes/sessions.js +0 -100
  125. package/dist/dashboard/runtime.js +1 -58
  126. package/dist/dashboard/server.js +0 -49
  127. package/dist/dashboard/session-control.js +2 -69
  128. package/dist/model/catalog.js +1 -17
  129. package/dist/model/index.js +0 -15
  130. package/dist/model/injector.js +1 -181
  131. package/dist/model/provider-models.js +1 -48
  132. package/dist/model/responses-bridge.js +3 -45
  133. package/dist/model/store.js +0 -17
  134. package/dist/model/types.js +0 -12
  135. package/dist/model/validation.js +1 -18
  136. package/dist/pikichannel/adapter-pikiloom.js +0 -31
  137. package/dist/pikichannel/code.js +0 -12
  138. package/dist/pikichannel/codec.js +0 -14
  139. package/dist/pikichannel/host.js +4 -33
  140. package/dist/pikichannel/protocol.js +0 -52
  141. package/dist/pikichannel/rendezvous-broker.js +5 -26
  142. package/dist/pikichannel/rendezvous-host.js +3 -16
  143. package/dist/pikichannel/server.js +6 -55
  144. package/dist/pikichannel/transport.js +0 -25
  145. package/dist/pikichannel/transports/webrtc-host.js +0 -18
  146. package/dist/pikichannel/transports/webrtc-shared.js +4 -34
  147. package/dist/pikichannel/transports/websocket-host.js +1 -14
  148. package/dist/pikichannel/turn.js +2 -69
  149. package/dist/pikichannel/web/sdk.js +14 -71
  150. package/package.json +1 -1
  151. package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
  152. package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
  153. package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
  154. package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
  155. 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(); // nodeId → host socket
28
- sessions = new Map(); // sessionId → pair
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 { /* ignore */ }
22
+ catch { }
43
23
  }
44
24
  }
45
25
  onConnection(ws) {
46
- let myNodeId = null; // set if this socket registers as a host
47
- const mySessions = new Set(); // sessions this socket participates in
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 { /* ignore */ }
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; // datachannel already live; ignore late signals
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); // pc now owned by the connection
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 { /* persisted best-effort */ }
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 { /* best-effort */ }
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); // initial from env/config
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 { /* best-effort */ }
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; // not ours
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(); // unknown /pikichannel/* subpath (or webrtc disabled)
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 { /* drop on closing channel */ }
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 { /* ignore */ }
51
+ catch { }
75
52
  try {
76
53
  this.pc.close();
77
54
  }
78
- catch { /* ignore */ }
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 { /* ignore */ } },
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 { /* ignore */ }
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
  }
@@ -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 { /* fall through */ }
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); // background; don't block the handshake
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;