@factoidal/core 0.35.1 → 0.37.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 +30 -0
- package/bin/factoidal.mjs +107 -4
- package/bin/foafnaut.mjs +49 -0
- package/l4-assets/l4factoidal.js +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +6 -6
- package/lib/version.mjs +1 -1
- package/lws/README.md +25 -0
- package/lws/inpage.mjs +4 -0
- package/lws/server.mjs +37 -2
- package/package.json +6 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.37.0 (2026-11-14)
|
|
4
|
+
|
|
5
|
+
- Foafnaut: Web Platform Tests on the Linux engine, full measured runs: 85.06% on the morning of 2026-11-14, 91.76% (2,082,990 of 2,270,129 subtests) at commit 53326e401; 66 crashed tests and 4,876 harness timeouts in that run. Test262 on BubblePopJS: 42,515 of 53,833 (79.0%).
|
|
6
|
+
- Foafnaut: CSS Grid placement, track sizing, alignment and absolutely positioned grid items as pure modules with bound theorems; a page's own web fonts are its fonts when the host holds none, which most grid tests need (css-grid 9,115 to 11,121 subtests).
|
|
7
|
+
- Foafnaut: CSS animations and transitions on the effect stack: replaced animations, `commitStyles()`, animated values in the layout of pages with `::before`/`::after`, one cascade for a layout and an element's matches made once a measure.
|
|
8
|
+
- Foafnaut: the cascade works out structural pseudo-classes and `:has()` once per document: the first layout of a page with 2,000 rules and 3,000 elements went from 845 ms to about 340 ms on the macOS engine.
|
|
9
|
+
- Foafnaut: real pages start sooner: scripts, images and the icon are read while the style sheets are, and the engine's first call is made while the page is fetched (Hacker News load 3.4 s to 1.9 s, measured on an M1).
|
|
10
|
+
- BubblePopJS: the RegExp `Symbol.match`, `replace`, `search`, `split` and `matchAll` protocol, ArraySpeciesCreate and TypedArraySpeciesCreate, typed array `[[DefineOwnProperty]]`, regular expression literals checked at parse time.
|
|
11
|
+
- Foafnaut: reflected IDL attributes such as `el.title` read and write through one host call (5.4 to 3.9 µs a read).
|
|
12
|
+
- Foafnaut: client storage has a budget: 32 MiB per storage key over IndexedDB, the file system and the Cache API, with theorems for the bound; storage buckets, Cookie Store `maxAge` and change events.
|
|
13
|
+
- Foafnaut: networking fixes for workers and service workers (a worker script fetch asked while frames are answered is now answered), the WebSocket close code, ORB for classic scripts, and network theorems for credentials across redirects.
|
|
14
|
+
- Foafnaut: `getComputedStyle(el).paddingLeft` and `.borderRightWidth` answer their values again: two internal operation numbers had collided with the computed-style range; theorems now keep the ranges apart.
|
|
15
|
+
- Build: the engine build refuses a Lean module that imports parts of `Std` the WebAssembly engine does not link.
|
|
16
|
+
|
|
17
|
+
## 0.36.0 (2026-11-12)
|
|
18
|
+
|
|
19
|
+
- 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.
|
|
20
|
+
- 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.
|
|
21
|
+
- 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).
|
|
22
|
+
- 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.
|
|
23
|
+
- 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.
|
|
24
|
+
- 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).
|
|
25
|
+
- 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.
|
|
26
|
+
- Foafnaut (macOS): Open File (Cmd-O), Save Page As and Close Window (Shift-Cmd-W) in the File menu.
|
|
27
|
+
- Foafnaut: a 600 ms splash drawn by the built-in engine at start; `--no-splash` skips it.
|
|
28
|
+
- `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).
|
|
29
|
+
- 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`.
|
|
30
|
+
|
|
31
|
+
- 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`.
|
|
32
|
+
|
|
3
33
|
## 0.35.1 (2026-11-11)
|
|
4
34
|
|
|
5
35
|
- 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;
|
package/l4-assets/l4factoidal.js
CHANGED
|
Binary file
|
package/l4-assets/version.json
CHANGED
|
@@ -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-
|
|
6
|
-
"releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/
|
|
7
|
-
"gitSha": "
|
|
8
|
-
"builtAt": "2026-11-
|
|
5
|
+
"release": "danbri/factoidal-builds build-ff075ad5af99-web",
|
|
6
|
+
"releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/43633972604",
|
|
7
|
+
"gitSha": "ff075ad5af998de7918e3ffe5846559fd6d5e6b2",
|
|
8
|
+
"builtAt": "2026-11-14T22:33:16Z",
|
|
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": "
|
|
15
|
-
"wasmBytes":
|
|
14
|
+
"wasmSha256": "f8b5d5ddacbbc195f75a5f32f9ba5027ce559f029c091fe3d16016953e1471d2",
|
|
15
|
+
"wasmBytes": 32774942,
|
|
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
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.
|
|
3
|
+
"version": "0.37.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": "
|
|
40
|
-
"foafnaut": "
|
|
41
|
-
"foafnaut-latest": "
|
|
42
|
-
"foafnaut-bleedingedge": "
|
|
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
|
".": {
|