arcane-os 0.7.2 → 0.7.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.3
4
+
5
+ - Correct public development serving for OPFS/DBOPFS and other browser APIs
6
+ requiring a secure context. `npm run dev -- --app <id> --public` now serves
7
+ HTTPS using a workspace-local development certificate pair, with explicit
8
+ `--cert`/`--key` paths and `--https` available for any selected app.
9
+ - Add HTTPS transport to the shared development server and report the actual
10
+ protocol in local and network URLs. Preserve ordinary HTTP localhost,
11
+ selected source/runtime routes, refresh behavior, and owned cancellation.
12
+ - Document certificate setup and Android CA installation. Keep TLS material
13
+ out of operation events and report missing certificates before binding.
14
+ - Clarify the browser accelerator setup instruction to close and reopen the
15
+ browser after changing a flag, with that final step underlined and emphasized.
16
+
3
17
  ## 0.7.2
4
18
 
5
19
  - Keep both supported browser NPU and GPU flag address copy controls available
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
20
  event, cancellation, and browser run contracts.
21
21
 
22
- This checkout defines the `0.7.2` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.7.3` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -35,7 +35,7 @@ Create one browser application, install its pinned SDK, and start its source
35
35
  server:
36
36
 
37
37
  ```bash
38
- npx arcane-os@0.7.2 new hello-speech --path ./hello-speech --target browser
38
+ npx arcane-os@0.7.3 new hello-speech --path ./hello-speech --target browser
39
39
  cd hello-speech
40
40
  npm install
41
41
  npm run dev
@@ -49,12 +49,20 @@ npm run dev -- --app hello-speech --public
49
49
  ```
50
50
 
51
51
  Replace `hello-speech` with any app in the workspace. The server binds to
52
- `0.0.0.0` and prints network URLs to open on the other device. Plain `npm run dev`
53
- keeps its localhost default. An explicit `--host` overrides the public bind
54
- address, and `--port` selects the port. Network reachability depends on the
55
- machine's firewall and network; the command does not configure internet hosting
56
- or HTTPS. Browser features requiring a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts)
57
- need HTTPS when opened through a LAN address.
52
+ `0.0.0.0` and serves HTTPS using `.arcane/dev/server-cert.pem` and
53
+ `.arcane/dev/server-key.pem` in the workspace. Supply a certificate covering the
54
+ LAN address or hostname you will open, and trust its issuing development CA on
55
+ each device. Keep these local files ignored by Git. The same certificate pair
56
+ works for any selected app in that workspace. The command reports missing TLS
57
+ files instead of starting an HTTP listener.
58
+
59
+ Plain `npm run dev` keeps its HTTP localhost default. An explicit `--host`
60
+ overrides the public bind address, and `--port` selects the port. Use `--https`
61
+ for HTTPS on localhost, or `--cert <file> --key <file>` for an explicit PEM pair;
62
+ relative paths resolve from the workspace. The server prints HTTPS network URLs
63
+ for public mode. Network reachability depends on the machine's firewall and
64
+ network. See the [development HTTPS setup](docs/reference/cli.md#development-https-setup)
65
+ for device trust and DBOPFS's secure-context requirement.
58
66
 
59
67
  Open the URL printed by the server. The generated page owns its import map and
60
68
  Arcane theme; its application module is `apps/hello-speech/modules/App.js`.
@@ -378,7 +386,7 @@ uses the same controller for automatic memory extraction.
378
386
  Create a new repository-shaped Arcane application with the exact stable SDK:
379
387
 
380
388
  ```bash
381
- npx arcane-os@0.7.2 new my-app --path ./my-app --target portable --git
389
+ npx arcane-os@0.7.3 new my-app --path ./my-app --target portable --git
382
390
  cd my-app
383
391
  npm install
384
392
  npm run dev
@@ -388,7 +396,7 @@ To enroll an existing repository, install the exact SDK and initialize only
388
396
  missing Arcane files:
389
397
 
390
398
  ```bash
391
- npm install --save-dev --save-exact arcane-os@0.7.2
399
+ npm install --save-dev --save-exact arcane-os@0.7.3
392
400
  npm exec -- arcane init my-app --target portable
393
401
  ```
