arcane-os 0.28.0 → 0.28.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.28.2
4
+
5
+ - Redirect unmatched Mail server routes with `303 See Other` to
6
+ `https://<current hostname>/404.html`, using the request's HTTP/1 Host or
7
+ HTTP/2 authority and omitting the API listener port. The redirect has an empty
8
+ body and uses the existing HTTP server response interface.
9
+ - Preserve `/v1/mail` requests with query strings, CORS and OPTIONS behavior,
10
+ method and API errors, subscription handling, and the complete Mail provider
11
+ and cancellation lifecycle. Client APIs, npm resource paths, dependencies,
12
+ and listener configuration are unchanged.
13
+
14
+ ## 0.28.1
15
+
16
+ - DBOPFS initialization opens the application's existing storage scope without
17
+ creating product-specific table directories. Applications create the tables
18
+ they need through the existing `getTableHandle(name)` API after readiness.
19
+ - Create tables on demand and coalesce concurrent requests for the same physical
20
+ table. Logical `memories` and physical `memory` retain one shared handle and
21
+ deletion path. Existing directory discovery, complete reads and exports, CRUD,
22
+ worker fallback, events, application identity and saved data remain supported.
23
+ - Explicit `clearAllStorage()` leaves the cleared application scope empty rather
24
+ than recreating default folders. This update performs no saved-data migration
25
+ or automatic removal of existing folders. The 0.28.0 npm resource paths remain
26
+ unchanged.
27
+
3
28
  ## 0.28.0
4
29
 
5
30
  - Expose existing runtime modules and entities directly through
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
20
  event, cancellation, and browser run contracts.
21
21
 
22
- This checkout defines the `0.28.0` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.28.2` 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
 
@@ -712,6 +712,22 @@ browser and gateway preserve complete request and response content without
712
712
  body-size gates. With no event observer, the gateway does not construct event
713
713
  payloads or parse a second provider-request representation for observation.
714
714
 
715
+ Requests whose path does not match `/v1/mail` receive `303 See Other` with an
716
+ empty body and `Location: https://<current-hostname>/404.html`. The destination
717
+ uses the request's HTTP/2 authority or HTTP/1.1 Host hostname, omits the API port,
718
+ and replaces the requested path and query. For example:
719
+
720
+ | Unmatched request URL | Redirect destination |
721
+ | --- | --- |
722
+ | `https://mail.thewizardnexus.com:4433/` | `https://mail.thewizardnexus.com/404.html` |
723
+ | `https://mail.precrisis.ai:4433/missing?example=1` | `https://mail.precrisis.ai/404.html` |
724
+
725
+ The HTTPS website on the current hostname owns that page. The mail listener
726
+ only returns the redirect. Requests to `/v1/mail`, including its query-bearing
727
+ form, retain the existing CORS policy, OPTIONS preflight, method handling, and
728
+ structured API errors. For example, `GET /v1/mail` still receives `405`.
729
+ Subscription verification and its automatic same-IP exception remain unchanged.
730
+
715
731
  The gateway returns `202` only after Resend returns a nonempty string provider id.
716
732
  Transport loss, an explicit caller-selected timeout, an invalid success body,
717
733
  or an unreadable provider response returns an explicit uncertain result and never claims
