arcane-os 0.24.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.26.0
4
+
5
+ - Add explicit installed-package browser runtime routes. Source development,
6
+ managed import maps, application test-map resolution, and PWA resource
7
+ selection can use the npm dependency directly without a generated workspace
8
+ runtime or lock. Keep existing browser URLs, aliases, and materialized layouts.
9
+ - Let a shared package route select its complete source directory with
10
+ `include: ["."]`. Portable app output includes the selected runtime,
11
+ dependencies, assets, and notices without requiring the SDK at deployment.
12
+ - Expose lowercase public module subpaths for shared AI preferences, provider
13
+ state, model definitions, conversation helpers, application data, document
14
+ libraries, and local AI readiness. Browser maps and Node package resolution
15
+ select the same canonical implementations; browser runtime requirements remain.
16
+ - Share the existing portable Mail aggregation with browser import maps while
17
+ preserving the `arcane-os/mail` API and Node mail CLI used by server consumers.
18
+
19
+ ## 0.25.0
20
+
21
+ - Automatically skip configured mail subscription verification when the actual
22
+ requester connection IP equals the server-side IP of that connection. This
23
+ lets services sharing that IP and local development send without a subscription
24
+ key. Other requester IPs, including other intranet machines, still use the
25
+ configured verification callback.
26
+ - Use native connection addresses for HTTP/1 and HTTP/2, with no environment
27
+ setting, domain lookup, forwarded-header interpretation, or special loopback rule.
28
+ Preserve CORS, report handling, provider delivery, and existing caller APIs.
29
+ - Document the IP rule and its purpose gates; update focused test source for
30
+ the exemption and the remaining subscription-verification paths.
31
+
3
32
  ## 0.24.1
4
33
 
5
34
  - Add a mail CLI parameter table with command scope, purpose, and defaults.