394
402
 
@@ -404,7 +412,7 @@ npm exec -- arcane-os targets
404
412
  No global SDK install or standalone Arcane CLI is required. The application
405
413
  repository's exact npm dependency and lockfile own the CLI and toolchain version.
406
414
 
407
- Use `npx arcane-os@0.7.2` for the initial bootstrap because it names this npm
415
+ Use `npx arcane-os@0.7.3` for the initial bootstrap because it names this npm
408
416
  package explicitly; bare `npx arcane` outside an installed project could resolve
409
417
  a different package. Both installed commands invoke the same headless toolchain.
410
418
  Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
@@ -424,7 +432,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
424
432
 
425
433
  # From the generated app repository
426
434
  cd ../local-app
427
- npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.7.2.tgz
435
+ npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.7.3.tgz
428
436
  npm ci
429
437
  ```
430
438
 
@@ -433,7 +441,7 @@ same location. The lockfile retains the selected package dependency while
433
441
  Arcane uses the installed package name and version. Local directory `file:` dependencies are not
434
442
  accepted because npm may install them as links; use a packed `.tgz`. A GitHub
435
443
  runner also needs that tarball at the locked path. After publication, replace
436
- the local declaration with the exact `arcane-os@0.7.2` registry package and
444
+ the local declaration with the exact `arcane-os@0.7.3` registry package and
437
445
  commit the regenerated lock.
438
446
 
439
447
  Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
@@ -490,7 +498,7 @@ arcane new <id> [--path <directory>] [--display-name <name>] [--target <target>]
490
498
  arcane init [id] [--workspace <directory>] [--display-name <name>] [--target <target>]
491
499
  arcane doctor [--workspace <directory>] [--arcane-root <directory>]
492
500
  arcane import-map [--workspace <directory>] [--app <id>]
493
- arcane dev [--app <id>] [--public] [--host <address>] [--port 8000]
501
+ arcane dev [--app <id>] [--public] [--https] [--cert <file> --key <file>] [--host <address>] [--port 8000]
494
502
  arcane test [--app <id>] [--scope app]
495
503
  arcane test --scope shared --test-file <repo-relative.test.mjs>
496
504
  arcane check [--app <id>] [--scope app] [--skip-tests]
@@ -572,7 +580,7 @@ package installation, or assertions.
572
580
 
573
581
  ## Current target support
574
582
 
575
- Version `0.7.2` exposes one browser target and five explicitly paired
583
+ Version `0.7.3` exposes one browser target and five explicitly paired
576
584
  native development targets: a non-runnable portable directory, a
577
585
  Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
578
586
  unsigned-local-test DEBs, and an Android development-signed APK. The
@@ -116,6 +116,16 @@ files into `dist`, or restarting the server. Restarting is not a content
116
116
  synchronization step; when a refresh is stale, first verify the command, URL,
117
117
  workspace, selected app, and resolved source route.
118
118
 
119
+ The shared dev server owns HTTP and HTTPS transport for the same selected
120
+ routes. `arcane dev --public` selects HTTPS on the IPv4 wildcard address;
121
+ explicit `--host` controls the bind address, while `--https` selects HTTPS
122
+ without changing it. Public/HTTPS CLI startup reads one workspace-local PEM
123
+ pair before binding. The certificate covers the device-facing address, and
124
+ each client trusts its issuing CA through that platform's certificate setup.
125
+ This supplies the secure origin required by OPFS/DBOPFS on LAN devices. The
126
+ server does not install trust, generate certificates, or expose private TLS
127
+ material through CLI events. Ordinary localhost development remains HTTP.
128
+
119
129
  Development is an intentionally fast feedback loop. Keep each increment small
120
130
  and independently understandable so its effect has one clear cause and a
121
131
  mistake can be isolated without untangling unrelated work. A development
@@ -279,7 +289,7 @@ paths are withheld from the native provider. The provider copies the complete
279
289
  selected release rather than accepting an unrelated source path. Verification
280
290
  is a separate explicit operation for a selected release artifact.
281
291
 
282
- The SDK `0.7.2` runtime requires Arcane `0.8.12` or newer. Compatibility
292
+ The SDK `0.7.3` runtime requires Arcane `0.8.12` or newer. Compatibility
283
293
  is contractual rather than exact-version pinning: the prepared Core must meet
284
294
  the highest minimum declared by the runtime, selected app, and bundled app
285
295
  dependencies; keep each app's Arcane protocol generation; and provide every
@@ -156,7 +156,8 @@ documents the **Enables WebNN API** flag and model/operator requirements.
156
156
  The **GPU performance** section shows a short adapter status and a **Copy GPU
157
157
  flag address** button on desktop Windows Chromium browsers. The button remains
158
158
  available for every adapter class, unavailable API, or detection failure. One
159
- shared instruction explains pasting, enabling, saving work, and relaunching. Identified
159
+ shared instruction explains pasting, enabling, saving work, then closing the
160
+ browser and reopening it, with the final step underlined and emphasized. Identified
160
161
  Chrome uses `chrome://flags/#force-high-performance-gpu`; other recognized
