roxxie-proxy-u-prod 0.1.0 → 0.2.2

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 (47) hide show
  1. package/README.md +62 -74
  2. package/dist/assets/{index-DcvPYWS3.js → index-CttvagWK.js} +164 -172
  3. package/dist/assets/index-EeRhLAof.css +1 -0
  4. package/dist/assets/{inject-BnVzMVpl.js → inject-xtH4w5z9.js} +8 -5
  5. package/dist/favicon.png +0 -0
  6. package/embed.js +2 -1
  7. package/examples/single-page.html +3 -1
  8. package/package.json +43 -41
  9. package/runtime/index.html +5 -4
  10. package/scripts/build.mjs +11 -5
  11. package/scripts/smoke.mjs +264 -73
  12. package/scripts/verify.mjs +100 -1
  13. package/source/packages/chrome/index.html +3 -2
  14. package/source/packages/chrome/public/favicon.png +0 -0
  15. package/source/packages/chrome/src/Tab/Tab.tsx +1 -1
  16. package/source/packages/chrome/src/assets/favicon.ts +1 -1
  17. package/source/packages/chrome/src/components/Button.tsx +11 -22
  18. package/source/packages/chrome/src/components/Checkbox.tsx +8 -12
  19. package/source/packages/chrome/src/components/Input.tsx +3 -4
  20. package/source/packages/chrome/src/components/StartScreen.tsx +34 -49
  21. package/source/packages/chrome/src/components/TopSiteButton.tsx +2 -2
  22. package/source/packages/chrome/src/pages/NewTabPage.tsx +42 -10
  23. package/source/packages/chrome/src/pages/SettingsPage.tsx +10 -11
  24. package/source/packages/chrome/src/runtime-base.ts +1 -1
  25. package/source/packages/chrome/src/services/SettingsService.ts +1 -1
  26. package/source/packages/chrome/src/style.css +7 -7
  27. package/source/packages/chrome/src/themes.ts +21 -21
  28. package/source/packages/inject/src/errorpage/errorpage.css +22 -35
  29. package/source/packages/inject/src/errorpage/errorpage.ts +6 -3
  30. package/source/packages/roxxie-transport/package.json +1 -1
  31. package/source/packages/roxxie-transport/src/connection.ts +1 -1
  32. package/source/packages/roxxie-transport/src/transport.ts +2 -2
  33. package/source/packages/scramjet/packages/core/dist/scramjet.js +1 -1
  34. package/source/packages/scramjet/packages/core/dist/scramjet.mjs +1 -1
  35. package/source/packages/scramjet/packages/core/dist/scramjet_bundled.js +1 -1
  36. package/source/packages/scramjet/packages/core/dist/scramjet_bundled.mjs +1 -1
  37. package/source/packages/tracker-protocol/package.json +1 -1
  38. package/source/packages/tracker-protocol/src/index.ts +34 -10
  39. package/source/pnpm-lock.yaml +4 -1
  40. package/standalone.html +27 -0
  41. package/dist/assets/index-BdRlQ7d_.css +0 -1
  42. package/dist/icon.png +0 -0
  43. package/dist/roxxie-icon.svg +0 -12
  44. package/source/packages/chrome/public/icon.png +0 -0
  45. package/source/packages/chrome/public/roxxie-icon.svg +0 -12
  46. package/source/packages/scramjet/packages/core/dist/temp-types-build/index.js +0 -23
  47. package/source/packages/scramjet/packages/core/dist/temp-types-build/index.js.map +0 -1
