@factoidal/core 0.35.1 → 0.36.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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.36.0 (2026-11-12)
4
+
5
+ - Foafnaut: dedicated Web Workers on BubblePopJS. `new Worker`, `postMessage` both ways through a structured clone, transfer of ArrayBuffers, `importScripts`; WPT `.any.worker.html` and `.worker.html` variants run (11,749/11,866 subtests on 78 pages). A worker start takes about 1.2 ms and a `postMessage` round trip about 0.2 ms. Each worker seeds `Math.random` from its own entropy.
6
+ - Foafnaut: `XMLHttpRequest` and `fetch()` for page scripts, on both script engines, in the desktop window and on the Hub. The origin policy, CORS with a preflight on each hop, redirects, forbidden fields and methods, and mixed content are decided by the built-in engine; no cookies or credentials are sent. `data:` URLs and `Blob`, `File` and `FormData` bodies; synchronous XHR. WPT xhr/: 1156/1449 subtests on BubblePopJS (was 816/1407), 1022/1433 on QuickJS-NG (was 681/1399). Security: a name is resolved before anything is sent and refused if any address is special-purpose, else one address is pinned for the transfer (no DNS rebinding); a method-override field naming a forbidden method is dropped; `Authorization` is dropped on a cross-origin redirect; a cross-origin redirect to a URL with credentials is refused; on the Hub, a final URL outside the policy is not read.
7
+ - Foafnaut: Shadow DOM and sandboxed iframes. Shadow trees and slots, declarative shadow roots, events through shadow trees (retargeting, `composedPath`), `slotchange`, `:host` and `::slotted`; a page with no shadow host pays nothing for them. `iframe` `sandbox` flags and opaque origins, and `postMessage` between frames. Theorems include `sandboxed_no_access`, `frameFlags_narrows` (flags only narrow), `nested_sandboxed_opaque` and `exchange_data` (only posted message records cross). WPT shadow-dom: 440/690 subtests on BubblePopJS, 547/695 on QuickJS-NG (both were 18/694).
8
+ - Foafnaut: the built-in engine keeps its state as one value passed through each call, not in hidden cells. Starting fib(24) on QuickJS-NG in the Wasm engine went from about 1330 ms to 800 ms; frame times are unchanged.
9
+ - Foafnaut: five bitwise SIMD instructions of the built-in Wasm engine take 4 to 6% less time, with a proof that the fast path gives the same results.
10
+ - Foafnaut: `foafnaut:about` is written by the built-in engine, from facts compiled into it (version, commit, build time, hygiene counts) and the list of specifications it implements. Facts the desktop program reports are in a section of their own, and a line says when the engine and app builds differ. `foafnaut --version` prints the engine's line first. Menu item About Foafnaut (macOS).
11
+ - Foafnaut (macOS): find in page. Cmd-F, Cmd-G and Shift-Cmd-G; matches and marks are made by the built-in engine. The Hub says find is in the desktop app only.
12
+ - Foafnaut (macOS): Open File (Cmd-O), Save Page As and Close Window (Shift-Cmd-W) in the File menu.
13
+ - Foafnaut: a 600 ms splash drawn by the built-in engine at start; `--no-splash` skips it.
14
+ - `foafnaut --dev-help`: developer options, in-browser tools, the developer proxy (`HTTP_PROXY`, `HTTPS_PROXY`, `DENO_CERT`; `tools/devproxy`), and what is not there yet (CDP, WebDriver BiDi, MCP).
15
+ - LWS: no lockout of the root ACL. With authorization on, `lws-serve` refuses with 409 any request that would leave `/.acl` with no agent that has `acl:Control` on the storage root (a PUT or PATCH that drops the last Control agent, a DELETE of `/.acl`, a move, an access grant); the refused request changes nothing. A write that keeps a Control agent, or swaps the owner for another, is accepted as before; `--insecure-no-auth` is not changed. The decision is in Lean (`L4Factoidal/LWS/NoLockout.lean`, `NoLockoutTheorems.lean`; theorems `step_keeps_root_control`, `lockout_iff`, `hasRootControl_iff`, `guard_refused_unchanged`). New command `factoidal lws-acl-reset DIR FILE --operator`: the offline operator recovery of the root ACL of a stopped storage, under the storage directory lock, through the storage library (new op `lwsAclReset`, theorems `reset_has_control`, `reset_other_paths`, `reset_refuses`). Test: `test/lws-no-lockout.test.mjs`.
16
+
17
+ - LWS: `lws-serve DIR --authz AS --root-acl FILE` (and `--owner`) on an EXISTING storage with no root ACL, for example one made with `--insecure-no-auth`, installs FILE as `/.acl` at start, before any request is answered. Before, the root ACL was written only into a new storage, and such a storage refused every agent (403), its owner too, so the only way out was to serve it with no authorization for a moment. An existing `/.acl` is never replaced; a line on standard error says which happened. No other resource changes, and with `--insecure-no-auth` nothing is installed. The decision is in Lean (`L4Factoidal/LWS/RootAclInstall.lean`; theorems `install_only_when_absent`, `install_keeps_existing`, `install_has_root_acl`, `install_other_paths`, `allowed_without_acl`). `openLwsStorage` and `openInPageLwsStorage` do the same with a snapshot. Test: `test/lws-root-acl-install.test.mjs`.
18
+
3
19
  ## 0.35.1 (2026-11-11)