161
162
  Chromium browsers use their corresponding internal scheme. **Refresh**
162
163
  requests a new availability result; concurrent
@@ -49,7 +49,9 @@ meaning and cardinality rules:
49
49
  | `--app` | app id | Workspace/app operations except shared scope and `verify-bundle`; also the exact `mail serve` caller id. |
50
50
  | `--arcane-root` | directory | `doctor`, native `build`/`run`, `native-doctor`, `native-prepare` |
51
51
  | `--host` / `--port` | host / integer 0–65535 | Browser `dev`/`run` default to `127.0.0.1:8000`; `mail serve` defaults to `127.0.0.1:8025` and admits numeric loopback only. |
52
- | `--public` | flag | `dev`; binds to `0.0.0.0` unless `--host` explicitly selects another address. |
52
+ | `--public` | flag | `dev`; serves HTTPS and binds to `0.0.0.0` unless `--host` explicitly selects another address. |
53
+ | `--https` | flag | `dev`; serves HTTPS with the configured or default workspace PEM pair. |
54
+ | `--cert` / `--key` | PEM file paths | `dev`; supply both to select HTTPS with an explicit certificate chain and private key. Relative paths resolve from the workspace. |
53
55
  | `--target` | target id | `new`, `init`, native diagnostics, `build`, `run` |
54
56
  | `--format` / `--signing` | target-supported values | Native diagnostics, `build`, `run` |
55
57
  | `--output-root` | directory | Native `build` and `run` |
@@ -310,7 +312,7 @@ npm exec -- arcane upgrade --workspace . --app hello-world
310
312
 
311
313
  Starts one development server for one selected app and maps the exact
312
314
  workspace/runtime routes. It defaults to localhost; `--public` enables access
313
- from other devices on the network.
315
+ from other devices on the network over HTTPS.
314
316
 
315
317
  For an external workspace, the server exposes the selected projected
316
318
  `arcane/` root, including `arcane/sdk` and `arcane/dependencies`, alongside the
@@ -319,7 +321,7 @@ The explicit live-source SDK mapping remains unchanged and does not replace the
319
321
  installed projection.
320
322
 
321
323
  ```text
322
- arcane dev [--app <id>] [--public] [--host <address>] [--port 8000]
324
+ arcane dev [--app <id>] [--public] [--https] [--cert <file> --key <file>] [--host <address>] [--port 8000]
323
325
  ```
324
326
 
325
327
  ### Lifecycle
@@ -333,10 +335,43 @@ device, since `localhost` refers to that device and `0.0.0.0` is a bind address.
333
335
  Network URLs come from one interface snapshot at startup and do not establish
334
336
  remote reachability through the machine's firewall or network.
335
337
 