Binary file
package/embed.js CHANGED
@@ -2,7 +2,8 @@
2
2
  (function installRoxxieProxyU(global, document) {
3
3
  "use strict";
4
4
 
5
- var DEFAULT_SHELL_URL = "https://100.58.76.81/browser/";
5
+ var DEFAULT_SHELL_URL =
6
+ "https://unpkg.com/roxxie-proxy-u-prod@0.2.2/runtime/index.html";
6
7
  var loadingScript = document.currentScript;
7
8
 
8
9
  function resolveContainer(target) {
@@ -18,7 +18,9 @@
18
18
  <div id="roxxie-browser"></div>
19
19
  <script
20
20
  defer
21
- src="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.1.0/embed.js"
21
+ src="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.2.2/embed.js"
22
+ integrity="sha384-3QVF5CQiWk0SKqCDSEAoupN0KXK0bxo9qaZeS5Tj8/8mO8J719dv+fvmrqmdthH2"
23
+ crossorigin="anonymous"
22
24
  data-roxxie-target="#roxxie-browser"
23
25
  ></script>
24
26
  </body>
package/package.json CHANGED
@@ -1,42 +1,44 @@
1
1
  {
2
- "name": "roxxie-proxy-u-prod",
3
- "version": "0.1.0",
4
- "description": "Production CDN assets and embed loader for Roxxie Proxy U",
5
- "license": "AGPL-3.0-only",
6
- "private": false,
7
- "main": "embed.js",
8
- "browser": "embed.js",
9
- "jsdelivr": "embed.js",
10
- "unpkg": "embed.js",
11
- "files": [
12
- "dist",
13
- "runtime",
14
- "examples",
15
- "scripts",
16
- "source",
17
- "third_party",
18
- "embed.js",
19
- "LICENSE",
20
- "SOURCE.md",
21
- "THIRD_PARTY_NOTICES.md",
22
- "README.md"
23
- ],
24
- "publishConfig": {
25
- "access": "public"
26
- },
27
- "devDependencies": {
28
- "playwright": "1.59.1"
29
- },
30
- "keywords": [
31
- "roxxie",
32
- "browser",
33
- "proxy",
34
- "webrtc",
35
- "embed"
36
- ],
37
- "scripts": {
38
- "build": "node scripts/build.mjs",
39
- "smoke": "node scripts/smoke.mjs",
40
- "verify": "node scripts/verify.mjs"
41
- }
42
- }
2
+ "name": "roxxie-proxy-u-prod",
3
+ "version": "0.2.2",
4
+ "description": "Production CDN assets and embed loader for Roxxie Proxy U",
5
+ "license": "AGPL-3.0-only",
6
+ "private": false,
7
+ "main": "embed.js",
8
+ "browser": "embed.js",
9
+ "jsdelivr": "embed.js",
10
+ "unpkg": "embed.js",
11
+ "files": [
12
+ "dist",
13
+ "runtime",
14
+ "examples",
15
+ "scripts",
16
+ "source",
17
+ "third_party",
18
+ "embed.js",
19
+ "standalone.html",
20
+ "LICENSE",
21
+ "SOURCE.md",
22
+ "THIRD_PARTY_NOTICES.md",
23
+ "README.md"
24
+ ],
25
+ "scripts": {
26
+ "build": "node scripts/build.mjs",
27
+ "smoke": "node scripts/smoke.mjs",
28
+ "verify": "node scripts/verify.mjs",
29
+ "prepack": "npm run verify"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "devDependencies": {
35
+ "playwright": "1.59.1"
36
+ },
37
+ "keywords": [
38
+ "roxxie",
39
+ "browser",
40
+ "proxy",
41
+ "webrtc",
42
+ "embed"
43
+ ]
44
+ }
@@ -2,16 +2,17 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.1.0/dist/roxxie-icon.svg" />
5
+ <link rel="icon" type="image/png" sizes="any" href="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.2.2/dist/favicon.png" />
6
+ <meta name="theme-color" content="#222230" />
6
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
8
  <title>Roxxie Proxy U</title>
8
9
 
9
10
  <!--ssr-head-->
10
- <script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.1.0/dist/assets/index-DcvPYWS3.js"></script>
11
- <link rel="stylesheet" crossorigin href="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.1.0/dist/assets/index-BdRlQ7d_.css">
11
+ <script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.2.2/dist/assets/index-CttvagWK.js"></script>
12
+ <link rel="stylesheet" crossorigin href="https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.2.2/dist/assets/index-EeRhLAof.css">
12
13
  </head>
13
14
 
14
- <body class="light-mode">
15
+ <body>
15
16
  <!--ssr-body-->
16
17
  <div id="app"></div>
17
18
  </body>
package/scripts/build.mjs CHANGED
@@ -44,12 +44,14 @@ const thirdPartyRoot = join(packageRoot, "third_party");
44
44
  const manifest = JSON.parse(
45
45
  await readFile(join(packageRoot, "package.json"), "utf8")
46
46
  );
47
+ const packageSpec = `${manifest.name}@${manifest.version}`;
47
48
 
48
49
  const trackerUrl = process.env.ROXXIE_TRACKER_URL ?? "https://100.58.76.81/";
49
- const runtimeBase = process.env.ROXXIE_RUNTIME_BASE ?? "/browser/";
50
+ const expectedRuntimeBase = `/${packageSpec}/runtime/`;
51
+ const runtimeBase = process.env.ROXXIE_RUNTIME_BASE ?? expectedRuntimeBase;
50
52
  const cdnBase =
51
53
  process.env.ROXXIE_CDN_BASE ??
52
- `https://cdn.jsdelivr.net/npm/${manifest.name}@${manifest.version}/dist/`;
54
+ `https://cdn.jsdelivr.net/npm/${packageSpec}/dist/`;
53
55
 
54
56
  function validateConfiguration() {
55
57
  const tracker = new URL(trackerUrl);
@@ -66,8 +68,10 @@ function validateConfiguration() {
66
68
  "ROXXIE_TRACKER_URL must be the production origin https://100.58.76.81/"
67
69
  );
68
70
  }
69
- if (!runtimeBase.startsWith("/") || !runtimeBase.endsWith("/")) {
70
- throw new Error("ROXXIE_RUNTIME_BASE must be an absolute path ending in /");
71
+ if (runtimeBase !== expectedRuntimeBase) {
72
+ throw new Error(
73
+ `ROXXIE_RUNTIME_BASE must be the versioned unpkg path ${expectedRuntimeBase}`
74
+ );
71
75
  }
72
76
  if (!cdnBase.startsWith("https://") || !cdnBase.endsWith("/")) {
73
77
  throw new Error("ROXXIE_CDN_BASE must be an HTTPS URL ending in /");
@@ -338,7 +342,9 @@ if (!buildingFromPackagedSource) {
338
342
  }
339
343
  console.log(`Built ${manifest.name}@${manifest.version}`);
340
344
  console.log(`CDN assets: ${outputRoot}`);
341
- console.log(`Same-origin runtime: ${runtimeRoot}`);
345
+ console.log(
346
+ `Same-origin runtime: https://unpkg.com/${packageSpec}/runtime/index.html (${runtimeRoot})`
347
+ );
342
348
  console.log(
343
349
  `Build source: ${buildingFromPackagedSource ? "packaged source/ workspace" : "repository checkout"}`
344
350
  );
package/scripts/smoke.mjs CHANGED
@@ -1,16 +1,33 @@
1
1
  /* SPDX-License-Identifier: AGPL-3.0-only */
2
2
 
3
3
  import assert from "node:assert/strict";
4
+ import { createHash } from "node:crypto";
4
5
  import { createServer } from "node:http";
5
6
  import { readFile } from "node:fs/promises";
6
- import { dirname, extname, join, normalize, resolve } from "node:path";
7
- import { fileURLToPath } from "node:url";
7
+ import { dirname, extname, join, normalize, resolve, sep } from "node:path";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
9
  import { chromium } from "playwright";
9
10
 
10
11
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
11
12
  const runtimeRoot = join(packageRoot, "runtime");
12
13
  const distRoot = join(packageRoot, "dist");
13
- const cdnPath = "/npm/roxxie-proxy-u-prod@0.1.0/dist/";
14
+ const standalonePath = join(packageRoot, "standalone.html");
15
+ const manifest = JSON.parse(
16
+ await readFile(join(packageRoot, "package.json"), "utf8")
17
+ );
18
+ const packageSpec = `${manifest.name}@${manifest.version}`;
19
+ const cdnRoot = `https://cdn.jsdelivr.net/npm/${packageSpec}/`;
20
+ const cdnPath = `/npm/${packageSpec}/`;
21
+ const cdnDistRoot = `${cdnRoot}dist/`;
22
+ const unpkgRuntimeRoot = `https://unpkg.com/${packageSpec}/runtime/`;
23
+ const unpkgRuntimePath = `/${packageSpec}/runtime/`;
24
+ const shellUrl = `${unpkgRuntimeRoot}index.html`;
25
+ const workerUrl = `${unpkgRuntimeRoot}localcontrollersw.js`;
26
+ const workerScope = `${unpkgRuntimeRoot}~/sj/`;
27
+ const embedBytes = await readFile(join(packageRoot, "embed.js"));
28
+ const embedIntegrity = `sha384-${createHash("sha384")
29
+ .update(embedBytes)
30
+ .digest("base64")}`;
14
31
 
15
32
  const contentTypes = {
16
33
  ".bin": "application/octet-stream",
@@ -25,99 +42,273 @@ const contentTypes = {
25
42
 
26
43
  function safePath(root, relative) {
27
44
  const resolved = resolve(root, normalize(relative));
28
- if (resolved !== root && !resolved.startsWith(root + "/")) {
45
+ if (resolved !== root && !resolved.startsWith(root + sep)) {
29
46
  throw new Error("Invalid test path");
30
47
  }
31
48
  return resolved;
32
49
  }
33
50
 
34
- async function sendFile(response, path) {
35
- try {
36
- const body = await readFile(path);
37
- response.writeHead(200, {
38
- "content-type": contentTypes[extname(path)] ?? "application/octet-stream",
39
- });
40
- response.end(body);
41
- } catch {
42
- response.writeHead(404).end("Not found");
43
- }
51
+ async function fulfillFile(route, path) {
52
+ await route.fulfill({
53
+ body: await readFile(path),
54
+ contentType: contentTypes[extname(path)] ?? "application/octet-stream",
55
+ headers: {
56
+ "access-control-allow-origin": "*",
57
+ "cache-control": "public, max-age=31536000, immutable",
58
+ "x-content-type-options": "nosniff",
59
+ },
60
+ });
61
+ }
62
+
63
+ async function installRoutes(context, hits) {
64
+ await context.route(`${cdnRoot}**`, async (route) => {
65
+ const url = new URL(route.request().url());
66
+ assert.equal(url.origin, "https://cdn.jsdelivr.net");
67
+ assert(url.pathname.startsWith(cdnPath));
68
+ const relative = url.pathname.slice(cdnPath.length);
69
+ const path =
70
+ relative === "embed.js"
71
+ ? join(packageRoot, "embed.js")
72
+ : relative.startsWith("dist/")
73
+ ? safePath(distRoot, relative.slice("dist/".length))
74
+ : undefined;
75
+ assert(path, `Unexpected jsDelivr package request: ${url.href}`);
76
+ hits.add(url.href);
77
+ await fulfillFile(route, path);
78
+ });
79
+
80
+ await context.route(`${unpkgRuntimeRoot}**`, async (route) => {
81
+ const url = new URL(route.request().url());
82
+ assert.equal(url.origin, "https://unpkg.com");
83
+ assert(url.pathname.startsWith(unpkgRuntimePath));
84
+ const relative = url.pathname.slice(unpkgRuntimePath.length);
85
+ assert(relative.length > 0, `Unexpected unpkg request: ${url.href}`);
86
+ hits.add(url.href);
87
+ await fulfillFile(route, safePath(runtimeRoot, relative));
88
+ });
89
+ }
90
+
91
+ async function registerRuntimeWorker(frame, scriptURL, scope) {
92
+ return frame.evaluate(
93
+ async ({ scriptURL, scope }) => {
94
+ if (!("serviceWorker" in navigator)) {
95
+ throw new Error("Service workers are unavailable in the unpkg shell");
96
+ }
97
+ const registration = await navigator.serviceWorker.register(scriptURL, {
98
+ scope,
99
+ });
100
+ let worker =
101
+ registration.installing ?? registration.waiting ?? registration.active;
102
+ if (!worker) throw new Error("Service worker registration has no worker");
103
+ if (worker.state !== "activated") {
104
+ await new Promise((resolveActivation, rejectActivation) => {
105
+ const timeout = setTimeout(
106
+ () =>
107
+ rejectActivation(
108
+ new Error("Service worker activation timed out")
109
+ ),
110
+ 10_000
111
+ );
112
+ worker.addEventListener("statechange", () => {
113
+ if (worker?.state !== "activated") return;
114
+ clearTimeout(timeout);
115
+ resolveActivation();
116
+ });
117
+ });
118
+ }
119
+ worker = registration.active ?? worker;
120
+ return {
121
+ scriptURL: worker.scriptURL,
122
+ scope: registration.scope,
123
+ secureContext: globalThis.isSecureContext,
124
+ };
125
+ },
126
+ { scriptURL, scope }
127
+ );
44
128
  }
45
129
 
46
- const server = createServer(async (request, response) => {
130
+ const standalone = await readFile(standalonePath);
131
+ const hostServer = createServer(async (request, response) => {
47
132
  const path = new URL(request.url ?? "/", "http://localhost").pathname;
48
- if (path === "/browser/" || path === "/browser/index.html") {
49
- await sendFile(response, join(runtimeRoot, "index.html"));
50
- return;
51
- }
52
- if (path.startsWith("/browser/")) {
53
- await sendFile(response, safePath(runtimeRoot, path.slice(9)));
54
- return;
55
- }
56
- if (path === "/embed.js") {
57
- await sendFile(response, join(packageRoot, "embed.js"));
58
- return;
59
- }
60
- if (path === "/embed-test/") {
133
+ if (path === "/" || path === "/standalone.html") {
61
134
  response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
62
- response.end(`<!doctype html>
63
- <div id="target" style="width:800px;height:600px"></div>
64
- <script src="/embed.js" data-roxxie-target="#target"
65
- data-roxxie-src="/browser/"></script>`);
135
+ response.end(standalone);
66
136
  return;
67
137
  }
138
+ if (path.startsWith(unpkgRuntimePath)) {
139
+ const relative = path.slice(unpkgRuntimePath.length);
140
+ if (relative.length === 0) {
141
+ response.writeHead(308, { location: `${path}index.html` }).end();
142
+ return;
143
+ }
144
+ try {
145
+ const file = safePath(runtimeRoot, relative);
146
+ response.writeHead(200, {
147
+ "content-type":
148
+ contentTypes[extname(file)] ?? "application/octet-stream",
149
+ "cache-control": "no-cache",
150
+ "x-content-type-options": "nosniff",
151
+ });
152
+ response.end(await readFile(file));
153
+ return;
154
+ } catch {
155
+ response.writeHead(404).end("Not found");
156
+ return;
157
+ }
158
+ }
68
159
  response.writeHead(404).end("Not found");
69
160
  });
70
-
71
161
  await new Promise((resolveListen) =>
72
- server.listen(0, "127.0.0.1", resolveListen)
162
+ hostServer.listen(0, "127.0.0.1", resolveListen)
73
163
  );
74
- const address = server.address();
75
- assert(address && typeof address === "object");
76
- const origin = `http://127.0.0.1:${address.port}`;
164
+ const hostAddress = hostServer.address();
165
+ assert(hostAddress && typeof hostAddress === "object");
166
+ const hostOrigin = `http://127.0.0.1:${hostAddress.port}`;
77
167
 
78
168
  const browser = await chromium.launch({ headless: true });
79
169
  try {
80
- const context = await browser.newContext();
81
- await context.route(
82
- "https://cdn.jsdelivr.net/npm/roxxie-proxy-u-prod@0.1.0/dist/**",
83
- async (route) => {
84
- const url = new URL(route.request().url());
85
- assert(url.pathname.startsWith(cdnPath));
86
- const path = safePath(distRoot, url.pathname.slice(cdnPath.length));
87
- const body = await readFile(path);
88
- await route.fulfill({
89
- body,
90
- contentType: contentTypes[extname(path)] ?? "application/octet-stream",
91
- headers: { "access-control-allow-origin": "*" },
92
- });
93
- }
94
- );
170
+ for (const [label, hostUrl] of [
171
+ ["file", pathToFileURL(standalonePath).href],
172
+ ["loopback HTTP", `${hostOrigin}/`],
173
+ ]) {
174
+ const context = await browser.newContext({ serviceWorkers: "allow" });
175
+ const hits = new Set();
176
+ await installRoutes(context, hits);
177
+ const page = await context.newPage();
178
+ const pageErrors = [];
179
+ const failedRequests = [];
180
+ page.on("pageerror", (error) => pageErrors.push(error.message));
181
+ page.on("requestfailed", (request) =>
182
+ failedRequests.push(
183
+ `${request.url()}: ${request.failure()?.errorText ?? "failed"}`
184
+ )
185
+ );
95
186
 
96
- const page = await context.newPage();
97
- const pageErrors = [];
98
- page.on("pageerror", (error) => pageErrors.push(error));
99
- await page.goto(`${origin}/browser/`);
100
- await page.getByRole("button", { name: "Start" }).waitFor();
101
- assert.equal(pageErrors.length, 0);
102
- assert.equal(
103
- await page.locator('script[src^="https://cdn.jsdelivr.net/"]').count(),
104
- 1
105
- );
187
+ await page.goto(hostUrl);
188
+ const embedded = page.locator("iframe[data-roxxie-proxy-u]");
189
+ await embedded.waitFor();
190
+ assert.equal(await embedded.getAttribute("src"), shellUrl);
191
+ const launcher = page.locator(`script[src="${cdnRoot}embed.js"]`);
192
+ assert.equal(await launcher.getAttribute("integrity"), embedIntegrity);
193
+ assert.equal(await launcher.getAttribute("crossorigin"), "anonymous");
194
+ await page
195
+ .frameLocator("iframe[data-roxxie-proxy-u]")
196
+ .getByRole("button", { name: "Start" })
197
+ .waitFor();
198
+ const runtimeFrame = page
199
+ .frames()
200
+ .find((frame) => frame.url() === shellUrl);
201
+ assert(runtimeFrame, "The unpkg runtime frame was not created");
202
+ const productMark = runtimeFrame.locator("img.product-mark");
203
+ await productMark.waitFor();
204
+ assert.equal(
205
+ await productMark.getAttribute("src"),
206
+ `${cdnDistRoot}favicon.png`
207
+ );
208
+ const brandedStyles = await runtimeFrame.evaluate(() => {
209
+ const startScreen = document.querySelector(".start-screen");
210
+ const panel = document.querySelector(".start-panel");
211
+ const action = document.querySelector(".start-button");
212
+ if (!(startScreen instanceof HTMLElement)) {
213
+ throw new Error("Start screen is missing");
214
+ }
215
+ if (!(panel instanceof HTMLElement)) {
216
+ throw new Error("Start panel is missing");
217
+ }
218
+ if (!(action instanceof HTMLElement)) {
219
+ throw new Error("Start action is missing");
220
+ }
221
+ return {
222
+ background: getComputedStyle(startScreen).backgroundColor,
223
+ panel: getComputedStyle(panel).backgroundColor,
224
+ action: getComputedStyle(action).backgroundColor,
225
+ panelShadow: getComputedStyle(panel).boxShadow,
226
+ actionShadow: getComputedStyle(action).boxShadow,
227
+ };
228
+ });
229
+ assert.deepEqual(brandedStyles, {
230
+ background: "rgb(17, 17, 27)",
231
+ panel: "rgb(34, 34, 48)",
232
+ action: "rgb(235, 69, 139)",
233
+ panelShadow: "none",
234
+ actionShadow: "none",
235
+ });
106
236
 
107
- await page.goto(`${origin}/embed-test/`);
108
- const embedded = page.locator("iframe[data-roxxie-proxy-u]");
109
- await embedded.waitFor();
110
- await page
111
- .frameLocator("iframe[data-roxxie-proxy-u]")
112
- .getByRole("button", { name: "Start" })
113
- .waitFor();
114
- assert.equal(await embedded.count(), 1);
115
- assert.equal(pageErrors.length, 0);
237
+ const network = await runtimeFrame.evaluate(async () => {
238
+ const response = await fetch(
239
+ new URL("roxxie-network.json", location.href),
240
+ {
241
+ cache: "no-store",
242
+ }
243
+ );
244
+ if (!response.ok)
245
+ throw new Error(`Network document returned ${response.status}`);
246
+ return response.json();
247
+ });
248
+ assert.equal(network?.tracker?.url, "https://100.58.76.81/");
116
249
 
117
- console.log("Production shell and one-page embed smoke test passed");
250
+ assert(hits.has(`${cdnRoot}embed.js`));
251
+ assert(hits.has(shellUrl));
252
+ assert(hits.has(`${unpkgRuntimeRoot}roxxie-network.json`));
253
+ assert([...hits].some((url) => url.startsWith(cdnDistRoot)));
254
+ assert.deepEqual(pageErrors, []);
255
+ assert.deepEqual(failedRequests, []);
256
+ await context.close();
257
+ console.log(`${label} standalone embed passed`);
258
+ }
259
+
260
+ // Service-worker script requests intentionally bypass Playwright routing.
261
+ // Exercise the same versioned pathname and scope on a real loopback origin;
262
+ // the embed checks above independently pin their production unpkg origin.
263
+ const runtimeContext = await browser.newContext({ serviceWorkers: "allow" });
264
+ const runtimeHits = new Set();
265
+ await installRoutes(runtimeContext, runtimeHits);
266
+ const runtimePage = await runtimeContext.newPage();
267
+ const localRuntimeRoot = `${hostOrigin}${unpkgRuntimePath}`;
268
+ await runtimePage.goto(`${localRuntimeRoot}index.html`);
269
+ await runtimePage.getByRole("button", { name: "Start" }).waitFor();
270
+ const localNetwork = await runtimePage.evaluate(async () => {
271
+ const response = await fetch(
272
+ new URL("roxxie-network.json", location.href),
273
+ { cache: "no-store" }
274
+ );
275
+ if (!response.ok)
276
+ throw new Error(`Network document returned ${response.status}`);
277
+ return response.json();
278
+ });
279
+ assert.equal(localNetwork?.tracker?.url, "https://100.58.76.81/");
280
+
281
+ const localWorkerUrl = `${localRuntimeRoot}localcontrollersw.js`;
282
+ const localWorkerScope = `${localRuntimeRoot}~/sj/`;
283
+ const localRegistration = await registerRuntimeWorker(
284
+ runtimePage.mainFrame(),
285
+ localWorkerUrl,
286
+ localWorkerScope
287
+ );
288
+ assert.equal(localRegistration.secureContext, true);
289
+ assert.equal(localRegistration.scriptURL, localWorkerUrl);
290
+ assert.equal(localRegistration.scope, localWorkerScope);
291
+ assert.equal(
292
+ new URL(localRegistration.scriptURL).pathname,
293
+ new URL(workerUrl).pathname
294
+ );
295
+ assert.equal(
296
+ new URL(localRegistration.scope).pathname,
297
+ new URL(workerScope).pathname
298
+ );
299
+ await runtimePage.evaluate(async () => {
300
+ for (const registration of await navigator.serviceWorker.getRegistrations()) {
301
+ await registration.unregister();
302
+ }
303
+ });
304
+ await runtimeContext.close();
305
+ console.log("Versioned same-origin runtime service worker passed");
306
+ console.log(
307
+ "Pinned unpkg runtime, service worker, and tracker config passed"
308
+ );
118
309
  } finally {
119
310
  await browser.close();
120
311
  await new Promise((resolveClose, rejectClose) =>
121
- server.close((error) => (error ? rejectClose(error) : resolveClose()))
312
+ hostServer.close((error) => (error ? rejectClose(error) : resolveClose()))
122
313
  );
123
314
  }