4
20
 
5
21
  - Foafnaut (macOS): a heavy page no longer freezes the app. The host keeps one frame of GPU work in flight for heavy frames (three for light ones); after three frames over 250 ms it slows the page's animation and shows "SLOWED: HEAVY PAGE". Quitting or closing the window stops GPU work, so the app quits within seconds (before, up to 209 s on the Spinal Tap page at 2x). `--gpu-frames-in-flight N` sets the limit.
package/bin/factoidal.mjs CHANGED
@@ -98,7 +98,7 @@ const REVIEW_WARNING = 'EXPERIMENTAL UNDER-REVIEWED CODE - USE WITH CARE - DO NO
98
98
 
99
99
  /** The commands that serve, speak a protocol, or handle keys and tokens. */
100
100
  const WARNED_COMMANDS = new Set(['serve', 'lws-serve', 'solid-serve', 'solid-client', 'lws-client',
101
- 'authz-serve', 'authz-keys', 'xmpp-serve', 'webdav-serve', 'webdav-user', 'xmpp-http-proxy', 'lws-keygen'])
101
+ 'authz-serve', 'authz-keys', 'xmpp-serve', 'webdav-serve', 'webdav-user', 'xmpp-http-proxy', 'lws-keygen', 'lws-acl-reset'])
102
102
 
103
103
  const USAGE = `factoidal - the Factoidal persisted store, from Node or Deno
104
104
 
@@ -123,6 +123,9 @@ commands:
123
123
  lws-keygen print a new storage key for encryption at rest
124
124
  (FACTOIDAL_LWS_KEY of lws-serve, solid-serve and
125
125
  webdav-serve local mounts)
126
+ lws-acl-reset DIR FILE --operator
127
+ offline operator recovery: replace the root ACL
128
+ (/.acl) of a stopped LWS storage with FILE
126
129
  solid-client OP URL run one Solid client operation
127
130
  lws-client OP URL run one Linked Web Storage client operation
128
131
  authz-serve DIR serve an OAuth 2.0 authorization server for LWS
@@ -667,7 +670,12 @@ is required):
667
670
  https://github.com/danbri/factoidal/issues/1124
668
671
  --owner WEBID the storage owner: a new storage gets a root ACL
669
672
  that gives this agent Read, Write and Control
670
- --root-acl FILE a Turtle root ACL for a new storage instead
673
+ --root-acl FILE a Turtle root ACL for a new storage instead.
674
+ An EXISTING storage with no root ACL (/.acl), for
675
+ example one made with --insecure-no-auth, gets this
676
+ root ACL (or the --owner one) at start, before the
677
+ first request is answered; an existing /.acl is
678
+ kept and never replaced (a notice says which)
671
679
  --audience IRI the aud value tokens must carry (default: the
672
680
  storage IRI, BASE.well-known/lws-storage, which
673
681
  every lws#storage link names)
@@ -1125,6 +1133,37 @@ The random bytes come from the host (WebCrypto getRandomValues); the engine
1125
1133
  makes the key text and the key id (formal/lean4/L4Factoidal/LWS/DirLock.lean,
1126
1134
  newStorageKey).`,
