arcane-os 0.21.0 → 0.22.1

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,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.22.1
4
+
5
+ - Change the mail gateway's default HTTPS/HTTP2 port from 8025 to 4433 in
6
+ the CLI and server configuration. Preserve explicit port overrides and the
7
+ existing occupied-port message; update current help, references and gate report.
8
+
9
+ ## 0.22.0
10
+
11
+ - Export `generateDocumentImportMaps()` from `arcane-os` for explicitly selected
12
+ host HTML documents and an existing materialized runtime. Generate each
13
+ document's SDK import URLs from its authored base without app discovery or
14
+ imposing an application layout.
15
+ - Preserve complete authored HTML, resource URLs, custom import maps and script
16
+ loading order while updating only SDK-managed map blocks. Encode runtime
17
+ filenames and rebase both targets and URL compatibility keys.
18
+ - Inventory the selected runtime once per batch, prepare every document before
19
+ writing, and expose ordered write events, cancellation and event-delivery
20
+ failures. Document the public API and add focused behavioral test source.
21
+ - Preserve the platform's existing cancellation code in the import-map and
22
+ runtime-inventory paths, including ordinary `AbortController.abort()`.
23
+
3
24
  ## 0.21.0
4
25
 
5
26
  - Serve mail over HTTPS with HTTP/2 through the published `node-http-server`
package/README.md CHANGED
@@ -19,11 +19,11 @@ 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.21.0` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.22.1` 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
 
26
- The [mail gateway](docs/reference/mail.md) serves HTTPS with HTTP/2 on port 8025,
26
+ The [mail gateway](docs/reference/mail.md) serves HTTPS with HTTP/2 on port 4433,
27
27
  using certificate paths from `.env.json`, and defaults browser mail to
28
28
  `/v1/mail` on the current domain. Multiple applications can share
29
29
  one server with explicit allowed origins. Subscription verification is disabled
@@ -74,6 +74,16 @@ migration; the current tree has no OS-to-SDK synchronization path.
74
74
 
75
75
  ## Workspace profiles
76
76
 
77
+ Hosts with an existing HTML layout can call the root-exported
78
+ [`generateDocumentImportMaps()`](reference/sdk-api.md#generatedocumentimportmaps)
79
+ with a document root, explicit page paths, and an already materialized runtime.
80
+ This SDK-owned operation builds the shared map once and renders each page's
81
+ URLs from its authored base, preserving page content and script order. It
82
+ updates only SDK-managed inline maps; the host owns document selection,
83
+ runtime materialization, serving, and coordination with other writers. This
84
+ path requires no application descriptor or `apps/<id>/` layout and leaves the
85
+ app-scoped toolchain operation unchanged.
86
+
77
87
  An external workspace maps the exact runtime shipped by its locked `arcane-os`
78
88
  dependency. An Arcane OS checkout is an integrated SDK consumer, not the owner
79
89
  of portable runtime source. For live shared development, the explicit
@@ -48,7 +48,7 @@ meaning and cardinality rules:
48
48
  | `--workspace` | directory | Commands that select an external or integrated workspace; defaults to `.`. |
49
49
  | `--app` | app id or label | Workspace/app operations except shared scope and `verify-bundle`; optional diagnostic label for `mail serve`. |
50
50
  | `--arcane-root` | directory | `doctor`, native `build`/`run`, `native-doctor`, `native-prepare` |
51
- | `--host` / `--port` | host / integer 0–65535 | Browser `dev`/`run` default to HTTPS at `127.0.0.1:8000`; `mail serve` defaults to HTTPS with HTTP/2 at `0.0.0.0:8025` and accepts an explicit bind host. |
51
+ | `--host` / `--port` | host / integer 0–65535 | Browser `dev`/`run` default to HTTPS at `127.0.0.1:8000`; `mail serve` defaults to HTTPS with HTTP/2 at `0.0.0.0:4433` and accepts an explicit bind host. |
52
52
  | `--http-port` | integer 0–65535 | Browser `dev`/`run` HTTP redirect listener; defaults to `0`, which selects an available port. |
53
53
  | `--public` | flag | `dev`; binds to `0.0.0.0` unless `--host` explicitly selects another address. |
54
54
  | `--http` | flag | `dev` only; serves source and PWA routes on one HTTP listener selected by `--port`, without TLS. |
@@ -909,7 +909,7 @@ loss after the attempt begins is ambiguous because Resend may have accepted it.
909
909
  `mail serve` starts one owned HTTPS gateway with HTTP/2:
910
910
 
911
911
  ```text
912
- arcane mail serve [--profile <profile>] [--from <verified-sender>] [--app <label>] [--origin <exact-origin>] [--allow-to <addresses>] [--host 0.0.0.0] [--port 8025] [--request-timeout <ms>]
912
+ arcane mail serve [--profile <profile>] [--from <verified-sender>] [--app <label>] [--origin <exact-origin>] [--allow-to <addresses>] [--host 0.0.0.0] [--port 4433] [--request-timeout <ms>]
913
913
  ```
914
914
 
915
915
  The selected `.env.json` profile supplies only the server-side Resend API key;
@@ -932,9 +932,9 @@ opens; the TLS owner reports PEM file errors. Keep private-key material outside
932
932
  tracked source. The SDK repository already ignores `.arcane/` and `.env.json`.
933
933
 
934
934
  The selected `node-http-server` module negotiates HTTP/2 with HTTP/1.1 fallback
935
- on the same HTTPS port, default `8025`, with no plain-HTTP listener. Callers use
935
+ on the same HTTPS port, default `4433`, with no plain-HTTP listener. Callers use
936
936
  a hostname covered by the certificate, such as
937
- `https://mail.example.com:8025/v1/mail`; `0.0.0.0` identifies the bind address.
937
+ `https://mail.example.com:4433/v1/mail`; `0.0.0.0` identifies the bind address.
938
938
  Restart the gateway after replacing renewed certificate files. Certificate
939
939
  issuance and renewal remain with the deployment's certificate owner.
940
940
 
@@ -6,7 +6,7 @@
6
6
  "minimumVersion": "22.23.2 for Node entrypoints",
7
7
  "moduleSystem": "ESM"
8
8
  },