@@ -4,6 +4,9 @@ The [same-IP subscription exception follow-up](#same-ip-subscription-exception-f
4
4
  records the later user-selected change to when the optional verifier runs. The
5
5
  earlier inventories retain their stated historical scope; current verification
6
6
  skips requests whose actual connection source and destination IPs are equal.
7
+ The [unmatched-route redirect follow-up](#unmatched-route-redirect-follow-up)
8
+ records the later change from a missing-route JSON error to the hostname's
9
+ HTTPS 404 page.
7
10
 
8
11
  | Decision | Behavior | Why it matters | Source status at this review |
9
12
  | --- | --- | --- | --- |
@@ -601,3 +604,28 @@ This follow-up records the selected behavior and source review. No local test,
601
604
  check, build, server launch, live mail send, or platform execution was performed
602
605
  by this documentation author. Release and runtime evidence remain with the
603
606
  corresponding operation's owner.
607
+
608
+ ## Unmatched-route redirect follow-up
609
+
610
+ The user selected the Stripe server's redirect behavior for unmatched mail
611
+ routes. The existing SDK `handleMailRequest` route decision owns this behavior:
612
+ an unmatched path returns `303 See Other` with an empty body and a Location of
613
+ `https://<current-hostname>/404.html`. The request authority supplies the hostname;
614
+ the destination omits the API port and replaces the original path and query.
615
+ The website on that hostname owns the page content.
616
+
617
+ | 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 |
618
+ | --- | --- | --- | --- | --- |
619
+ | Unmatched-path JSON `404` response | No. The selected result is navigation to the hostname's 404 page. | Keeping it would preserve the superseded response instead of the requested navigation. | Yes, when replaced with the redirect at the existing route decision. | Replace only this response with `303` and an empty body. |
620
+ | Current-hostname HTTPS redirect without the API port | Yes. It sends visitors to the requested website page. | One request-authority parse supplies the hostname for every deployment without another setting or domain table. | No. Removing it loses the explicitly selected destination. | Keep the decision inline in the existing native request handler; add no server, dependency, proxy, or file-serving path. |
621
+ | Matched `/v1/mail` CORS, OPTIONS, method errors, subscription verification, same-IP exception, and delivery results | Yes. Existing applications depend on these mail contracts. | An unmatched navigation does not change how an actual API request is handled or reported. | No. Redirecting actual API failures would hide the caller's delivery outcome. | Preserve `/v1/mail` and its query-bearing form, including OPTIONS preflight, `405` for unsupported methods, and structured API errors. |
622
+
623
+ An unmatched request ends at its existing route branch without entering
624
+ subscription verification, report parsing, or provider delivery. There is one
625
+ redirect response per unmatched request, with no new asynchronous wait, DNS
626
+ lookup, configuration read, timer, or retained state. This is a source-level
627
+ operation description; no timing improvement or deployed result is claimed.
628
+
629
+ This follow-up is based on source review. This documentation author ran no local
630
+ tests, checks, builds, server launches, or production requests. Delivery and
631
+ selected-package verification remain with their respective owners.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
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",
@@ -85,7 +85,7 @@
85
85
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
86
86
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
87
87
  "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",
88
- "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",
88
+ "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/dbopfs.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",
89
89
  "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.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",
90
90
  "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",
91
91
  "check": "node tools/check-source.mjs && npm test",
@@ -1905,7 +1905,7 @@
1905
1905
  let tableNames=dirs;
1906
1906
 
1907
1907
  if(!tableNames?.length){
1908
- tableNames=await dbopfs.getTableNames(Boolean(layout));
1908
+ tableNames=await dbopfs.getTableNames(true);
1909
1909
  }
1910
1910
  if(destroyed||sequence!==gridLoadSequence){
1911
1911
  return [];
@@ -16,20 +16,20 @@ const dbopfsReasons={
16
16
  export const DBOPFS_EVENT_TYPES={...dbopfsEventTypes};
17
17
  export const DBOPFS_REASONS={...dbopfsReasons};
18
18
 
19
- const DEFAULT_TABLE_DIRECTORIES={
20
- users:'users',
21
- scores:'scores',
22
- chats:'chats',
23
- notes:'notes',
24
- documents:'documents',
25
- songs:'songs',
26
- images:'images',
27
- journal_entries:'journal_entries',
28
- streams_of_consciousness:'streams_of_consciousness',
29
- reports:'reports',
30
- errors:'errors',
19
+ const TABLE_DIRECTORY_ALIASES={
31
20
  memories:'memory'
32
21
  };
22
+ const DIRECTORY_TABLE_ALIASES={
23
+ memory:'memories'
24
+ };
25
+
26
+ function directoryNameForTable(tableName=''){
27
+ return TABLE_DIRECTORY_ALIASES[tableName]||tableName;
28
+ }
29
+
30
+ function tableNameForDirectory(directoryName=''){
31
+ return DIRECTORY_TABLE_ALIASES[directoryName]||directoryName;
32
+ }
33
33
 
34
34
  function parseFileValue(fileName='',textContent=''){
35
35
  let value=textContent;
@@ -79,6 +79,11 @@ if(navigator.storage?.persist){
79
79
  * Handles to directories inside OPFS.
80
80
  */
81
81
 
82
+ /**
83
+ * @typedef {Object<string,Promise<FileSystemDirectoryHandle>>} DBOPFSTableHandlePromises
84
+ * Pending table handle requests keyed by logical table name.
85
+ */
86
+
82
87
  /**
83
88
  * @typedef {Object<string,Promise>} DBOPFSWriteLocks
84
89
  * Promise based write locks to serialize writes to the same file.
@@ -119,6 +124,9 @@ class DBOPFS {
119
124
  /** @type {DBOPFSTableHandles} */
120
125
  #tableHandles={}
121
126
 
127
+ /** @type {DBOPFSTableHandlePromises} */
128
+ #tableHandlePromises={}
129
+
122
130
  /** @type {DBOPFSTables} */
123
131
  #tables={}
124
132
 
@@ -297,7 +305,7 @@ class DBOPFS {
297
305
  }
298
306
 
299
307
  /**
300
- * Initializes OPFS database and default tables.
308
+ * Initializes the application OPFS database scope.
301
309
  * Dispatches `dbopfs-ready` event when complete.
302
310
  *
303
311
  * @returns {Promise<void>}
@@ -313,7 +321,6 @@ class DBOPFS {
313
321
  this.#applicationId=scope.applicationId;
314
322
  this.#storagePath=scope.path;
315
323
  this.#db=scope.directory;
316
- await this.#createDefaultTables();
317
324
 
318
325
  this.ready=true;
319
326
 
@@ -338,15 +345,6 @@ class DBOPFS {
338
345
  projectArcaneDOMEvent(window,occurrence);
339
346
  }
340
347
 
341
- async #createDefaultTables(){
342
- for(const [alias,directoryName]of Object.entries(DEFAULT_TABLE_DIRECTORIES)){
343
- this.#tableHandles[alias]=await this.#db.getDirectoryHandle(
344
- directoryName,
345
- {create:true}
346
- );
347
- }
348
- }
349
-
350
348
  /**
351
349
  * Canonical application identity owning this database.
352
350
  *
@@ -403,19 +401,49 @@ class DBOPFS {
403
401
  await this.readyPromise;
404
402
  }
405
403
 
406
- if(!this.#tableHandles[tableName]){
407
- const existingHandle=Object.values(this.#tableHandles).find(
408
- handle=>handle.name===tableName
409
- )
404
+ const directoryName=directoryNameForTable(tableName);
405
+ const registeredTableName=tableNameForDirectory(directoryName);
410
406
 
411
- if(existingHandle){
412
- return existingHandle
407
+ if(this.#tableHandles[registeredTableName]){
408
+ return this.#tableHandles[registeredTableName];
409
+ }
410
+
411
+ const existingHandle=Object.values(this.#tableHandles).find(
412
+ function matchingTableDirectory(handle){
413
+ return handle.name===directoryName;
413
414
  }
415
+ );
414
416
 
415
- this.#tableHandles[tableName]=await this.#db.getDirectoryHandle(tableName,{create:true});
417
+ if(existingHandle){
418
+ this.#tableHandles[registeredTableName]=existingHandle;
419
+ return existingHandle;
416
420
  }
417
421
 
418
- return this.#tableHandles[tableName];
422
+ if(!this.#tableHandlePromises[registeredTableName]){
423
+ const handlePromise=this.#db.getDirectoryHandle(
424
+ directoryName,
425
+ {create:true}
426
+ ).then(
427
+ function registerRequestedTable(handle){
428
+ this.#tableHandles[registeredTableName]=handle;
429
+ return handle;
430
+ }.bind(this)
431
+ );
432
+ this.#tableHandlePromises[registeredTableName]=handlePromise;
433
+
434
+ function clearTableHandlePromise(){
435
+ if(this.#tableHandlePromises[registeredTableName]===handlePromise){
436
+ delete this.#tableHandlePromises[registeredTableName];
437
+ }
438
+ }
439
+
440
+ handlePromise.then(
441
+ clearTableHandlePromise.bind(this),
442
+ clearTableHandlePromise.bind(this)
443
+ );
444
+ }
445
+
446
+ return this.#tableHandlePromises[registeredTableName];
419
447
  }
420
448
 
421
449
  /**
@@ -779,11 +807,17 @@ class DBOPFS {
779
807
  * @returns {Promise<boolean>}
780
808
  */
781
809
  async deleteTable(tableName){
810
+ const directoryName=directoryNameForTable(tableName);
811
+ const registeredTableName=tableNameForDirectory(directoryName);
812
+
782
813
  try{
783
- await this.#db.removeEntry(tableName,{recursive:true})
814
+ await this.#db.removeEntry(directoryName,{recursive:true})
784
815
 
785
816
  delete this.#tables[tableName]
786
- delete this.#tableHandles[tableName]
817
+ delete this.#tables[directoryName]
818
+ delete this.#tables[registeredTableName]
819
+ delete this.#tableHandles[registeredTableName]
820
+ delete this.#tableHandlePromises[registeredTableName]
787
821
  }catch(error){
788
822
  arcaneLogging.error(error)
789
823
  }
@@ -807,8 +841,8 @@ class DBOPFS {
807
841
 
808
842
  this.#tables={}
809
843
  this.#tableHandles={}
844
+ this.#tableHandlePromises={}
810
845
  this.#writeLocks={}
811
- await this.#createDefaultTables()
812
846
 
813
847
  return this
814
848
  }
@@ -864,12 +898,15 @@ class DBOPFS {
864
898
  continue
865
899
  }
866
900
 
901
+ const registeredTableName=tableNameForDirectory(name);
867
902
  const registered=Object.values(this.#tableHandles).some(
868
- tableHandle=>tableHandle.name===name
869
- )
903
+ function matchingDiscoveredDirectory(tableHandle){
904
+ return tableHandle.name===name;
905
+ }
906
+ );
870
907
 
871
908
  if(!registered){
872
- this.#tableHandles[name]=handle
909
+ this.#tableHandles[registeredTableName]=handle
873
910
  }
874
911
 
875
912
  tableNames.push(name)
@@ -877,8 +877,21 @@ function createConfiguredMailHandler(configuration){
877
877
  });
878
878
 
879
879
  try{
880
- if(request.url!==RESEND_MAIL_PATH&&!request.url?.startsWith(`${RESEND_MAIL_PATH}?`)){
881
- throw new MailGatewayFault('mail_route_not_found',{statusCode:404});
880
+ if (request.url !== RESEND_MAIL_PATH && !request.url?.startsWith(`${RESEND_MAIL_PATH}?`)) {
881
+ const requestAuthority = request.authority || request.headers.host || '';
882
+ const hostname = new URL(`https://${requestAuthority}`).hostname;
883
+ response.writeHead(
884
+ 303,
885
+ {
886
+ 'content-type': 'text/plain; charset=utf-8',
887
+ 'location': `https://${hostname}/404.html`
888
+ }
889
+ );
890
+ response.end();
891
+ if (!request.readableEnded && !request.destroyed) {
892
+ request.resume();
893
+ }
894
+ return;
882
895
  }
883
896
  const requestOrigin=request.headers.origin;
884
897
  if(requestOrigin){