336
- This option changes the listener address. It does not configure a firewall,
337
- router forwarding, an internet tunnel, authentication, or HTTPS. Browser
338
- features that require a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts)
339
- still require HTTPS when accessed through a LAN address.
338
+ Public mode also selects HTTPS. Plain localhost development stays HTTP.
339
+ `--https` selects HTTPS without changing the bind address; supplying both
340
+ `--cert` and `--key` also selects HTTPS. The command does not configure a
341
+ firewall, router forwarding, or an internet tunnel.
342
+
343
+ ### Development HTTPS setup
344
+
345
+ Before starting public mode, place the development server's PEM certificate
346
+ chain at `.arcane/dev/server-cert.pem` and its PEM private key at
347
+ `.arcane/dev/server-key.pem`, relative to the workspace. Alternatively, pass
348
+ `--cert <file> --key <file>` together. The certificate must cover the LAN IP
349
+ address or hostname opened by each device. Certificate creation and renewal
350
+ belong to the developer's certificate tooling; the server does not generate a
351
+ CA or alter device trust stores. Keep `.arcane/dev/` ignored by Git and keep the
352
+ private key on the development computer.
353
+
354
+ The server reads the selected pair once asynchronously per startup, before
355
+ binding. Missing files or certificate/key parse errors produce a startup error; public
356
+ mode never silently falls back to HTTP. Certificate/key contents are not
357
+ included in operation events or JSON/NDJSON output. Restart the server after
358
+ replacing its certificate pair; ordinary app source edits still appear on
359
+ refresh without restarting.
360
+
361
+ DBOPFS uses [OPFS, which requires a secure context](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/getDirectory).
362
+ HTTP localhost is a special case on the device running the server; a LAN HTTP
363
+ address does not receive that exception. For HTTPS issued by a development CA,
364
+ each accessing device must trust that CA and use an address covered by the
365
+ server certificate.
366
+
367
+ On Android Chrome, transfer only the public CA certificate to the phone. In
368
+ Android Settings, open the security settings, then **Encryption & credentials
369
+ > Install a certificate > CA certificate**, and select that public certificate.
370
+ Samsung devices may label the entry **Install from device storage**. Menu names
371
+ vary by device; Google's [Android CA installation instructions](https://support.google.com/device-usage-study-help/answer/15713321?co=GENIE.Platform%3DAndroid&hl=en)
372
+ show these paths. Then open the printed HTTPS network URL in Chrome. The CA
373
+ installation is a device action; server readiness does not prove Android trust
374
+ or remote reachability.
340
375
 
341
376
  ### Example
342
377
 
@@ -165,7 +165,8 @@ receive a **Copy GPU flag address** button for the matching browser,
165
165
  including `chrome://flags/#force-high-performance-gpu` for identified Chrome.
166
166
  Both controls remain available regardless of API availability, adapter class,
167
167
  or detection failure. Each flag has one short explanation, followed by a shared
168
- paste, enable, save, and relaunch instruction.
168
+ instruction: paste the address, enable the flag, save work, then close the
169
+ browser and reopen it. The closing and reopening instruction is underlined and emphasized.
169
170
  Settings addresses use Copy controls and appear as selectable text if copying
170
171
  fails. The component does not attempt navigation to internal browser pages.
171
172
 
@@ -210,7 +211,7 @@ explicitly and invoke it from a user action. It returns a promise resolving to
210
211
  `true` only after clipboard success, or `false` for destruction, an unsupported
211
212
  target, or copy failure. The complete address remains selectable if clipboard
212
213
  access fails. On-screen instructions explain pasting it into the address bar,
213
- enabling WebNN, and relaunching.
214
+ enabling WebNN, saving work, then closing the browser and reopening it.
214
215
 
215
216
  `destroy()` aborts owned listeners, disposes the event source, marks `ready`
216
217
  false, and suppresses UI updates from pending clipboard and adapter operations.
@@ -3461,17 +3461,30 @@ async startDevServer(options={})
3461
3461
 
3462
3462
  Import it from `arcane-os`. Source mode accepts