package/README.md CHANGED
@@ -19,10 +19,19 @@ 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.24.1` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.26.0` 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
+ External browser apps can use the installed npm package directly, without a
27
+ generated workspace `arcane/` directory or `arcane.lock.json`. Select the
28
+ [four installed-package routes](docs/reference/protocols.md#installed-package-browser-routes)
29
+ in `arcane-packager.json`: development, managed import maps, and PWA resources
30
+ read the installed SDK while keeping `/arcane/...` browser URLs. Portable app
31
+ packages still contain their selected runtime. Existing materialized workspaces
32
+ and the scaffold's current layout remain supported. Node services continue to
33
+ use the installed CLI or public imports such as `arcane-os/mail`.
34
+
26
35
  The [mail gateway](docs/reference/mail.md) serves HTTPS with HTTP/2 on port 4433
27
36
  by default. Configure its host, port, origin list, certificate paths, and other
28
37
  mail settings in `arcane.config.json.mail`; keep provider keys in the ignored
@@ -31,7 +40,11 @@ Existing root credential, profile, and TLS settings remain supported. Browser
31
40
  mail defaults to `/v1/mail` on the current domain, and multiple applications can
32
41
  share one server with explicit allowed origins. Subscription verification is disabled
33
42
  until a `verifySubscription` callback is configured; that callback receives the
34
- application name and bearer subscription key before each provider attempt.
43
+ application name and bearer subscription key before provider attempts from other
44
+ requester IPs. Requests whose actual connection source IP equals the server-side
45
+ IP of that connection automatically skip subscription verification, including
46
+ local development. Other intranet machines still need verification. The exception
47
+ requires no environment setting or domain lookup.
35
48
  See the [method and action gate report](docs/reviews/mail-server-purpose-review.md)
36
49
  for the removals, retained responsibilities, and naming decisions.
37
50
 
@@ -58,7 +71,7 @@ Create one browser application, install its pinned SDK, and start its source
58
71
  server:
59
72
 
60
73
  ```bash
61
- npx arcane-os@0.12.0 new hello-speech --path ./hello-speech --target browser
74
+ npx arcane-os@0.26.0 new hello-speech --path ./hello-speech --target browser
62
75
  cd hello-speech
63
76
  npm install
64
77
  ```
@@ -84,13 +84,22 @@ runtime materialization, serving, and coordination with other writers. This
84
84
  path requires no application descriptor or `apps/<id>/` layout and leaves the
85
85
  app-scoped toolchain operation unchanged.
86
86
 
87
- An external workspace maps the exact runtime shipped by its locked `arcane-os`
88
- dependency. An Arcane OS checkout is an integrated SDK consumer, not the owner
87
+ An external workspace can select `installed-v1` routes directly from its
88
+ `node_modules/arcane-os` installation, without a workspace `arcane/` tree or
89
+ `arcane.lock.json`. The SDK reads the installed package version and maps its
90
+ runtime, browser runtime, runtime dependency, and licenses to the established
91
+ browser destinations. Existing `physical-v1` workspaces and the explicit
92
+ materializer remain supported; scaffolding retains its existing physical
93
+ layout. See [installed-package routes](reference/protocols.md#installed-package-browser-routes)
94
+ for the configuration and npm-alias form.
95
+
96
+ An Arcane OS checkout is an integrated SDK consumer, not the owner
89
97
  of portable runtime source. For live shared development, the explicit
90
98
  development-only SDK source mount maps the canonical SDK runtime and dependency
91
- paths into that consumer. Without the mount, the workspace uses its locked SDK
92
- projection. The development server and packager consume the same route
93
- destinations in both cases, so app imports do not change. Integrated
99
+ paths into that consumer. Without the mount, integrated workspaces retain their
100
+ physical projection; external workspaces select either layout above. The development server,
101
+ import-map generator, packager, and PWA inventory consume the same route
102
+ destinations, so app imports do not change. Integrated
94
103
  initialization creates only app-owned files and never rewrites Arcane OS or SDK
95
104
  root configuration.
96
105
 
@@ -235,13 +244,14 @@ exact schema-1 `arcane-package.json` for current consumers. Existing Arcane
235
244
  apps synthesize that descriptor from their schema-1 package plus the current
236
245
  native registry during migration.
237
246
 
238
- An external app's `arcane-packager.json` has three exact shared routes. They map
239
- the installed SDK runtime to `/arcane`, its vendored strong-type dependency to
240
- `/node_modules/strong-type`, and the SDK's `LICENSE`,
241
- `COMMERCIAL-LICENSE.md`, and `NOTICE` to `/licenses/arcane-os`. Development does
242
- not copy SDK runtime source into the app repository. Distribution materializes
243
- those selected SDK routes completely inside the portable artifact, so the
244
- finished app has no Arcane OS runtime dependency.
247
+ An external app can use four `installed-v1` shared routes in
248
+ `arcane-packager.json`: the installed SDK's `runtime/arcane` maps to `/arcane`,
249
+ `browser-runtime` to `/arcane/sdk`, `runtime/strong-type` to
250
+ `/arcane/dependencies/strong-type`, and its three license files to
251
+ `/licenses/arcane-os`. Source serving and import-map generation read those
252
+ installed files without creating a workspace runtime copy. Distribution copies
253
+ the selected routes completely inside the portable artifact, preserving the
254
+ same URLs. Existing physical `arcane/` routes retain their behavior.
245
255
 
246
256
  Release schema 1 and builder identity `arcane-app-packager-v1` remain unchanged
247
257
  because current Arcane native consumers treat them as public contracts. Native
@@ -184,7 +184,7 @@ npm exec -- arcane doctor --workspace . --arcane-root "../Arcane OS"
184
184
 
185
185
  ### Overview
186
186
 
187
- Refreshes one selected application's physical browser runtime map, generates
187
+ Refreshes one selected application's browser runtime map, generates
188
188
  its standard browser import map, discovers every directly navigable
189
189
  `.html`/`.htm` document admitted by the selected descriptor's existing
190
190
  include/exclude rules, and commits the map artifact plus those managed documents
@@ -211,10 +211,10 @@ The generated artifact is
211
211
  `apps/<id>/modules/arcane.importmap.json`. Its exact JSON is also installed in
212
212
  the configured entry and every other admitted browser document as `<script
213
213
  type="importmap" data-arcane-import-map>` before module loading. The complete
214
- physical-v1 runtime derives its entries from the installed runtime and
215
- browser-runtime inventories. It intentionally has no package-root mapping;
214
+ runtime map derives its entries from the selected runtime and browser-runtime
215
+ inventories. It intentionally has no package-root mapping;
216
216
  portable runtime subpaths such as `arcane-os/preference-store` and
217
- `arcane-os/speech-playback` instead map directly to their canonical projected
217
+ `arcane-os/speech-playback` instead map directly to their canonical runtime
218
218
  modules. The result reports the complete map written to the selected
219
219
  application; no fixed entry count is a release contract.
220
220
 
@@ -224,6 +224,15 @@ list. External and modern integrated routes require `components`, `css`,
224
224
  may omit only an optional trailing `security` include. The external license
225
225
  route remains separate and second.
226
226
 
227
+ External applications can instead select the four ordered
228
+ [`installed-v1` routes](protocols.md#installed-package-browser-routes) in
229
+ `arcane-packager.json`. This reads directly from the installed SDK package and
230
+ requires neither a workspace `arcane/` directory nor `arcane.lock.json`.
231
+ The generated browser destinations remain `arcane/`, `arcane/sdk/`, and
232
+ `arcane/dependencies/strong-type/`; an npm alias changes package source paths,
233
+ not those browser URLs. The SDK version comes from that installed package.
234
+ Existing physical workspaces and scaffold output remain supported unchanged.
235
+
227
236
  ### Result and safety
228
237
 
229
238
  Success returns the normal selected-workspace wrapper:
@@ -308,9 +317,12 @@ Starts one development server for one selected app and maps the exact
308
317
  workspace/runtime routes. It defaults to HTTPS on localhost; `--public` enables access
309
318
  from other devices on the network, using HTTPS by default.
310
319
 
311
- For an external workspace, the server exposes the selected projected
312
- `arcane/` root, including `arcane/sdk` and `arcane/dependencies`, alongside the
313
- application. Integrated workspaces retain their configured physical routes.
320
+ For an external workspace, the server exposes the selected SDK routes alongside
321
+ the application. `installed-v1` serves directly from the installed package
322
+ under `/arcane`, `/arcane/sdk`, and `/arcane/dependencies/strong-type`, without
323
+ creating a workspace runtime directory. Existing `physical-v1` workspaces serve
324
+ their projected `arcane/` root. Integrated workspaces retain their configured
325
+ physical routes.
314
326
  The explicit live-source SDK mapping remains unchanged and does not replace the
315
327
  installed projection.
316
328
 
@@ -504,6 +516,11 @@ shared route destinations. When selected shared content supplies no root
504
516
  Source document bases and resource URLs therefore retain their development
505
517
  layout. Packaging does not run tests or checks automatically.
506
518
 
519
+ For `installed-v1`, the same four configured source routes supply the complete
520
+ selected SDK content directly from `node_modules`. Only the portable output
521
+ receives copies; no workspace `arcane/` projection is required. Its runtime URLs,
522
+ managed import-map targets, and PWA inventory destinations match source serving.
523
+
507
524
  ```text
508
525
  arcane package [--app <id>] [--dry-run]
509
526
  ```
@@ -985,7 +1002,16 @@ application with `Authorization: Bearer <subscription_key>`.
985
1002
  Subscription verification is disabled when no callback is configured. The
986
1003
  programmatic `createToolchain().mail({action: 'serve', ...})` path accepts
987
1004
  `verifySubscription({appName, subscriptionKey, signal})`; supplying that callback
988
- enables verification before each provider attempt. It must resolve to `true`
1005
+ enables verification for requests whose actual connection source IP differs
1006
+ from the server-side IP of that connection. The gateway compares the native
1007
+ `request.socket.remoteAddress` with `request.socket.localAddress`. A nonempty,
1008
+ equal requester IP skips subscription verification and needs neither a bearer
1009
+ key nor an application header for that check. No environment setting or domain
1010
+ lookup is involved. Local development qualifies under the same rule; another
1011
+ machine on an intranet still needs verification. Missing connection metadata
1012
+ does not qualify. CORS and the remaining mail requirements still apply.
1013
+
1014
+ For requests requiring verification, the callback must resolve to `true`
989
1015
  to accept the request. An invalid subscription receives 401; verifier service
990
1016
  failure receives retryable 503; cancellation stops verification before sending.
991
1017
  The callback connects the actual TWiN Stripe endpoint when its contract is
@@ -1016,7 +1042,7 @@ deadline. `mail.bodyTimeoutMs` selects the optional request-body deadline.
1016
1042
  After binding, `server.ready` reports lifecycle fields such as
1017
1043
  protocol, optional app label, bind address, port, URL, and `callerAuthentication`
1018
1044
  (`none` or `subscription`). Human output states whether verification is disabled
1019
- or configured.
1045
+ or configured; configured verification includes the automatic same-IP exception.
1020
1046
  The command owns the server until its lifecycle ends or `SIGINT`/`SIGTERM`
1021
1047
  cancels it. The server's Resend credential remains outside results and events;
1022
1048
  per-request observer events preserve the complete delivery, report, provider
@@ -115,7 +115,8 @@ An absent key, `null`, or an empty string omits the Authorization header; the
115
115
  transport does not block initial setup because a key is missing. Explicit
116
116
  `subscriptionKey: null` or `subscriptionKey: ''` also skips User lookup.
117
117
  A supplied value of another type is rejected. A gateway with subscription
118
- verification configured owns rejection of requests without a usable key.
118
+ verification configured owns rejection of requests without a usable key,
119
+ subject to its [automatic same-IP exception](#automatic-same-ip-exception).
119
120
 
120
121
  For a caller-owned key and shared endpoint:
121
122
 
@@ -586,10 +587,40 @@ Subscription verification is disabled during initial setup when
586
587
  subscription key. Starting the ordinary CLI gateway uses this mode. This does
587
588
  not claim that a subscription was checked.
588
589
 
590
+ ### Automatic same-IP exception
591
+
592
+ The gateway automatically skips subscription verification when the incoming
593
+ connection's nonempty `request.socket.remoteAddress` equals
594
+ `request.socket.localAddress`. This also applies when a verifier is configured
595
+ and the request has no subscription key or supplies an invalid one. The
596
+ verification callback is not called for that request.
597
+
598
+ No `.arcane.env.json` setting or domain allowlist is needed. The comparison uses
599
+ the actual source and destination IP addresses exposed by Node's request socket
600
+ for HTTP/1.1 and HTTP/2. It adds no DNS lookup, configuration read, cache, or
601
+ interface scan. `Origin`, `Host`, `Forwarded`, and `X-Forwarded-For` do not
602
+ establish the connection's IP equality.
603
+
604
+ | Caller connection | Subscription behavior with a verifier configured |
605
+ | --- | --- |
606
+ | The verification service connects to mail using the same source and destination IP | Skips subscription checking, allowing its service-generated emails without a subscriber key. |
607
+ | A local development request has equal source and destination IPs, including an ordinary loopback connection | Uses the same automatic exception. There is no special loopback rule. |
608
+ | Another machine on the same intranet connects from a different IP | Uses the configured subscription verifier. Sharing a network or subnet supplies no exception. |
609
+ | Two services on one machine connect through different interface IPs | Uses the configured subscription verifier when the actual addresses differ. Machine identity alone supplies no exception. |
610
+ | The request has no usable socket source address | Uses the configured subscription verifier; absent addresses do not count as equal. |
611
+
612
+ The exception skips only subscription checking. Existing CORS, route, method,
613
+ idempotency-key, report, recipient, and provider handling still apply. The gateway
614
+ continues to require its Resend provider credential and reports acceptance only
615
+ after the provider accepts the email.
616
+
617
+ ### Verification callback contract
618
+
589
619
  The hosting process enables verification by supplying the programmatic
590
620
  `verifySubscription` function through
591
621
  `createToolchain().mail({action:'serve', verifySubscription, ...options})`.
592
- The server then reports `callerAuthentication: 'subscription'`. The callback
622
+ The server then reports `callerAuthentication: 'subscription'`, identifying the
623
+ configured mode while retaining the same-IP exception above. The callback
593
624
  contract is:
594
625
 
595
626
  ```javascript
@@ -602,13 +633,15 @@ The gateway also treats any other returned value as invalid. `appName` is the
602
633
  exact incoming `X-Mail-App` value, `subscriptionKey` is the incoming Bearer
603
634
  key, and `signal` follows the request lifecycle. These control fields stay
604
635
  separate from the mail report and Resend payload. The verifier runs for each
605
- POST request before any provider attempt; results are not cached.
636
+ POST request outside the same-IP exception before any provider attempt; results
637
+ are not cached.
606
638
 
607
639
  `X-Mail-App` identifies the originating app so the verifier can select that
608
640
  app's subscription account, such as the appropriate Stripe account for `BOSS`
609
641
  or `TWiN`. It is separate from the gateway's optional `--app` event label.
610
642
  The missing-header and Bearer errors below apply only when `verifySubscription`
611
- is configured. Ordinary `mail serve` does not require either header.
643
+ is configured and the same-IP exception does not apply. Ordinary `mail serve`
644
+ does not require either header.
612
645
 
613
646
  | Configured-verifier outcome | Gateway response |
614
647
  | --- | --- |
@@ -654,10 +687,14 @@ error report, and trigger another mail event. Subscriber failures cannot change
654
687
  an already committed mail result, though global capture can report them as new
655
688
  errors. This is a source-level possibility, not evidence of a deployed loop.
656
689
 
657
- For a verification service that sends its own emails, its host integration must
658
- also avoid the circular request path `mail -> verify subscription -> send mail ->
659
- verify subscription`. The SDK supplies a callback for verification; it does not
660
- choose that service's caller flow or enable it in ordinary CLI startup.
690
+ For a verification service's own emails sent through the same IP, the gateway
691
+ skips its subscription callback and proceeds to provider delivery. That removes
692
+ the circular dependency `mail -> verify subscription -> send mail -> verify
693
+ subscription` for this connection path. Connections from a different IP still
694
+ use the configured callback, whose owning service must preserve a verification
695
+ path that can return without recursively depending on another verification.
696
+ The exception does not change the separate browser event-subscriber behavior
697
+ described above.
661
698
 
662
699
  ## Gateway request lifecycle
663
700
 
@@ -61,13 +61,19 @@ explicit identity fails, and the selected page then must pass exact path-relativ
61
61
  base validation.
62
62
  Included HTML with neither signal is a component fragment and remains a
63
63
  complete package file.
64
- `arcane-os/preference-store` and `arcane-os/speech-playback` are the two
65
- portable subpaths that resolve directly to canonical runtime-module namespaces
66
- from both Node package exports and managed browser keys;
64
+ Public lowercase runtime subpaths resolve directly to canonical runtime-module
65
+ namespaces from both Node package exports and managed browser keys. These
66
+ include `arcane-os/preference-store`, `arcane-os/speech-playback`,
67
+ `arcane-os/ai-preference-tuple`, `arcane-os/ai-preference-runtime`,
68
+ `arcane-os/ai-provider-runtime`, `arcane-os/ai-runtime-state`,
69
+ `arcane-os/model-definition`, `arcane-os/conversation-timebox`,
70
+ `arcane-os/conversation-action-items`, `arcane-os/conversation-closing-report`,
71
+ and `arcane-os/chat-records`.
67
72
  `arcane/PreferenceStore` and `arcane/SpeechPlayback` remain their established
68
73
  browser import-map names. The additional portable `arcane-os/speech-text`
69
74
  subpath owns shared speech-input cleanup and has the same
70
- managed browser key. There is
75
+ managed browser key. `arcane-os/mail` remains the existing Mail aggregation.
76
+ There is
71
77
  no exported `importMapApplication()` function, `generateImportMap()` function, or
72
78
  `arcane-os/import-map` package subpath.
73
79
  Explicit host document lists use the separate root-exported
@@ -90,6 +96,68 @@ materializes the complete required SDK runtime,
90
96
  browser-runtime, and managed-import-map closure inside that app's own artifact.
91
97
  No application polls for SDK changes.
92
98
 
99
+ ## Installed-package browser routes
100
+
101
+ An external application can serve and package its installed SDK directly,
102
+ without creating a top-level `arcane/` directory or requiring
103
+ `arcane.lock.json`. Select `installed-v1` through these four ordered routes in
104
+ the existing schema-1 `arcane-packager.json`; no additional layout field is
105
+ needed:
106
+
107
+ ```json
108
+ {
109
+ "schemaVersion": 1,
110
+ "appsRoot": "apps",
111
+ "distRoot": "dist",
112
+ "sharedPayloads": {
113
+ "browser-runtime": [
114
+ {
115
+ "source": "node_modules/arcane-os/runtime/arcane",
116
+ "destination": "arcane",
117
+ "include": ["components", "css", "entities", "img", "modules"],
118
+ "exclude": []
119
+ },
120
+ {
121
+ "source": "node_modules/arcane-os/browser-runtime",
122
+ "destination": "arcane/sdk",
123
+ "include": ["."],
124
+ "exclude": []
125
+ },
126
+ {
127
+ "source": "node_modules/arcane-os/runtime/strong-type",
128
+ "destination": "arcane/dependencies/strong-type",
129
+ "include": ["."],
130
+ "exclude": []
131
+ },
132
+ {
133
+ "source": "node_modules/arcane-os",
134
+ "destination": "licenses/arcane-os",
135
+ "include": ["LICENSE", "COMMERCIAL-LICENSE.md", "NOTICE"],
136
+ "exclude": []
137
+ }
138
+ ]
139
+ }
140
+ }
141
+ ```
142
+
143
+ The first include list also accepts a final `security` entry. An npm alias such
144
+ as `arcane-sdk` substitutes `node_modules/arcane-sdk` for the package prefix in
145
+ all four source paths. Destinations remain unchanged. Package selection and SDK
146
+ version come from the installed dependency, not a generated runtime lock.
147
+
148
+ `arcane import-map`, `arcane dev`, packaging, and generated PWA inventories use
149
+ the same logical destinations. Source serving and map generation read installed
150
+ files without materializing them into the workspace. A selected portable
151
+ package copies its complete selected resources inside the artifact, preserving
152
+ the same browser URLs and saved managed import maps.
153
+
154
+ Existing `physical-v1` configurations whose first source is `arcane` remain
155
+ supported. The explicit materializer below still refreshes those projections,
156
+ and scaffolding retains its existing physical layout. Selecting `installed-v1`
157
+ does not delete any preexisting workspace files. The separate host-document
158
+ `generateDocumentImportMaps()` API continues to accept an already materialized
159
+ runtime; its input contract is unchanged.
160
+
93
161
  ## Installed SDK runtime materialization
94
162
 
95
163
  `materializeInstalledSdkRuntime()` is the Node entrypoint for refreshing one
@@ -108,6 +108,15 @@ app's `arcane-app.json`. Startup refreshes that app's generated
108
108
  maps and starting the server. No packaging or `dist` output is required.
109
109
  Package-only applications retain their existing descriptor workflow.
110
110
 
111
+ With the external [`installed-v1` routes](protocols.md#installed-package-browser-routes),
112
+ the server inventories the selected SDK directly in `node_modules`. Runtime
113
+ resources retain their public `arcane/`, `arcane/sdk/`, and
114
+ `arcane/dependencies/strong-type/` URLs, so offline selections use those logical
115
+ paths rather than package filesystem paths. No workspace `arcane/` tree or
116
+ `arcane.lock.json` is needed. Packaging uses the same route selection and copies
117
+ the selected resources into the portable output; existing physical projections
118
+ remain supported.
119
+
111
120
  Add a file or directory to `package.include` to make it part of the app's
112
121
  resources. A new file inside an already included directory needs no separate
113
122
  entry. `package.include` is an application resource selection, not a file list
@@ -5,11 +5,12 @@ The npm package exposes a Node.js ESM control plane, the portable
5
5
  `arcane-os/preference-store`, `arcane-os/speech-playback`,
6
6
  `arcane-os/speech-text`, `arcane-os/ai/tool-text-stream`, and `arcane-os/browser-device` entrypoints, and the browser-only
7
7
  `arcane-os/pwa`, `arcane-os/ai/browser-wasm` and `arcane-os/ai/browser-speech` entrypoints.
8
- Those package subpaths are distinct from application-facing projection modules
9
- in the managed browser map, such as `arcane/AIProviderRuntime`,
10
- `arcane/AIRuntimeState`, and `arcane/ThemeBootstrap`. Applications use those
11
- mapped runtime modules and call `globalThis.Arcane` for capability-gated host
12
- behavior; they are not additional `package.json#exports` entrypoints.
8
+ Lowercase runtime subpaths also expose existing module namespaces through both
9
+ Node package exports and managed browser imports, as listed below. Established
10
+ browser names such as `arcane/AIProviderRuntime`, `arcane/AIRuntimeState`, and
11
+ `arcane/ThemeBootstrap` remain managed-map names rather than Node package
12
+ entrypoints. Applications call `globalThis.Arcane` for capability-gated host
13
+ behavior.
13
14
 
14
15
  This page is the canonical inventory for every JavaScript name reachable through `package.json#exports`. The same binding can appear at the root and a focused subpath; those entrypoints are listed together. The root workspace `discoverApps` and the low-level packager `discoverApps` are intentionally separate records because they are different functions.
15
16
 
@@ -19,7 +20,8 @@ This table is the Node `package.json#exports` map: it defines package
19
20
  entrypoints for SDK/tooling code. It is distinct from the generated browser
20
21
  import map that resolves application-facing `arcane/*` modules and the focused
21
22
  EventManager entry. See [browser runtime delivery](protocols.md#browser-runtime-delivery)
22
- for the installed-inventory-derived physical-runtime contract in SDK `0.5.18`.
23
+ for the shared browser destinations used by installed-package and physical
24
+ runtime layouts.
23
25
 
24
26
  | Specifier | Purpose |
25
27
  | --- | --- |
@@ -36,6 +38,21 @@ for the installed-inventory-derived physical-runtime contract in SDK `0.5.18`.
36
38
  | `arcane-os/event-manager` | Central synchronous events, complete time-travel history, playback, and optional DOM instrumentation. |
37
39
  | `arcane-os/logging` | Shared console diagnostics controlled by the existing `user.developer` preference. |
38
40
  | `arcane-os/preference-store` | Portable preference records and injected storage adapters. |
41
+ | `arcane-os/ai` | Existing `AI.js` provider-neutral inference and speech namespace. |
42
+ | `arcane-os/ai-preference-tuple` | Existing `AIPreferenceTuple.js` module namespace. |
43
+ | `arcane-os/ai-preference-runtime` | Existing `AIPreferenceRuntime.js` module namespace. |
44
+ | `arcane-os/ai-provider-runtime` | Existing `AIProviderRuntime.js` provider selection and lifecycle namespace. |
45
+ | `arcane-os/ai-runtime-state` | Existing `AIRuntimeState.js` state and event namespace. |
46
+ | `arcane-os/model-definition` | Existing `ModelDefinition.js` module namespace. |
47
+ | `arcane-os/conversation-timebox` | Existing `ConversationTimebox.js` module namespace. |
48
+ | `arcane-os/conversation-action-items` | Existing `ConversationActionItems.js` module namespace. |
49
+ | `arcane-os/conversation-closing-report` | Existing `ConversationClosingReport.js` module namespace. |
50
+ | `arcane-os/chat-records` | Existing `ChatRecords.js` module namespace. |
51
+ | `arcane-os/app-data-scope` | Existing `AppDataScope.js` application storage scope namespace. |
52
+ | `arcane-os/core-local-model-catalog` | Existing `CoreLocalModelCatalog.js` local model catalog namespace. |
53
+ | `arcane-os/dbopfs-document-library` | Existing `DBOPFSDocumentLibrary.js` document storage namespace. |
54
+ | `arcane-os/local-ai-readiness` | Existing `LocalAIReadiness.js` local AI readiness namespace. |
55
+ | `arcane-os/ollama-model-identifier` | Existing `OllamaModelIdentifier.js` model identifier namespace. |
39
56
  | `arcane-os/speech-playback` | Portable speech preparation, playback state, and injected media adapters. |
40
57
  | `arcane-os/speech-text` | Speech-input formatting cleanup for complete text and streamed chunks. |
41
58
  | `arcane-os/browser-device` | Synchronous mobile or desktop identity hints for application-owned settings. |
@@ -45,6 +62,18 @@ for the installed-inventory-derived physical-runtime contract in SDK `0.5.18`.
45
62
  | `arcane-os/ai/browser-speech` | Caller-selected browser-local Whisper STT and Kokoro TTS provider mechanisms, ordinary upstream assets, materialized/native routing, Workers, and cancellation. |
46
63
  | `arcane-os/mail` | Portable Mail runtime, durable outbox, complete transport responses, and provider-neutral acceptance contracts. |
47
64
 
65
+ These lowercase runtime-module entrypoints expose their existing exports; they
66
+ do not duplicate the module implementations or change their platform and
67
+ lifecycle requirements. Node resolves them inside the installed package, while
68
+ the managed browser map resolves the same names to the selected runtime URLs.
69
+ Node package resolution does not make browser-only operations available in
70
+ Node: a module still needs its documented browser globals, storage, media,
71
+ Worker, or host capabilities when its initialization or operation uses them.
72
+ Use `arcane-os/mail` for the existing Mail aggregation rather than importing a
73
+ private runtime file. Its package facade and managed browser entry both expose
74
+ the canonical `MailApi.mjs` aggregation and the same Mail, outbox, and transport
75
+ bindings.
76
+
48
77
  Eight JSON schemas and `package.json` are data-only export subpaths. In Node ESM, import JSON with `with {type: 'json'}`, or resolve and read it explicitly.
49
78
 
50
79
  ### Managed AI narration
@@ -1496,6 +1525,11 @@ After full runtime replacement, it writes or replaces semantic
1496
1525
  `arcane.lock.json` from the actual installed dependency name, package name,
1497
1526
  package version, alias source, and projected roots.
1498
1527
 
1528
+ This operation remains available for physical workspace projections. External
1529
+ applications using [installed-package routes](protocols.md#installed-package-browser-routes)
1530
+ read directly from their installed dependency and do not need to call it or
1531
+ create `arcane.lock.json` before serving, refreshing app import maps, or packaging.
1532
+
1499
1533
  ### Signature and result
1500
1534
 
1501
1535
  ```text
@@ -3643,6 +3677,10 @@ certPath, keyPath, tls, signal, onEvent}` and serves one validated
3643
3677
  workspace application plus its complete SDK or integrated runtime. Packaged mode uses
3644
3678
  `{mode:'packaged', releaseRoot, workspaceRoot, host, port, httpPort, certPath, keyPath, tls,
3645
3679
  signal, onEvent}` and serves the complete selected release files.
3680
+ External source mode accepts both the installed-package route layout and the
3681
+ existing physical runtime layout. Installed-package mode serves the configured
3682
+ SDK resources directly from `node_modules` at their logical browser URLs;
3683
+ it does not create a workspace `arcane/` directory or require `arcane.lock.json`.
3646
3684
  `host` defaults to `127.0.0.1` and accepts an
3647
3685
  explicit network address or hostname. Use `0.0.0.0` for all IPv4 interfaces or
3648
3686
  `::` for the platform's IPv6 wildcard listeners. `port` selects the HTTPS
@@ -3866,8 +3904,10 @@ async validateWorkspace({
3866
3904
 
3867
3905
  Import it from `arcane-os`. It resolves to a validation result with
3868
3906
  `valid`, `workspaceMode`, `workspaceRoot`, `appId`, `appRoot`, the selected
3869
- configuration/application, lock data, and completed checks. For an external
3870
- workspace it additionally returns the exact installed package authority:
3907
+ configuration/application, lock data where the layout uses it, and completed
3908
+ checks. External `installed-v1` selection reads package metadata directly and
3909
+ does not require a workspace `arcane.lock.json` or runtime projection. For an
3910
+ external workspace the result additionally includes the installed package:
3871
3911
 
3872
3912
  ```javascript
3873
3913
  {
@@ -3876,17 +3916,17 @@ workspace it additionally returns the exact installed package authority:
3876
3916
  packageSource,
3877
3917
  canonicalPackageRoot,
3878
3918
  packageName: 'arcane-os',
3879
- packageVersion: '0.5.18',
3919
+ packageVersion,
3880
3920
  runtimeRoot,
3881
3921
  browserRuntimeRoot
3882
3922
  }
3883
3923
  }
3884
3924
  ```
3885
3925
 
3886
- The dependency can be named `arcane-os` or be one exact npm alias for
3887
- `npm:arcane-os@0.5.18`. The selected installation must still be one direct,
3888
- physical, non-link package directory whose manifest identifies exactly as
3889
- `arcane-os@0.5.18`; duplicate canonical/alias declarations reject.
3926
+ The dependency can be named `arcane-os` or use an npm alias for the selected
3927
+ `arcane-os` version. The configured package source must match that declaration;
3928
+ `packageVersion` reports the installed package's version. Duplicate
3929
+ canonical/alias declarations reject.
3890
3930
  `allowMissingManagedImportMap` is an internal packaging/development seam. An
3891
3931
  ordinary caller should leave it `false`.
3892
3932
 
@@ -1,5 +1,10 @@
1
1
  [Roshi's Codex PRIME] The gateway review and its portable credential follow-up apply the same three purpose gates. The gateway removals preserve complete delivery outcomes, configured CORS and recipient policy, and the optional subscription verifier. Mail credentials now belong in the user-selected `.arcane.env.json`, with one portable Node implementation for Windows, Linux, and macOS and a compatible Node host as the Android adaptation boundary. The original gateway inventory below uses baseline `f055e05`; the credential follow-up reviews the Windows storage implementation present at SDK `0.18.0`. Commit, package, publication, and actual platform execution outcomes belong to the delivery record rather than to this source-review report.
2
2
 
3
+ The [same-IP subscription exception follow-up](#same-ip-subscription-exception-follow-up)
4
+ records the later user-selected change to when the optional verifier runs. The
5
+ earlier inventories retain their stated historical scope; current verification
6
+ skips requests whose actual connection source and destination IPs are equal.
7
+
3
8
  | Decision | Behavior | Why it matters | Source status at this review |
4
9
  | --- | --- | --- | --- |
5
10
  | Remove | Loopback-only admission, manual header reconstruction, address/origin normalization, local payload grammar, manual content-length scan, dead queue knobs | These restrict supported use, rewrite supplied values, or repeat work already owned elsewhere. | Removed from the inspected source; naming-only changes carry no runtime claim. |
@@ -541,3 +546,58 @@ global error handler; it is not an automatic server response to an ordinary
541
546
  mail failure. This is a static source-path finding, not an observed production
542
547
  incident or an executed reproduction. This documentation clarification changes
543
548
  no error, subscription, retry, or delivery behavior.
549
+
550
+ ## Same-IP subscription exception follow-up
551
+
552
+ The user selected an automatic same-IP exception so the verification service can
553
+ send its own emails without a subscriber key. This supersedes the proposed
554
+ domain exception in `.arcane.env.json`; neither an environment setting nor a
555
+ domain allowlist is needed. The reusable decision belongs in the SDK's existing
556
+ `handleMailRequest`, immediately at its subscription-verification branch.
557
+ Application-owned messages, sender choices, subscription accounts, and the
558
+ verification callback remain with their existing owners.
559
+
560
+ The condition is a nonempty `request.socket.remoteAddress` equal to
561
+ `request.socket.localAddress`. Matching requests skip `verifyMailSubscription`
562
+ and the configured `verifySubscription` callback, including when the request
563
+ has no subscriber key or supplies an invalid one. Requests with different or
564
+ unavailable socket addresses retain the configured verification path. Omitting
565
+ the callback continues to select the existing no-verification setup.
566
+
567
+ | Method or action | Gate 1: Do we care? | Gate 2: Why is it worth the work? | Gate 3: Can we remove it without losing the required result? | Decision and concrete effect |
568
+ | --- | --- | --- | --- | --- |
569
+ | Automatic source/destination IP equality at `handleMailRequest` | Yes. The verifier needs to send service mail without a subscriber key. | It identifies the exact connection case the user selected using already-available socket fields. | No. Removing the exception would restore the verifier's dependency on subscription checking for its own mail. | Keep one inline equality decision with a nonempty source address. No new helper or configuration owner. |
570
+ | Subscription checking for a matching connection | The subscription-verification capability matters; this particular call does not. | Requiring it for the verifier's own email adds an unnecessary dependency and can create a circular service call. | Yes. The user expressly permits these senders regardless of their subscription. | Skip the callback and its asynchronous wait for matching requests. A callback that contacts the verification service makes no such request on this path. |
571
+ | Proposed `.arcane.env.json` domain exception, DNS resolution, address cache, and interface enumeration | No. The selected rule is actual IP equality and requires no setup. | These would introduce configuration, I/O, retained state, or a broader machine/domain interpretation absent from the selected outcome. | Yes. Native request socket fields already supply both values. | Add none. Secret files, named provider profiles, and current configuration loading remain unchanged. |
572
+ | `Origin`, `Host`, `Forwarded`, or `X-Forwarded-For` as evidence of matching connection IPs | No. These request fields do not define the native socket endpoints. | Reading them for this exception would change its meaning and duplicate unrelated routing or CORS concerns. | Yes. The actual socket exposes the relevant addresses directly. | Keep header handling at its existing owner; none of these headers selects the exception. |
573
+ | Special loopback, intranet, subnet, or same-machine rules | No. The selected condition is equal IPs. | A larger address policy would grant a different exception and add work. | Yes. Equal local-development addresses already use the ordinary equality path. | Add none. Another intranet machine with a different IP still uses configured verification. Services on one machine using different interface IPs may also require it. |
574
+ | CORS, routing, methods, idempotency, report/recipient handling, provider result, and cancellation | Yes. These continue to determine the actual mail outcome. | Skipping subscription checking does not change the requested delivery or lifecycle contracts. | No. Their retained behavior supports complete delivery and honest results. | Preserve these owners and their current ordering. Provider acceptance remains separate from the subscription decision. |
575
+
576
+ The operation graph adds one synchronous connection-address comparison at the
577
+ existing request owner. A matching request avoids subscription header checking,
578
+ callback invocation, and its asynchronous verification wait, then uses the
579
+ ordinary provider path. There is no new file read, DNS request, timer, polling,
580
+ cache, helper, shared-state lock, startup barrier, or per-platform process.
581
+ Requests continue independently under the existing handler lifecycle. These are
582
+ source-level operation changes, not measured timing improvements.
583
+
584
+ The selected `node-http-server` raw-request hook supplies the native Node request.
585
+ HTTP/1.1 and HTTP/2 both expose the needed address properties; the same Node
586
+ contract applies on Windows, Linux, and macOS, with Android requiring a compatible
587
+ Node host. The local socket address is the actual destination used by the
588
+ connection, including when the listener binds `0.0.0.0`.
589
+ [Node socket addresses](https://nodejs.org/api/net.html#socketlocaladdress) and
590
+ [HTTP/2 request sockets](https://nodejs.org/api/http2.html#requestsocket)
591
+ document these properties.
592
+
593
+ For equal connection IPs, the verification service's own mail proceeds without
594
+ calling that service again. This removes its circular subscription dependency
595
+ on this path. It does not claim that arbitrary callback code or a repeatedly
596
+ failing browser mail-event subscriber cannot create a separate feedback loop.
597
+ The existing error and retry distinctions remain documented in the
598
+ [mail reference](../reference/mail.md#error-reports-and-retries).
599
+
600
+ This follow-up records the selected behavior and source review. No local test,
601
+ check, build, server launch, live mail send, or platform execution was performed
602
+ by this documentation author. Release and runtime evidence remain with the
603
+ corresponding operation's owner.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.24.1",
3
+ "version": "0.26.0",
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",
@@ -17,6 +17,21 @@
17
17
  "./browser-device": "./browser-runtime/browser-device.mjs",
18
18
  "./pwa": "./browser-runtime/pwa.mjs",
19
19
  "./preference-store": "./runtime/arcane/modules/PreferenceStore.js",
20
+ "./ai": "./runtime/arcane/modules/AI.js",
21
+ "./ai-preference-tuple": "./runtime/arcane/modules/AIPreferenceTuple.js",
22
+ "./ai-preference-runtime": "./runtime/arcane/modules/AIPreferenceRuntime.js",
23
+ "./ai-provider-runtime": "./runtime/arcane/modules/AIProviderRuntime.js",
24
+ "./ai-runtime-state": "./runtime/arcane/modules/AIRuntimeState.js",
25
+ "./model-definition": "./runtime/arcane/modules/ModelDefinition.js",
26
+ "./conversation-timebox": "./runtime/arcane/modules/ConversationTimebox.js",
27
+ "./conversation-action-items": "./runtime/arcane/modules/ConversationActionItems.js",
28
+ "./conversation-closing-report": "./runtime/arcane/modules/ConversationClosingReport.js",
29
+ "./chat-records": "./runtime/arcane/modules/ChatRecords.js",
30
+ "./app-data-scope": "./runtime/arcane/modules/AppDataScope.js",
31
+ "./core-local-model-catalog": "./runtime/arcane/modules/CoreLocalModelCatalog.js",
32
+ "./dbopfs-document-library": "./runtime/arcane/modules/DBOPFSDocumentLibrary.js",
33
+ "./local-ai-readiness": "./runtime/arcane/modules/LocalAIReadiness.js",
34
+ "./ollama-model-identifier": "./runtime/arcane/modules/OllamaModelIdentifier.js",
20
35
  "./speech-playback": "./runtime/arcane/modules/SpeechPlayback.js",
21
36
  "./speech-text": "./browser-runtime/speech-text.mjs",
22
37
  "./ai/browser-wasm": "./browser-runtime/ai/browser-wasm.mjs",
@@ -69,7 +84,7 @@
69
84
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
70
85
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
71
86
  "test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/update-check.test.mjs",
72
- "test:integration": "node ./bin/arcane-test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
87
+ "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
73
88
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
74
89
  "check": "node tools/check-source.mjs && npm test",
75
90
  "check:release": "node tools/check-source.mjs --package-only && npm run test:release",
@@ -0,0 +1,19 @@
1
+ export {
2
+ default,
3
+ default as Mail,
4
+ resolveMailConfig
5
+ } from './Mail.js';
6
+ export {
7
+ MAIL_OUTBOX_IDEMPOTENCY_WINDOW_MS,
8
+ MAIL_OUTBOX_PROTOCOL,
9
+ MAIL_OUTBOX_STATES,
10
+ MAIL_OUTBOX_TABLE,
11
+ MailOutbox,
12
+ createMailOutbox
13
+ } from './MailOutbox.mjs';
14
+ export {
15
+ MailTransportError,
16
+ normalizeMailEndpoint,
17
+ sendMailReport,
18
+ serializeMailReport
19
+ } from './MailTransport.mjs';
@@ -6,6 +6,7 @@ import https from 'node:https';
6
6
  import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import {resolveWorkspace} from './workspace.mjs';
9
+ import {readInstalledSdkLayout} from './sdk-runtime-layout.mjs';
9
10
  import {APP_DESCRIPTOR_NAME, projectPackageManifest} from './app-descriptor.mjs';
10
11
  import {APP_CONFIG_NAME, validateAppConfig} from './packager/core.mjs';
11
12
  import {createEventQueue} from './event-queue.mjs';
@@ -436,6 +437,7 @@ function sharedPathAllowed(relative,route){
436
437
  });
437
438
  if(excluded)return false;
438
439
  return route.include.some(item=>{
440
+ if(item==='.')return true;
439
441
  const candidate=inventoryKey(item);
440
442
  return comparable===candidate||comparable.startsWith(`${candidate}/`);
441
443
  });
@@ -471,11 +473,16 @@ async function sourceRoutes(workspaceRoot,appId,{
471
473
  mappings:[appMapping,...sdkSource.mappings]
472
474
  };
473
475
  }
474
- if(resolved.config.workspaceMode==='integrated'){
476
+ if(resolved.config.workspaceMode==='integrated'
477
+ ||resolved.config.browserRuntimeLayout==='installed-v1'){
478
+ const installed=resolved.config.browserRuntimeLayout==='installed-v1'
479
+ ?await readInstalledSdkLayout(resolved.workspaceRoot,resolved.config)
480
+ :null;
475
481
  return {
476
482
  workspaceRoot:resolved.workspaceRoot,
477
- workspaceMode:'integrated',
483
+ workspaceMode:resolved.config.workspaceMode,
478
484
  config:resolved.config,
485
+ ...(installed?{installed}:{}),
479
486
  appId:resolved.appId,
480
487
  app:resolved.app.manifest,
481
488
  startPath:`/apps/${resolved.appId}/${resolved.app.manifest.entry}`,
@@ -579,7 +586,7 @@ async function sourcePwaAssets(routeSet, mappings, signal, resourceUrls, resourc
579
586
  }
580
587
  }
581
588
  for (const selected of mapping.include ?? ['']) {
582
- await visitSourceResource(selected ? selected.split('/') : []);
589
+ await visitSourceResource(selected && selected!=='.' ? selected.split('/') : []);
583
590
  }
584
591
  }
585
592
  const origin = 'http://arcane.invalid';
@@ -866,7 +873,7 @@ async function startOwnedDevServer({
866
873
  const mappings=deterministicMappings(routeSet.mappings);
867
874
  const versionPath = mode === 'source'
868
875
  ? sdkRuntimeSourceRoot === undefined
869
- ? path.join(routeSet.workspaceRoot, 'arcane.lock.json')
876
+ ? routeSet.installed?.versionPath ?? path.join(routeSet.workspaceRoot, 'arcane.lock.json')
870
877
  : path.join(routeSet.runtime.sourceRoot, 'package.json')
871
878
  : undefined;
872
879
  const [generatorInputs, initialAssetVersion, versionInput] = await Promise.all(
@@ -897,6 +904,9 @@ async function startOwnedDevServer({
897
904
  );
898
905
  async function selectedAssetVersion(){
899
906
  if(mode!=='source')return undefined;
907
+ if(routeSet.installed)return JSON.parse(await readFile(
908
+ routeSet.installed.versionPath,'utf8'
909
+ )).version;
900
910
  return sdkRuntimeSourceRoot===undefined
901
911
  ?readWorkspaceAssetVersion(routeSet.workspaceRoot)
902
912
  :JSON.parse(await readFile(
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import {pathToFileURL} from 'node:url';
5
5
  import {SDK_VERSION} from './constants.mjs';
6
6
  import {listRuntimeFiles} from './runtime.mjs';
7
+ import {readInstalledSdkLayout,installedRuntimeFiles,installedRuntimeTarget} from './sdk-runtime-layout.mjs';
7
8
 
8
9
  const is = new Is(false);
9
10
 
@@ -19,7 +20,23 @@ const SDK_BROWSER_AI_ENTRY='sdk/ai/browser-wasm.mjs';
19
20
  const SDK_BROWSER_SPEECH_ENTRY='sdk/ai/browser-speech.mjs';
20
21
  const STATIC_RUNTIME_PACKAGE_IMPORTS=new Map([
21
22
  ['arcane-os/preference-store','modules/PreferenceStore.js'],
22
- ['arcane-os/speech-playback','modules/SpeechPlayback.js']
23
+ ['arcane-os/speech-playback','modules/SpeechPlayback.js'],
24
+ ['arcane-os/ai','modules/AI.js'],
25
+ ['arcane-os/ai-preference-tuple','modules/AIPreferenceTuple.js'],
26
+ ['arcane-os/ai-preference-runtime','modules/AIPreferenceRuntime.js'],
27
+ ['arcane-os/ai-provider-runtime','modules/AIProviderRuntime.js'],
28
+ ['arcane-os/ai-runtime-state','modules/AIRuntimeState.js'],
29
+ ['arcane-os/model-definition','modules/ModelDefinition.js'],
30
+ ['arcane-os/conversation-timebox','modules/ConversationTimebox.js'],
31
+ ['arcane-os/conversation-action-items','modules/ConversationActionItems.js'],
32
+ ['arcane-os/conversation-closing-report','modules/ConversationClosingReport.js'],
33
+ ['arcane-os/chat-records','modules/ChatRecords.js'],
34
+ ['arcane-os/app-data-scope','modules/AppDataScope.js'],
35
+ ['arcane-os/core-local-model-catalog','modules/CoreLocalModelCatalog.js'],
36
+ ['arcane-os/dbopfs-document-library','modules/DBOPFSDocumentLibrary.js'],
37
+ ['arcane-os/local-ai-readiness','modules/LocalAIReadiness.js'],
38
+ ['arcane-os/ollama-model-identifier','modules/OllamaModelIdentifier.js'],
39
+ ['arcane-os/mail','modules/MailApi.mjs']
23
40
  ]);
24
41
  const SDK_BROWSER_SELF_IMPORTS=new Map([
25
42
  ['arcane-os/event-manager',SDK_BROWSER_ENTRY],
@@ -1436,9 +1453,10 @@ async function physicalRuntime(workspaceRoot,signal){
1436
1453
  }
1437
1454
 
1438
1455
  async function managedImportMapBuild(resolvedWorkspace,signal,pwaEnabled=false){
1456
+ const installed=await readInstalledSdkLayout(resolvedWorkspace);
1439
1457
  const [runtime,version]=await Promise.all([
1440
- physicalRuntime(resolvedWorkspace,signal),
1441
- pwaEnabled?null:readWorkspaceAssetVersion(resolvedWorkspace)
1458
+ installed?installedRuntimeFiles(resolvedWorkspace,installed,signal):physicalRuntime(resolvedWorkspace,signal),
1459
+ pwaEnabled?null:installed?.version??readWorkspaceAssetVersion(resolvedWorkspace)
1442
1460
  ]);
1443
1461
  const built=await buildImportMap({files:runtime.files,signal,version});
1444
1462
  const json=`${JSON.stringify({imports:built.imports},null,2).replaceAll('<','\\u003c')}\n`;
@@ -1446,6 +1464,8 @@ async function managedImportMapBuild(resolvedWorkspace,signal,pwaEnabled=false){
1446
1464
  }
1447
1465
 
1448
1466
  export async function readWorkspaceAssetVersion(workspaceRoot){
1467
+ const installed=await readInstalledSdkLayout(workspaceRoot);
1468
+ if(installed)return installed.version;
1449
1469
  let source;
1450
1470
  try{source=await readFileFromDisk(path.join(workspaceRoot,'arcane.lock.json'),'utf8');}
1451
1471
  catch(error){
@@ -2434,10 +2454,17 @@ export async function readApplicationTestImportMapContext({
2434
2454
  ||is.array(document.imports)){
2435
2455
  fail('Application test import-map artifact must contain an imports object.');
2436
2456
  }
2457
+ const installed=await readInstalledSdkLayout(resolvedWorkspaceRoot);
2458
+ const imports=installed?Object.fromEntries(Object.entries(document.imports).map(
2459
+ function installedApplicationTestTarget([specifier,target]){
2460
+ return [specifier,is.string(target)&&target.startsWith('./')
2461
+ ?`./${installedRuntimeTarget(target.slice(2),installed)}`:target];
2462
+ }
2463
+ )):document.imports;
2437
2464
  return createApplicationTestImportMapContext({
2438
2465
  applicationRoot:resolvedWorkspaceRoot,
2439
2466
  boundary:'source',
2440
- imports:document.imports,
2467
+ imports,
2441
2468
  signal
2442
2469
  });
2443
2470
  }
package/src/mail-api.mjs CHANGED
@@ -1,19 +1,2 @@
1
- export {
2
- default,
3
- default as Mail,
4
- resolveMailConfig
5
- } from '../runtime/arcane/modules/Mail.js';
6
- export {
7
- MAIL_OUTBOX_IDEMPOTENCY_WINDOW_MS,
8
- MAIL_OUTBOX_PROTOCOL,
9
- MAIL_OUTBOX_STATES,
10
- MAIL_OUTBOX_TABLE,
11
- MailOutbox,
12
- createMailOutbox
13
- } from '../runtime/arcane/modules/MailOutbox.mjs';
14
- export {
15
- MailTransportError,
16
- normalizeMailEndpoint,
17
- sendMailReport,
18
- serializeMailReport
19
- } from '../runtime/arcane/modules/MailTransport.mjs';
1
+ export {default} from '../runtime/arcane/modules/MailApi.mjs';
2
+ export * from '../runtime/arcane/modules/MailApi.mjs';
@@ -899,8 +899,16 @@ function createConfiguredMailHandler(configuration){
899
899
  if(request.method!=='POST'){
900
900
  throw new MailGatewayFault('mail_method_not_allowed',{statusCode:405});
901
901
  }
902
- if(configuration.verifySubscription){
903
- appId=await verifyMailSubscription(request,configuration,requestController.signal);
902
+ if (configuration.verifySubscription) {
903
+ const requesterIp = request.socket?.remoteAddress;
904
+ const serverIp = request.socket?.localAddress;
905
+ if (!requesterIp || requesterIp !== serverIp) {
906
+ appId = await verifyMailSubscription(
907
+ request,
908
+ configuration,
909
+ requestController.signal
910
+ );
911
+ }
904
912
  }
905
913
  idempotencyKey=requireRequestHeader(request,'idempotency-key');
906
914
  const requestText=await readRequestBodyText(request,{
@@ -136,12 +136,13 @@ function isGlobLike(value){
136
136
  return /[*?\[\]{}]/u.test(value);
137
137
  }
138
138
 
139
- function validatePathList(value,label,{required=false}={}){
139
+ function validatePathList(value,label,{required=false,allowRoot=false}={}){
140
140
  if(!is.array(value)||(required&&value.length===0)){
141
141
  fail(`${label} must be ${required?'a non-empty':'an'} array of literal relative paths.`);
142
142
  }
143
143
  const normalized=value.map((entry,index)=>{
144
- const item=normalizeRelativePath(entry,`${label}[${index}]`);
144
+ const item=allowRoot?normalizeRelativeRoot(entry,`${label}[${index}]`)
145
+ :normalizeRelativePath(entry,`${label}[${index}]`);
145
146
  if(isGlobLike(item))fail(`${label}[${index}] must be literal; directories include descendants.`);
146
147
  return item;
147
148
  });
@@ -151,7 +152,8 @@ function validatePathList(value,label,{required=false}={}){
151
152
  if(required){
152
153
  for(let left=0;left<normalized.length;left+=1){
153
154
  for(let right=left+1;right<normalized.length;right+=1){
154
- if(sameOrDescendant(normalized[left],normalized[right])
155
+ if(normalized[left]==='.'||normalized[right]==='.'
156
+ ||sameOrDescendant(normalized[left],normalized[right])
155
157
  ||sameOrDescendant(normalized[right],normalized[left])){
156
158
  fail(`${label} has overlapping paths: ${normalized[left]} and ${normalized[right]}`);
157
159
  }
@@ -256,7 +258,7 @@ function validateSharedRoute(route,label){
256
258
  assertOnlyKeys(route,new Set(['source','destination','include','exclude']),label);
257
259
  const source=normalizeRelativeRoot(route.source,`${label}.source`);
258
260
  const destination=normalizeRelativeRoot(route.destination,`${label}.destination`);
259
- const include=validatePathList(route.include,`${label}.include`,{required:true});
261
+ const include=validatePathList(route.include,`${label}.include`,{required:true,allowRoot:true});
260
262
  const exclude=validatePathList(route.exclude??[],`${label}.exclude`);
261
263
  if(source==='.'||source==='apps'||source.startsWith('apps/')
262
264
  ||source==='dist'||source.startsWith('dist/')||source==='node_modules'
@@ -421,7 +423,7 @@ async function loadContext(requestedWorkspaceRoot,appId){
421
423
  }
422
424
 
423
425
  function destinationJoin(root,relative){
424
- return root==='.'?relative:`${root}/${relative}`;
426
+ return relative==='.'?root:root==='.'?relative:`${root}/${relative}`;
425
427
  }
426
428
 
427
429
  function appPackagePath(context, relative) {
@@ -442,12 +444,13 @@ async function collectSelectedPath({
442
444
  records,
443
445
  destinations,
444
446
  signal,
445
- label
447
+ label,
448
+ allowRoot=false
446
449
  }){
447
450
  throwIfAborted(signal);
448
451
  if(isExcluded(selected,excludes))return;
449
452
  if(reject(selected))fail(`${label} selects a reserved private or generated path: ${selected}.`);
450
- const absolute=resolveInside(sourceRoot,selected,label);
453
+ const absolute=resolveInside(sourceRoot,selected,label,{allowRoot});
451
454
  let info;
452
455
  try{info=await lstat(absolute);}
453
456
  catch(error){
@@ -455,15 +458,16 @@ async function collectSelectedPath({
455
458
  throw error;
456
459
  }
457
460
  if(info.isSymbolicLink())fail(`${label} contains a symbolic link or junction: ${selected}.`);
461
+ if(selected==='.'&&!info.isDirectory())fail(`${label} root selection must be a directory.`);
458
462
  if(info.isDirectory()){
459
463
  const entries=await readdir(absolute,{withFileTypes:true});
460
464
  entries.sort((left,right)=>compareText(left.name,right.name));
461
465
  for(const entry of entries){
462
- const child=`${selected}/${entry.name}`;
466
+ const child=destinationJoin(selected,entry.name);
463
467
  await collectSelectedPath({
464
468
  sourceRoot,
465
469
  selected:child,
466
- destination:`${destination}/${entry.name}`,
470
+ destination:destinationJoin(destination,entry.name),
467
471
  excludes,
468
472
  reject,
469
473
  records,
@@ -515,7 +519,8 @@ async function collectPackageRecords(context,{signal}={}){
515
519
  records,
516
520
  destinations,
517
521
  signal,
518
- label:`sharedPayloads.${sharedId}`
522
+ label:`sharedPayloads.${sharedId}`,
523
+ allowRoot:true
519
524
  });
520
525
  }
521
526
  }
@@ -0,0 +1,99 @@
1
+ import Is from 'strong-type';
2
+ import {readFile,readdir} from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ const is=new Is(false);
6
+
7
+ // The browser URLs stay the same; only the source of their files changes.
8
+ export function installedSdkRoutes(packageSource,{security=false}={}){
9
+ return [
10
+ {
11
+ source:`${packageSource}/runtime/arcane`,destination:'arcane',
12
+ include:['components','css','entities','img','modules',...(security?['security']:[])],exclude:[]
13
+ },
14
+ {
15
+ source:`${packageSource}/browser-runtime`,destination:'arcane/sdk',
16
+ include:['.'],exclude:[]
17
+ },
18
+ {
19
+ source:`${packageSource}/runtime/strong-type`,destination:'arcane/dependencies/strong-type',
20
+ include:['.'],exclude:[]
21
+ },
22
+ {
23
+ source:packageSource,destination:'licenses/arcane-os',
24
+ include:['LICENSE','COMMERCIAL-LICENSE.md','NOTICE'],exclude:[]
25
+ }
26
+ ];
27
+ }
28
+
29
+ export function installedSdkPackageSource(config){
30
+ const routes=config?.sharedPayloads?.['browser-runtime'];
31
+ if(!is.array(routes)||routes.length!==4)return null;
32
+ const source=routes[3]?.source;
33
+ if(!is.string(source)||!/^node_modules\/(?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*$/u.test(source))return null;
34
+ const expected=installedSdkRoutes(source,{security:routes[0]?.include?.at(-1)==='security'});
35
+ return routes.every(function matchesInstalledRoute(route,index){
36
+ const wanted=expected[index];
37
+ return route?.source===wanted.source&&route?.destination===wanted.destination
38
+ &&is.array(route.include)&&route.include.length===wanted.include.length
39
+ &&route.include.every(function matchesSelectedPath(value,item){return value===wanted.include[item];})
40
+ &&(route.exclude===undefined||(is.array(route.exclude)&&route.exclude.length===0));
41
+ })?source:null;
42
+ }
43
+
44
+ export async function readInstalledSdkLayout(workspaceRoot,config){
45
+ if(config===undefined){
46
+ try{config=JSON.parse(await readFile(path.join(workspaceRoot,'arcane-packager.json'),'utf8'));}
47
+ catch(error){if(error.code==='ENOENT')return null;throw error;}
48
+ }
49
+ const packageSource=installedSdkPackageSource(config);
50
+ if(packageSource===null)return null;
51
+ const packageRoot=path.join(workspaceRoot,...packageSource.split('/'));
52
+ const versionPath=path.join(packageRoot,'package.json');
53
+ const manifest=JSON.parse(await readFile(versionPath,'utf8'));
54
+ if(manifest.name!=='arcane-os'||!is.string(manifest.version)||!manifest.version){
55
+ const error=new Error('The installed SDK runtime must identify its arcane-os package version.');
56
+ error.code='ARCANE_WORKSPACE_INVALID';
57
+ throw error;
58
+ }
59
+ return {packageSource,packageRoot,versionPath,version:manifest.version,routes:config.sharedPayloads['browser-runtime']};
60
+ }
61
+
62
+ export async function installedRuntimeFiles(workspaceRoot,layout,signal){
63
+ const files=[];
64
+ async function visit(directory,logical){
65
+ signal?.throwIfAborted();
66
+ const entries=await readdir(directory,{withFileTypes:true});
67
+ for(const entry of entries){
68
+ signal?.throwIfAborted();
69
+ const relative=logical?`${logical}/${entry.name}`:entry.name;
70
+ if(entry.isDirectory())await visit(path.join(directory,entry.name),relative);
71
+ else if(entry.isFile())files.push(relative);
72
+ }
73
+ }
74
+ for(const route of layout.routes){
75
+ if(route.destination!=='arcane'&&!route.destination.startsWith('arcane/'))continue;
76
+ const prefix=route.destination==='arcane'?'':route.destination.slice('arcane/'.length);
77
+ for(const selected of route.include){
78
+ const suffix=selected==='.'?'':selected;
79
+ await visit(
80
+ path.join(workspaceRoot,...route.source.split('/'),suffix),
81
+ [prefix,suffix].filter(Boolean).join('/')
82
+ );
83
+ }
84
+ }
85
+ return {files:files.sort()};
86
+ }
87
+
88
+ export function installedRuntimeTarget(relative,layout){
89
+ for(const route of [...layout.routes].sort(function longestDestinationFirst(left,right){
90
+ return right.destination.length-left.destination.length;
91
+ })){
92
+ if(!relative.startsWith(`${route.destination}/`))continue;
93
+ const suffix=relative.slice(route.destination.length+1);
94
+ if(route.include.some(function includesRuntimeTarget(selected){
95
+ return selected==='.'||suffix===selected||suffix.startsWith(`${selected}/`);
96
+ }))return `${route.source}/${suffix}`;
97
+ }
98
+ return relative;
99
+ }
package/src/workspace.mjs CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  import {loadAppDescriptor} from './app-descriptor.mjs';
10
10
  import {SDK_NAME as EXPECTED_SDK_NAME} from './constants.mjs';
11
11
  import {inspectImportMapHtml} from './import-map.mjs';
12
+ import {installedSdkPackageSource} from './sdk-runtime-layout.mjs';
12
13
 
13
14
  const is = new Is(false);
14
15
 
@@ -159,6 +160,13 @@ function classifyRootConfig(config){
159
160
  const validated=validatePackagerRootConfig(config,ROOT_CONFIG_NAME);
160
161
  const routes=validated.sharedPayloads['browser-runtime'];
161
162
  if(!is.array(routes))fail(`${ROOT_CONFIG_NAME} must define browser-runtime routes.`);
163
+ const installedPackageSource=installedSdkPackageSource(validated);
164
+ if(installedPackageSource!==null){
165
+ return {
166
+ ...validated,workspaceMode:'external',browserRuntimeLayout:'installed-v1',
167
+ sdkPackageSource:installedPackageSource
168
+ };
169
+ }
162
170
  const externalPackageSource=routes.length===2
163
171
  &&dependencyNameForSdkPackageSource(routes[1]?.source)!==null
164
172
  ?routes[1].source