@factoidal/core 0.27.0 → 0.28.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
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.28.0 (2026-10-31)
6
+
7
+ - The public LWS SBM11 reader rejects resource paths that escape its configured container before calling transport. Default Node HTTP reads refuse redirects. Browser default XHR now rejects caller-supplied headers, including authorization; authenticated browser use requires an explicit trusted, redirect-safe transport. Custom transports remain responsible for redirect and credential policy. WebDAV read-only behavior is unchanged.
8
+ - The Lean HTML parser improvements merged in PR269 now reach the package, including processing instructions, namespace/template handling and public parse-html regressions. This is parser support, not a browser layout engine or a claim of complete HTML conformance.
9
+ - Valid zero-second IETF dates no longer emit panic diagnostics. The targeted native fn-parse-ietf-date set passes 105 cases.
10
+ - Bounded slice and positional consumers of fn:generate evaluate finite prefixes without increasing the generation cap. Native fn-generate improves from 13 pass / 3 fail to 15 pass / 1 fail; the constructed-node identity failure remains visible. All 104 fn-slice cases pass. These targeted native results are not whole-catalog WASM conformance claims.
11
+
12
+ Native server executables and the SVG graphics experiment have separate release gates; this package version does not claim new executable or SVG compatibility coverage.
13
+
5
14
  ## 0.27.0 (2026-10-30)
6
15
 
7
16
  - New CLI commands `factoidal xpath4`, `factoidal xquery` and `factoidal xslt4` expose the existing Lean XML operations. They accept explicit local input files, display JSON or the engine's serialized result, and preserve QT4 error codes such as `XPST0003`. See the README for arguments and examples.
package/index.d.ts CHANGED
@@ -1556,14 +1556,21 @@ export interface Sbm11ReadEvidence {
1556
1556
  bytes: number;
1557
1557
  }
1558
1558
 
1559
- /** A synchronous HTTP GET: `(url, headers) -> answer`. */
1559
+ /** A trusted synchronous HTTP GET: `(url, headers) -> answer`.
1560
+ * It must refuse redirects before forwarding scoped headers. */
1560
1561
  export type Sbm11Transport = (url: string, headers: Record<string, string>) =>
1561
1562
  { status: number; header(name: string): string | null; bytes: Uint8Array };
1562
1563
 
1563
1564
  /** A reader of the SBM11 files held as LWS resources under the container
1564
1565
  * `url` (one resource per file, CURRENT a resource too), by HTTP Range GETs
1565
1566
  * when the server answers 206, else by whole GETs kept in memory. Node: the
1566
- * default transport is a fetch in a worker thread; close() stops it. */
1567
+ * default transport is a fetch in a worker thread that refuses redirects;
1568
+ * close() stops it. `url` must be absolute HTTP(S), without userinfo, query
1569
+ * or fragment. File arguments are raw relative paths; invalid paths return
1570
+ * null before transport and appear in errors().
1571
+ * Browser worker default XHR accepts no caller auth/headers; provide an explicit
1572
+ * trusted transport for those. Its final-URL check cannot prevent an already
1573
+ * sent anonymous request or ambient same-origin browser cookies. */
1567
1574
  export function lwsSbm11Reader(url: string, opts?: {
1568
1575
  auth?: string; headers?: Record<string, string>; transport?: Sbm11Transport; timeoutMs?: number
1569
1576
  }): Sbm11Reader & { evidence(): Sbm11ReadEvidence[]; errors(): string[]; close(): void };
package/index.mjs CHANGED
@@ -107,7 +107,12 @@ export { registerSbm11Source, mapReader };
107
107
  export function lwsSbm11Reader(url, opts = {}) {
108
108
  if (typeof opts.transport === 'function') return lwsSbm11ReaderWith(url, opts);
109
109
  const transport = nodeSyncTransport({ timeoutMs: opts.timeoutMs });
110
- return lwsSbm11ReaderWith(url, { ...opts, transport, onClose: () => transport.close() });
110
+ try {
111
+ return lwsSbm11ReaderWith(url, { ...opts, transport, onClose: () => transport.close() });
112
+ } catch (error) {
113
+ transport.close();
114
+ throw error;
115
+ }
111
116
  }
112
117
 
113
118
  // The persisted store