3463
3463
  `{workspaceRoot=process.cwd(), appId, mode='source', host='127.0.0.1', port=0,
3464
- signal, onEvent}` and serves one validated workspace application plus its
3465
- complete SDK or integrated runtime. Packaged mode uses
3464
+ https=false, certPath, keyPath, tls, signal, onEvent}` and serves one validated
3465
+ workspace application plus its complete SDK or integrated runtime. Packaged mode uses
3466
3466
  `{mode:'packaged', releaseRoot, host, port, signal, onEvent}` and serves the
3467
3467
  complete selected release files. `host` defaults to `127.0.0.1` and accepts an
3468
3468
  explicit network address or hostname. Use `0.0.0.0` for all IPv4 interfaces or
3469
3469
  `::` for the platform's IPv6 wildcard listener; port `0` asks the operating
3470
3470
  system for an available port.
3471
3471
 
3472
+ `https:true` reads `.arcane/dev/server-cert.pem` and
3473
+ `.arcane/dev/server-key.pem` relative to `workspaceRoot` unless explicit
3474
+ `certPath` and `keyPath` are supplied together. The path pair also selects
3475
+ HTTPS without `https:true`; relative paths resolve from the workspace. A
3476
+ direct `tls` object instead supplies Node HTTPS server options, including
3477
+ `cert` and `key`, without reading certificate files. Keep private material
3478
+ server-side. Missing PEM files and Node certificate/key parse errors reject
3479
+ startup without falling back to HTTP. Node owns TLS option handling and the
3480
+ handshake; browser trust and address matching are evaluated when a client
3481
+ connects. The CLI's `--public` selects HTTPS and the wildcard bind;
3482
+ the API's `host` option alone changes only the bind address.
3483
+
3472
3484
  The promise settles after the listener is ready and resolves to
3473
- `{server, mode, workspaceRoot, appId, host, port, origin, cleanUrl, url,
3474
- networkUrls, close, closed, lifecycle}`. `server` is the raw Node HTTP server.
3485
+ `{server, protocol, mode, workspaceRoot, appId, host, port, origin, cleanUrl, url,
3486
+ networkUrls, close, closed, lifecycle}`. `server` is the raw Node HTTP or HTTPS
3487
+ server; `protocol` is `'http:'` or `'https:'`.
3475
3488
  `url` and `cleanUrl` are the same application URL. Wildcard listeners use
3476
3489
  `localhost` in that local URL; `host` retains the actual bound address.
3477
3490
  `networkUrls` lists application URLs for applicable non-loopback interface
@@ -3480,6 +3493,12 @@ not evidence of reachability from another device. The server adds no session
3480
3493
  capability or authentication. In packaged mode, `workspaceRoot` and `appId`
3481
3494
  are `null`.
3482
3495
 
3496
+ All returned application URLs use the selected transport's scheme. The server
3497
+ reads one PEM pair per startup and does not create certificates or modify trust
3498
+ stores. Each client must trust the issuing CA and open an address covered by the
3499
+ server certificate. Lifecycle events and CLI summaries exclude TLS options and
3500
+ private key contents. See [development HTTPS setup](cli.md#development-https-setup).
3501
+
3483
3502
  Starting the server opens the selected listener and emits awaited,
3484
3503
  backpressured `server.starting` and `server.started` events. Request failures
3485
3504
  emit `server.request.failed`; shutdown emits `server.stopped` after owned
@@ -3881,6 +3900,11 @@ async function usedescribeTargets(...arguments_) {
3881
3900
 
3882
3901
  Starts one owned browser development server for the selected application.
3883
3902
 
3903
+ `https`, `certPath`, `keyPath`, and `tls` follow the
3904
+ [`startDevServer()` TLS contract](#startdevserver), alongside `host` and `port`.
3905
+ The operation refreshes the selected app's managed import maps once, then owns
3906
+ one source listener and returns its protocol, URLs, and shutdown lifecycle.
3907
+
3884
3908
  ### Signature and result
3885
3909
 
3886
3910
  ```text
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -61,7 +61,7 @@
61
61
  <p>“Force High Performance GPU” prefers your faster GPU.</p>
62
62
  <button id="copyGpuAddress" class="arcane-button arcane-button--secondary" type="button">Copy GPU flag address</button>
63
63
  </div>
64
- <p>Paste a copied address into your browser's address bar. Enable the flag, save your work, and relaunch.</p>
64
+ <p>Paste a copied address into your browser's address bar. Enable the flag, save your work, <u><em>then close the browser and reopen it</em></u>.</p>
65
65
  <details>
66
66
  <summary>Setup help</summary>
67
67
  <p><a href="https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html">WebNN setup documentation</a></p>
