@jsenv/core 41.5.35 → 41.5.38

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.
@@ -1,4 +1,5 @@
1
1
  import { WebSocketResponse, pickContentType, ServerEvents, serverPluginErrorHandler, fetchDirectory, composeTwoResponses, serverPluginCORS, jsenvAccessControlAllowedHeaders, startServer } from "@jsenv/server";
2
+ import { randomUUID, X509Certificate } from "node:crypto";
2
3
  import { existsSync, statSync, readFileSync, realpathSync, readdirSync, lstatSync } from "node:fs";
3
4
  import { pathToFileURL } from "node:url";
4
5
  import { urlToRelativeUrl, registerFileLifecycle, lookupPackageDirectory, readPackageAtOrNull, generateContentFrame, errorToHTML, URL_META, asUrlWithoutSearch, DATA_URL, CONTENT_TYPE, normalizeImportMap, composeTwoImportMaps, resolveImport, createDetailedMessage, UNICODE, JS_QUOTES, urlToExtension, urlToBasename, applyNodeEsmResolution, readCustomConditionsFromProcessArgs, urlIsOrIsInsideOf, collectFiles, registerDirectoryLifecycle, readEntryStatSync, applyFileSystemMagicResolution, getExtensionsToTry, urlToFilename, ensurePathnameTrailingSlash, compareFileUrls, setUrlExtension, stringifyUrlSite, injectQueryParamsIntoSpecifier, isSpecifierForNodeBuiltin, injectQueryParams, urlToFileSystemPath, writeFileSync, moveUrl, ensureWindowsDriveLetter, validateResponseIntegrity, setUrlFilename, getCallerPosition, asSpecifierWithoutSearch, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, createLogger, normalizeUrl, ANSI, RUNTIME_COMPAT, formatError, assertAndNormalizeDirectoryUrl, browserDefaultRuntimeCompat, inferRuntimeCompatFromClosestPackage, createTaskLog } from "./jsenv_core_packages.js";