9
- "memberCount": 209,
9
+ "memberCount": 210,
10
10
  "members": [
11
11
  {
12
12
  "id": "root:APP_BUNDLE_DESCRIPTOR_NAME",
@@ -772,6 +772,22 @@
772
772
  "protocol": "Node ESM",
773
773
  "normalization": "ArcaneError and JSON-safe error normalization"
774
774
  },
775
+ {
776
+ "id": "root:generateDocumentImportMaps",
777
+ "name": "generateDocumentImportMaps",
778
+ "displayName": "generateDocumentImportMaps()",
779
+ "kind": "function",
780
+ "signature": "async generateDocumentImportMaps({documentRoot, runtimeRoot, documents, version=SDK_VERSION, deploymentUrl, signal, onEvent}={})",
781
+ "entrypoints": [
782
+ "arcane-os"
783
+ ],
784
+ "primaryImport": "arcane-os",
785
+ "group": "Runtime and app descriptors",
786
+ "summary": "Writes SDK-managed inline import maps for explicit host documents using an existing materialized runtime and each document's authored base.",
787
+ "availability": "Node on Windows, Linux, and macOS",
788
+ "protocol": "Explicit host documents",
789
+ "normalization": "Absolute filesystem result paths, document-relative browser maps, ordered write events, cancellation, and degraded event-delivery reporting"
790
+ },
775
791
  {
776
792
  "id": "root:getSdkBrowserRuntimeRoot",
777
793
  "name": "getSdkBrowserRuntimeRoot",
@@ -364,10 +364,10 @@ deadline.
364
364
  Start the gateway:
365
365
 
366
366
  ```text
367
- npm exec -- arcane mail serve --profile mail --host 0.0.0.0 --port 8025
367
+ npm exec -- arcane mail serve --profile mail --host 0.0.0.0 --port 4433
368
368
  ```
369
369
 
370
- The default listener is `0.0.0.0:8025`; `--host` and `--port` select its bind
370
+ The default listener is `0.0.0.0:4433`; `--host` and `--port` select its bind
371
371
  address and port. The server can serve callers from multiple domains on the
372
372
  same machine. Route the page's `/v1/mail` to this listener, or configure an
373
373
  explicit shared endpoint in the caller.
@@ -376,7 +376,7 @@ explicit shared endpoint in the caller.
376
376
  `node-http-server` PEM API owns TLS and negotiates HTTP/2 or HTTP/1.1 on the
377
377
  same listener. It creates no additional plain-HTTP listener. The returned URL
378
378
  uses `https://`; `0.0.0.0` is the bind address, so callers use the deployed
379
- domain, for example `https://mail.example.com:8025/v1/mail`.
379
+ domain, for example `https://mail.example.com:4433/v1/mail`.
380
380
 
381
381
  Set `MAIL_TLS_CERT_PATH` to the PEM certificate chain and `MAIL_TLS_KEY_PATH`
382
382
  to its PEM private-key file. These top-level settings belong to the listener
@@ -47,7 +47,7 @@ These protocols normalize orchestration and results. They do not normalize a
47
47
  Windows EXE, Linux DEB, Android APK, and portable directory into the same
48
48
  artifact kind.
49
49
 
50
- Managed browser imports have three supported control-plane entrypoints. The CLI
50
+ App-scoped managed browser imports have three supported control-plane entrypoints. The CLI
51
51
  uses `arcane import-map`; Node callers use
52
52
  `executeOperation('import-map', options)` or
53
53
  `createToolchain(defaults).importMap(options)`. These are three routes to the
@@ -70,6 +70,9 @@ subpath owns shared speech-input cleanup and has the same
70
70
  managed browser key. There is
71
71
  no exported `importMapApplication()` function, `generateImportMap()` function, or
72
72
  `arcane-os/import-map` package subpath.
73
+ Explicit host document lists use the separate root-exported
74
+ [`generateDocumentImportMaps()`](sdk-api.md#generatedocumentimportmaps) API
75
+ described below; they do not enter app discovery.
73
76
 
74
77
  The application dependency boundary is conditional. Only an application that
75
78
  actually consumes Arcane declares one exact published `arcane-os` version in
@@ -147,6 +150,27 @@ unsafe structure and grants no Core capability or provider authority.
147
150
  See [EventManager and time-travel review](event-manager.md) for the callable
148
151
  surface, DOM privacy defaults, playback modes, and recovery behavior.
149
152
 
153
+ ## Explicit host documents
154
+
155
+ `generateDocumentImportMaps()` accepts one document root, an existing
156
+ materialized runtime root, and explicit document-root-relative paths. It
157
+ inventories the runtime and constructs the shared browser map once per call,
158
+ then resolves each document's map against its first href-bearing base or its
159
+ own URL. An absolute or root-relative base, or a relative base traversing above
160
+ the document root, requires the caller's directory `deploymentUrl`; target-only
161
+ bases leave URL resolution unchanged.
162
+
163
+ The operation preserves authored content, resource URLs, custom import maps,
164
+ and script order, updating only SDK-managed inline map blocks. It prepares all
165
+ documents before sequential writes and reports `import-map.documents.started`,
166
+ `import-map.write.progress`, and `import-map.documents.completed`. Cancellation
167
+ can leave completed document writes in place. Successful writes remain
168
+ successful when event delivery fails, with a degraded-delivery record in the
169
+ return value. Runtime materialization, serving, and writer coordination remain
170
+ with the caller. See the [API guide](sdk-api.md#generatedocumentimportmaps) for
171
+ the complete inputs, results, errors, and example. The app-scoped discovery and
172
+ map-artifact lifecycle below remains separate.
173
+
150
174
  ## Browser runtime delivery
151
175
 
152
176
  External and modern integrated workspaces keep the same application URLs and a
@@ -153,6 +153,7 @@ browser map are cataloged separately in [Runtime modules](runtime-modules.md).
153
153
  | `executeNativeBuildPlan()` | function | `arcane-os` | Targets, native plans, and providers | Node; selected browser/native target or provider as documented |
154
154
  | `executeOperation()` | function | `arcane-os` | Headless toolchain operations | Node; selected operation may produce browser or native output |
155
155
  | `fail()` | function | `arcane-os` | Errors | Node |
156
+ | `generateDocumentImportMaps()` | function | `arcane-os` | Runtime and app descriptors | Node; explicit documents and an existing materialized runtime |
156
157
  | `getSdkBrowserRuntimeRoot()` | function | `arcane-os` | Runtime and app descriptors | Node |
157
158
  | `getSdkRoot()` | function | `arcane-os` | Runtime and app descriptors | Node |
158
159
  | `getTargetAdapter()` | function | `arcane-os` | Targets, native plans, and providers | Node; selected browser/native target or provider as documented |
@@ -1137,6 +1138,161 @@ import {APP_DESCRIPTOR_SCHEMA_VERSION} from 'arcane-os';
1137
1138
  console.log(APP_DESCRIPTOR_SCHEMA_VERSION);
1138
1139
  ```
1139
1140
 
1141
+ ## generateDocumentImportMaps()
1142
+
1143
+ ### Overview
1144
+
1145
+ Writes SDK-managed inline browser import maps into explicitly selected HTML
1146
+ documents. Hosts can keep their existing page layout and use an already
1147
+ materialized Arcane runtime without an app descriptor, app discovery, or an
1148
+ `apps/<id>/` directory. The existing app-scoped `arcane import-map` operation
1149
+ continues to own descriptor-selected application pages and its map artifact.
1150
+
1151
+ ### Signature and result
1152
+
1153
+ ```text
1154
+ async generateDocumentImportMaps({
1155
+ documentRoot,
1156
+ runtimeRoot = path.join(documentRoot, 'arcane'),
1157
+ documents,
1158
+ version = SDK_VERSION,
1159
+ deploymentUrl,
1160
+ signal,
1161
+ onEvent
1162
+ } = {})
1163
+ ```
1164
+
1165
+ Import it from `arcane-os`.
1166
+
1167
+ | Input | Contract |
1168
+ | --- | --- |
1169
+ | `documentRoot` | Required filesystem directory containing the selected documents. Returned root paths are absolute. |
1170
+ | `runtimeRoot` | Existing materialized runtime directory containing `modules/`, `entities/`, `sdk/`, and its other selected content. Defaults to `arcane/` under `documentRoot`. Pass an absolute path when selecting another directory; relative root arguments resolve from the current working directory. |
1171
+ | `documents` | Required nonempty array of document-root-relative file paths, such as `['shell/home.html', 'settings.html']`. Use forward slashes and normalized paths without a leading `./` or parent traversal. Duplicate paths are processed once, in first-selected order. |
1172
+ | `version` | Version used in generated import-map targets and URL compatibility keys. Defaults to the installed SDK's `SDK_VERSION`; `null` selects unversioned map URLs. Authored resource URLs outside the managed map stay unchanged. |
1173
+ | `deploymentUrl` | Optional absolute directory URL ending in `/` that corresponds to `documentRoot`, such as `https://example.com/control/`. Required when a selected document has an absolute or root-relative base URL, or a relative base that traverses above `documentRoot`. |
1174
+ | `signal` | Optional `AbortSignal`, observed during inventory, document preparation, and before each write. |
1175
+ | `onEvent` | Optional named callback receiving the ordered events below. An asynchronous callback is awaited before the operation continues. |
1176
+
1177
+ The resolved result contains:
1178
+
1179
+ ```text
1180
+ {
1181
+ documentRoot,
1182
+ runtimeRoot,
1183
+ documentPaths,
1184
+ documentCount,
1185
+ documents: [{path, filePath, imports}],
1186
+ committed: true
1187
+ }
1188
+ ```
1189
+
1190
+ `documentPaths` and each `filePath` are absolute filesystem paths. Each `path`
1191
+ is relative to `documentRoot`; `imports` is the complete map rendered for that
1192
+ document. The operation inventories the runtime once, builds the shared map
1193
+ once, and reads the selected documents with up to four concurrent readers.
1194
+ It renders every document before writing any of them, then writes in selected
1195
+ order. `committed: true` means all selected writes completed; the batch is not
1196
+ an atomic filesystem transaction.
1197
+
1198
+ ### Document URLs and authored content
1199
+
1200
+ The first `<base>` with an `href` determines each document's effective base.
1201
+ A target-only base, such as `<base target="_blank">`, does not change URL
1202
+ resolution. With no base, `shell/home.html` resolves the default runtime through
1203
+ `../arcane/`. With `<base href="../">`, the same page resolves it through
1204
+ `./arcane/`. Generated URLs include the selected version when one is supplied.
1205
+
1206
+ The host serves the runtime at the same relative location as `runtimeRoot`
1207
+ has to `documentRoot`. `deploymentUrl` supplies that directory's public URL
1208
+ when an authored base needs an origin or deployment path to resolve correctly.
1209
+ For example, `https://example.com/control/` corresponds to `documentRoot`, so
1210
+ its default runtime is served at `https://example.com/control/arcane/`.
1211
+ Cross-origin bases produce absolute runtime map URLs. Without `deploymentUrl`,
1212
+ an absolute or root-relative base causes a `TypeError` before any document is
1213
+ written. A relative base that traverses above `documentRoot` also requires
1214
+ `deploymentUrl`, even if later path segments return into the physical directory:
1215
+ the filesystem's parent names do not establish public deployment paths.
1216
+ The two filesystem roots must share a volume.
1217
+
1218
+ The generator replaces only SDK-marked `<script type="importmap"
1219
+ data-arcane-import-map>` ranges, consolidating multiple SDK blocks into one.
1220
+ It preserves complete authored HTML around those ranges, including whitespace,
1221
+ resource URLs, custom import maps, and the attributes and order of classic,
1222
+ module, `async`, and `defer` scripts. Custom maps remain separate and are not
1223
+ merged into the SDK map. The managed block follows the effective base and
1224
+ precedes executable scripts and module preloads. An authored base that follows
1225
+ one of those loads is reported as `ARCANE_IMPORT_MAP_INVALID` before writes.
1226
+
1227
+ This operation writes only the selected HTML files. The caller owns runtime
1228
+ materialization, document selection, serving, and coordination with other
1229
+ document writers. It creates no map sidecar, copies no runtime content, and
1230
+ starts no build or browser.
1231
+
1232
+ ### Events, errors, and cancellation
1233
+
1234
+ | Event | Fields |
1235
+ | --- | --- |
1236
+ | `import-map.documents.started` | `documentRoot`, `runtimeRoot`, and the complete `documentPaths` selection. |
1237
+ | `import-map.write.progress` | `paths`: all absolute document paths successfully written so far. |
1238
+ | `import-map.documents.completed` | The successful result fields, including `committed: true`. |
1239
+
1240
+ If an event callback throws or rejects, generation continues. A successful
1241
+ return additionally reports `eventDelivery: {status: 'degraded', errorCode:
1242
+ 'ARCANE_EVENT_DELIVERY_FAILED', message}` for the first callback failure.
1243
+
1244
+ Invalid options can throw `TypeError`; document/map structure failures use
1245
+ `ARCANE_IMPORT_MAP_INVALID`, runtime inventory failures use
1246
+ `ARCANE_RUNTIME_INVALID`, and filesystem failures propagate. All started
1247
+ preparation reads settle before rejection; multiple failures are
1248
+ reported together in an `AggregateError`. Cancellation
1249
+ rejects with the signal's error reason when supplied, otherwise an operation
1250
+ cancellation error; an error lacking a code receives `ARCANE_CANCELLED`.
1251
+ Preparation failures occur before writes. Cancellation or a filesystem failure
1252
+ during writing can leave earlier completed files updated; the operation does
1253
+ not roll those files back. An in-flight filesystem write completes before the
1254
+ next cancellation boundary.
1255
+
1256
+ ### Availability and normalization
1257
+
1258
+ **Node on Windows, Linux, and macOS.** Filesystem paths are resolved with the
1259
+ host path API; browser import URLs use forward slashes and URL encoding. Deep
1260
+ protocol: [Explicit host documents](protocols.md#explicit-host-documents).
1261
+
1262
+ ### Example
1263
+
1264
+ A dragon observatory keeps its control page at `shell/home.html`, settings at
1265
+ `settings.html`, and its selected Arcane runtime at `arcane/`. Run this from
1266
+ that document root after materializing the runtime:
1267
+
1268
+ ```javascript
1269
+ import {generateDocumentImportMaps} from 'arcane-os';
1270
+
1271
+ const controller = new AbortController();
1272
+
1273
+ function reportImportMapEvent(event) {
1274
+ console.log(event);
1275
+ }
1276
+
1277
+ const result = await generateDocumentImportMaps(
1278
+ {
1279
+ documentRoot: process.cwd(),
1280
+ documents: ['shell/home.html', 'settings.html'],
1281
+ signal: controller.signal,
1282
+ onEvent: reportImportMapEvent
1283
+ }
1284
+ );
1285
+
1286
+ for (const document of result.documents) {
1287
+ console.log(document.path, document.imports['arcane/AI']);
1288
+ }
1289
+ ```
1290
+
1291
+ Change the explicit document list to update another host page. If either page
1292
+ uses `<base href="/control/">`, add
1293
+ `deploymentUrl: 'https://example.com/control/'` to the options. Both pages keep
1294
+ their authored layout and receive map URLs appropriate to their own base.
1295
+
1140
1296
  ## getSdkBrowserRuntimeRoot()
1141
1297
 
1142
1298
  ### Overview
@@ -52,7 +52,7 @@ Configuration and foundational methods follow. Each baseline name is recorded so
52
52
  | `positiveInteger` | Resolves the retry delay; baseline exposes unused `allowZero`. | Y/Y/Y — Simplify. | The uncalled zero branch and its extra option/prose are removed. Current name: `readRetryDelayMs`, which resolves the actual retry-delay setting. |
53
53
  | `optionalTimeoutMs` | Reads body/provider deadlines only when supplied. | Y/Y/N — Keep. | Omitting a deadline produces no timer. Node timer-range handling prevents caller-selected delays from changing meaning. Current name remains `optionalTimeoutMs`. |
54
54
  | `normalizeRetryAfter` | Error and result constructors keep a usable retry delay. | Y/Y/N — Keep. | Retry scheduling consumes this value. It is control metadata, not a transformation of message content. Current name: `retryDelayOrZero`. |
55
- | `portNumber` | Startup resolves default or configured port, including ephemeral port zero. | Y/Y/Y — Remove wrapper; retain configuration value. | The HTTP owner handles actual port binding. Preserve default 8025 and explicit port zero; no extra port validator or domain policy is required. |
55
+ | `portNumber` | Startup resolves default or configured port, including ephemeral port zero. | Y/Y/Y — Remove wrapper; retain configuration value. | The HTTP owner handles actual port binding. Use the selected default 4433 and preserve explicit port zero; no extra port validator or domain policy is required. |
56
56
  | `validateSignal` | Direct send and server options accept caller cancellation. | Y/Y/Y — Remove wrapper; retain original signal. | Native signal operations already own their contract. Cancellation controllers/listeners and the abortable adapter remain. |
57
57
  | `validateApiKey` | Provider configuration supplies the credential used by Resend Fetch. | Y/Y/Y — Remove local format validator. | CLI credential retrieval reports missing provider credentials; Fetch/provider own actual transport validity. Never echo the credential. |
58
58
  | `validateAppId` | Validates application identity using the old lowercase-slug grammar. | Y/Y/Y — Remove the grammar and closed-list/equality admission. | Per-request identity matters and remains an ordinary string. Any application may use the shared server. Read the supplied identity for subscription verification without rewriting it; no standalone wrapper is needed solely to restrict spelling. |
@@ -395,7 +395,7 @@ cancellation before writes, and CLI defaults. Local tests and checks were not ru
395
395
  ## HTTPS and HTTP/2 follow-up
396
396
 
397
397
  The user explicitly selected HTTPS with HTTP/2. The mail listener remains on
398
- its dedicated configured port, default 8025, with the published
398
+ its dedicated configured port, now default 4433, with the published
399
399
  `node-http-server` module owning TLS, protocol negotiation and listener/session
400
400
  shutdown. This increment preserves the provider attempt, report content,
401
401
  subscription callback, sender selection, recipient configuration and cancellation.
@@ -423,3 +423,16 @@ JSON paths and missing TLS settings. Synthetic TLS selection is not evidence of
423
423
  an encrypted handshake. No local tests, checks, server launch, real TLS
424
424
  negotiation or provider send were performed for this follow-up review. Selected
425
425
  package and publication results belong to the delivery record.
426
+
427
+ ## Main mail port follow-up
428
+
429
+ The user selected 4433 as the main mail-server port. Both the CLI's omitted-port
430
+ setting and the server's programmatic default now select 4433. Explicit ports,
431
+ including ephemeral port zero, continue through the existing configuration.
432
+
433
+ The port setting passes Y/Y/N: the listener needs a bind port, deployments need
434
+ an override, and removing either would lose required behavior. Changing the two
435
+ defaults adds no runtime work or helper. TLS negotiation, the returned endpoint
436
+ and the occupied-port message already use the selected port. Existing test
437
+ source was updated for the omitted-port path; explicit-port cases were retained.
438
+ No local tests, checks or server launch were performed for this increment.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
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",
package/src/cli/main.mjs CHANGED
@@ -85,7 +85,7 @@ Usage:
85
85
  ${CLI_NAME} mail key status [profile]
86
86
  ${CLI_NAME} mail key delete [profile]
87
87
  ${CLI_NAME} mail send [--profile <profile>] [--from <address>] --report-key <id> --report-stdin [--request-timeout <ms>]
88
- ${CLI_NAME} mail serve [--profile <profile>] [--from <address>] [--app <label>] [--origin <origin>] [--allow-to <addresses>] [--host 0.0.0.0] [--port 8025] [--request-timeout <ms>]
88
+ ${CLI_NAME} mail serve [--profile <profile>] [--from <address>] [--app <label>] [--origin <origin>] [--allow-to <addresses>] [--host 0.0.0.0] [--port 4433] [--request-timeout <ms>]
89
89
  HTTPS/HTTP2; .env.json supplies RESEND_API_KEY, MAIL_TLS_CERT_PATH, and MAIL_TLS_KEY_PATH.
90
90
 
91
91
  Development:
@@ -698,7 +698,7 @@ function operationOptions(command,parsed,cwd){
698
698
  origin:values.origin,
699
699
  allowTo:values['allow-to'],
700
700
  host:values.host??'0.0.0.0',
701
- port:readPort(values.port,8025),
701
+ port:readPort(values.port,4433),
702
702
  requestTimeout:readMailRequestTimeout(values['request-timeout']),
703
703
  };
704
704
  }
@@ -3,6 +3,7 @@ import {lstat,mkdir,readFile as readFileFromDisk,readdir,realpath,writeFile} fro
3
3
  import path from 'node:path';
4
4
  import {pathToFileURL} from 'node:url';
5
5
  import {SDK_VERSION} from './constants.mjs';
6
+ import {listRuntimeFiles} from './runtime.mjs';
6
7
 
7
8
  const is = new Is(false);
8
9
 
@@ -39,7 +40,7 @@ function fail(message,code='ARCANE_IMPORT_MAP_INVALID'){
39
40
  function throwIfAborted(signal){
40
41
  if(!signal?.aborted)return;
41
42
  const error=signal.reason instanceof Error?signal.reason:new Error('Operation cancelled.');
42
- error.code=error.code||'ARCANE_CANCELLED';
43
+ if(error.code===undefined)error.code='ARCANE_CANCELLED';
43
44
  throw error;
44
45
  }
45
46
 
@@ -1296,9 +1297,16 @@ function validateInventory(files){
1296
1297
  return exact;
1297
1298
  }
1298
1299
 
1299
- export async function buildImportMap({files,signal,version=SDK_VERSION}={}){
1300
+ function encodedUrlPath(relative) {
1301
+ return relative.split('/').map(encodeURIComponent).join('/');
1302
+ }
1303
+
1304
+ export async function buildImportMap({files,signal,version=SDK_VERSION,encodePaths=false}={}){
1300
1305
  throwIfAborted(signal);
1301
1306
  const inventory=validateInventory(files);
1307
+ function runtimeTarget(relative) {
1308
+ return `./arcane/${encodePaths ? encodedUrlPath(relative) : relative}`;
1309
+ }
1302
1310
  const modules=[...inventory]
1303
1311
  .filter(relative=>relative.startsWith('modules/')
1304
1312
  &&!relative.slice('modules/'.length).includes('/')
@@ -1308,7 +1316,7 @@ export async function buildImportMap({files,signal,version=SDK_VERSION}={}){
1308
1316
  for(const relative of modules){
1309
1317
  throwIfAborted(signal);
1310
1318
  const name=path.posix.basename(relative).replace(JAVASCRIPT_EXTENSION,'');
1311
- registerSpecifier(namedRegistry,`arcane/${name}`,`./arcane/${relative}`);
1319
+ registerSpecifier(namedRegistry,`arcane/${name}`,runtimeTarget(relative));
1312
1320
  }
1313
1321
  const entities=[...inventory].filter(relative=>relative.startsWith('entities/')
1314
1322
  &&!relative.slice('entities/'.length).includes('/')
@@ -1316,7 +1324,7 @@ export async function buildImportMap({files,signal,version=SDK_VERSION}={}){
1316
1324
  for(const relative of entities){
1317
1325
  throwIfAborted(signal);
1318
1326
  const name=path.posix.basename(relative).replace(JAVASCRIPT_EXTENSION,'');
1319
- registerSpecifier(namedRegistry,`arcane/entities/${name}`,`./arcane/${relative}`);
1327
+ registerSpecifier(namedRegistry,`arcane/entities/${name}`,runtimeTarget(relative));
1320
1328
  }
1321
1329
  for(const [specifier,relative] of STATIC_RUNTIME_PACKAGE_IMPORTS){
1322
1330
  if(inventory.has(relative)){
@@ -1363,7 +1371,8 @@ export async function buildImportMap({files,signal,version=SDK_VERSION}={}){
1363
1371
  }
1364
1372
  for(const relative of [...inventory].sort(compareText)){
1365
1373
  if(JAVASCRIPT_EXTENSION.test(relative)){
1366
- registerSpecifier(namedRegistry,`./arcane/${relative}`,`./arcane/${relative}`);
1374
+ const target=runtimeTarget(relative);
1375
+ registerSpecifier(namedRegistry,target,target);
1367
1376
  }
1368
1377
  }
1369
1378
  const imports={};
@@ -2062,8 +2071,7 @@ function removeManagedBlocks(html,blocks){
2062
2071
  return result;
2063
2072
  }
2064
2073
 
2065
- function firstModulePosition(html){
2066
- const structure=scanHtmlStructure(html);
2074
+ function firstModulePosition(html,structure=scanHtmlStructure(html)){
2067
2075
  let first=-1;
2068
2076
  for(const script of structure.scripts){
2069
2077
  const attributes=parseTagAttributes(script.open);
@@ -2434,6 +2442,312 @@ export async function readApplicationTestImportMapContext({
2434
2442
  });
2435
2443
  }
2436
2444
 
2445
+ function documentImportMapStructure(html) {
2446
+ const structure = scanHtmlStructure(html);
2447
+ const managed = [];
2448
+ const executableTypes = new Set(
2449
+ [
2450
+ '', 'module', 'application/ecmascript', 'application/javascript',
2451
+ 'application/x-ecmascript', 'application/x-javascript',
2452
+ 'text/ecmascript', 'text/javascript', 'text/javascript1.0',
2453
+ 'text/javascript1.1', 'text/javascript1.2', 'text/javascript1.3',
2454
+ 'text/javascript1.4', 'text/javascript1.5', 'text/jscript',
2455
+ 'text/livescript', 'text/x-ecmascript', 'text/x-javascript'
2456
+ ]
2457
+ );
2458
+ let firstLoad = firstModulePosition(html, structure);
2459
+ let base = null;
2460
+ for (const element of structure.bases) {
2461
+ const attributes = parseTagAttributes(element.open);
2462
+ if (!attributes.has('href')) continue;
2463
+ base = {
2464
+ ...element,
2465
+ href:structuralAttribute(attributes, 'href', 'base')
2466
+ };
2467
+ break;
2468
+ }
2469
+ for (const script of structure.scripts) {
2470
+ const attributes = parseTagAttributes(script.open);
2471
+ const type = scriptType(attributes);
2472
+ if (type === 'importmap' && attributes.has(MANAGED_IMPORT_MAP_ATTRIBUTE)) {
2473
+ if (!script.closed) fail('Document contains an unterminated SDK import-map script.');
2474
+ managed.push(script);
2475
+ }
2476
+ if (executableTypes.has(type) && (firstLoad < 0 || script.start < firstLoad)) {
2477
+ firstLoad = script.start;
2478
+ }
2479
+ }
2480
+ if (base && firstLoad >= 0 && base.end > firstLoad) {
2481
+ fail('Document base must precede executable scripts and modulepreloads.');
2482
+ }
2483
+ return {
2484
+ structure,
2485
+ managed,
2486
+ firstLoad,
2487
+ base
2488
+ };
2489
+ }
2490
+
2491
+ function documentRelativeUrl(target, base) {
2492
+ if (target.protocol !== base.protocol || target.host !== base.host) return target.href;
2493
+ const directory = new URL('.', base);
2494
+ const baseSegments = directory.pathname.split('/');
2495
+ baseSegments.pop();
2496
+ const targetSegments = target.pathname.split('/');
2497
+ let shared = 0;
2498
+ while (shared < baseSegments.length && shared < targetSegments.length - 1
2499
+ && baseSegments[shared] === targetSegments[shared]) {
2500
+ shared += 1;
2501
+ }
2502
+ // URL paths retain empty segments; filesystem relative() would collapse them.
2503
+ const relative = [
2504
+ ...new Array(baseSegments.length - shared).fill('..'),
2505
+ ...targetSegments.slice(shared)
2506
+ ].join('/');
2507
+ const pathname = relative.startsWith('./') || relative.startsWith('../')
2508
+ ? relative : `./${relative}`;
2509
+ return `${pathname}${target.search}${target.hash}`;
2510
+ }
2511
+
2512
+ function documentImports(imports, context, baseHref, relative) {
2513
+ const documentUrl = new URL(encodedUrlPath(relative), context.deployment);
2514
+ // Match URL parser normalization only for resolving the base; preserve authored HTML.
2515
+ const baseAddress = (baseHref ?? '').replace(/[\t\r\n]/gu, '').replace(
2516
+ /^[\u0000-\u0020]+|[\u0000-\u0020]+$/gu,
2517
+ ''
2518
+ );
2519
+ if (!context.explicitDeployment && /^(?:[\\/]|[A-Za-z][A-Za-z0-9+.-]*:)/u.test(baseAddress)) {
2520
+ throw new TypeError('An absolute or root-relative document base requires deploymentUrl.');
2521
+ }
2522
+ if (!context.explicitDeployment) {
2523
+ let depth = relative.split('/').length - 1;
2524
+ const basePath = baseAddress.split(/[?#]/u)[0].replaceAll('\\', '/');
2525
+ for (const part of basePath.split('/')) {
2526
+ const segment = part.replace(/%2e/giu, '.');
2527
+ if (segment === '..') {
2528
+ if (depth === 0) {
2529
+ throw new TypeError('A document base traversing above documentRoot requires deploymentUrl.');
2530
+ }
2531
+ depth -= 1;
2532
+ } else if (segment !== '.') {
2533
+ depth += 1;
2534
+ }
2535
+ }
2536
+ }
2537
+ const base = baseHref === undefined ? documentUrl : new URL(baseHref, documentUrl);
2538
+ function rebaseUrl(value) {
2539
+ if (value.startsWith('./arcane/')) {
2540
+ return documentRelativeUrl(new URL(value.substring('./arcane/'.length), context.runtime), base);
2541
+ }
2542
+ if (value.startsWith('./node_modules/')) {
2543
+ return documentRelativeUrl(new URL(value, context.deployment), base);
2544
+ }
2545
+ return value;
2546
+ }
2547
+ const rebased = {};
2548
+ for (const [specifier, target] of Object.entries(imports)) {
2549
+ rebased[rebaseUrl(specifier)] = rebaseUrl(target);
2550
+ }
2551
+ return rebased;
2552
+ }
2553
+
2554
+ function renderDocumentImportMap(html, imports, state) {
2555
+ const {structure, managed, firstLoad, base} = state;
2556
+ const newline = html.includes('\r\n') ? '\r\n' : '\n';
2557
+ const json = JSON.stringify({imports}, null, 2).replaceAll('<', '\\u003c');
2558
+ const block = `<script type="importmap" ${MANAGED_IMPORT_MAP_ATTRIBUTE}>${newline}${json}${newline}</script>`;
2559
+ const firstManaged = managed[0];
2560
+ const replaceInPlace = firstManaged
2561
+ && (!base || firstManaged.start >= base.end)
2562
+ && (firstLoad < 0 || firstManaged.start < firstLoad);
2563
+ const edits = managed.map(
2564
+ function replaceSdkMap(script, index) {
2565
+ return {
2566
+ start:script.start,
2567
+ end:script.end,
2568
+ value:replaceInPlace && index === 0 ? block : ''
2569
+ };
2570
+ }
2571
+ );
2572
+ if (!replaceInPlace) {
2573
+ const candidate = firstLoad >= 0 ? firstLoad
2574
+ : structure.headClose >= 0 ? structure.headClose
2575
+ : structure.bodyClose >= 0 ? structure.bodyClose : html.length;
2576
+ const insertion = Math.max(candidate, base?.end ?? 0);
2577
+ edits.push(
2578
+ {
2579
+ start:insertion,
2580
+ end:insertion,
2581
+ value:block
2582
+ }
2583
+ );
2584
+ }
2585
+ // Exact script ranges are SDK-owned; surrounding whitespace and author maps are not.
2586
+ return applyReferenceEdits(html, edits);
2587
+ }
2588
+
2589
+ /** Generate SDK maps for explicitly selected documents without imposing an app layout. */
2590
+ export async function generateDocumentImportMaps({
2591
+ documentRoot,
2592
+ runtimeRoot,
2593
+ documents,
2594
+ version = SDK_VERSION,
2595
+ deploymentUrl,
2596
+ signal,
2597
+ onEvent
2598
+ } = {}) {
2599
+ if (!is.string(documentRoot) || !documentRoot.trim()) {
2600
+ throw new TypeError('generateDocumentImportMaps documentRoot must be a nonempty string.');
2601
+ }
2602
+ if (!is.array(documents) || documents.length === 0) {
2603
+ throw new TypeError('generateDocumentImportMaps documents must be a nonempty array of relative paths.');
2604
+ }
2605
+ throwIfAborted(signal);
2606
+ const root = path.resolve(documentRoot);
2607
+ const runtime = path.resolve(runtimeRoot ?? path.join(root, 'arcane'));
2608
+ const selected = [...new Set(documents.map(
2609
+ function documentPath(value) {
2610
+ return safeRelativePath(value, 'document path');
2611
+ }
2612
+ ))];
2613
+ const deployment = deploymentUrl === undefined
2614
+ ? pathToFileURL(`${root}${path.sep}`) : new URL(deploymentUrl);
2615
+ if (!deployment.pathname.endsWith('/')) {
2616
+ throw new TypeError('deploymentUrl must name a directory URL ending in /.');
2617
+ }
2618
+ const relativeRuntime = path.relative(root, runtime).split(path.sep).join('/');
2619
+ if (path.isAbsolute(path.relative(root, runtime))) {
2620
+ throw new TypeError('documentRoot and runtimeRoot must share a filesystem volume.');
2621
+ }
2622
+ const context = {
2623
+ deployment,
2624
+ runtime:new URL(relativeRuntime ? `${encodedUrlPath(relativeRuntime)}/` : './', deployment),
2625
+ explicitDeployment:deploymentUrl !== undefined
2626
+ };
2627
+ const documentPaths = selected.map(
2628
+ function resolveDocumentPath(relative) {
2629
+ return path.join(root, ...relative.split('/'));
2630
+ }
2631
+ );
2632
+ let eventError = await emit(
2633
+ onEvent,
2634
+ {
2635
+ type:'import-map.documents.started',
2636
+ documentRoot:root,
2637
+ runtimeRoot:runtime,
2638
+ documentPaths
2639
+ }
2640
+ );
2641
+ const documentStates = new Array(selected.length);
2642
+ const pending = selected.entries();
2643
+ async function readDocuments() {
2644
+ for (const [index, relative] of pending) {
2645
+ throwIfAborted(signal);
2646
+ const html = await readFileFromDisk(documentPaths[index], 'utf8');
2647
+ throwIfAborted(signal);
2648
+ documentStates[index] = {
2649
+ path:relative,
2650
+ filePath:documentPaths[index],
2651
+ html,
2652
+ state:documentImportMapStructure(html)
2653
+ };
2654
+ }
2655
+ }
2656
+ const readers = Array.from(
2657
+ {length:Math.min(4, selected.length)},
2658
+ readDocuments
2659
+ );
2660
+ const outcomes = await Promise.allSettled(
2661
+ [
2662
+ listRuntimeFiles(
2663
+ {
2664
+ runtimeRoot:runtime,
2665
+ signal
2666
+ }
2667
+ ),
2668
+ ...readers
2669
+ ]
2670
+ );
2671
+ throwIfAborted(signal);
2672
+ const errors = outcomes.filter(
2673
+ function failedRead(outcome) {
2674
+ return outcome.status === 'rejected';
2675
+ }
2676
+ ).map(
2677
+ function readError(outcome) {
2678
+ return outcome.reason;
2679
+ }
2680
+ );
2681
+ if (errors.length === 1) throw errors[0];
2682
+ if (errors.length > 1) throw new AggregateError(errors, 'Document import-map preparation failed.');
2683
+ const built = await buildImportMap(
2684
+ {
2685
+ files:outcomes[0].value,
2686
+ signal,
2687
+ version,
2688
+ encodePaths:true
2689
+ }
2690
+ );
2691
+ const rendered = documentStates.map(
2692
+ function renderSelectedDocument(document) {
2693
+ throwIfAborted(signal);
2694
+ const imports = documentImports(built.imports, context, document.state.base?.href, document.path);
2695
+ return {
2696
+ path:document.path,
2697
+ filePath:document.filePath,
2698
+ imports,
2699
+ html:renderDocumentImportMap(document.html, imports, document.state)
2700
+ };
2701
+ }
2702
+ );
2703
+ const paths = [];
2704
+ for (const document of rendered) {
2705
+ throwIfAborted(signal);
2706
+ await writeFile(document.filePath, document.html, 'utf8');
2707
+ paths.push(document.filePath);
2708
+ const deliveryError = await emit(
2709
+ onEvent,
2710
+ {
2711
+ type:'import-map.write.progress',
2712
+ paths:[...paths]
2713
+ }
2714
+ );
2715
+ eventError ??= deliveryError;
2716
+ }
2717
+ const result = {
2718
+ documentRoot:root,
2719
+ runtimeRoot:runtime,
2720
+ documentPaths,
2721
+ documentCount:documentPaths.length,
2722
+ documents:rendered.map(
2723
+ function documentResult(document) {
2724
+ return {
2725
+ path:document.path,
2726
+ filePath:document.filePath,
2727
+ imports:document.imports
2728
+ };
2729
+ }
2730
+ ),
2731
+ committed:true
2732
+ };
2733
+ const completedError = await emit(
2734
+ onEvent,
2735
+ {
2736
+ type:'import-map.documents.completed',
2737
+ ...result
2738
+ }
2739
+ );
2740
+ eventError ??= completedError;
2741
+ if (eventError) {
2742
+ result.eventDelivery = {
2743
+ status:'degraded',
2744
+ errorCode:'ARCANE_EVENT_DELIVERY_FAILED',
2745
+ message:String(eventError?.message ?? eventError)
2746
+ };
2747
+ }
2748
+ return result;
2749
+ }
2750
+
2437
2751
  async function generateImportMapUnlocked({
2438
2752
  workspaceRoot,
2439
2753
  appId,
package/src/index.mjs CHANGED
@@ -135,6 +135,7 @@ export {
135
135
  readSdkBrowserRuntimeFile
136
136
  } from './sdk-browser-runtime.mjs';
137
137
  export {materializeWorkspaceRuntimeContent} from './workspace-runtime.mjs';
138
+ export {generateDocumentImportMaps} from './import-map.mjs';
138
139
  export {
139
140
  discoverApps,
140
141
  inspectWorkspaceProfile,
@@ -113,7 +113,7 @@ function resolveMailServerConfiguration(options={}){
113
113
  host:options.host??'0.0.0.0',
114
114
  callerAuthentication:options.verifySubscription?'subscription':'none',
115
115
  onEvent:options.onEvent,
116
- port:options.port??8025,
116
+ port:options.port??4433,
117
117
  providerTimeoutMs:optionalTimeoutMs(options.providerTimeoutMs,'providerTimeoutMs'),
118
118
  requestIdFactory:options.requestIdFactory??randomUUID,
119
119
  retryableDelayMs:readRetryDelayMs(options.retryableDelayMs),
package/src/runtime.mjs CHANGED
@@ -17,7 +17,7 @@ function fail(message,code='ARCANE_RUNTIME_INVALID'){
17
17
  function throwIfAborted(signal){
18
18
  if(!signal?.aborted)return;
19
19
  const error=signal.reason instanceof Error?signal.reason:new Error('Operation cancelled.');
20
- error.code=error.code||'ARCANE_CANCELLED';
20
+ if(error.code===undefined)error.code='ARCANE_CANCELLED';
21
21
  throw error;
22
22
  }
23
23