package/src/cli/main.mjs CHANGED
@@ -18,6 +18,8 @@ const VALUE_OPTIONS=new Set([
18
18
  'arcane-root',
19
19
  'host',
20
20
  'port',
21
+ 'cert',
22
+ 'key',
21
23
  'target',
22
24
  'format',
23
25
  'signing',
@@ -36,6 +38,7 @@ const VALUE_OPTIONS=new Set([
36
38
  const FLAG_OPTIONS=new Set([
37
39
  'git',
38
40
  'public',
41
+ 'https',
39
42
  'skip-tests',
40
43
  'dry-run',
41
44
  'require-local-ai',
@@ -61,7 +64,7 @@ Usage:
61
64
  ${CLI_NAME} upgrade [--workspace <directory>] [--app <id>]
62
65
  ${CLI_NAME} doctor [--workspace <directory>] [--arcane-root <directory>]
63
66
  ${CLI_NAME} import-map [--workspace <directory>] [--app <id>]
64
- ${CLI_NAME} dev [--app <id>] [--public] [--host <address>] [--port 8000] [--sdk-runtime-source <sdk-root>]
67
+ ${CLI_NAME} dev [--app <id>] [--public] [--https] [--cert <pem>] [--key <pem>] [--host <address>] [--port 8000] [--sdk-runtime-source <sdk-root>]
65
68
  ${CLI_NAME} test [--app <id>] [--scope app]
66
69
  ${CLI_NAME} test --scope shared --test-file <repo-relative.test.mjs>
67
70
  ${CLI_NAME} check [--app <id>] [--scope app] [--skip-tests]
@@ -84,7 +87,9 @@ Usage:
84
87
  ${CLI_NAME} mail serve --profile <profile> --from <address> --app <id> --origin <origin> [--allow-to <addresses>] [--app-key-stdin] [--host 127.0.0.1] [--port 8025] [--request-timeout <ms>]
85
88
 
86
89
  Development:
87
- --public Bind all IPv4 interfaces (0.0.0.0) and print network URLs.
90
+ --public Serve HTTPS on all IPv4 interfaces (0.0.0.0) and print network URLs.
91
+ --https Use HTTPS with .arcane/dev/server-cert.pem and server-key.pem.
92
+ --cert <pem> --key <pem> Use an existing certificate pair; paths are relative to the workspace.
88
93
  --host <address> Override the bind address; takes precedence over --public.
89
94
  --sdk-runtime-source <sdk-root> Dev-only live SDK checkout; omitted preserves the workspace runtime mode.
90
95
 
@@ -451,6 +456,9 @@ function operationOptions(command,parsed,cwd){
451
456
  if(flags.has('public')&&command!=='dev'){
452
457
  usage('--public is supported only by dev.');
453
458
  }
459
+ if((flags.has('https')||values.cert!==undefined||values.key!==undefined)&&command!=='dev'){
460
+ usage('--https, --cert, and --key are supported only by dev.');
461
+ }
454
462
  if(flags.has('overwrite')&&command!=='bundle'){
455
463
  usage('--overwrite is supported only by bundle.');
456
464
  }
@@ -510,10 +518,18 @@ function operationOptions(command,parsed,cwd){
510
518
  }
511
519
  if(command==='dev'){
512
520
  noExtraPositionals(command,positionals);
521
+ if((values.cert===undefined)!==(values.key===undefined)){
522
+ usage('HTTPS development requires --cert and --key together.');
523
+ }
513
524
  return {
514
525
  ...common,
515
526
  host:values.host??(flags.has('public')?'0.0.0.0':'127.0.0.1'),
516
527
  port:readPort(values.port,8000),
528
+ https:flags.has('public')||flags.has('https')||values.cert!==undefined,
529
+ ...(values.cert===undefined?{}:{
530
+ certPath:path.resolve(workspaceRoot,values.cert),
531
+ keyPath:path.resolve(workspaceRoot,values.key)
532
+ }),
517
533
  ...(values['sdk-runtime-source']===undefined?{}:{
518
534
  sdkRuntimeSourceRoot:path.resolve(cwd,values['sdk-runtime-source'])
519
535
  })
@@ -837,6 +853,7 @@ function serverSummary(result){
837
853
  host:result.host,
838
854
  port:result.port,
839
855
  url:result.url,
856
+ ...(result.protocol===undefined?{}:{protocol:result.protocol}),
840
857
  ...(result.networkUrls===undefined?{}:{networkUrls:result.networkUrls}),
841
858
  ...(result.callerAuthentication
842
859
  ?{callerAuthentication:result.callerAuthentication}
@@ -2,6 +2,7 @@ import Is from 'strong-type';
2
2
  import {constants as FS_CONSTANTS} from 'node:fs';
3
3
  import {lstat,open,readFile,realpath} from 'node:fs/promises';
4
4
  import http from 'node:http';
5
+ import https from 'node:https';
5
6
  import os from 'node:os';
6
7
  import path from 'node:path';
7
8
  import {resolveWorkspace} from './workspace.mjs';
@@ -494,7 +495,7 @@ function browserHostname(host){
494
495
  return host.includes(':')?`[${host}]`:host;
495
496
  }
496
497
 
497
- function networkUrlsForAddress(address,startPath){
498
+ function networkUrlsForAddress(address,startPath,protocol){
498
499
  if(address.address==='127.0.0.1'||address.address==='::1')return [];
499
500
  const allIPv4=address.address==='0.0.0.0';
500
501
  const allInterfaces=address.address==='::';
@@ -507,12 +508,49 @@ function networkUrlsForAddress(address,startPath){
507
508
  if(entry.family==='IPv6'&&entry.scopeid)continue;
508
509
  if(allIPv4&&entry.family!=='IPv4')continue;
509
510
  if(!allIPv4&&!allInterfaces&&entry.address!==address.address)continue;
510
- urls.add(`http://${browserHostname(entry.address)}:${address.port}${startPath}`);
511
+ urls.add(`${protocol}//${browserHostname(entry.address)}:${address.port}${startPath}`);
511
512
  }
512
513
  }
513
514
  return [...urls];
514
515
  }
515
516
 
517
+ async function resolveDevelopmentTls({workspaceRoot,https:useHttps,tls,certPath,keyPath,signal}){
518
+ if(tls!==undefined&&tls!==null&&tls!==false){
519
+ if(!is.object(tls)||is.array(tls)){
520
+ fail('Development server tls must be a Node HTTPS options object.','ARCANE_USAGE');
521
+ }
522
+ return {options:tls};
523
+ }
524
+ if(certPath===undefined&&keyPath===undefined&&!useHttps)return undefined;
525
+ if((certPath===undefined)!==(keyPath===undefined)){
526
+ fail('HTTPS development requires both certPath and keyPath when either is supplied.','ARCANE_USAGE');
527
+ }
528
+ const certificatePath=path.resolve(workspaceRoot,certPath??'.arcane/dev/server-cert.pem');
529
+ const privateKeyPath=path.resolve(workspaceRoot,keyPath??'.arcane/dev/server-key.pem');
530
+ try{
531
+ const reads=await Promise.allSettled([
532
+ readFile(certificatePath,{signal}),
533
+ readFile(privateKeyPath,{signal})
534
+ ]);
535
+ for(const read of reads){
536
+ if(read.status==='rejected')throw read.reason;
537
+ }
538
+ return {
539
+ options:{cert:reads[0].value,key:reads[1].value},
540
+ privateKeyPath:await realpath(privateKeyPath)
541
+ };
542
+ }catch(error){
543
+ if(error.code==='ENOENT'){
544
+ fail(
545
+ `HTTPS development requires a certificate at ${certificatePath} and a key at ${privateKeyPath}. `
546
+ +'Provide that pair or select existing files with --cert and --key.',
547
+ 'ARCANE_DEV_TLS_MISSING'
548
+ );
549
+ }
550
+ throw error;
551
+ }
552
+ }
553
+
516
554
  async function startOwnedDevServer({
517
555
  workspaceRoot=process.cwd(),
518
556
  appId,
@@ -520,6 +558,10 @@ async function startOwnedDevServer({
520
558
  releaseRoot,
521
559
  host='127.0.0.1',
522
560
  port=0,
561
+ https:useHttps=false,
562
+ tls,
563
+ certPath,
564
+ keyPath,
523
565
  signal,
524
566
  sdkRuntimeSourceRoot
525
567
  }={},events,releaseSignal){
@@ -543,6 +585,11 @@ async function startOwnedDevServer({
543
585
  appId,
544
586
  ...(requestedRuntimeMode?{runtimeMode:requestedRuntimeMode}:{})
545
587
  });
588
+ const selectedTls=await resolveDevelopmentTls({
589
+ workspaceRoot,https:useHttps,tls,certPath,keyPath,signal
590
+ });
591
+ const protocol=selectedTls?'https:':'http:';
592
+ throwIfAborted(signal);
546
593
  const routeSet=mode==='source'
547
594
  ?await sourceRoutes(workspaceRoot,appId,{
548
595
  sdkRuntimeSourceRoot,
@@ -570,7 +617,7 @@ async function startOwnedDevServer({
570
617
  const resourcePaths=new Set([routeSet.startPath]);
571
618
  const requestTasks=new Set();
572
619
  const runFileWork=createFileWorkLimiter();
573
- const server=http.createServer((request,response)=>{
620
+ function serveDevelopmentRequest(request,response){
574
621
  let task;
575
622
  task=(async()=>{
576
623
  if(request.method!=='GET'&&request.method!=='HEAD'){
@@ -595,6 +642,11 @@ async function startOwnedDevServer({
595
642
  await runFileWork(async()=>{
596
643
  const opened=await openSafeFile(mapping.root,relative);
597
644
  if(!opened){deny(response,404,'Not found.');return;}
645
+ if(selectedTls?.privateKeyPath
646
+ &&canonicalLocationKey(opened.candidate)===canonicalLocationKey(selectedTls.privateKeyPath)){
647
+ deny(response,404,'Not found.');
648
+ return;
649
+ }
598
650
  const extension=path.extname(opened.candidate).toLowerCase();
599
651
  const html=extension==='.html'||extension==='.htm';
600
652
  let managedDocument=false;
@@ -648,7 +700,10 @@ async function startOwnedDevServer({
648
700
  requestTasks.delete(task);
649
701
  });
650
702
  requestTasks.add(task);
651
- });
703
+ }
704
+ const server=selectedTls
705
+ ?https.createServer(selectedTls.options,serveDevelopmentRequest)
706
+ :http.createServer(serveDevelopmentRequest);
652
707
  await listen(server,{host,port,signal});
653
708
  const address=server.address();
654
709
  if(!address||is.string(address)){
@@ -658,7 +713,7 @@ async function startOwnedDevServer({
658
713
  const visibleHost=address.address==='0.0.0.0'||address.address==='::'
659
714
  ?'localhost'
660
715
  :browserHostname(address.address);
661
- const endpoint=new URL(`http://${visibleHost}:${address.port}`);
716
+ const endpoint=new URL(`${protocol}//${visibleHost}:${address.port}`);
662
717
  const origin=endpoint.origin;
663
718
  const cleanUrl=`${origin}${routeSet.startPath}`;
664
719
  const url=cleanUrl;
@@ -742,6 +797,7 @@ async function startOwnedDevServer({
742
797
  server.once('close',()=>{void finishLifecycle();});
743
798
  const result={
744
799
  server,
800
+ protocol,
745
801
  mode,
746
802
  workspaceRoot:routeSet.workspaceRoot,
747
803
  appId:routeSet.appId,
@@ -760,9 +816,10 @@ async function startOwnedDevServer({
760
816
  lifecycle
761
817
  };
762
818
  try{
763
- result.networkUrls=networkUrlsForAddress(address,routeSet.startPath);
819
+ result.networkUrls=networkUrlsForAddress(address,routeSet.startPath,protocol);
764
820
  await events.send({
765
821
  type:'server.started',
822
+ protocol,
766
823
  mode,
767
824
  host:result.host,
768
825
  port:result.port,
package/src/toolchain.mjs CHANGED
@@ -466,6 +466,10 @@ export async function developApplication(options={}){
466
466
  }),
467
467
  host:options.host,
468
468
  port:options.port,
469
+ https:options.https,
470
+ tls:options.tls,
471
+ certPath:options.certPath,
472
+ keyPath:options.keyPath,
469
473
  signal:options.signal,
470
474
  onEvent:options.onEvent
471
475
  });