@@ -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 = "b2be461959a2";
40
+ const WASM_VERSION = "5ca09ab0343e";
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-a9bd50c3b28b-web",
6
- "releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/41040584515",
7
- "gitSha": "a9bd50c3b28b3d42c5071cc8c3e74a5ede8706db",
8
- "builtAt": "2026-10-30T11:09:24Z",
5
+ "release": "danbri/factoidal-builds build-e5057984e388-web",
6
+ "releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/41154673265",
7
+ "gitSha": "e5057984e388be6534fef19cdbb869b5b5652a17",
8
+ "builtAt": "2026-10-31T02:40:08Z",
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": "b2be461959a2ac90ffe8c010b61e70fa4b12f894c2973365904096f41fa6f6ee",
15
- "wasmBytes": 30524277,
14
+ "wasmSha256": "5ca09ab0343eac5cfea0295461af8c4ea963a8bf083afc6f3aeadf1f92ef4804",
15
+ "wasmBytes": 30629431,
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/sbm11-lws.mjs CHANGED
@@ -20,7 +20,11 @@
20
20
  // mixed). The engine checks every byte it decodes against the manifest's
21
21
  // Merkle roots (openSbm11's default `verify: 'merkle'`), so it asks for whole
22
22
  // 65,536-byte chunks and the `.merkle` sidecars; this file does no check.
23
- // Limits: no eviction of the in-memory copies, no retry.
23
+ // Limits: no eviction of the in-memory copies, no retry. Resource paths are
24
+ // confined to the configured container. A custom transport must independently
25
+ // refuse redirects before forwarding headers. Default Node fetch refuses them;
26
+ // default worker XHR accepts no caller headers and checks the final response URL
27
+ // only after sending. XHR may still use ambient same-origin browser cookies.
24
28
  //
25
29
  // EVIDENCE
26
30
  // `evidence()` lists each resource read: { resource, etag, lastModified,
@@ -35,13 +39,41 @@
35
39
  // browser worker it is a synchronous XMLHttpRequest. A browser window
36
40
  // thread has neither: pass `transport` or run the engine in a worker.
37
41
 