@@ -8,7 +9,6 @@ import { createMagicSource, composeTwoSourcemaps, generateSourcemapFileUrl, gene
8
9
  import { jsenvPluginSupervisor } from "@jsenv/plugin-supervisor";
9
10
  import { jsenvPluginTranspilation } from "@jsenv/plugin-transpilation";
10
11
  import { bundleJsModules } from "@jsenv/plugin-bundling";
11
- import { randomUUID } from "node:crypto";
12
12
  import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/plugins/filesystem/filesystem_error_to_response.js";
13
13
  import "./jsenv_core_node_modules.js";
14
14
  import "node:process";
@@ -982,15 +982,15 @@ const runtimeFromRequest = (request) => {
982
982
  };
983
983
 
984
984
  const clientReporterFileUrl = new URL(
985
- "../js/client_reporter.js",
985
+ "../client/client_monitoring/client_reporter.js",
986
986
  import.meta.url,
987
987
  ).href;
988
988
  const clientsPageFileUrl = new URL(
989
- "../html/clients_page.html",
989
+ "../client/client_monitoring/clients_page.html",
990
990
  import.meta.url,
991
991
  ).href;
992
992
  const clientMonitorPageFileUrl = new URL(
993
- "../html/client_monitor_page.html",
993
+ "../client/client_monitoring/client_monitor_page.html",
994
994
  import.meta.url,
995
995
  ).href;
996
996
 
@@ -1549,7 +1549,7 @@ const jsenvPluginClientMonitoring = () => {
1549
1549
  */
1550
1550
 
1551
1551
 
1552
- const clientFileUrl$1 = new URL("../js/page_switcher.js", import.meta.url)
1552
+ const clientFileUrl$1 = new URL("../client/page_switcher/page_switcher.js", import.meta.url)
1553
1553
  .href;
1554
1554
 
1555
1555
  const jsenvPluginPageSwitcher = () => {
@@ -5136,7 +5136,7 @@ ${reason}`,
5136
5136
  return createFailedToFetchUrlContentError({
5137
5137
  code: "NOT_FOUND",
5138
5138
  reason: "no entry on filesystem",
5139
- ...detailsFromInjectionsOnOwner(urlInfo.firstReference),
5139
+ ...detailsFromHtmlUrlNotFound(urlInfo.firstReference),
5140
5140
  });
5141
5141
  }
5142
5142
  }
@@ -5396,6 +5396,31 @@ const detailsFromInjectionsOnOwner = (reference) => {
5396
5396
  };
5397
5397
  };
5398
5398
 
5399
+ // An url of an html attribute leading to no file may be meant for a server rather
5400
+ // than for jsenv: a route of a backend, which "jsenv-ignore" leaves alone. Only the
5401
+ // build is concerned: in dev the browser requests the url whatever jsenv does with it.
5402
+ const detailsFromHtmlUrlNotFound = (reference) => {
5403
+ const detailsFromInjections = detailsFromInjectionsOnOwner(reference);
5404
+ if (detailsFromInjections.suggestion) {
5405
+ return detailsFromInjections;
5406
+ }
5407
+ if (!reference) {
5408
+ return {};
5409
+ }
5410
+ const ownerUrlInfo = reference.ownerUrlInfo;
5411
+ if (ownerUrlInfo.type !== "html" || !ownerUrlInfo.context.build) {
5412
+ return {};
5413
+ }
5414
+ const { node, attributeName } = reference.astInfo || {};
5415
+ if (!node || !attributeName) {
5416
+ return {};
5417
+ }
5418
+ return {
5419
+ suggestion: `when "${reference.specifier}" is answered by a server rather than a file, add "jsenv-ignore" so the build leaves that url alone:
5420
+ <${node.nodeName} jsenv-ignore ${attributeName}="${reference.specifier}" />`,
5421
+ };
5422
+ };
5423
+
5399
5424
  const detailsFromPluginController = (jsenvPluginsController) => {
5400
5425
  const currentPlugin = jsenvPluginsController.getCurrentPlugin();
5401
5426
  if (!currentPlugin) {
@@ -7721,7 +7746,7 @@ const STATIC_IMPORT_SUBTYPES = ["import_static", "export_named", "export_all"];
7721
7746
  */
7722
7747
 
7723
7748
 
7724
- const clientFileUrl = import.meta.resolve("../js/dependency_status.js");
7749
+ const clientFileUrl = import.meta.resolve("../client/dependency_status/dependency_status.js");
7725
7750
 
7726
7751
  const jsenvPluginDependencyStatus = ({
7727
7752
  dependencyProblemEventEmitter,
@@ -8602,6 +8627,99 @@ const devServerPluginChromeDevToolsJson = ({ sourceDirectoryUrl }) => {
8602
8627
  };
8603
8628
  };
8604
8629
 
8630
+ /*
8631
+ * Lets another device (a phone) trust the root certificate that signs the dev
8632
+ * server certificate, so it opens the dev server over https without a warning.
8633
+ *
8634
+ * Everything under HTTPS_TRUST_PATHNAME is served over plain http too (see the
8635
+ * redirectHttpToHttps passed by startDevServer): it is the one page that must
8636
+ * load before the certificate is trusted, over https it would show the very
8637
+ * warning it exists to remove. Only the certificate is served, never its key.
8638
+ *
8639
+ * The page is sent as written, not cooked like the project pages: it needs
8640
+ * nothing cooking injects, and a project plugin rewriting html must not be able
8641
+ * to break the one page reached before trust.
8642
+ *
8643
+ * What it means for the device, and why the fingerprint must be compared with
8644
+ * the terminal: docs/users/b_dev/b_dev.md, "Trusting it on a phone".
8645
+ */
8646
+
8647
+
8648
+ const HTTPS_TRUST_PATHNAME = "/.internal/https/";
8649
+
8650
+ const httpsTrustPageFileUrl = new URL(
8651
+ "../client/https_trust/https_trust.html",
8652
+ import.meta.url,
8653
+ );
8654
+
8655
+ // rootCertificate is a node:crypto X509Certificate
8656
+ const devServerPluginHttpsTrust = ({ rootCertificate }) => {
8657
+ const commonName = readCommonName(rootCertificate.subject);
8658
+ const rootCertificateInfo = {
8659
+ fingerprint256: rootCertificate.fingerprint256,
8660
+ commonName,
8661
+ };
8662
+ // "https local root certificate" -> "https_local_root_certificate.crt"
8663
+ const rootCertificateFilename = `${commonName.replace(/[^\w.-]+/g, "_")}.crt`;
8664
+
8665
+ return {
8666
+ name: "jsenv:https_trust",
8667
+ routes: [
8668
+ {
8669
+ endpoint: `GET ${HTTPS_TRUST_PATHNAME}`,
8670
+ description:
8671
+ "Page to trust the dev server https on another device (a phone). Served over http.",
8672
+ declarationSource: import.meta.url,
8673
+ fetch: () => {
8674
+ return new Response(readFileSync(httpsTrustPageFileUrl), {
8675
+ headers: { "content-type": "text/html" },
8676
+ });
8677
+ },
8678
+ },
8679
+ {
8680
+ endpoint: `GET ${HTTPS_TRUST_PATHNAME}root.crt`,
8681
+ description: "The root certificate signing the dev server certificate.",
8682
+ declarationSource: import.meta.url,
8683
+ fetch: () => {
8684
+ return new Response(rootCertificate.toString(), {
8685
+ headers: {
8686
+ "content-type": "application/x-x509-ca-cert",
8687
+ "content-disposition": `attachment; filename="${rootCertificateFilename}"`,
8688
+ },
8689
+ });
8690
+ },
8691
+ },
8692
+ {
8693
+ endpoint: `GET ${HTTPS_TRUST_PATHNAME}root.json`,
8694
+ description: "Fingerprint and name of the root certificate.",
8695
+ declarationSource: import.meta.url,
8696
+ fetch: () => {
8697
+ return Response.json(rootCertificateInfo);
8698
+ },
8699
+ },
8700
+ {
8701
+ endpoint: `GET ${HTTPS_TRUST_PATHNAME}ping`,
8702
+ description:
8703
+ "Fetched over https by the page: it fails as long as the device does not trust the certificate.",
8704
+ declarationSource: import.meta.url,
8705
+ fetch: () => {
8706
+ return new Response(null, { status: 204 });
8707
+ },
8708
+ },
8709
+ ],
8710
+ };
8711
+ };
8712
+
8713
+ // "CN=https local root certificate\nO=..." -> "https local root certificate"
8714
+ const readCommonName = (subject) => {
8715
+ for (const line of subject.split("\n")) {
8716
+ if (line.startsWith("CN=")) {
8717
+ return line.slice("CN=".length);
8718
+ }
8719
+ }
8720
+ return subject;
8721
+ };
8722
+
8605
8723
  const devServerPluginInjectServerResponseHeader = ({
8606
8724
  sourceDirectoryUrl,
8607
8725
  }) => {
@@ -12527,7 +12645,7 @@ const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
12527
12645
  * @param {number} [params.port=3456] - Port to listen on (0 = a free port).
12528
12646
  * @param {string} [params.hostname] - Hostname to bind to.
12529
12647
  * @param {boolean} [params.acceptAnyIp=false] - Also accept connections on the machine's IPs (so other devices on the network — a phone — can reach the server). Off by default: exposing the dev server beyond localhost is an explicit choice, not something a dev tool decides.
12530
- * @param {boolean|object} [params.https=false] - HTTPS as `{ certificate, privateKey }`.
12648
+ * @param {object} [params.https] - HTTPS as `{ certificate, privateKey }` (PEM strings). With `rootCertificate` (the PEM of the authority that signed `certificate`) and `acceptAnyIp`, the dev server also serves `http://<ip>:<port>/.internal/https/`, a page to trust that authority on a phone (see docs/users/b_dev/b_dev.md, "Trusting it on a phone").
12531
12649
  * @param {boolean} [params.http2=false] - HTTP/2 (requires https).
12532
12650
  * @param {Array} [params.plugins=[]] - jsenv plugins (transformUrlContent, serverRoutes, serverEvents, effect, …).
12533
12651
  * @param {Array} [params.serverPlugins=[]] - `@jsenv/server`-level plugins.
@@ -12659,6 +12777,7 @@ const startDevServer = async ({
12659
12777
  );
12660
12778
  }
12661
12779
  }
12780
+ let httpsTrust = null;
12662
12781
  // params normalization
12663
12782
  {
12664
12783
  if (runtimeCompat === undefined) {
@@ -12673,6 +12792,16 @@ const startDevServer = async ({
12673
12792
  if (clientAutoreload === false) {
12674
12793
  clientAutoreload = { enabled: false };
12675
12794
  }
12795
+ if (https) {
12796
+ const { certificate, privateKey, rootCertificate } = https;
12797
+ https = { certificate, privateKey };
12798
+ // a localhost-only server has no phone to serve
12799
+ if (rootCertificate && acceptAnyIp) {
12800
+ httpsTrust = {
12801
+ rootCertificate: new X509Certificate(rootCertificate),
12802
+ };
12803
+ }
12804
+ }
12676
12805
  }
12677
12806
 
12678
12807
  const logger = createLogger({ logLevel });
@@ -12830,6 +12959,7 @@ const startDevServer = async ({
12830
12959
  }),
12831
12960
  // chrome devtools
12832
12961
  devServerPluginChromeDevToolsJson({ sourceDirectoryUrl }),
12962
+ ...(httpsTrust ? [devServerPluginHttpsTrust(httpsTrust)] : []),
12833
12963
  ...serverPlugins,
12834
12964
  devServerPluginServeSourceFiles({
12835
12965
  packageDirectory,
@@ -12868,6 +12998,12 @@ const startDevServer = async ({
12868
12998
  startLog: false,
12869
12999
 
12870
13000
  https,
13001
+ ...(httpsTrust
13002
+ ? {
13003
+ redirectHttpToHttps: ({ resource }) =>
13004
+ !resource.startsWith(HTTPS_TRUST_PATHNAME),
13005
+ }
13006
+ : {}),
12871
13007
  http2,
12872
13008
  acceptAnyIp,
12873
13009
  hostname,
@@ -12893,6 +13029,17 @@ const startDevServer = async ({
12893
13029
  logger.info(``);
12894
13030
  Object.keys(server.origins).forEach((key) => {
12895
13031
  logger.info(`- ${server.origins[key]}`);
13032
+ if (httpsTrust && key === "externalip") {
13033
+ const externalHostname = new URL(server.origins.externalip).hostname;
13034
+ const httpsTrustPageUrl = new URL(
13035
+ HTTPS_TRUST_PATHNAME,
13036
+ `http://${externalHostname}:${server.port}`,
13037
+ );
13038
+ logger.info(` trust it on a phone: ${httpsTrustPageUrl}`);
13039
+ logger.info(
13040
+ ` root certificate SHA-256: ${httpsTrust.rootCertificate.fingerprint256}`,
13041
+ );
13042
+ }
12896
13043
  });
12897
13044
  logger.info(``);
12898
13045
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "41.5.35",
3
+ "version": "41.5.38",
4
4
  "type": "module",
5
5
  "description": "Tool to develop, test and build js projects",
6
6
  "repository": {
@@ -80,7 +80,7 @@
80
80
  "@jsenv/plugin-minification": "1.7.27",
81
81
  "@jsenv/plugin-supervisor": "1.8.29",
82
82
  "@jsenv/plugin-transpilation": "1.7.14",
83
- "@jsenv/server": "17.6.8",
83
+ "@jsenv/server": "17.6.9",
84
84
  "@jsenv/sourcemap": "1.4.13",
85
85
  "react-table": "7.8.0"
86
86
  },
@@ -0,0 +1,352 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Trust the dev server https</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light dark;
10
+ --ink: #111827;
11
+ --muted: #6b7280;
12
+ --paper: #f3f4f6;
13
+ --card: #ffffff;
14
+ --line: #e5e7eb;
15
+ --accent: #2563eb;
16
+ --accent-ink: #ffffff;
17
+ --ok: #15803d;
18
+ --ok-paper: #dcfce7;
19
+ --warn: #b45309;
20
+ --warn-paper: #fef3c7;
21
+ }
22
+ @media (prefers-color-scheme: dark) {
23
+ :root {
24
+ --ink: #e5e7eb;
25
+ --muted: #9ca3af;
26
+ --paper: #0b1020;
27
+ --card: #111827;
28
+ --line: #1f2937;
29
+ --accent: #3b82f6;
30
+ --ok: #4ade80;
31
+ --ok-paper: #052e16;
32
+ --warn: #fbbf24;
33
+ --warn-paper: #2d1a02;
34
+ }
35
+ }
36
+ body {
37
+ margin: 0;
38
+ padding: 16px;
39
+ color: var(--ink);
40
+ font:
41
+ 15px/1.5 -apple-system,
42
+ BlinkMacSystemFont,
43
+ "Segoe UI",
44
+ Roboto,
45
+ sans-serif;
46
+ background: var(--paper);
47
+ }
48
+ main {
49
+ display: flex;
50
+ max-width: 34rem;
51
+ margin: 0 auto;
52
+ flex-direction: column;
53
+ gap: 16px;
54
+ }
55
+ h1 {
56
+ margin: 8px 0 0;
57
+ font-size: 1.35rem;
58
+ }
59
+ h2 {
60
+ margin: 0;
61
+ font-size: 1rem;
62
+ }
63
+ p {
64
+ margin: 0;
65
+ }
66
+ ol {
67
+ margin: 0;
68
+ padding-left: 1.3em;
69
+ }
70
+ li + li {
71
+ margin-top: 6px;
72
+ }
73
+ code {
74
+ font:
75
+ 0.85em ui-monospace,
76
+ SFMono-Regular,
77
+ Menlo,
78
+ monospace;
79
+ }
80
+ .muted {
81
+ color: var(--muted);
82
+ }
83
+ .card {
84
+ display: flex;
85
+ padding: 14px 16px;
86
+ flex-direction: column;
87
+ gap: 10px;
88
+ background: var(--card);
89
+ border: 1px solid var(--line);
90
+ border-radius: 10px;
91
+ }
92
+ .button {
93
+ display: block;
94
+ padding: 12px 16px;
95
+ color: var(--accent);
96
+ font-weight: 600;
97
+ text-align: center;
98
+ text-decoration: none;
99
+ border: 1px solid var(--accent);
100
+ border-radius: 8px;
101
+ }
102
+ .button[data-primary] {
103
+ color: var(--accent-ink);
104
+ background: var(--accent);
105
+ }
106
+ #status {
107
+ font-weight: 600;
108
+ }
109
+ #status_card[data-state="trusted"] {
110
+ background: var(--ok-paper);
111
+ border-color: var(--ok);
112
+ }
113
+ #status_card[data-state="trusted"] #status {
114
+ color: var(--ok);
115
+ }
116
+ #status_card[data-state="untrusted"] {
117
+ background: var(--warn-paper);
118
+ border-color: var(--warn);
119
+ }
120
+ #status_card[data-state="untrusted"] #status {
121
+ color: var(--warn);
122
+ }
123
+ #fingerprint {
124
+ display: block;
125
+ word-break: break-all;
126
+ }
127
+ details {
128
+ padding: 14px 16px;
129
+ background: var(--card);
130
+ border: 1px solid var(--line);
131
+ border-radius: 10px;
132
+ }
133
+ summary {
134
+ font-weight: 600;
135
+ cursor: pointer;
136
+ }
137
+ details[open] summary {
138
+ margin-bottom: 10px;
139
+ }
140
+ details .this_device {
141
+ display: none;
142
+ color: var(--muted);
143
+ font-weight: 400;
144
+ }
145
+ details[data-current] .this_device {
146
+ display: inline;
147
+ }
148
+ details p + ol,
149
+ details ol + p,
150
+ details p + p {
151
+ margin-top: 10px;
152
+ }
153
+ </style>
154
+ </head>
155
+ <body>
156
+ <main>
157
+ <h1>Trust the dev server https</h1>
158
+ <p class="muted">
159
+ Install the root certificate of the computer running the dev server and
160
+ this device opens it over https without a warning. Service workers,
161
+ push, install and the other secure-context APIs need it.
162
+ </p>
163
+
164
+ <div id="status_card" class="card" data-state="checking">
165
+ <p id="status">Checking whether this device trusts the dev server…</p>
166
+ <a id="continue" class="button" jsenv-ignore href="/">
167
+ Continue to https
168
+ </a>
169
+ </div>
170
+
171
+ <div class="card">
172
+ <a
173
+ id="download"
174
+ class="button"
175
+ jsenv-ignore
176
+ href="./root.crt"
177
+ data-primary
178
+ >
179
+ Download the root certificate
180
+ </a>
181
+ <p>
182
+ <span class="muted">SHA-256 fingerprint</span>
183
+ <code id="fingerprint">…</code>
184
+ </p>
185
+ <p class="muted">
186
+ Once installed, the fingerprint the device shows must match the one
187
+ the dev server prints in its terminal: this page travels over plain
188
+ http, so the network could swap the file.
189
+ </p>
190
+ </div>
191
+
192
+ <div id="platforms">
193
+ <details data-platform="android" open>
194
+ <summary>
195
+ Android <span class="this_device">(this device)</span>
196
+ </summary>
197
+ <ol>
198
+ <li>
199
+ Tap <b>Download the root certificate</b>. Opening the file only
200
+ says CA certificates must be installed from Settings: that is
201
+ expected.
202
+ </li>
203
+ <li>
204
+ In <b>Settings</b>, search for <b>CA certificate</b>. Or go to
205
+ Security → Encryption &amp; credentials → Install a certificate →
206
+ CA certificate.
207
+ </li>
208
+ <li>
209
+ Tap <b>Install anyway</b> and pick the file in Downloads. Android
210
+ asks for a screen lock if there is none.
211
+ </li>
212
+ <li>Come back here, the check above turns green.</li>
213
+ </ol>
214
+ <p class="muted">
215
+ Android then shows “Network may be monitored”: expected, the device
216
+ trusts a certificate installed by hand. Chrome honors it, most apps
217
+ do not.
218
+ </p>
219
+ </details>
220
+ <details data-platform="ios" open>
221
+ <summary>
222
+ iPhone, iPad <span class="this_device">(this device)</span>
223
+ </summary>
224
+ <ol>
225
+ <li>
226
+ Open this page in <b>Safari</b>, tap
227
+ <b>Download the root certificate</b>, then <b>Allow</b>.
228
+ </li>
229
+ <li>
230
+ In <b>Settings</b>, tap <b>Profile Downloaded</b> →
231
+ <b>Install</b>.
232
+ </li>
233
+ <li>
234
+ Then
235
+ <b>Settings → General → About → Certificate Trust Settings</b> and
236
+ turn on <b class="common_name">the root certificate</b>. Without
237
+ this step the certificate is installed but not trusted.
238
+ </li>
239
+ <li>Come back here, the check above turns green.</li>
240
+ </ol>
241
+ </details>
242
+ <details data-platform="desktop" open>
243
+ <summary>
244
+ Computer <span class="this_device">(this device)</span>
245
+ </summary>
246
+ <p>
247
+ On the computer running the dev server,
248
+ <code>npx @jsenv/https-local init</code> installs and trusts the
249
+ root certificate.
250
+ </p>
251
+ <p>
252
+ On another computer, download it and add it to the trusted root
253
+ certificates of the system. On macOS: open the file, then set it to
254
+ “Always Trust” in Keychain Access.
255
+ </p>
256
+ </details>
257
+ </div>
258
+
259
+ <details open>
260
+ <summary>Good to know</summary>
261
+ <p>
262
+ The device trusts anything signed by this root certificate. Its key
263
+ never leaves the computer running the dev server, only the certificate
264
+ is served: the trade-off already made on that computer.
265
+ </p>
266
+ <p>
267
+ <b>See the fingerprint, or remove it.</b> Android: Settings →
268
+ Encryption &amp; credentials → Trusted credentials → User → tap it.
269
+ iPhone, iPad: Settings → General → VPN &amp; Device Management → the
270
+ profile.
271
+ </p>
272
+ </details>
273
+ </main>
274
+
275
+ <script type="module">
276
+ const readPlatform = () => {
277
+ const userAgent = window.navigator.userAgent;
278
+ if (/android/i.test(userAgent)) {
279
+ return "android";
280
+ }
281
+ if (/iPhone|iPad|iPod/.test(userAgent)) {
282
+ return "ios";
283
+ }
284
+ // iPadOS Safari introduces itself as a Mac
285
+ if (
286
+ /Macintosh/.test(userAgent) &&
287
+ window.navigator.maxTouchPoints > 1
288
+ ) {
289
+ return "ios";
290
+ }
291
+ return "desktop";
292
+ };
293
+ const platform = readPlatform();
294
+ const platforms = document.querySelector("#platforms");
295
+ for (const details of platforms.querySelectorAll("[data-platform]")) {
296
+ if (details.getAttribute("data-platform") === platform) {
297
+ details.setAttribute("data-current", "");
298
+ platforms.prepend(details);
299
+ } else {
300
+ details.open = false;
301
+ }
302
+ }
303
+
304
+ // http and https share the port of the dev server
305
+ const port =
306
+ window.location.port ||
307
+ (window.location.protocol === "https:" ? "443" : "80");
308
+ const httpsOrigin = new URL(`https://${window.location.hostname}:${port}`)
309
+ .origin;
310
+ const continueLink = document.querySelector("#continue");
311
+ continueLink.href = `${httpsOrigin}/`;
312
+
313
+ const statusCard = document.querySelector("#status_card");
314
+ const status = document.querySelector("#status");
315
+ const download = document.querySelector("#download");
316
+ const checkTrust = async () => {
317
+ try {
318
+ // rejects as long as the certificate is not trusted, unless this
319
+ // browser clicked through the certificate warning for this host
320
+ await fetch(`${httpsOrigin}${window.location.pathname}ping`, {
321
+ mode: "no-cors",
322
+ cache: "no-store",
323
+ });
324
+ } catch {
325
+ statusCard.setAttribute("data-state", "untrusted");
326
+ status.textContent = "This device does not trust the dev server yet.";
327
+ continueLink.removeAttribute("data-primary");
328
+ download.setAttribute("data-primary", "");
329
+ return;
330
+ }
331
+ statusCard.setAttribute("data-state", "trusted");
332
+ status.textContent = "✓ This device trusts the dev server.";
333
+ continueLink.setAttribute("data-primary", "");
334
+ download.removeAttribute("data-primary");
335
+ };
336
+ checkTrust();
337
+ // back from the settings where the certificate was installed
338
+ document.addEventListener("visibilitychange", () => {
339
+ if (document.visibilityState === "visible") {
340
+ checkTrust();
341
+ }
342
+ });
343
+
344
+ const response = await fetch("./root.json");
345
+ const { fingerprint256, commonName } = await response.json();
346
+ document.querySelector("#fingerprint").textContent = fingerprint256;
347
+ for (const element of document.querySelectorAll(".common_name")) {
348
+ element.textContent = commonName;
349
+ }
350
+ </script>
351
+ </body>
352
+ </html>