viveworker 0.7.0 → 0.8.0
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 +33 -4
- package/package.json +7 -3
- package/scripts/lib/remote-pairing/README.md +164 -0
- package/scripts/lib/remote-pairing/_browser-bundle-entry.mjs +86 -0
- package/scripts/lib/remote-pairing/audit.mjs +122 -0
- package/scripts/lib/remote-pairing/bridge-relay-client.mjs +737 -0
- package/scripts/lib/remote-pairing/control.mjs +156 -0
- package/scripts/lib/remote-pairing/envelope.mjs +224 -0
- package/scripts/lib/remote-pairing/http-dispatch.mjs +530 -0
- package/scripts/lib/remote-pairing/keys-core.mjs +110 -0
- package/scripts/lib/remote-pairing/keys.mjs +181 -0
- package/scripts/lib/remote-pairing/noise.mjs +436 -0
- package/scripts/lib/remote-pairing/orchestrator.mjs +356 -0
- package/scripts/lib/remote-pairing/pairings.mjs +446 -0
- package/scripts/lib/remote-pairing/rpc.mjs +381 -0
- package/scripts/moltbook-scout-auto.sh +16 -8
- package/scripts/share-cli.mjs +14 -130
- package/scripts/viveworker-bridge.mjs +1324 -103
- package/scripts/viveworker.mjs +27 -6
- package/web/app.css +634 -9
- package/web/app.js +1731 -187
- package/web/i18n.js +187 -9
- package/web/index.html +32 -2
- package/web/remote-pairing/api-router.js +873 -0
- package/web/remote-pairing/keys.js +237 -0
- package/web/remote-pairing/pairing-state.js +313 -0
- package/web/remote-pairing/rpc-client.js +765 -0
- package/web/remote-pairing/transport.js +804 -0
- package/web/remote-pairing/wake.js +149 -0
- package/web/remote-pairing-test.html +400 -0
- package/web/remote-pairing.bundle.js +3 -0
- package/web/remote-pairing.bundle.js.LEGAL.txt +15 -0
- package/web/remote-pairing.bundle.js.map +7 -0
- package/web/sw.js +190 -20
package/web/sw.js
CHANGED
|
@@ -1,9 +1,91 @@
|
|
|
1
|
-
const CACHE_NAME = "viveworker-
|
|
1
|
+
const CACHE_NAME = "viveworker-v127";
|
|
2
|
+
const APP_BUILD_ID = "20260428-remote-token-refresh";
|
|
3
|
+
const APP_SCRIPT_URL = `/app.js?v=${APP_BUILD_ID}`;
|
|
4
|
+
const API_ROUTER_URL = `/remote-pairing/api-router.js?v=${APP_BUILD_ID}`;
|
|
2
5
|
const NOTIFICATION_INTENT_CACHE = "viveworker-notification-intent-v1";
|
|
3
6
|
const NOTIFICATION_INTENT_PATH = "/__viveworker_notification_intent__";
|
|
4
|
-
|
|
7
|
+
// Cold off-LAN start requires the app shell plus remote-pairing modules in the
|
|
8
|
+
// precache. The first installed /app?pairToken=... navigation may happen before
|
|
9
|
+
// this service worker controls the page, so cache /app during install instead
|
|
10
|
+
// of relying on a later stale-while-revalidate pass.
|
|
11
|
+
//
|
|
12
|
+
// Bumping CACHE_NAME forces a re-cache so existing clients pick up the wider
|
|
13
|
+
// asset set on next launch.
|
|
14
|
+
const APP_ASSETS = [
|
|
15
|
+
"/app",
|
|
16
|
+
APP_SCRIPT_URL,
|
|
17
|
+
"/app.css",
|
|
18
|
+
"/app.js",
|
|
19
|
+
"/i18n.js",
|
|
20
|
+
"/icons/viveworker-v-pulse.svg",
|
|
21
|
+
API_ROUTER_URL,
|
|
22
|
+
"/remote-pairing/api-router.js",
|
|
23
|
+
"/remote-pairing/keys.js",
|
|
24
|
+
"/remote-pairing/pairing-state.js",
|
|
25
|
+
"/remote-pairing/rpc-client.js",
|
|
26
|
+
"/remote-pairing/transport.js",
|
|
27
|
+
"/remote-pairing/wake.js",
|
|
28
|
+
"/remote-pairing.bundle.js",
|
|
29
|
+
];
|
|
5
30
|
const APP_ROUTES = new Set(["/", "/app", "/app/"]);
|
|
6
|
-
const CACHED_PATHS = new Set(APP_ASSETS);
|
|
31
|
+
const CACHED_PATHS = new Set(APP_ASSETS.map((asset) => new URL(asset, self.location.origin).pathname));
|
|
32
|
+
const VERSIONED_CACHE_PATHS = new Set([
|
|
33
|
+
"/app.js",
|
|
34
|
+
"/remote-pairing/api-router.js",
|
|
35
|
+
]);
|
|
36
|
+
const NETWORK_FIRST_PATHS = new Set([
|
|
37
|
+
"/app.js",
|
|
38
|
+
"/remote-pairing/api-router.js",
|
|
39
|
+
]);
|
|
40
|
+
const APP_NAVIGATION_NETWORK_TIMEOUT_MS = 1800;
|
|
41
|
+
const ASSET_NETWORK_TIMEOUT_MS = 900;
|
|
42
|
+
const APP_SHELL_FALLBACK_HTML = `<!doctype html>
|
|
43
|
+
<html lang="en">
|
|
44
|
+
<head>
|
|
45
|
+
<meta charset="utf-8">
|
|
46
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
47
|
+
<meta name="theme-color" content="#101418">
|
|
48
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
49
|
+
<link rel="manifest" href="/manifest.webmanifest">
|
|
50
|
+
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
|
51
|
+
<link rel="icon" type="image/png" sizes="192x192" href="/icons/viveworker-icon-192.png">
|
|
52
|
+
<link rel="stylesheet" href="/app.css">
|
|
53
|
+
<style>
|
|
54
|
+
html, body { min-height: 100%; margin: 0; background: #081015; color: #f5fbff; }
|
|
55
|
+
.boot-splash { position: fixed; inset: 0; display: grid; place-items: center; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; background: radial-gradient(circle at 50% 18%, rgba(47, 143, 103, 0.22), transparent 30%), linear-gradient(180deg, #081015 0%, #091015 100%); }
|
|
56
|
+
.boot-splash__card { display: grid; justify-items: center; gap: 0.9rem; text-align: center; }
|
|
57
|
+
.boot-splash__logo { width: 6rem; height: 6rem; border-radius: 28%; }
|
|
58
|
+
.boot-splash__title { margin: 0; font-size: 2rem; letter-spacing: -0.04em; }
|
|
59
|
+
.boot-splash__status { min-height: 1.25em; margin: 0; color: rgba(205, 220, 231, 0.72); }
|
|
60
|
+
.boot-splash__hint { max-width: 15rem; margin: -0.35rem 0 0; color: rgba(178, 196, 210, 0.58); font-size: 0.78rem; line-height: 1.45; opacity: 0; transition: opacity 220ms ease; }
|
|
61
|
+
.boot-splash__hint.is-visible { opacity: 1; }
|
|
62
|
+
.viveworker-ready .boot-splash { opacity: 0; visibility: hidden; }
|
|
63
|
+
</style>
|
|
64
|
+
<title>viveworker</title>
|
|
65
|
+
</head>
|
|
66
|
+
<body>
|
|
67
|
+
<div id="boot-splash" class="boot-splash" role="status" aria-live="polite" aria-label="viveworker is starting">
|
|
68
|
+
<div class="boot-splash__card">
|
|
69
|
+
<img class="boot-splash__logo" src="/icons/viveworker-v-pulse.svg" alt="" width="112" height="112" decoding="async">
|
|
70
|
+
<h1 class="boot-splash__title">viveworker</h1>
|
|
71
|
+
<p id="boot-splash-status" class="boot-splash__status">Checking your trusted Wi-Fi...</p>
|
|
72
|
+
<p id="boot-splash-hint" class="boot-splash__hint" hidden>The first remote connection can take tens of seconds.</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div id="app"></div>
|
|
76
|
+
<script>
|
|
77
|
+
(() => {
|
|
78
|
+
const isJa = (navigator.language || "").toLowerCase().startsWith("ja");
|
|
79
|
+
const message = isJa ? "同じWi-Fi内のPCを確認中..." : "Checking your trusted Wi-Fi...";
|
|
80
|
+
const status = document.getElementById("boot-splash-status");
|
|
81
|
+
const splash = document.getElementById("boot-splash");
|
|
82
|
+
if (status) status.textContent = message;
|
|
83
|
+
if (splash) splash.setAttribute("aria-label", "viveworker " + message);
|
|
84
|
+
})();
|
|
85
|
+
</script>
|
|
86
|
+
<script type="module" src="${APP_SCRIPT_URL}"></script>
|
|
87
|
+
</body>
|
|
88
|
+
</html>`;
|
|
7
89
|
|
|
8
90
|
self.addEventListener("install", (event) => {
|
|
9
91
|
event.waitUntil(
|
|
@@ -44,12 +126,20 @@ self.addEventListener("fetch", (event) => {
|
|
|
44
126
|
}
|
|
45
127
|
|
|
46
128
|
if (APP_ROUTES.has(url.pathname)) {
|
|
47
|
-
event.respondWith(
|
|
129
|
+
event.respondWith(networkFirstWithFallback(event, "/app", {
|
|
130
|
+
timeoutMs: APP_NAVIGATION_NETWORK_TIMEOUT_MS,
|
|
131
|
+
fallbackAppShell: true,
|
|
132
|
+
}));
|
|
48
133
|
return;
|
|
49
134
|
}
|
|
50
135
|
|
|
51
136
|
if (CACHED_PATHS.has(url.pathname)) {
|
|
52
|
-
|
|
137
|
+
const cacheKey = cacheKeyForUrl(url);
|
|
138
|
+
event.respondWith(
|
|
139
|
+
NETWORK_FIRST_PATHS.has(url.pathname)
|
|
140
|
+
? networkFirstWithFallback(event, cacheKey, { timeoutMs: ASSET_NETWORK_TIMEOUT_MS })
|
|
141
|
+
: staleWhileRevalidate(event, cacheKey)
|
|
142
|
+
);
|
|
53
143
|
}
|
|
54
144
|
});
|
|
55
145
|
|
|
@@ -72,7 +162,17 @@ self.addEventListener("push", (event) => {
|
|
|
72
162
|
data: payload.data || { url: "/app" },
|
|
73
163
|
};
|
|
74
164
|
|
|
75
|
-
event.waitUntil(
|
|
165
|
+
event.waitUntil((async () => {
|
|
166
|
+
await self.registration.showNotification(title, options);
|
|
167
|
+
// Phase 2d: if the push is tagged as a remote-pairing wake hint, also
|
|
168
|
+
// broadcast a postMessage to open clients so any active transport
|
|
169
|
+
// instance can kick its reconnect immediately. Background-only pushes
|
|
170
|
+
// (no PWA window open) hit the notification path; the user's tap then
|
|
171
|
+
// foregrounds the PWA which kicks the transport via visibilitychange.
|
|
172
|
+
if (shouldBroadcastRemotePairingWake(payload)) {
|
|
173
|
+
await notifyClients("remote-pairing-wake", { reason: "push" });
|
|
174
|
+
}
|
|
175
|
+
})());
|
|
76
176
|
});
|
|
77
177
|
|
|
78
178
|
self.addEventListener("notificationclick", (event) => {
|
|
@@ -172,29 +272,38 @@ async function persistNotificationIntent(url) {
|
|
|
172
272
|
}
|
|
173
273
|
}
|
|
174
274
|
|
|
175
|
-
async function notifyClients(type) {
|
|
275
|
+
async function notifyClients(type, extra) {
|
|
176
276
|
const clients = await self.clients.matchAll({
|
|
177
277
|
type: "window",
|
|
178
278
|
includeUncontrolled: true,
|
|
179
279
|
});
|
|
280
|
+
// Spread `extra` after `type` so callers can't accidentally clobber the
|
|
281
|
+
// discriminator the receiving client uses to dispatch.
|
|
282
|
+
const message = extra ? { ...extra, type } : { type };
|
|
180
283
|
for (const client of clients) {
|
|
181
|
-
client.postMessage(
|
|
284
|
+
client.postMessage(message);
|
|
182
285
|
}
|
|
183
286
|
}
|
|
184
287
|
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
288
|
+
// Decide whether a Web Push payload is a remote-pairing wake hint. Both an
|
|
289
|
+
// explicit `data.kind === "remote-pairing-wake"` and a generic
|
|
290
|
+
// `data.kind === "remote-pairing-event"` count — the latter lets the bridge
|
|
291
|
+
// piggy-back on existing pushes (approval requests, etc.) without sending a
|
|
292
|
+
// dedicated wake-only notification.
|
|
293
|
+
function shouldBroadcastRemotePairingWake(payload) {
|
|
294
|
+
const kind = payload?.data?.kind;
|
|
295
|
+
return kind === "remote-pairing-wake" || kind === "remote-pairing-event";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function cacheKeyForUrl(url) {
|
|
299
|
+
if (VERSIONED_CACHE_PATHS.has(url.pathname) && url.search) {
|
|
300
|
+
return `${url.pathname}${url.search}`;
|
|
301
|
+
}
|
|
302
|
+
return url.pathname;
|
|
303
|
+
}
|
|
196
304
|
|
|
197
|
-
|
|
305
|
+
function fetchAndCache(event, cache, cacheKey) {
|
|
306
|
+
return fetch(event.request, { cache: "no-store" })
|
|
198
307
|
.then(async (response) => {
|
|
199
308
|
if (response && response.ok) {
|
|
200
309
|
await cache.put(cacheKey, response.clone());
|
|
@@ -202,6 +311,58 @@ async function staleWhileRevalidate(event, cacheKey) {
|
|
|
202
311
|
return response;
|
|
203
312
|
})
|
|
204
313
|
.catch(() => null);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function networkFirstWithFallback(event, cacheKey, options = {}) {
|
|
317
|
+
const cache = await caches.open(CACHE_NAME);
|
|
318
|
+
const cachedPromise = cache.match(cacheKey);
|
|
319
|
+
const networkPromise = fetchAndCache(event, cache, cacheKey);
|
|
320
|
+
const timeoutMs = Math.max(0, Number(options.timeoutMs ?? 0) || 0);
|
|
321
|
+
|
|
322
|
+
let response = null;
|
|
323
|
+
if (timeoutMs > 0) {
|
|
324
|
+
response = await Promise.race([
|
|
325
|
+
networkPromise,
|
|
326
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs)),
|
|
327
|
+
]);
|
|
328
|
+
} else {
|
|
329
|
+
response = await networkPromise;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (response) {
|
|
333
|
+
return response;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// If the network attempt is still pending after the timeout, keep the
|
|
337
|
+
// Service Worker alive so LAN updates are cached for the next launch.
|
|
338
|
+
event.waitUntil(networkPromise);
|
|
339
|
+
|
|
340
|
+
const cached = await cachedPromise;
|
|
341
|
+
if (cached) {
|
|
342
|
+
return cached;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (options.fallbackAppShell) {
|
|
346
|
+
return new Response(APP_SHELL_FALLBACK_HTML, {
|
|
347
|
+
status: 200,
|
|
348
|
+
headers: {
|
|
349
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
350
|
+
"Cache-Control": "no-store",
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const lateResponse = await networkPromise;
|
|
356
|
+
return lateResponse || Response.error();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Cache-first with background revalidation for non-critical assets. App entry
|
|
360
|
+
// points stay network-first so LAN refreshes can replace stale PWA code before
|
|
361
|
+
// the device goes back to relay-only mode.
|
|
362
|
+
async function staleWhileRevalidate(event, cacheKey) {
|
|
363
|
+
const cache = await caches.open(CACHE_NAME);
|
|
364
|
+
const cached = await cache.match(cacheKey);
|
|
365
|
+
const networkPromise = fetchAndCache(event, cache, cacheKey);
|
|
205
366
|
|
|
206
367
|
if (cached) {
|
|
207
368
|
// Keep the SW alive long enough to persist the background refresh even
|
|
@@ -214,5 +375,14 @@ async function staleWhileRevalidate(event, cacheKey) {
|
|
|
214
375
|
if (response) {
|
|
215
376
|
return response;
|
|
216
377
|
}
|
|
378
|
+
if (cacheKey === "/app") {
|
|
379
|
+
return new Response(APP_SHELL_FALLBACK_HTML, {
|
|
380
|
+
status: 200,
|
|
381
|
+
headers: {
|
|
382
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
383
|
+
"Cache-Control": "no-store",
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
}
|
|
217
387
|
return Response.error();
|
|
218
388
|
}
|