42
+ // This reader grants its transport authority only within one HTTP container.
43
+ // A file is a raw relative filename/path, never a URL reference. Reject both
44
+ // literal traversal and encoded structural characters before attaching headers.
38
45
  function containerUrl (url) {
39
- const s = String(url)
40
- return s.endsWith('/') ? s : s + '/'
46
+ let base
47
+ try { base = new URL(String(url)) } catch { throw new TypeError('lwsSbm11Reader: expected an absolute HTTP(S) container URL') }
48
+ if (!['http:', 'https:'].includes(base.protocol) || base.username || base.password || /[?#]/.test(base.href)) {
49
+ throw new TypeError('lwsSbm11Reader: container must be HTTP(S), without userinfo, query or fragment; use opts.auth/headers')
50
+ }
51
+ if (!base.pathname.endsWith('/')) base.pathname += '/'
52
+ return base
41
53
  }
42
54
 
43
55
  function resourceUrl (base, file) {
44
- return new URL(String(file).split('/').map(encodeURIComponent).join('/'), base).href
56
+ if (typeof file !== 'string' || file.length === 0 || file.length > 4096) {
57
+ throw new TypeError('invalid relative resource path')
58
+ }
59
+ let probe = file
60
+ for (;;) {
61
+ if (/^[a-z][a-z0-9+.-]*:/i.test(probe) || /[\\?#\x00-\x1f\x7f]/.test(probe) ||
62
+ probe.split('/').some(part => part === '' || part === '.' || part === '..') ||
63
+ /%(?:2f|5c|3f|23)/i.test(probe)) {
64
+ throw new TypeError('resource path must remain inside the configured container')
65
+ }
66
+ // Decode only for rejection, never for the requested filename. Each changed
67
+ // pass shortens the string, so nested percent encodings are also bounded.
68
+ const next = probe.replace(/%([a-f0-9]{2})/gi, (_match, hex) => String.fromCharCode(parseInt(hex, 16)))
69
+ if (next === probe) break
70
+ probe = next
71
+ }
72
+ const resource = new URL(file.split('/').map(encodeURIComponent).join('/'), base)
73
+ if (resource.origin !== base.origin || !resource.pathname.startsWith(base.pathname)) {
74
+ throw new TypeError('resource URL escaped the configured container')
75
+ }
76
+ return resource.href
45
77
  }
46
78
 
47
79
  /** A synchronous GET with XMLHttpRequest (a browser worker). */
@@ -51,6 +83,9 @@ export function xhrSyncTransport (url, headers) {
51
83
  x.responseType = 'arraybuffer'
52
84
  for (const [k, v] of Object.entries(headers)) x.setRequestHeader(k, v)
53
85
  x.send()
86
+ // XHR follows redirects internally. This rejects returned bytes, not the
87
+ // already-sent request. Caller headers require an explicit trusted transport.
88
+ if (x.responseURL !== new URL(url).href) throw new Error('LWS reader: redirected XHR resource refused')
54
89
  return {
55
90
  status: x.status,
56
91
  header: (name) => x.getResponseHeader(name),
@@ -73,26 +108,27 @@ function unsatisfiedTotal (value) {
73
108
  * A reader ({ readAt, size, evidence, close }) of the SBM11 files held as
74
109
  * LWS resources under the container `url`.
75
110
  *
76
- * @param {string} url the container that holds CURRENT and the generation
111
+ * @param {string} url absolute HTTP(S), no userinfo/query/fragment; container that holds CURRENT and the generation
77
112
  * (or the generation container itself)
78
113
  * @param {object} [opts]
79
- * @param {Function} [opts.transport] the synchronous GET (see above)
114
+ * @param {Function} [opts.transport] trusted synchronous GET; must constrain redirect/header forwarding
80
115
  * @param {string} [opts.auth] a value for the Authorization header, such as
81
116
  * `Bearer <access token>` (an LWS access token)
82
- * @param {object} [opts.headers] further request headers
117
+ * @param {object} [opts.headers] further request headers; browser use requires explicit trusted transport
83
118
  */
84
119
  export function lwsSbm11Reader (url, opts = {}) {
85
120
  const base = containerUrl(url)
121
+ const extra = { ...(opts.headers || {}) }
122
+ if (opts.auth) extra.authorization = String(opts.auth)
86
123
  let transport = opts.transport
87
124
  if (typeof transport !== 'function') {
88
125
  if (typeof XMLHttpRequest === 'function' && typeof document === 'undefined') {
126
+ if (Object.keys(extra).length > 0) throw new TypeError('lwsSbm11Reader: caller headers require an explicit redirect-safe transport; default XHR cannot constrain forwarding')
89
127
  transport = xhrSyncTransport
90
128
  } else {
91
129
  throw new TypeError('lwsSbm11Reader: no synchronous transport here; pass opts.transport (Node: @factoidal/core gives one)')
92
130
  }
93
131
  }
94
- const extra = { ...(opts.headers || {}) }
95
- if (opts.auth) extra.authorization = String(opts.auth)
96
132
  // file -> { url, etag, lastModified, total, whole, requests, partial, bytes }
97
133
  const files = new Map()
98
134
  const errors = []
@@ -100,7 +136,12 @@ export function lwsSbm11Reader (url, opts = {}) {
100
136
  function entry (file) {
101
137
  let e = files.get(file)
102
138
  if (!e) {
103
- e = { url: resourceUrl(base, file), etag: null, lastModified: null, total: undefined, whole: null, requests: 0, partial: 0, bytes: 0 }
139
+ let url
140
+ try { url = resourceUrl(base, file) } catch (error) {
141
+ fail(file, error.message)
142
+ return null
143
+ }
144
+ e = { url, etag: null, lastModified: null, total: undefined, whole: null, requests: 0, partial: 0, bytes: 0 }
104
145
  files.set(file, e)
105
146
  }
106
147
  return e
@@ -136,6 +177,7 @@ export function lwsSbm11Reader (url, opts = {}) {
136
177
 
137
178
  function sizeOf (file) {
138
179
  const e = entry(file)
180
+ if (e === null) return null
139
181
  if (e.total !== undefined) return e.total
140
182
  const r = get(e, file, 0, 0)
141
183
  if (r === null) return null
@@ -161,6 +203,7 @@ export function lwsSbm11Reader (url, opts = {}) {
161
203
  return {
162
204
  readAt (file, offset, length) {
163
205
  const e = entry(file)
206
+ if (e === null) return null
164
207
  if (length === 0) return new Uint8Array(0)
165
208
  if (e.whole !== null) {
166
209
  return offset + length <= e.whole.length ? e.whole.subarray(offset, offset + length) : null
@@ -3,7 +3,8 @@
3
3
  // The engine is inside WebAssembly and asks for bytes synchronously; Node's
4
4
  // fetch is asynchronous. A worker thread makes the fetch; this thread waits
5
5
  // on a shared Int32Array (Atomics.wait) and takes the answer with
6
- // receiveMessageOnPort. Pure I/O: no header is read here, no status judged.
6
+ // receiveMessageOnPort. Redirects are refused before forwarding scoped headers.
7
+ // No RDF or response-header decision is made here.
7
8
  //
8
9
  // The thread that calls the transport is blocked until the answer comes, so
9
10
  // the server must not run on that thread (start it in another process or
@@ -18,7 +19,7 @@ const flag = new Int32Array(signal)
18
19
  port.on('message', async (req) => {
19
20
  let out
20
21
  try {
21
- const r = await fetch(req.url, { headers: req.headers, redirect: 'follow' })
22
+ const r = await fetch(req.url, { headers: req.headers, redirect: 'error' })
22
23
  const headers = {}
23
24
  r.headers.forEach((v, k) => { headers[k] = v })
24
25
  out = { status: r.status, headers, bytes: new Uint8Array(await r.arrayBuffer()) }
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.27.0';
5
+ export const version = '0.28.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factoidal/core",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "EXPERIMENTAL UNREVIEWED 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",