1127
1135
 
1136
+ 'lws-acl-reset': `factoidal lws-acl-reset - offline operator recovery of the root ACL of an LWS storage
1137
+
1138
+ usage: factoidal lws-acl-reset DIR FILE --operator [--base IRI] [--storage files|snapshot]
1139
+
1140
+ Replaces the root ACL resource (/.acl) of the LWS storage in DIR with the
1141
+ Turtle in FILE. It is the last resort when no agent can change /.acl through
1142
+ the protocol any more (lws-serve refuses a request that would leave no agent
1143
+ with acl:Control on the storage root, with 409; a storage can still lose its
1144
+ controller, for example when the only control key is lost). First try the
1145
+ recovery agent: an agent with acl:Control on the root can PUT /.acl.
1146
+
1147
+ --operator is required: it confirms that you operate this storage and that
1148
+ the server is stopped. The command takes the storage directory lock
1149
+ (DIR/lws.lock) and refuses while a server holds it; it opens only an existing
1150
+ storage, through the same storage library as lws-serve (the engine reads and
1151
+ writes every file), answers no request, and changes no resource but /.acl.
1152
+ The write goes through the storage journal, and the command prints a notice
1153
+ on standard error.
1154
+
1155
+ FILE must give some agent acl:Control on the storage root: an Authorization
1156
+ with acl:accessTo (or acl:default) the root container, acl:mode acl:Control,
1157
+ and an acl:agent, an acl:agentGroup with members, or an acl:agentClass.
1158
+ Otherwise the command refuses and nothing is written. Relative IRIs (<./>)
1159
+ are read against the root ACL's own IRI; when FILE names the root with an
1160
+ absolute IRI, give --base with the storage's base IRI (the one lws-serve
1161
+ serves, for example http://127.0.0.1:3000/).
1162
+
1163
+ The decision is in Lean (formal/lean4/L4Factoidal/LWS/NoLockout.lean,
1164
+ reset; theorems reset_has_control, reset_other_paths, reset_refuses) and the
1165
+ lock in L4Factoidal/LWS/DirLock.lean.`,
1166
+
1128
1167
  'webdav-user': `factoidal webdav-user - add or replace a user in a webdav-serve users file
1129
1168
 
1130
1169
  usage: factoidal webdav-user FILE NAME [--iterations N]
@@ -1317,7 +1356,8 @@ const VALUE_OPTIONS = {
1317
1356
  'webdav-user': new Set(['iterations']),
1318
1357
  'xmpp-http-proxy': new Set(['port', 'host', 'xmpp', 'xmpp-password-file', 'target', 'xmpp-host', 'xmpp-port',
1319
1358
  'xmpp-resource', 'xmpp-ca', 'timeout']),
1320
- 'lws-keygen': new Set([])
1359
+ 'lws-keygen': new Set([]),
1360
+ 'lws-acl-reset': new Set(['base', 'storage'])
1321
1361
  }
1322
1362
 
1323
1363
  // ------------------------------------------------------------- commands
@@ -2529,6 +2569,68 @@ async function commandAuthzServe (positional, options) {
2529
2569
  return untilInterrupted(running, quiet)
2530
2570
  }
2531
2571
 
2572
+ // lws-acl-reset: the offline operator recovery of the root ACL of an LWS
2573
+ // storage. The storage library opens the storage under the directory lock
2574
+ // (`operator: true`: an existing storage only, no request answered), and the
2575
+ // engine decides the reset (L4Factoidal/LWS/NoLockout.lean, `reset`). This
2576
+ // host reads FILE and prints what the engine says.
2577
+ async function commandLwsAclReset (positional, options) {
2578
+ if (positional.length !== 2) throw new UsageError('lws-acl-reset needs DIR and FILE')
2579
+ if (options.operator !== true) {
2580
+ throw new UsageError('lws-acl-reset replaces the root ACL of a storage offline; give --operator ' +
2581
+ 'to confirm that you operate this storage and that its server is stopped')
2582
+ }
2583
+ const [dir, file] = positional
2584
+ const { statSync } = await import('node:fs')
2585
+ let isDir = false
2586
+ try { isDir = statSync(dir).isDirectory() } catch (_error) { isDir = false }
2587
+ if (!isDir) {
2588
+ err(`factoidal lws-acl-reset: ${dir} is not a directory; nothing was written`)
2589
+ return EXIT_FAILURE
2590
+ }
2591
+ const acl = new TextDecoder('utf-8', { fatal: true }).decode(readWhole(file))
2592
+ const { openLwsStorage, LwsHostError } = await import('../lws/server.mjs')
2593
+ let storage
2594
+ try {
2595
+ storage = await openLwsStorage({
2596
+ dir,
2597
+ operator: true,
2598
+ lockCommand: 'lws-acl-reset',
2599
+ ...(typeof options.base === 'string' ? { baseIri: options.base } : {}),
2600
+ ...(typeof options.storage === 'string' ? { storage: options.storage } : {})
2601
+ })
2602
+ } catch (error) {
2603
+ if (error instanceof LwsHostError && error.unknownOp) return notWired('lws-acl-reset', error.message)
2604
+ if (error !== null && typeof error === 'object' && (error.envelope !== undefined || error.stateFile === true)) {
2605
+ err(`factoidal lws-acl-reset: ${error.envelope !== undefined ? error.envelope.error : error.message}`)
2606
+ return EXIT_FAILURE
2607
+ }
2608
+ throw error
2609
+ }
2610
+ try {
2611
+ for (const n of storage.notices || []) err(`lws-acl-reset: ${n}`)
2612
+ let notice
2613
+ try {
2614
+ notice = storage.resetRootAcl(acl)
2615
+ } catch (error) {
2616
+ const message = error && error.message ? String(error.message) : String(error)
2617
+ if (message.indexOf('unknown op') >= 0) return notWired('lws-acl-reset', message)
2618
+ if (error !== null && typeof error === 'object' && error.envelope !== undefined && error.envelope !== null) {
2619
+ err(`factoidal ${error.envelope.error}`)
2620
+ if (typeof options.base !== 'string') {
2621
+ err('lws-acl-reset: if FILE names the storage root with an absolute IRI, give --base with the storage\'s base IRI')
2622
+ }
2623
+ return EXIT_FAILURE
2624
+ }
2625
+ throw error
2626
+ }
2627
+ err(notice)
2628
+ return EXIT_OK
2629
+ } finally {
2630
+ storage.close()
2631
+ }
2632
+ }
2633
+
2532
2634
  // lws-keygen (https://github.com/danbri/factoidal/issues/1135): 32 random
2533
2635
  // bytes from the host's random source go to the engine (`lwsStorageKeyNew`),
2534
2636
  // which gives the key text and its key id. The key goes to standard output
@@ -3315,7 +3417,8 @@ const COMMANDS = {
3315
3417
  'webdav-serve': commandWebdavServe,
3316
3418
  'webdav-user': commandWebdavUser,
3317
3419
  'xmpp-http-proxy': commandXmppHttpProxy,
3318
- 'lws-keygen': commandLwsKeygen
3420
+ 'lws-keygen': commandLwsKeygen,
3421
+ 'lws-acl-reset': commandLwsAclReset
3319
3422
  }
3320
3423
 
3321
3424
  // ----------------------------------------------------------------- main
package/bin/foafnaut.mjs CHANGED
@@ -25,6 +25,7 @@ const HELP = `foafnaut [--latest|--bleeding-edge] [PAGE | --url URL | --html FIL
25
25
  --which print the app that would run
26
26
  --version print the commit and build time of that app (the app answers)
27
27
  --help this text
28
+ --dev-help options and tools for developing and testing the browser
28
29
 
29
30
  App options, passed to the app as they are:
30
31
  --perf-summary print frame and input latency figures when the window closes
@@ -45,6 +46,53 @@ closes; --detach returns at once.
45
46
 
46
47
  EXPERIMENTAL UNDER-REVIEWED CODE - USE WITH CARE - DO NOT CREATE PUBLIC SERVICES USING THIS WITHOUT CAREFUL INDEPENDENT EXPERT REVIEW.
47
48
  `;
49
+ const DEV_HELP = `foafnaut --dev-help: tools for developing and testing the browser
50
+
51
+ More app options (passed to the app as they are)
52
+ --offscreen no window: draw into a GPU texture (for tests and captures)
53
+ --shot FILE write a PNG of the page (with --offscreen)
54
+ --print-pdf FILE print the page to a PDF file
55
+ --no-scripts run no page scripts
56
+ --strict refuse pages with features the browser skips (the default)
57
+ --no-state no saved state: no history, bookmarks or session
58
+ --state-dir DIR keep the saved state in DIR
59
+ --restore-session reopen the tabs of the last run
60
+ --no-splash no splash at start (off already offscreen and in checks)
61
+ --single-frame draw the page as one layer (no partial frames)
62
+ --gpu-frames-in-flight N GPU frames queued at most (default: 1 for heavy work, 3 for light)
63
+ --trace-input log every input event and edge drag to stderr
64
+ --check-engine run the engine on the main thread and check its render plan (CPU)
65
+ --quickjs-wasm FILE run page scripts on QuickJS-NG in BubblePopWASM from FILE
66
+ (BubblePopJS, the built-in JavaScript engine, is the default)
67
+ --*-sweep scripted checks used by the tests (scroll, resize, find, ...);
68
+ see labs/demos/foafnaut/sweeps.sh
69
+
70
+ Inside the browser
71
+ Option-Command-J the console: page script output, refusals, skipped features
72
+ about:foafnaut version, commit, build time and the specifications implemented,
73
+ written by the engine; facts from the desktop program are marked
74
+ foafnaut:console, foafnaut:sparql, foafnaut:wasm developer pages
75
+ Command-F find in page
76
+
77
+ Through a proxy (developer proxy: tools/devproxy/README.md, in the source repository)
78
+ The app uses HTTP_PROXY, HTTPS_PROXY and DENO_CERT from its environment, so it can run
79
+ through a local proxy with no change. tools/devproxy/devproxy.mjs records real pages once
80
+ to WARC and replays them offline, and can simplify pages as it serves them:
81
+ full no change
82
+ no-scripts scripts and on* attributes removed
83
+ text-css-links structure, text, links and stylesheets kept; scripts, frames, media,
84
+ images, canvas and svg removed
85
+ Run it with tools/devproxy/foafnaut-through-proxy.sh (--record NAME, --replay NAME, or
86
+ devproxy.mjs --passthrough --default-service LEVEL). It listens on 127.0.0.1 only, with a
87
+ private CA for this process only. A test tool, not a sanitiser and not a security
88
+ boundary. Page scripts' XMLHttpRequest and fetch() may not go through the proxy yet.
89
+
90
+ Not yet: a debugging protocol (Chrome DevTools Protocol, WebDriver BiDi), an MCP server,
91
+ breakpoints, console input. Plan: https://github.com/danbri/factoidal-core/issues/372
92
+
93
+ Logs: ~/Library/Logs/foafnaut/trace.jsonl. Source and issues:
94
+ https://github.com/danbri/factoidal-core (labs/demos/foafnaut/).
95
+ `;
48
96
  const RELEASES = 'https://api.github.com/repos/danbri/factoidal-builds/releases?per_page=60';
49
97
  const APP = 'foafnaut.app';
50
98
  const home = () => process.env.FOAFNAUT_HOME || join(homedir(), '.foafnaut');
@@ -139,6 +187,7 @@ export async function main(argv, channel = 'current') {
139
187
  for (let i = 0; i < options.length; i++) {
140
188
  const arg = options[i];
141
189
  if (arg === '--help' || arg === '-h') { process.stdout.write(HELP); return; }
190
+ if (arg === '--dev-help') { process.stdout.write(DEV_HELP); return; }
142
191
  else if (arg === '--latest') channel = 'latest';
143
192
  else if (arg === '--bleeding-edge') channel = 'bleeding-edge';
144
193
  else if (arg === '--which') which = true;
@@ -37,7 +37,7 @@
37
37
  // bytes change.
38
38
 
39
39
  // Stamped by formal/lean4/Wasm/build-wasm.sh step 9 -- do not hand-edit.
40
- const WASM_VERSION = "2e241365bd79";
40
+ const WASM_VERSION = "ccdd8676f40a";
41
41
 
42
42
  import createModule from './l4factoidal.mjs';
43
43
 
Binary file
@@ -2,17 +2,17 @@
2
2
  "engine": "lean4",
3
3
  "note": "The Lean 4 (L4Factoidal) engine build provenance and claims: the only engine of @factoidal/core since 0.9.0 (issue 713). This committed file is a placeholder. tools/fetch-builds.sh --set web (through tools/stamp-l4-version.mjs) writes the provenance members from the danbri/factoidal-builds release that built the wasm; formal/lean4/Wasm/build-wasm.sh step 9 writes them for a local build. Do not commit a stamped copy.",
4
4
  "stamped": true,
5
- "release": "danbri/factoidal-builds build-6f57afba14e0-web",
6
- "releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/42968513371",
7
- "gitSha": "6f57afba14e04ed648358e4693790edff69a6cd3",
8
- "builtAt": "2026-11-11T07:07:29Z",
5
+ "release": "danbri/factoidal-builds build-2fd7fc4d38f3-web",
6
+ "releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/43144347600",
7
+ "gitSha": "2fd7fc4d38f352c221004f3e407707f69afbb3e8",
8
+ "builtAt": "2026-11-12T04:22:57Z",
9
9
  "leanToolchain": "leanprover/lean4:v4.33.1",
10
10
  "emscripten": "6.0.8",
11
11
  "abiVersion": "1",
12
12
  "engineVersion": "l4factoidal-wasm 0.2.0 (dispatch: RDF parse/serialize, SPARQL query+update, RDFS/OWL-RL closures, RDFC-1.0, CL/IKL)",
13
13
  "version": "l4factoidal-wasm 0.2.0 (dispatch: RDF parse/serialize, SPARQL query+update, RDFS/OWL-RL closures, RDFC-1.0, CL/IKL)",
14
- "wasmSha256": "2e241365bd7906474320610a65af99641474534fa0fe0e744ed3d9edeef4ee41",
15
- "wasmBytes": 32703824,
14
+ "wasmSha256": "ccdd8676f40a0aa78a3a0a997cf561bff59c9af2e544e1231eed5007ecb94250",
15
+ "wasmBytes": 32729309,
16
16
  "claims": {
17
17
  "source": "formal/lean4 (L4Factoidal): no sorry, no user axioms, no native_decide; W3C behaviour pinned by build-time #guard",
18
18
  "suitesAtBuildSha": "see docs/test-results and formal/lean4/PORT_NOTES.md at gitSha"
package/lib/version.mjs CHANGED
@@ -2,4 +2,4 @@
2
2
  // needs import attributes, which not every runtime this package targets
3
3
  // reads). test/dts-drift.test.js checks that it equals package.json's
4
4
  // `version`.
5
- export const version = '0.35.1';
5
+ export const version = '0.36.0';
package/lws/README.md CHANGED
@@ -91,6 +91,31 @@ const running = await listen({
91
91
  the keys inline instead.
92
92
  * `insecureNoAuth: true` serves every request with no authorization. It is
93
93
  for development only, and must be asked for.
94
+ * A root ACL (`rootAcl`, or the owner's from `owner`) is written into a
95
+ NEW storage. An EXISTING storage with no root ACL resource (`/.acl`), for
96
+ example one made with `insecureNoAuth`, gets it at open, before any
97
+ request is answered; an existing `/.acl` is kept and never replaced. The
98
+ start notices say which (`lws-serve` prints them on standard error). So a
99
+ storage made with no authorization moves to authorization with no moment
100
+ of open access: `lws-serve DIR --authz AS --root-acl FILE`. The decision
101
+ and its theorems are in `formal/lean4/L4Factoidal/LWS/RootAclInstall.lean`
102
+ (listed in `formal/lean4/L4Factoidal/LWS/README.md`); with authorization
103
+ off nothing is installed.
104
+ * No lockout: with authorization on, a request that would leave the root ACL
105
+ (`/.acl`) with no agent that has `acl:Control` on the storage root (a PUT
106
+ or PATCH that drops the last one, a DELETE of `/.acl`, a move, an access
107
+ grant) is refused with 409 and changes nothing. A write that keeps a
108
+ Control agent, or swaps one for another, is accepted. Theorems
109
+ `step_keeps_root_control` and `lockout_iff`
110
+ (`formal/lean4/L4Factoidal/LWS/NoLockout.lean`, `NoLockoutTheorems.lean`).
111
+ * The offline operator recovery: `factoidal lws-acl-reset DIR FILE --operator`
112
+ replaces only `/.acl` of a stopped storage with FILE, under the storage
113
+ directory lock, through this storage library (`openLwsStorage({ dir,
114
+ operator: true })` and `resetRootAcl(acl)`; the op `lwsAclReset`). It
115
+ refuses while a server holds the lock, in a directory with no storage, and
116
+ for a FILE that gives no agent `acl:Control` on the root (theorems
117
+ `reset_has_control`, `reset_other_paths`, `reset_refuses`). Relative IRIs
118
+ (`<./>`) need no `--base`.
94
119
 
95
120
  ## Pagination and client links
96
121
 
package/lws/inpage.mjs CHANGED
@@ -85,6 +85,10 @@ export async function openInPageLwsStorage (options = {}) {
85
85
  }
86
86
  return answer.snapshot
87
87
  }
88
+ // A snapshot with no root ACL resource, reopened with `rootAcl` (or
89
+ // `owner`), gets that root ACL at open (L4Factoidal/LWS/RootAclInstall.lean);
90
+ // the engine says `changed`, and the caller is given the new snapshot.
91
+ if (opened !== null && typeof opened === 'object' && opened.changed === true && onSnapshot !== null) onSnapshot(snapshot())
88
92
 
89
93
  /** A fetch to this storage: the same arguments as `fetch`, a real Response back. */
90
94
  async function podFetch (input, init) {
package/lws/server.mjs CHANGED
@@ -98,7 +98,7 @@ import { readSnapshot, persistIfChanged, storageKeyText, storageModeText, loadFi
98
98
 
99
99
  /** The ops this host needs from the engine. */
100
100
  export const LWS_OPS = ['lwsOpen', 'lwsStep', 'lwsClose', 'lwsSnapshot', 'lwsFetched', 'lwsDelivered', 'lwsEgress',
101
- 'lwsLoad', 'lwsPersist', 'lwsRead', 'lwsDirLock', 'lwsAtRestNotice']
101
+ 'lwsLoad', 'lwsPersist', 'lwsRead', 'lwsDirLock', 'lwsAtRestNotice', 'lwsAclReset']
102
102
 
103
103
  /** How long this host waits for the answer to one delivery request before
104
104
  * it reports "no answer" (status 0) to the engine. */
@@ -349,6 +349,10 @@ export async function openLwsStorage (options = {}) {
349
349
  // HTTP: createLwsServer refuses it.
350
350
  if (typeof options.principal === 'string') config.principal = options.principal
351
351
  if (typeof options.rootAcl === 'string') config.rootAcl = options.rootAcl
352
+ // The offline operator recovery (`factoidal lws-acl-reset`): a handle that
353
+ // opens only an existing storage, answers no request, and accepts
354
+ // `resetRootAcl` (L4Factoidal/LWS/NoLockout.lean, `reset`).
355
+ if (options.operator === true) config.operator = true
352
356
  // Notifications are on unless the caller turns them off; the engine
353
357
  // decides everything else about them.
354
358
  if (options.notifications === false) config.notifications = false
@@ -472,9 +476,22 @@ export async function openLwsStorage (options = {}) {
472
476
  // files: the engine is told none is present.
473
477
  filesMode = opened !== null && typeof opened === 'object' &&
474
478
  opened.load !== undefined && opened.load !== null
479
+ // An existing storage with no root ACL resource, opened with
480
+ // authorization and a root ACL (`rootAcl`, or the owner's), gets that
481
+ // root ACL here, before any request is answered: the engine decides
482
+ // (L4Factoidal/LWS/RootAclInstall.lean) and says `changed`; this host
483
+ // persists it and prints the engine's `rootAclNotice`. Requests that
484
+ // reach the socket meanwhile wait for this open (createLwsServer).
485
+ let started = opened
475
486
  if (filesMode) {
476
487
  if (dir === null) throw new LwsHostError('a storage kept in files needs a storage directory (dir)')
477
- loadFiles(engine, handle, opened, dir)
488
+ started = loadFiles(engine, handle, opened, dir)
489
+ persistFilesIfChanged(engine, handle, started, dir)
490
+ } else if (dir !== null) {
491
+ persistIfChanged(engine, 'lwsSnapshot', handle, started, dir)
492
+ }
493
+ if (started !== null && typeof started === 'object' && typeof started.rootAclNotice === 'string') {
494
+ notices.push(started.rootAclNotice)
478
495
  }
479
496
  // What a server prints at start, after the experimental code notice:
480
497
  // a take-over of a stale lock, and whether the storage on disk is
@@ -655,6 +672,24 @@ export async function openLwsStorage (options = {}) {
655
672
  if (typeof envelope.wake === 'number') scheduleFlush(envelope.wake)
656
673
  return answer
657
674
  },
675
+ /**
676
+ * The offline operator recovery: replace the root ACL resource (/.acl)
677
+ * with `turtle` (the new ACL text). Only on a storage opened with `operator: true`,
678
+ * which holds the storage directory lock and answers no request. The
679
+ * engine decides (`lwsAclReset`, L4Factoidal/LWS/NoLockout.lean `reset`):
680
+ * only /.acl changes, through the journaled store, and a `turtle` that gives
681
+ * no agent acl:Control on the storage root is refused (thrown with the
682
+ * engine's message in `envelope`). Returns the engine's notice.
683
+ */
684
+ resetRootAcl (turtle) {
685
+ const told = engine.call('lwsAclReset', [handle, JSON.stringify({ 'acl': turtle })])
686
+ if (told === null || typeof told !== 'object' || told.ok === false) {
687
+ throw Object.assign(new Error(told && told.error ? told.error : 'lwsAclReset: no answer'),
688
+ { envelope: told })
689
+ }
690
+ persist(told)
691
+ return told.notice
692
+ },
658
693
  /** Resolves when no delivery request is in flight. */
659
694
  async settled () {
660
695
  await new Promise((resolve) => setTimeout(resolve, 0))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factoidal/core",
3
- "version": "0.35.1",
3
+ "version": "0.36.0",
4
4
  "description": "EXPERIMENTAL UNDER-REVIEWED CODE: do not run public services on it without careful independent expert review. RDF parsing and writing (Turtle, TriG, N-Triples, N-Quads, RDF/XML, JSON-LD), SPARQL 1.1 and 1.2, RDFS and OWL reasoning, SHACL and ShEx validation, CSVW and RML mapping, RDFC-1.0, and SPARQL, LWS and Solid servers, for Node, Deno and browsers.",
5
5
  "keywords": [
6
6
  "sparql",
@@ -28,18 +28,17 @@
28
28
  "author": "Dan Brickley <danbri@danbri.org>",
29
29
  "repository": {
30
30
  "type": "git",
31
- "url": "git+https://github.com/danbri/factoidal.git",
32
- "directory": "npm/factoidal"
31
+ "url": "git+https://github.com/danbri/factoidal-builds.git"
33
32
  },
34
33
  "main": "./index.js",
35
34
  "module": "./index.mjs",
36
35
  "browser": "./browser.js",
37
36
  "types": "./index.d.ts",
38
37
  "bin": {
39
- "factoidal": "./bin/factoidal.mjs",
40
- "foafnaut": "./bin/foafnaut.mjs",
41
- "foafnaut-latest": "./bin/foafnaut-latest.mjs",
42
- "foafnaut-bleedingedge": "./bin/foafnaut-bleedingedge.mjs"
38
+ "factoidal": "bin/factoidal.mjs",
39
+ "foafnaut": "bin/foafnaut.mjs",
40
+ "foafnaut-latest": "bin/foafnaut-latest.mjs",
41
+ "foafnaut-bleedingedge": "bin/foafnaut-bleedingedge.mjs"
43
42
  },
44
43
  "exports": {
45
44
  ".": {