pipane 0.1.8 → 0.1.10
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/README.md +3 -1
- package/dist/build-info.json +2 -2
- package/dist/client/assets/__vite-browser-external-DGfF_P2w.js +1 -0
- package/dist/client/assets/backend-landing-page-CF2aHs0U.js +1 -0
- package/dist/client/assets/{__vite-browser-external-DtLbhLGx.js → backend-protocol-BvNAsFvb.js} +1 -1
- package/dist/client/assets/device-identity-C_w2NbSk.js +4 -0
- package/dist/client/assets/index-DtrSEX3O.css +1 -0
- package/dist/client/assets/index-bOr1Y6MY.js +2 -0
- package/dist/client/assets/{main-DEfSB8wO.js → main-CQj3ADar.js} +531 -427
- package/dist/client/assets/pairing-page-CiEb8c3R.js +1 -0
- package/dist/client/assets/remote-backend-manager-Bbt7CXRP.js +1 -0
- package/dist/client/assets/rendezvous-trust-api-xLnrhe-v.js +1 -0
- package/dist/client/assets/webrtc-frame-transport-Drpqhhdy.js +1 -0
- package/dist/client/assets/workspace-backend-client-kUrfC0HS.js +1 -0
- package/dist/client/assets/ws-agent-adapter-Cauprivt.js +6 -0
- package/dist/client/index.html +5 -17
- package/dist/server/server/backend-registration-config.js +7 -0
- package/dist/server/server/frame-connection.js +157 -40
- package/dist/server/server/frame-router.js +2 -1
- package/dist/server/server/server.js +16 -12
- package/dist/server/server/ws-handler.js +44 -23
- package/dist/server/shared/data-channel-framing.js +31 -1
- package/docs/protocol.md +5 -3
- package/package.json +1 -1
- package/dist/client/assets/backend-landing-page-B7SbyIXQ.js +0 -1
- package/dist/client/assets/index-C7_1ODks.js +0 -2
- package/dist/client/assets/index-DgI_gkjg.css +0 -1
- package/dist/client/assets/pairing-page-C0YByC2D.js +0 -1
- package/dist/client/assets/remote-backend-manager-DCSdS38m.js +0 -1
- package/dist/client/assets/rendezvous-trust-api-C2Zob5i4.js +0 -4
- package/dist/client/assets/webrtc-frame-transport-DGX5EO_A.js +0 -1
- package/dist/client/assets/ws-agent-adapter-DpkkZGPl.js +0 -6
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Any number of clients can connect; each subscribes to one session at a time.
|
|
9
9
|
* - Detached sessions are read from JSONL on demand.
|
|
10
10
|
*/
|
|
11
|
-
import { FRAME_CONNECTION_OPEN } from "./frame-connection.js";
|
|
11
|
+
import { FRAME_CONNECTION_OPEN, WebSocketFrameConnection, } from "./frame-connection.js";
|
|
12
12
|
import { copyFile } from "node:fs/promises";
|
|
13
13
|
import { constants as fsConstants, existsSync } from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
@@ -16,10 +16,13 @@ import { SessionJsonl, readSessionFromDisk, getSessionFileSize, serializeSession
|
|
|
16
16
|
import { getSessionCwd } from "./session-cwd.js";
|
|
17
17
|
import { checkCommandAvailable, installPiGlobal, isPiInstallable, makePiNotFoundMessage } from "./pi-runtime.js";
|
|
18
18
|
import { modelsMatch, toCompactModelRef } from "../shared/thinking-levels.js";
|
|
19
|
+
import { computeSyncOp } from "../shared/jsonl-sync.js";
|
|
19
20
|
import { COMPACT_RPC_TIMEOUT_MS } from "../shared/rpc-timeouts.js";
|
|
20
21
|
import { assertNever, decodeClientCommand, encodeServerMessage, } from "../shared/ws-protocol.js";
|
|
21
22
|
import { SessionPathError, SessionPathGuard } from "./session-path.js";
|
|
22
23
|
import { extensionStatusSnapshot, isValidExtensionStatusKey, normalizeExtensionStatusText, providerForUsageStatus, PROVIDER_USAGE_STATUS_KEY, } from "./extension-status.js";
|
|
24
|
+
const DETACHED_SYNC_COALESCE_MS = 75;
|
|
25
|
+
const SESSION_SYNC_TRANSFER_KEY = "active-session-sync";
|
|
23
26
|
let nextTurnId = 0;
|
|
24
27
|
function makeTurnId() {
|
|
25
28
|
return `turn_${Date.now()}_${++nextTurnId}`;
|
|
@@ -49,6 +52,8 @@ export class WsHandler {
|
|
|
49
52
|
subscribedFileSizes = new Map();
|
|
50
53
|
/** Last revision/hash published for each authoritative session state. */
|
|
51
54
|
sessionRevisions = new Map();
|
|
55
|
+
/** Per-session trailing-edge disk refreshes for detached JSONL bursts. */
|
|
56
|
+
pendingDetachedSyncs = new Map();
|
|
52
57
|
piAvailable;
|
|
53
58
|
piInstalling = false;
|
|
54
59
|
constructor(options) {
|
|
@@ -186,29 +191,36 @@ export class WsHandler {
|
|
|
186
191
|
}
|
|
187
192
|
/**
|
|
188
193
|
* Called by the file watcher when a JSONL file changes on disk.
|
|
189
|
-
*
|
|
190
|
-
*
|
|
194
|
+
* Detached writes are coalesced per session so a burst is read, diffed, and
|
|
195
|
+
* published once at its trailing edge. Attached actor events remain authoritative.
|
|
191
196
|
*/
|
|
192
197
|
notifySessionFileChanged(sessionPath) {
|
|
193
|
-
|
|
194
|
-
if (this.registry.find(sessionPath)?.isAttached)
|
|
198
|
+
if (this.registry.find(sessionPath)?.isAttached || !this.hasSubscribers(sessionPath))
|
|
195
199
|
return;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
const pending = this.pendingDetachedSyncs.get(sessionPath);
|
|
201
|
+
if (pending)
|
|
202
|
+
clearTimeout(pending);
|
|
203
|
+
const timer = setTimeout(() => {
|
|
204
|
+
this.pendingDetachedSyncs.delete(sessionPath);
|
|
205
|
+
this.flushDetachedSessionSync(sessionPath);
|
|
206
|
+
}, DETACHED_SYNC_COALESCE_MS);
|
|
207
|
+
timer.unref?.();
|
|
208
|
+
this.pendingDetachedSyncs.set(sessionPath, timer);
|
|
209
|
+
}
|
|
210
|
+
hasSubscribers(sessionPath) {
|
|
211
|
+
for (const client of this.clients.values()) {
|
|
212
|
+
if (client.subscribedSession === sessionPath)
|
|
213
|
+
return true;
|
|
203
214
|
}
|
|
204
|
-
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
flushDetachedSessionSync(sessionPath) {
|
|
218
|
+
if (this.registry.find(sessionPath)?.isAttached || !this.hasSubscribers(sessionPath))
|
|
205
219
|
return;
|
|
206
|
-
// Check if the file actually changed
|
|
207
220
|
const oldSize = this.subscribedFileSizes.get(sessionPath) ?? 0;
|
|
208
221
|
const newSize = getSessionFileSize(sessionPath);
|
|
209
222
|
if (newSize === oldSize)
|
|
210
223
|
return;
|
|
211
|
-
// File changed — read from disk and push snapshot to subscribers
|
|
212
224
|
this.subscribedFileSizes.set(sessionPath, newSize);
|
|
213
225
|
const { json, hash } = readSessionFromDisk(sessionPath);
|
|
214
226
|
this.pushSnapshotToSubscribers(sessionPath, json, hash);
|
|
@@ -243,7 +255,7 @@ export class WsHandler {
|
|
|
243
255
|
ws.close(1008, "Unauthorized");
|
|
244
256
|
return;
|
|
245
257
|
}
|
|
246
|
-
this.acceptConnection(ws);
|
|
258
|
+
this.acceptConnection(new WebSocketFrameConnection(ws));
|
|
247
259
|
});
|
|
248
260
|
}
|
|
249
261
|
acceptAuthenticatedConnection(connection) {
|
|
@@ -279,8 +291,11 @@ export class WsHandler {
|
|
|
279
291
|
});
|
|
280
292
|
}
|
|
281
293
|
sendMessage(ws, payload) {
|
|
282
|
-
if (ws.readyState
|
|
283
|
-
|
|
294
|
+
if (ws.readyState !== FRAME_CONNECTION_OPEN)
|
|
295
|
+
return;
|
|
296
|
+
ws.send(encodeServerMessage(payload), payload.type === "session_sync"
|
|
297
|
+
? { priority: "bulk", transferKey: SESSION_SYNC_TRANSFER_KEY }
|
|
298
|
+
: { priority: "control" });
|
|
284
299
|
}
|
|
285
300
|
sendSuccess(ws, id, command, data) {
|
|
286
301
|
this.sendMessage(ws, {
|
|
@@ -417,6 +432,7 @@ export class WsHandler {
|
|
|
417
432
|
return;
|
|
418
433
|
const requestedPath = command.sessionPath;
|
|
419
434
|
if (!requestedPath) {
|
|
435
|
+
ws.cancelTransfer?.(SESSION_SYNC_TRANSFER_KEY);
|
|
420
436
|
client.subscribedSession = null;
|
|
421
437
|
client.lastVersion = 0;
|
|
422
438
|
client.lastJson = "";
|
|
@@ -438,7 +454,13 @@ export class WsHandler {
|
|
|
438
454
|
throw error;
|
|
439
455
|
sessionPath = pendingPath;
|
|
440
456
|
}
|
|
457
|
+
// A newly selected session makes every queued chunk from the previous
|
|
458
|
+
// snapshot stale. The carrier emits a cancellation for a partial frame.
|
|
459
|
+
ws.cancelTransfer?.(SESSION_SYNC_TRANSFER_KEY);
|
|
441
460
|
client.subscribedSession = sessionPath;
|
|
461
|
+
client.lastVersion = 0;
|
|
462
|
+
client.lastJson = "";
|
|
463
|
+
client.lastHash = "";
|
|
442
464
|
// If the session is attached, send from its actor-owned in-memory state
|
|
443
465
|
const attached = this.registry.find(sessionPath)?.session;
|
|
444
466
|
if (attached) {
|
|
@@ -1083,17 +1105,16 @@ export class WsHandler {
|
|
|
1083
1105
|
pushSnapshotToSubscribers(sessionPath, json, hash) {
|
|
1084
1106
|
const revision = this.revisionForState(sessionPath, hash);
|
|
1085
1107
|
for (const [ws, client] of this.clients) {
|
|
1086
|
-
if (client.subscribedSession !== sessionPath)
|
|
1108
|
+
if (client.subscribedSession !== sessionPath || ws.readyState !== FRAME_CONNECTION_OPEN)
|
|
1087
1109
|
continue;
|
|
1088
|
-
if (
|
|
1110
|
+
if (client.lastHash === hash)
|
|
1089
1111
|
continue;
|
|
1112
|
+
const sync = computeSyncOp(client.lastJson, json, client.lastHash, hash);
|
|
1090
1113
|
this.sendMessage(ws, {
|
|
1091
1114
|
type: "session_sync",
|
|
1092
1115
|
sessionPath,
|
|
1093
1116
|
revision,
|
|
1094
|
-
|
|
1095
|
-
data: json,
|
|
1096
|
-
hash,
|
|
1117
|
+
...sync,
|
|
1097
1118
|
});
|
|
1098
1119
|
client.lastJson = json;
|
|
1099
1120
|
client.lastHash = hash;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const CHUNK_MARKER = 1;
|
|
2
|
+
const CANCEL_MARKER = 1;
|
|
2
3
|
const CHUNK_PREFIX = `{"__pipaneDataChannelChunk":${CHUNK_MARKER},`;
|
|
4
|
+
const CANCEL_PREFIX = `{"__pipaneDataChannelCancel":${CANCEL_MARKER},`;
|
|
3
5
|
/** Keep physical messages below conservative browser/libdatachannel SCTP limits. */
|
|
4
6
|
export const DATA_CHANNEL_CHUNK_PAYLOAD_BYTES = 12_000;
|
|
5
7
|
export const MAX_DATA_CHANNEL_MESSAGE_BYTES = 16 * 1024;
|
|
@@ -41,13 +43,23 @@ export function encodeDataChannelFrame(frame, id) {
|
|
|
41
43
|
}
|
|
42
44
|
return messages;
|
|
43
45
|
}
|
|
46
|
+
/** Cancel one incomplete logical frame without exposing a carrier envelope upstream. */
|
|
47
|
+
export function encodeDataChannelFrameCancellation(id) {
|
|
48
|
+
if (!CHUNK_ID_PATTERN.test(id))
|
|
49
|
+
throw new Error("DataChannel frame id is invalid");
|
|
50
|
+
return JSON.stringify({ __pipaneDataChannelCancel: CANCEL_MARKER, id });
|
|
51
|
+
}
|
|
44
52
|
/** Reassemble carrier chunks while passing ordinary application frames through unchanged. */
|
|
45
53
|
export class DataChannelFrameDecoder {
|
|
46
54
|
pending = new Map();
|
|
47
55
|
accept(message) {
|
|
48
|
-
if (!message.startsWith(CHUNK_PREFIX))
|
|
56
|
+
if (!message.startsWith(CHUNK_PREFIX) && !message.startsWith(CANCEL_PREFIX))
|
|
49
57
|
return message;
|
|
50
58
|
try {
|
|
59
|
+
if (message.startsWith(CANCEL_PREFIX)) {
|
|
60
|
+
this.pending.delete(parseCancel(message).id);
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
51
63
|
const chunk = parseChunk(message);
|
|
52
64
|
let pending = this.pending.get(chunk.id);
|
|
53
65
|
if (!pending) {
|
|
@@ -90,6 +102,24 @@ export class DataChannelFrameDecoder {
|
|
|
90
102
|
this.pending.clear();
|
|
91
103
|
}
|
|
92
104
|
}
|
|
105
|
+
function parseCancel(message) {
|
|
106
|
+
let value;
|
|
107
|
+
try {
|
|
108
|
+
value = JSON.parse(message);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
throw new Error("DataChannel cancellation is not valid JSON");
|
|
112
|
+
}
|
|
113
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
114
|
+
throw new Error("DataChannel cancellation must be an object");
|
|
115
|
+
}
|
|
116
|
+
const cancellation = value;
|
|
117
|
+
if (cancellation.__pipaneDataChannelCancel !== CANCEL_MARKER
|
|
118
|
+
|| typeof cancellation.id !== "string" || !CHUNK_ID_PATTERN.test(cancellation.id)) {
|
|
119
|
+
throw new Error("DataChannel cancellation envelope is invalid");
|
|
120
|
+
}
|
|
121
|
+
return cancellation;
|
|
122
|
+
}
|
|
93
123
|
function parseChunk(message) {
|
|
94
124
|
let value;
|
|
95
125
|
try {
|
package/docs/protocol.md
CHANGED
|
@@ -52,7 +52,7 @@ The server sends a fresh random `challenge`. A backend responds with `register_b
|
|
|
52
52
|
|
|
53
53
|
The backend client reconnects its outbound WebSocket with bounded exponential backoff and repeats challenge authentication. STUN servers may be static. TURN credentials use coturn's REST convention: an expiring `timestamp:subject` username and HMAC-SHA1 credential; fresh backend credentials accompany each `connection_request` rather than expiring in a long-running peer manager. Browser and backend support relay-only ICE policy; deterministic E2E forces both peers through a local UDP TURN relay.
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Backends register with `https://pipane.dev` by default. `PIPANE_RENDEZVOUS_URL` overrides that endpoint (or disables registration when set to an empty value), while `PIPANE_APP_URL` optionally overrides generated browser links. `PIPANE_BACKEND_NAME` is optional and defaults to the machine's non-fully-qualified hostname. The rendezvous executable reads comma-separated `PIPANE_STUN_URLS` and `PIPANE_TURN_URLS`, plus `PIPANE_TURN_SECRET`; durable central identity/account state defaults to `~/.config/pipane-rendezvous` or `PIPANE_RENDEZVOUS_DATA_DIR`.
|
|
56
56
|
|
|
57
57
|
### Device identity and anonymous accounts
|
|
58
58
|
|
|
@@ -89,7 +89,9 @@ The answer-side implementation accepts only a reliable ordered DataChannel with
|
|
|
89
89
|
|
|
90
90
|
Authenticated DataChannels carry the existing versioned v1 application frames through the same server connection boundary as local WebSockets. A frame router keeps those application frames isolated from semantic v2 responses on the same ordered channel. Revocation closes active rendezvous routes and matching backend peers, prevents new ticket issuance, and is retained centrally so an offline backend clears stale local ownership when it next registers.
|
|
91
91
|
|
|
92
|
-
After the unfragmented authentication exchange, the carrier transparently splits logical frames larger than 12,000 UTF-8 bytes into ordered base64 chunk envelopes no larger than 16 KiB. Browser and backend reassemble at most 64 MiB per logical frame with bounded pending-frame and outgoing-queue memory. Application v1 and semantic v2 decoders therefore continue to receive exactly one complete JSON frame regardless of the negotiated SCTP message-size limit.
|
|
92
|
+
After the unfragmented authentication exchange, the carrier transparently splits logical frames larger than 12,000 UTF-8 bytes into ordered base64 chunk envelopes no larger than 16 KiB. The same transparent envelopes are used for large server-to-browser local WebSocket frames. Browser and backend reassemble at most 64 MiB per logical frame with bounded pending-frame and outgoing-queue memory. Application v1 and semantic v2 decoders therefore continue to receive exactly one complete JSON frame regardless of the negotiated SCTP message-size limit.
|
|
93
|
+
|
|
94
|
+
Server carriers schedule control and bulk lanes independently, so responses, status, steering, and abort traffic can overtake queued session-snapshot chunks. Selecting another session cancels the unsent portion of the stale snapshot with a reserved carrier envelope before the replacement begins. Detached JSONL watcher bursts are coalesced per session and hash-chained against each client's last state, producing a delta when it is smaller than a full snapshot and suppressing unchanged updates.
|
|
93
95
|
|
|
94
96
|
### Semantic backend protocol v2
|
|
95
97
|
|
|
@@ -112,7 +114,7 @@ The currently implemented semantic methods are:
|
|
|
112
114
|
|
|
113
115
|
Every method has runtime-validated parameters, correlated responses, stable error codes, bounded concurrency, and a bounded device-scoped completed-request cache. File uploads use bounded, offset-addressed base64 chunks so arbitrary non-image attachments can cross either local HTTP or the authenticated DataChannel, land in a private temporary backend path, and be referenced in the agent prompt. Pending browser requests retain their id across a carrier reconnect, so an in-flight mutation is resumed or answered from the cache instead of being executed twice. The backend uses one `LocalBackendApi` implementation for both the legacy local HTTP facade and semantic DataChannel requests. Remote session results are scoped to a structured `{ backendId, path }` identity; paths remain backend-local identifiers rather than authorization.
|
|
114
116
|
|
|
115
|
-
The browser's `RemoteBackendManager` maintains one client/store per
|
|
117
|
+
The browser's `RemoteBackendManager` maintains one client/store per authorized backend id, requests a fresh ticket whenever a WebRTC carrier reconnects, and never treats an arbitrary URL backend id as authorized until signed account discovery includes it. The account workspace opens a direct connection to each reachable authorized host so it can merge backend-scoped session metadata and live status in one sidebar; only the selected host/session drives the conversation renderer and receives a session subscription. Session metadata remains end-to-end between browser and backend and is not indexed by rendezvous. Terminal `pipane pair` remains the no-email recovery path when browser storage is lost.
|
|
116
118
|
|
|
117
119
|
Application streaming, turn control, and session snapshots remain on validated v1 frames during parity migration. Semantic v2 is deployed beside v1 rather than changing v1's renderer-state contract in place.
|
|
118
120
|
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{B as u}from"./__vite-browser-external-DtLbhLGx.js";import{R as x,a as b}from"./rendezvous-trust-api-C2Zob5i4.js";const y={loadIdentity:()=>b(),createTrustApi:()=>new x(window.location.origin),confirm:e=>window.confirm(e)};async function v(e=y){const l=document.getElementById("app")??document.body;l.replaceChildren();const r=document.createElement("main");r.dataset.testid="backend-landing",r.style.cssText="min-height:100vh;padding:40px 24px;font-family:ui-sans-serif,system-ui;color:var(--foreground,#111827);background:var(--background,#fff)";const i=document.createElement("section");i.style.cssText="width:min(760px,100%);margin:0 auto";const t=document.createElement("h1");t.textContent="Your pipane backends",t.style.cssText="margin:0 0 8px;font-size:28px";const s=document.createElement("p");s.textContent="Connections are opened directly and only when you choose a backend.",s.style.cssText="margin:0 0 24px;opacity:.72";const n=document.createElement("div");n.dataset.testid="backend-list",n.style.cssText="display:grid;gap:12px";const d=document.createElement("p");d.style.cssText="margin-top:24px;padding-top:18px;border-top:1px solid var(--border,#d1d5db);opacity:.78;line-height:1.5",d.textContent="Add a backend or recover this browser by running `pipane pair` in an owned backend terminal and scanning its QR code.",i.append(t,s,n,d),r.append(i),l.append(r);const c=await e.loadIdentity();if(!c){n.append(m("No paired browser key was found.","Use `pipane pair` on a backend to begin without creating an account."));return}const o=e.createTrustApi();let a;try{a=await o.listAuthorizedBackends(c)}catch(p){n.append(m("Backend access could not be recovered from this device key.",p instanceof Error?p.message:"Pair this browser again from an owned backend terminal."));return}if(a.length===0){n.append(m("No backends are paired.","Run `pipane pair` in a backend terminal to add one."));return}for(const p of a)n.append(f(p,c,o,e))}function f(e,l,r,i){const t=document.createElement("article");t.dataset.backendId=e.backendId,t.style.cssText="display:flex;align-items:center;gap:14px;border:1px solid var(--border,#d1d5db);border-radius:10px;padding:16px";const s=document.createElement("div");s.style.cssText="min-width:0;flex:1";const n=document.createElement("strong");n.textContent=e.name||`${e.backendId.slice(0,18)}…`,n.style.cssText="display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis";const d=document.createElement("small"),c=e.protocolVersions.includes(u);d.textContent=e.online?c?`Online${e.softwareVersion?` · v${e.softwareVersion}`:""}`:"Online · update required":"Offline",d.style.cssText=`color:${e.online&&c?"#15803d":"#a16207"}`,s.append(n,d);const o=document.createElement("a");o.textContent="Open",o.href=`/backend/${encodeURIComponent(e.backendId)}`,o.dataset.testid="open-backend",o.style.cssText="border-radius:8px;padding:8px 12px;background:#2563eb;color:white;text-decoration:none",(!e.online||!c)&&(o.removeAttribute("href"),o.setAttribute("aria-disabled","true"),o.style.opacity=".45");const a=document.createElement("button");return a.type="button",a.textContent="Remove",a.style.cssText="border:0;background:transparent;color:#dc2626;cursor:pointer",a.addEventListener("click",()=>{i.confirm(`Remove ${e.name||e.backendId} from this account?`)&&(a.disabled=!0,r.revokeBackend(l,e.backendId).then(()=>t.remove()).catch(p=>{a.disabled=!1,d.textContent=p instanceof Error?p.message:"Removal failed",d.style.color="#dc2626"}))}),t.append(s,o,a),t}function m(e,l){const r=document.createElement("div");r.style.cssText="border:1px solid var(--border,#d1d5db);border-radius:10px;padding:18px;line-height:1.5";const i=document.createElement("strong");i.textContent=e;const t=document.createElement("p");return t.textContent=l,t.style.cssText="margin:6px 0 0;opacity:.72",r.append(i,t),r}export{v as initializeBackendLandingPage};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/pairing-page-C0YByC2D.js","assets/rendezvous-trust-api-C2Zob5i4.js","assets/webrtc-frame-transport-DGX5EO_A.js","assets/backend-landing-page-B7SbyIXQ.js","assets/__vite-browser-external-DtLbhLGx.js","assets/remote-backend-manager-DCSdS38m.js","assets/ws-agent-adapter-DpkkZGPl.js","assets/main-DEfSB8wO.js","assets/app-runtime-Rw-O-AwW.js"])))=>i.map(i=>d[i]);
|
|
2
|
-
(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const t of document.querySelectorAll('link[rel="modulepreload"]'))a(t);new MutationObserver(t=>{for(const r of t)if(r.type==="childList")for(const i of r.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&a(i)}).observe(document,{childList:!0,subtree:!0});function o(t){const r={};return t.integrity&&(r.integrity=t.integrity),t.referrerPolicy&&(r.referrerPolicy=t.referrerPolicy),t.crossOrigin==="use-credentials"?r.credentials="include":t.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function a(t){if(t.ep)return;t.ep=!0;const r=o(t);fetch(t.href,r)}})();const w="modulepreload",y=function(n){return"/"+n},p={},f=function(e,o,a){let t=Promise.resolve();if(o&&o.length>0){let m=function(c){return Promise.all(c.map(l=>Promise.resolve(l).then(u=>({status:"fulfilled",value:u}),u=>({status:"rejected",reason:u}))))};document.getElementsByTagName("link");const i=document.querySelector("meta[property=csp-nonce]"),s=i?.nonce||i?.getAttribute("nonce");t=m(o.map(c=>{if(c=y(c),c in p)return;p[c]=!0;const l=c.endsWith(".css"),u=l?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${u}`))return;const d=document.createElement("link");if(d.rel=l?"stylesheet":w,l||(d.as="script"),d.crossOrigin="",d.href=c,s&&d.setAttribute("nonce",s),document.head.appendChild(d),l)return new Promise((h,g)=>{d.addEventListener("load",h),d.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${c}`)))})}))}function r(i){const s=new Event("vite:preloadError",{cancelable:!0});if(s.payload=i,window.dispatchEvent(s),!s.defaultPrevented)throw i}return t.then(i=>{for(const s of i||[])s.status==="rejected"&&r(s.reason);return e().catch(r)})};"serviceWorker"in navigator&&navigator.serviceWorker.getRegistrations().then(n=>{for(const e of n)e.unregister()});v();async function v(){if(P(window.location.pathname)){const{initializePairingPage:e}=await f(async()=>{const{initializePairingPage:o}=await import("./pairing-page-C0YByC2D.js");return{initializePairingPage:o}},__vite__mapDeps([0,1,2]));await e();return}if(window.location.pathname==="/"&&await E()){const{initializeBackendLandingPage:e}=await f(async()=>{const{initializeBackendLandingPage:o}=await import("./backend-landing-page-B7SbyIXQ.js");return{initializeBackendLandingPage:o}},__vite__mapDeps([3,4,1]));await e();return}const n=b(window.location.pathname);if(n)try{const[{configureAppRuntime:e},{RemoteBackendManager:o}]=await Promise.all([f(()=>import("./app-runtime-Rw-O-AwW.js"),[]),f(()=>import("./remote-backend-manager-DCSdS38m.js"),__vite__mapDeps([5,4,1,2,6]))]),a=new o(window.location.origin),t=await a.initialize();e({client:a.getClient(n),remote:{backendId:n,backends:t,manager:a}})}catch(e){_(e);return}await f(()=>import("./main-DEfSB8wO.js"),__vite__mapDeps([7,6,4,8]))}async function E(){try{const n=await fetch("/health",{cache:"no-store"});if(!n.ok||!n.headers.get("content-type")?.includes("application/json"))return!1;const e=await n.json();return!!e&&typeof e=="object"&&e.ok===!0}catch{return!1}}function _(n){const e=document.getElementById("app")??document.body;e.replaceChildren();const o=document.createElement("main");o.style.cssText="min-height:100vh;display:grid;place-items:center;padding:24px;font-family:ui-sans-serif,system-ui;color:var(--foreground,#111827);background:var(--background,#fff)";const a=document.createElement("section");a.style.cssText="max-width:520px;border:1px solid var(--border,#d1d5db);border-radius:12px;padding:28px";const t=document.createElement("h1");t.textContent="Cannot open backend";const r=document.createElement("p");r.textContent=n instanceof Error?n.message:"This browser is not authorized for the backend.";const i=document.createElement("p");i.textContent="To restore access, run `pipane pair` in the backend terminal and scan the new QR code with this browser.",a.append(t,r,i),o.append(a),e.append(o)}function P(n){return/^\/pair\/[^/]+$/u.test(n)}function b(n){const e=/^\/backend\/([^/]+)$/u.exec(n);return e?.[1]?decodeURIComponent(e[1]):void 0}export{f as _};
|