@factoidal/core 0.16.0 → 0.17.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 +13 -0
- package/README.md +3 -2
- package/authz/server.mjs +17 -3
- package/bin/engine.mjs +12 -10
- package/bin/factoidal.mjs +264 -22
- package/bin/pack-host.mjs +31 -1
- package/index.d.ts +9 -8
- package/l4-assets/l4factoidal.js +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +6 -6
- package/lib/api.mjs +8 -4
- package/lib/store.mjs +10 -8
- package/lib/version.mjs +1 -1
- package/lws/server.mjs +34 -3
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.17.0 (2026-10-27)
|
|
6
|
+
|
|
7
|
+
- **Breaking:** `canonicalHash(graph)` now returns the SHA-256 (64 lowercase hex digits) of the RDFC-1.0 canonical N-Quads, as the RDF Dataset Canonicalization specification uses it. Before, it returned the canonical text. Use `canonicalize()` for the text. The engine computes the digest in Lean (new op `datasetCanonicalHash`) (https://github.com/danbri/factoidal/issues/1040, https://github.com/danbri/factoidal-core/pull/127).
|
|
8
|
+
- SPARQL string functions follow SPARQL 1.1 §17.4.3 argument types: STRLEN, SUBSTR, UCASE, LCASE, STRSTARTS, STRENDS, CONTAINS, ENCODE_FOR_URI, MD5 and SHA* give a type error on a number, boolean or IRI argument (MD5/SHA* also on a language-tagged literal). STRSTARTS/STRENDS/CONTAINS check argument compatibility. SUBSTR follows `fn:substring`: `SUBSTR("12345", 0, 3)` is now `"12"` (was `"123"`) (https://github.com/danbri/factoidal/issues/1036, https://github.com/danbri/factoidal-core/pull/126).
|
|
9
|
+
- `factoidal pack --layout sbm11` writes an SBM11 generation: the same files as `l4sbm11-pack`, byte for byte (605 of 605 W3C census inputs). This layout also reads RDF/XML and JSON-LD. `factoidal activate`, `inspect` and `query` read SBM11 stores. `activate` checks the length, SHA-256 and Merkle root of every file before it moves `CURRENT`. `serve` does not read SBM11 stores yet. IBK5 stays the default layout. New wasm ops `storeLayout` and `sbm11Verify` (https://github.com/danbri/factoidal/issues/1009, https://github.com/danbri/factoidal-core/pull/131).
|
|
10
|
+
- Store open in Node is faster: on the londat store (142,314 quads, 1,046 blocks) `openStoreHandle` takes 562 ms, not 1485 ms (median of 7 runs, Apple M1, Node 22). The engine decodes the hexadecimal manifest over bytes, reads `JSON.stringify` arguments in place, verifies artifacts with HACL* SHA-256 in `storeOpen`, and decodes the arguments once per call. What remains is the manifest parse, which grows with the number of blocks; an SBM11 store opens in about 5 ms (https://github.com/danbri/factoidal/issues/1039, https://github.com/danbri/factoidal-core/pull/135).
|
|
11
|
+
- The chunked Turtle reader (the default `l4sbm11-pack` path and the source layer's split read) reads a subject such as `prefix:Atto`, `base:x` or `version:v` as a prefixed name. Before, it took the name for a `PREFIX`/`BASE`/`VERSION` directive and refused the document (QUDT-all-in-one-OWL.ttl) (https://github.com/danbri/factoidal/issues/1028, https://github.com/danbri/factoidal-core/pull/128).
|
|
12
|
+
- RML: a reference to a SPARQL result binding (`formats:SPARQL_Results_CSV`) keeps the literal's datatype when the term map states none. `"5"^^xsd:integer` gives `"5"^^xsd:integer`, not `"5"` (https://github.com/danbri/factoidal/issues/1034, https://github.com/danbri/factoidal-core/pull/129).
|
|
13
|
+
- Tests: 996 Apache Jena ARQ expression tests run as SPARQL on the package (`test/jena-arq-expr.test.mjs`). Each case that differs from Jena has a verdict: a Factoidal defect with an open issue (https://github.com/danbri/factoidal/issues/1041, https://github.com/danbri/factoidal/issues/1042, https://github.com/danbri/factoidal/issues/573), or a Jena extension or difference from the SPARQL specification (https://github.com/danbri/factoidal-core/pull/132). Graph names that are blank nodes: `test/graph-names-876.test.mjs` (https://github.com/danbri/factoidal/issues/876 part 2, fixed in 0.16.0 by https://github.com/danbri/factoidal-core/pull/75; https://github.com/danbri/factoidal-core/pull/130).
|
|
14
|
+
- Tests and release process: no npm test skips when the wasm lacks an op; each test file asserts the ops it needs, so a wasm without an op fails. The `serve --tdb2` test, which needs Apache Jena, moved to `npm run test:jena`. Core PR CI runs `tests/dataflow/run.sh` and `npm test`. `tools/npm-release-check.mjs` checks the version in package.json, package-lock.json and lib/version.mjs, and a dated changelog heading; the publish workflow runs it. `tools/fetch-builds.sh` warns when the installed release was built from a commit whose source differs from HEAD; `--strict` fails (https://github.com/danbri/factoidal/issues/1038, https://github.com/danbri/factoidal-core/pull/124, https://github.com/danbri/factoidal-core/pull/125).
|
|
15
|
+
- LWS: `factoidal lws-serve DIR --authz ASDIR` serves the storage and an authorization server on one port, the issuer the storage root without its trailing slash. The token endpoint and the JWK Set are under the storage root's `.well-known/` (`lws-token`, `lws-jwks`), so no storage resource name is taken from clients; the RFC 8414 metadata is at `/.well-known/lws-configuration` + the issuer's path. The engine decides which requests are the authorization server's (new op `authzOwns`, Lean `L4Factoidal/Authz/Mount.lean`, theorem `endpointOf?_mem`); the storage trusts the issuer with the JWK Set the engine publishes. JS: `listen({ authz: { dir, ... } })` from `@factoidal/core/lws/server`. `authzOpen` takes `mountedOn`, `tokenPath` and `jwksPath`, and answers `resources` and `jwks`.
|
|
16
|
+
- LWS and the authorization server read both forms of their well-known paths: the whole path (a client that reaches the server directly) and the path with the storage root's or issuer's path removed (a reverse proxy that serves the server at a path prefix and removes the prefix, as the router of Touchstone's `targetservers` does). Up to 0.16.0 the protected resource metadata named the wrong resource behind such a proxy (`L4Factoidal.LWS.resourceOfMetadataPath`). Test: `tests/authz/mounted.mjs`. Docker target for third-party testing: `deploy/lws-target/`.
|
|
17
|
+
|
|
5
18
|
## 0.16.0 (2026-10-26)
|
|
6
19
|
|
|
7
20
|
- Node settings in the evidence (https://github.com/danbri/factoidal/issues/975). The params of `node()` are the node's settings. With a wasm module that has the new op `pipelineCheckNodes` (Lean `Wasm/Ops/PipelineNodes.lean`, `Dataflow/NodeSettings.lean`), each evidence record carries `settings` (canonical: keys sorted, defaults of absent settings that narrow the contract such as `lenient: false` and `entail: 'none'`, a Dataset value as `{nquadsSha256}`) and `settingsDigest` (`sha256:` and the hex SHA-256 of the canonical text). A setting outside the proved domain of a proved stage (a lenient reader, an evaluator with `entail` other than `none` or with `output`, a key the stage's settings table does not name) makes the node's status `unproved`, with `registryStatus` and `outside` (each setting and the reason) in the record, so `run()` reports `proved: false`. A reader's `format` must name the stage's syntax (`ttl12` selects RDF 1.2 mode, reported as `derived.mode`); an evaluator's query must parse, else `node()` rejects. Design record: `docs/designissues/2026-10-25-pipeline-settings-and-provenance.md`.
|
package/README.md
CHANGED
|
@@ -8,14 +8,15 @@ npm install @factoidal/core
|
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
```js
|
|
11
|
-
import { parse, csvwToRdf } from "@factoidal/core";
|
|
11
|
+
import { parse, csvwToRdf, canonicalHash } from "@factoidal/core";
|
|
12
12
|
|
|
13
13
|
const ds = await parse('<http://x/a> <http://x/n> "A" .', { format: "ntriples" });
|
|
14
14
|
const rows = await ds.query("SELECT ?n WHERE { ?s <http://x/n> ?n }");
|
|
15
15
|
console.log(rows[0].get("n").value); // A
|
|
16
16
|
console.log(ds.toString("turtle"));
|
|
17
17
|
console.log(await ds.serialize({ format: "rdfxml" }));
|
|
18
|
-
console.log(await ds.canonicalize()); // RDFC-1.0
|
|
18
|
+
console.log(await ds.canonicalize()); // RDFC-1.0 canonical N-Quads
|
|
19
|
+
console.log(await canonicalHash(ds)); // its SHA-256, 64 hex digits
|
|
19
20
|
|
|
20
21
|
const table = await csvwToRdf("id,name\n1,a\n", "", { mode: "minimal", url: "http://x/t.csv" });
|
|
21
22
|
console.log(table.size); // 2
|
package/authz/server.mjs
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// authzFetched [handle, { url, status, body, now } | { host, addresses, now }] -> { ok }
|
|
23
23
|
// authzKeys [handle, { nonce?, private? }] -> { ok, keys } | { ok, jwks }
|
|
24
24
|
// authzRotate [handle, { now, random }] -> { ok, changed, kid }
|
|
25
|
+
// authzOwns [handle, target] -> { ok, owns }
|
|
25
26
|
// authzClose [handle] -> { ok }
|
|
26
27
|
//
|
|
27
28
|
// FETCHES. `resolve` names a host: this host resolves it (`dns.lookup`, every
|
|
@@ -38,7 +39,7 @@ import { loadEngine } from '../bin/engine.mjs'
|
|
|
38
39
|
import { storageKeyText } from '../lws/persist.mjs'
|
|
39
40
|
|
|
40
41
|
/** The ops this host needs from the engine. */
|
|
41
|
-
export const AUTHZ_OPS = ['authzOpen', 'authzStep', 'authzFetched', 'authzKeys', 'authzRotate', 'authzClose']
|
|
42
|
+
export const AUTHZ_OPS = ['authzOpen', 'authzStep', 'authzFetched', 'authzKeys', 'authzRotate', 'authzOwns', 'authzClose']
|
|
42
43
|
|
|
43
44
|
/** The key file in DIR. */
|
|
44
45
|
export const KEY_FILE = 'authz-keys.json'
|
|
@@ -174,12 +175,16 @@ export async function openAuthorizationServer (options = {}) {
|
|
|
174
175
|
const engine = options.engine !== undefined ? options.engine : await loadEngine()
|
|
175
176
|
const dir = typeof options.dir === 'string' ? options.dir : null
|
|
176
177
|
const config = {
|
|
177
|
-
issuer: options.issuer,
|
|
178
178
|
resources: Array.isArray(options.resources) ? options.resources : [],
|
|
179
179
|
now: nowSeconds(),
|
|
180
180
|
entropy: randomBase64(64)
|
|
181
181
|
}
|
|
182
|
-
|
|
182
|
+
// `mountedOn` (an LWS storage root that shares this issuer) or `issuer`:
|
|
183
|
+
// passed unread; the engine derives the issuer, the resource and the
|
|
184
|
+
// endpoint paths from `mountedOn` (L4Factoidal/Authz/Mount.lean).
|
|
185
|
+
if (typeof options.mountedOn === 'string') config.mountedOn = options.mountedOn
|
|
186
|
+
else config.issuer = options.issuer
|
|
187
|
+
for (const name of ['alg', 'tokenLifetime', 'leeway', 'rotateAfter', 'docTtl', 'tokenPath', 'jwksPath', 'audience']) {
|
|
183
188
|
if (options[name] !== undefined) config[name] = options[name]
|
|
184
189
|
}
|
|
185
190
|
if (options.allowHttp === true) config.allowHttp = true
|
|
@@ -208,6 +213,15 @@ export async function openAuthorizationServer (options = {}) {
|
|
|
208
213
|
engine,
|
|
209
214
|
handle,
|
|
210
215
|
issuer: opened.issuer,
|
|
216
|
+
/** The resources tokens may be issued for, as the engine set them. */
|
|
217
|
+
resources: opened.resources,
|
|
218
|
+
/** The published JWK Set when the handle was opened. */
|
|
219
|
+
jwks: opened.jwks,
|
|
220
|
+
/** Whether a request target is this server's (the engine decides:
|
|
221
|
+
* L4Factoidal.Authz.owns). */
|
|
222
|
+
owns (target) {
|
|
223
|
+
return engine.call('authzOwns', [handle, String(target)]).owns === true
|
|
224
|
+
},
|
|
211
225
|
/**
|
|
212
226
|
* One request.
|
|
213
227
|
* @param {{method: string, target: string, headers: Array<[string, string]>, body: string}} request
|
package/bin/engine.mjs
CHANGED
|
@@ -120,13 +120,8 @@ export function loadEngine (name = 'l4factoidal') {
|
|
|
120
120
|
return promise
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
for (let byte = 0; byte < 256; byte += 1) {
|
|
126
|
-
table[byte] = byte.toString(16).padStart(2, '0')
|
|
127
|
-
}
|
|
128
|
-
return table
|
|
129
|
-
})()
|
|
123
|
+
const HEX_DIGIT = Uint8Array.from('0123456789abcdef', (c) => c.charCodeAt(0))
|
|
124
|
+
const HEX_DECODER = new TextDecoder()
|
|
130
125
|
|
|
131
126
|
/**
|
|
132
127
|
* Lowercase hexadecimal of a byte array. This is a transport encoding for
|
|
@@ -137,7 +132,14 @@ const HEX = (() => {
|
|
|
137
132
|
* @returns {string}
|
|
138
133
|
*/
|
|
139
134
|
export function hexOfBytes (bytes) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
135
|
+
// One byte buffer of ASCII digits, decoded once: about 20 times faster
|
|
136
|
+
// than joining one string per byte for a 753 kB store manifest
|
|
137
|
+
// (https://github.com/danbri/factoidal/issues/1039).
|
|
138
|
+
const out = new Uint8Array(bytes.length * 2)
|
|
139
|
+
for (let index = 0, at = 0; index < bytes.length; index += 1, at += 2) {
|
|
140
|
+
const byte = bytes[index]
|
|
141
|
+
out[at] = HEX_DIGIT[byte >> 4]
|
|
142
|
+
out[at + 1] = HEX_DIGIT[byte & 15]
|
|
143
|
+
}
|
|
144
|
+
return HEX_DECODER.decode(out)
|
|
143
145
|
}
|
package/bin/factoidal.mjs
CHANGED
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
STACK_REMEDY, StoreOperationError, inspectManifest, openStore, planQuery,
|
|
36
36
|
queryStore, stackLimitAdvice, turtleOfNQuads
|
|
37
37
|
} from './store.mjs'
|
|
38
|
+
import { sbm11Api, setDefaultSbm11Reader } from '../lib/sbm11.mjs'
|
|
39
|
+
import { nodeDefaultReaderFactory } from '../lib/sbm11-node.mjs'
|
|
38
40
|
|
|
39
41
|
const EXIT_OK = 0
|
|
40
42
|
const EXIT_FAILURE = 1
|
|
@@ -45,7 +47,12 @@ const EXIT_NOT_WIRED = 3
|
|
|
45
47
|
// a time, so this is the window that catches one feed per report.
|
|
46
48
|
const FEED_PROGRESS = 65536
|
|
47
49
|
|
|
48
|
-
const PACK_LAYOUTS = ['ibk3', 'ibk4', 'ibk5']
|
|
50
|
+
const PACK_LAYOUTS = ['ibk3', 'ibk4', 'ibk5', 'sbm11']
|
|
51
|
+
// https://github.com/danbri/factoidal/issues/1009: an SBM11 generation is
|
|
52
|
+
// packed by the wasm op sbm11PackFiles, verified by sbm11Verify and queried
|
|
53
|
+
// by sbm11Open / sbm11Query (formal/lean4/Wasm/Ops/Sbm11*.lean). The op
|
|
54
|
+
// storeLayout says which store a directory holds; this file does not decide
|
|
55
|
+
// it from file names (iron rule 7).
|
|
49
56
|
// The syntaxes the packer reads and the file-name suffixes of each are the
|
|
50
57
|
// engine's tables (op engineTables: packSyntaxes, packSuffixes,
|
|
51
58
|
// packUnsupportedSuffixes), https://github.com/danbri/factoidal/issues/894.
|
|
@@ -226,8 +233,11 @@ normally STORE/gen-N. Packing does not activate: run activate next.
|
|
|
226
233
|
options:
|
|
227
234
|
--layout LAYOUT ibk5 (quads, wire version 10: RDF 1.2 terms,
|
|
228
235
|
out-of-line literals, zone maps; default), ibk4
|
|
229
|
-
(quads, wire version 9)
|
|
230
|
-
|
|
236
|
+
(quads, wire version 9), ibk3 (triples) or sbm11
|
|
237
|
+
(the files l4sbm11-pack writes; the document is read
|
|
238
|
+
whole, in memory; serve does not read it yet)
|
|
239
|
+
--syntax SYNTAX turtle, trig or nquads, and with sbm11 also rdfxml or
|
|
240
|
+
jsonld; default from the file extension
|
|
231
241
|
--chunk-bytes N Merkle chunk size; default is the engine's
|
|
232
242
|
--batch-bytes N ibk4 only: source bytes between publication batches;
|
|
233
243
|
default is the engine's. Blocks are published during
|
|
@@ -248,7 +258,10 @@ usage: factoidal activate STORE GENERATION [--json]
|
|
|
248
258
|
|
|
249
259
|
Verifies every artifact of GENERATION and every cross-artifact relation,
|
|
250
260
|
then atomically replaces STORE/CURRENT. A generation that fails
|
|
251
|
-
verification never becomes current
|
|
261
|
+
verification never becomes current. For an SBM11 generation the engine
|
|
262
|
+
checks the length, SHA-256 and Merkle root of every file and every
|
|
263
|
+
.merkle sidecar against the manifest (op sbm11Verify); it does not run
|
|
264
|
+
the decoder checks of l4sbm11 activate.`,
|
|
252
265
|
|
|
253
266
|
update: `factoidal update - apply a SPARQL Update through the delta log
|
|
254
267
|
|
|
@@ -379,8 +392,10 @@ private keys (with d): for a conformance harness that mints test tokens
|
|
|
379
392
|
|
|
380
393
|
'lws-serve': `factoidal lws-serve - serve Linked Web Storage 1.0 over HTTP
|
|
381
394
|
|
|
382
|
-
usage: factoidal lws-serve DIR (--issuer IRI | --auth-config FILE | --insecure-no-auth)
|
|
395
|
+
usage: factoidal lws-serve DIR (--authz DIR | --issuer IRI | --auth-config FILE | --insecure-no-auth)
|
|
383
396
|
[--owner WEBID] [--root-acl FILE] [--audience IRI]
|
|
397
|
+
[--alg ES256|EdDSA] [--token-lifetime SECONDS] [--rotate-after SECONDS]
|
|
398
|
+
[--allow-http] [--allow-private-fetch]
|
|
384
399
|
[--port N] [--host ADDRESS] [--base IRI] [--page-size N]
|
|
385
400
|
[--paginate-always] [--moves all|data|off]
|
|
386
401
|
[--no-linkset-put] [--unstable-search-results] [--merkle]
|
|
@@ -494,8 +509,21 @@ It protects the file on disk only (formal/lean4/L4Factoidal/LWS/SnapshotSeal.lea
|
|
|
494
509
|
--no-cors send no CORS fields; a page on another origin cannot read
|
|
495
510
|
any answer
|
|
496
511
|
|
|
497
|
-
Authorization (LWS editor's draft, section Authorization; one of the
|
|
512
|
+
Authorization (LWS editor's draft, section Authorization; one of the four
|
|
498
513
|
is required):
|
|
514
|
+
--authz DIR also serve an authorization server on this port, its
|
|
515
|
+
issuer the storage root without the trailing slash
|
|
516
|
+
(BASE less "/"): RFC 8414 metadata at
|
|
517
|
+
/.well-known/lws-configuration, the token endpoint at
|
|
518
|
+
BASE.well-known/lws-token, the JWK Set at
|
|
519
|
+
BASE.well-known/lws-jwks, signing keys in DIR (as
|
|
520
|
+
authz-serve; --alg, --token-lifetime, --rotate-after,
|
|
521
|
+
--allow-http and --allow-private-fetch as there). The
|
|
522
|
+
storage trusts it, and any --issuer too. Each request
|
|
523
|
+
is the authorization server's when the engine says so
|
|
524
|
+
(L4Factoidal/Authz/Mount.lean); every other is the
|
|
525
|
+
storage's. A proxy may serve BASE at a path prefix and
|
|
526
|
+
remove the prefix: both forms of every path are read.
|
|
499
527
|
--issuer IRI trust this authorization server; several may be
|
|
500
528
|
given separated by commas. Its metadata is read from
|
|
501
529
|
IRI/.well-known/lws-configuration and its keys from
|
|
@@ -750,7 +778,7 @@ const VALUE_OPTIONS = {
|
|
|
750
778
|
serve: new Set(['port', 'host', 'query-path', 'gsp-path', 'file', 'layout', 'tdb2']),
|
|
751
779
|
'lws-serve': new Set(['port', 'host', 'base', 'issuer', 'auth-config', 'owner', 'root-acl',
|
|
752
780
|
'audience', 'page-size', 'moves', 'controller-inbox', 'delivery-limit', 'delivery-window',
|
|
753
|
-
'redirects-max', 'storage', 'cors-origin']),
|
|
781
|
+
'redirects-max', 'storage', 'cors-origin', 'authz', 'alg', 'token-lifetime', 'rotate-after']),
|
|
754
782
|
'solid-serve': new Set(['port', 'host', 'base', 'owner', 'issuer', 'auth-config']),
|
|
755
783
|
'solid-client': new Set(['body', 'file', 'content-type', 'interpret']),
|
|
756
784
|
'lws-client': new Set(['body', 'file', 'content-type', 'slug', 'type', 'link', 'if-match',
|
|
@@ -931,14 +959,154 @@ function printTable (headers, rows) {
|
|
|
931
959
|
for (const row of rows) out(line(row))
|
|
932
960
|
}
|
|
933
961
|
|
|
962
|
+
// --------------------------------------------------------------- SBM11
|
|
963
|
+
|
|
964
|
+
let sbm11 = null
|
|
965
|
+
|
|
966
|
+
/** The SBM11 functions of lib/sbm11.mjs over this command's engine, with
|
|
967
|
+
* the Node directory reader as the default reader (it moves bytes only). */
|
|
968
|
+
function sbm11Functions () {
|
|
969
|
+
if (sbm11 === null) {
|
|
970
|
+
setDefaultSbm11Reader(nodeDefaultReaderFactory())
|
|
971
|
+
sbm11 = sbm11Api(loadEngine)
|
|
972
|
+
}
|
|
973
|
+
return sbm11
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* The engine's answer (op storeLayout) when `source` holds an SBM11
|
|
978
|
+
* generation, or names one through CURRENT: `{layout, generation,
|
|
979
|
+
* manifest}`. null for an IBK store, for a directory the op does not
|
|
980
|
+
* recognise, and for an engine built before the op; the IBK path then
|
|
981
|
+
* runs, and reports a missing store in its own words.
|
|
982
|
+
*/
|
|
983
|
+
function sbm11Layout (engine, source) {
|
|
984
|
+
sbm11Functions()
|
|
985
|
+
let answer
|
|
986
|
+
try {
|
|
987
|
+
answer = engine.call('storeLayout', [source])
|
|
988
|
+
} catch (_error) {
|
|
989
|
+
return null
|
|
990
|
+
}
|
|
991
|
+
return answer && answer.layout === 'sbm11' ? answer : null
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** The directory a command reads: STORE, or STORE/GENERATION when
|
|
995
|
+
* --generation names one. */
|
|
996
|
+
function storeSource (root, named) {
|
|
997
|
+
return named === null ? root : joinPath(root, named)
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/** The engine's words of a refusal, without the loader's prefix. */
|
|
1001
|
+
function engineWords (error) {
|
|
1002
|
+
const message = error && error.message ? String(error.message) : String(error)
|
|
1003
|
+
return message.replace(/^l4factoidal:\s*/, '')
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
async function inspectSbm11 (root, named, layout, options) {
|
|
1007
|
+
let store
|
|
1008
|
+
try {
|
|
1009
|
+
store = await sbm11Functions().openSbm11(storeSource(root, named))
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
err(`factoidal inspect: ${engineWords(error)}`)
|
|
1012
|
+
return EXIT_FAILURE
|
|
1013
|
+
}
|
|
1014
|
+
const terms = store.terms
|
|
1015
|
+
store.close()
|
|
1016
|
+
const generation = named !== null ? named : layout.generation
|
|
1017
|
+
const generationDir = named !== null || layout.generation === ''
|
|
1018
|
+
? storeSource(root, named)
|
|
1019
|
+
: joinPath(root, layout.generation)
|
|
1020
|
+
const files = listGeneration(generationDir)
|
|
1021
|
+
let directoryBytes = 0
|
|
1022
|
+
for (const file of files) directoryBytes += file.size
|
|
1023
|
+
if (options.json) {
|
|
1024
|
+
out(JSON.stringify({
|
|
1025
|
+
ok: true,
|
|
1026
|
+
layout: 'sbm11',
|
|
1027
|
+
generation,
|
|
1028
|
+
activated: named === null && layout.generation !== '',
|
|
1029
|
+
manifest: layout.manifest,
|
|
1030
|
+
terms,
|
|
1031
|
+
files: files.length,
|
|
1032
|
+
bytes: directoryBytes
|
|
1033
|
+
}, null, 2))
|
|
1034
|
+
return EXIT_OK
|
|
1035
|
+
}
|
|
1036
|
+
out(`store ${root}`)
|
|
1037
|
+
if (named !== null) out(`generation ${generation} (named on the command line)`)
|
|
1038
|
+
else if (layout.generation !== '') out(`generation ${generation} (activated through CURRENT)`)
|
|
1039
|
+
else out('generation directory (no CURRENT)')
|
|
1040
|
+
out(`manifest ${layout.manifest}`)
|
|
1041
|
+
out('layout sbm11')
|
|
1042
|
+
out(plural(terms, 'term'))
|
|
1043
|
+
out(`generation directory holds ${files.length} files, ${directoryBytes} bytes`)
|
|
1044
|
+
return EXIT_OK
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Answer a query on an SBM11 store (ops sbm11Open / sbm11Query /
|
|
1049
|
+
* sbm11Close) and print it as `query` prints an IBK answer. The engine
|
|
1050
|
+
* answers SPARQL Query Results JSON for SELECT and ASK and N-Triples for
|
|
1051
|
+
* CONSTRUCT and DESCRIBE; this only chooses which renderer prints it.
|
|
1052
|
+
*/
|
|
1053
|
+
async function querySbm11 (engine, root, named, sparql, format, limit, options) {
|
|
1054
|
+
const quiet = options.quiet === true
|
|
1055
|
+
if (options.explain === true) {
|
|
1056
|
+
err('factoidal query: --explain prints the plan of an IBK store; there is ' +
|
|
1057
|
+
'no plan operation for an SBM11 store yet ' +
|
|
1058
|
+
'(https://github.com/danbri/factoidal/issues/1009)')
|
|
1059
|
+
return EXIT_FAILURE
|
|
1060
|
+
}
|
|
1061
|
+
let answer
|
|
1062
|
+
try {
|
|
1063
|
+
const store = await sbm11Functions().openSbm11(storeSource(root, named))
|
|
1064
|
+
try {
|
|
1065
|
+
const got = store.query(sparql)
|
|
1066
|
+
// read the bytes while the handle is open
|
|
1067
|
+
answer = { path: got.path, format: got.format, reads: got.reads,
|
|
1068
|
+
bytesRead: got.bytesRead, text: got.result }
|
|
1069
|
+
} finally {
|
|
1070
|
+
store.close()
|
|
1071
|
+
}
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
err(`factoidal query: ${engineWords(error)}`)
|
|
1074
|
+
if (isStackOverflow(error)) {
|
|
1075
|
+
for (const line of stackLimitAdvice(STACK_REMEDY.query)) err(line)
|
|
1076
|
+
}
|
|
1077
|
+
return EXIT_FAILURE
|
|
1078
|
+
}
|
|
1079
|
+
if (!quiet) {
|
|
1080
|
+
err(`${answer.path}, ${plural(answer.reads, 'read')}, ${answer.bytesRead} bytes read`)
|
|
1081
|
+
}
|
|
1082
|
+
let result
|
|
1083
|
+
if (answer.format === 'srj') {
|
|
1084
|
+
const document = JSON.parse(answer.text)
|
|
1085
|
+
if (typeof document.boolean === 'boolean') {
|
|
1086
|
+
if (format === 'json') {
|
|
1087
|
+
out(JSON.stringify(document, null, 2))
|
|
1088
|
+
return EXIT_OK
|
|
1089
|
+
}
|
|
1090
|
+
result = { kind: 'ask', boolean: document.boolean }
|
|
1091
|
+
} else {
|
|
1092
|
+
result = { kind: 'select', srj: document }
|
|
1093
|
+
}
|
|
1094
|
+
} else {
|
|
1095
|
+
result = { kind: 'construct', nquads: answer.text }
|
|
1096
|
+
}
|
|
1097
|
+
return renderQueryResult(engine, result, format, limit, quiet)
|
|
1098
|
+
}
|
|
1099
|
+
|
|
934
1100
|
// ------------------------------------------------------------- inspect
|
|
935
1101
|
|
|
936
1102
|
async function commandInspect (positional, options) {
|
|
937
1103
|
if (positional.length !== 1) throw new UsageError('inspect needs exactly one STORE')
|
|
938
1104
|
const root = positional[0]
|
|
939
1105
|
const named = typeof options.generation === 'string' ? options.generation : null
|
|
940
|
-
const store = openStore(root, named)
|
|
941
1106
|
const engine = await loadEngine()
|
|
1107
|
+
const layout = sbm11Layout(engine, storeSource(root, named))
|
|
1108
|
+
if (layout !== null) return inspectSbm11(root, named, layout, options)
|
|
1109
|
+
const store = openStore(root, named)
|
|
942
1110
|
const envelope = inspectManifest(engine, store)
|
|
943
1111
|
|
|
944
1112
|
if (options.json) {
|
|
@@ -1102,8 +1270,11 @@ async function commandQuery (positional, options) {
|
|
|
1102
1270
|
const quiet = options.quiet === true
|
|
1103
1271
|
|
|
1104
1272
|
const named = typeof options.generation === 'string' ? options.generation : null
|
|
1105
|
-
const store = openStore(root, named)
|
|
1106
1273
|
const engine = await loadEngine()
|
|
1274
|
+
if (sbm11Layout(engine, storeSource(root, named)) !== null) {
|
|
1275
|
+
return querySbm11(engine, root, named, sparql, format, limit, options)
|
|
1276
|
+
}
|
|
1277
|
+
const store = openStore(root, named)
|
|
1107
1278
|
|
|
1108
1279
|
if (options.explain === true) {
|
|
1109
1280
|
let plan
|
|
@@ -1208,9 +1379,15 @@ function readHead (path, length) {
|
|
|
1208
1379
|
/** The syntax tag for an input: --syntax when given, else the engine's
|
|
1209
1380
|
* suffix table (op engineTables), else the engine's content detection
|
|
1210
1381
|
* (op detectFormat) on the first 64 KiB of the file. */
|
|
1211
|
-
function packSyntax (engine, input, options) {
|
|
1382
|
+
function packSyntax (engine, input, options, layout) {
|
|
1212
1383
|
const tables = engine.call('engineTables', []).tables
|
|
1213
|
-
|
|
1384
|
+
// The SBM11 pack reads the whole document (op sbm11PackFiles), so it also
|
|
1385
|
+
// reads the syntaxes of packWholeSuffixes (RDF/XML, JSON-LD).
|
|
1386
|
+
const whole = layout === 'sbm11' ? (tables.packWholeSuffixes ?? {}) : {}
|
|
1387
|
+
const syntaxes = tables.packSyntaxes.slice()
|
|
1388
|
+
for (const syntax of Object.values(whole)) {
|
|
1389
|
+
if (syntaxes.indexOf(syntax) < 0) syntaxes.push(syntax)
|
|
1390
|
+
}
|
|
1214
1391
|
if (typeof options.syntax === 'string') {
|
|
1215
1392
|
const syntax = options.syntax.toLowerCase()
|
|
1216
1393
|
if (syntaxes.indexOf(syntax) >= 0) return syntax
|
|
@@ -1220,11 +1397,14 @@ function packSyntax (engine, input, options) {
|
|
|
1220
1397
|
for (const [suffix, syntax] of Object.entries(tables.packSuffixes)) {
|
|
1221
1398
|
if (lower.endsWith(suffix)) return syntax
|
|
1222
1399
|
}
|
|
1400
|
+
for (const [suffix, syntax] of Object.entries(whole)) {
|
|
1401
|
+
if (lower.endsWith(suffix)) return syntax
|
|
1402
|
+
}
|
|
1223
1403
|
for (const [suffix, name] of Object.entries(tables.packUnsupportedSuffixes)) {
|
|
1224
1404
|
if (lower.endsWith(suffix)) {
|
|
1225
1405
|
throw new UsageError(
|
|
1226
|
-
`pack does not read ${name}. The packer
|
|
1227
|
-
`${syntaxes.join(', ')} only. Convert the file first, for ` +
|
|
1406
|
+
`pack does not read ${name}. The packer reads ` +
|
|
1407
|
+
`${syntaxes.join(', ')} only${layout === 'sbm11' ? '' : ' (--layout sbm11 also reads rdfxml and jsonld)'}. Convert the file first, for ` +
|
|
1228
1408
|
"example with: factoidal parse FILE --out nquads")
|
|
1229
1409
|
}
|
|
1230
1410
|
}
|
|
@@ -1253,8 +1433,11 @@ function packSyntax (engine, input, options) {
|
|
|
1253
1433
|
* `--base ''` asks for no base, which turns a relative IRI into a parse
|
|
1254
1434
|
* error rather than a silently different term.
|
|
1255
1435
|
*/
|
|
1256
|
-
function packBase (input, options) {
|
|
1436
|
+
function packBase (input, options, layout) {
|
|
1257
1437
|
if (typeof options.base === 'string') return options.base
|
|
1438
|
+
// `l4sbm11-pack` uses no base unless --base names one; the same default
|
|
1439
|
+
// gives the same bytes.
|
|
1440
|
+
if (layout === 'sbm11') return ''
|
|
1258
1441
|
const absolute = input.startsWith('/') ? input : joinPath(currentDirectory(), input)
|
|
1259
1442
|
return 'file://' + absolute
|
|
1260
1443
|
}
|
|
@@ -1275,8 +1458,8 @@ function packLayout (options) {
|
|
|
1275
1458
|
async function commandPack (positional, options) {
|
|
1276
1459
|
if (positional.length !== 2) throw new UsageError('pack needs INPUT and OUTPUT')
|
|
1277
1460
|
const [input, output] = positional
|
|
1278
|
-
const syntax = packSyntax(await loadEngine(), input, options)
|
|
1279
1461
|
const layout = packLayout(options)
|
|
1462
|
+
const syntax = packSyntax(await loadEngine(), input, options, layout)
|
|
1280
1463
|
// The pack fold needs a bigger call stack than either runtime gives by
|
|
1281
1464
|
// default (https://github.com/danbri/factoidal/issues/649). Under Node
|
|
1282
1465
|
// the work runs on a worker thread with a raised stack; under Deno the
|
|
@@ -1291,8 +1474,10 @@ async function commandPack (positional, options) {
|
|
|
1291
1474
|
let answer
|
|
1292
1475
|
try {
|
|
1293
1476
|
answer = await runPack(
|
|
1294
|
-
|
|
1295
|
-
|
|
1477
|
+
layout === 'sbm11'
|
|
1478
|
+
? { kind: 'packSbm11', input, output, syntax, base: packBase(input, options, layout) }
|
|
1479
|
+
: { kind: 'pack', input, output, syntax, layout, base: packBase(input, options, layout),
|
|
1480
|
+
batchBytes: options['batch-bytes'] },
|
|
1296
1481
|
quiet
|
|
1297
1482
|
? undefined
|
|
1298
1483
|
: (progress) => {
|
|
@@ -1331,7 +1516,7 @@ async function commandPack (positional, options) {
|
|
|
1331
1516
|
return EXIT_OK
|
|
1332
1517
|
}
|
|
1333
1518
|
out(`packed ${report.bytesRead} bytes of ${syntax} into ${output}`)
|
|
1334
|
-
out(`${report.written.length} artifacts, ${report.bytesWritten} bytes, layout ${layout}`)
|
|
1519
|
+
out(`${report.written.length} ${layout === 'sbm11' ? 'files' : 'artifacts'}, ${report.bytesWritten} bytes, layout ${layout}`)
|
|
1335
1520
|
if (typeof report.rows === 'number') out(`${plural(report.rows, 'row')}`)
|
|
1336
1521
|
out(`activate it with: factoidal activate ${dirOf(output)} ${nameOf(output)}`)
|
|
1337
1522
|
return EXIT_OK
|
|
@@ -1382,6 +1567,10 @@ async function commandActivate (positional, options) {
|
|
|
1382
1567
|
const host = { worker: options['no-worker'] !== true }
|
|
1383
1568
|
const reexec = await denoReexec(host)
|
|
1384
1569
|
if (reexec !== null) return reexec
|
|
1570
|
+
const engine = await loadEngine()
|
|
1571
|
+
if (sbm11Layout(engine, joinPath(root, generation)) !== null) {
|
|
1572
|
+
return activateSbm11(engine, root, generation, options)
|
|
1573
|
+
}
|
|
1385
1574
|
let answer
|
|
1386
1575
|
try {
|
|
1387
1576
|
answer = await runPack({ kind: 'activate', root, generation }, undefined, host)
|
|
@@ -1425,6 +1614,32 @@ async function commandActivate (positional, options) {
|
|
|
1425
1614
|
return EXIT_OK
|
|
1426
1615
|
}
|
|
1427
1616
|
|
|
1617
|
+
/**
|
|
1618
|
+
* Activate an SBM11 generation: the engine checks every file against the
|
|
1619
|
+
* manifest (op sbm11Verify), and only after its "ok" does CURRENT move.
|
|
1620
|
+
*/
|
|
1621
|
+
function activateSbm11 (engine, root, generation, options) {
|
|
1622
|
+
let verdict
|
|
1623
|
+
try {
|
|
1624
|
+
verdict = engine.call('sbm11Verify', [joinPath(root, generation)])
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
err(`factoidal activate: ${engineWords(error)}`)
|
|
1627
|
+
err('The generation is NOT activated; CURRENT is unchanged.')
|
|
1628
|
+
return EXIT_FAILURE
|
|
1629
|
+
}
|
|
1630
|
+
const synced = atomicReplace(joinPath(root, 'CURRENT'), new TextEncoder().encode(generation))
|
|
1631
|
+
if (options.json === true) {
|
|
1632
|
+
out(JSON.stringify({ ...verdict, layout: 'sbm11', generation, directorySynced: synced }, null, 2))
|
|
1633
|
+
return EXIT_OK
|
|
1634
|
+
}
|
|
1635
|
+
out(`activated ${generation}: ${verdict.artifacts} artifacts verified (${verdict.files} files, ${verdict.bytes} bytes), layout sbm11`)
|
|
1636
|
+
if (!synced) {
|
|
1637
|
+
err('CURRENT was replaced, but the directory entry was not synced; a ' +
|
|
1638
|
+
'crash now could lose the pointer update.')
|
|
1639
|
+
}
|
|
1640
|
+
return EXIT_OK
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1428
1643
|
function commandUpdate (positional, options) {
|
|
1429
1644
|
if (positional.length < 1) throw new UsageError('update needs a STORE')
|
|
1430
1645
|
const text = positional.length > 1
|
|
@@ -1529,6 +1744,10 @@ async function commandServe (positional, options) {
|
|
|
1529
1744
|
const { join } = await import('node:path')
|
|
1530
1745
|
const root = mkdtempSync(join(tmpdir(), 'factoidal-serve-'))
|
|
1531
1746
|
const packOptions = { quiet: true, layout: packLayout(options) }
|
|
1747
|
+
if (packOptions.layout === 'sbm11') {
|
|
1748
|
+
throw new UsageError('serve --file packs a store that accepts updates; --layout sbm11 ' +
|
|
1749
|
+
'packs a store that serve does not read yet. Use ibk3, ibk4 or ibk5.')
|
|
1750
|
+
}
|
|
1532
1751
|
let rc = await commandPack([options.file, join(root, 'gen-1')], packOptions)
|
|
1533
1752
|
if (rc !== 0) return rc
|
|
1534
1753
|
rc = await commandActivate([root, 'gen-1'], { quiet: true })
|
|
@@ -1549,6 +1768,12 @@ async function commandServe (positional, options) {
|
|
|
1549
1768
|
options = { ...options, 'query-path': `${dsName}/sparql`, 'gsp-path': `${dsName}/data` }
|
|
1550
1769
|
}
|
|
1551
1770
|
if (positional.length !== 1) throw new UsageError('serve needs one STORE')
|
|
1771
|
+
if (!tdb2 && sbm11Layout(await loadEngine(), positional[0]) !== null) {
|
|
1772
|
+
err(`factoidal serve: ${positional[0]} is an SBM11 store, and serve reads IBK ` +
|
|
1773
|
+
'stores only (the storeServe operations). Query it with: factoidal query ' +
|
|
1774
|
+
`${positional[0]} QUERY (https://github.com/danbri/factoidal/issues/1009)`)
|
|
1775
|
+
return EXIT_FAILURE
|
|
1776
|
+
}
|
|
1552
1777
|
const { listen, listenOnWorker, ServeHostError } = await import('./serve.mjs')
|
|
1553
1778
|
const serveOptions = {
|
|
1554
1779
|
port: servePort(options, 3030),
|
|
@@ -1590,12 +1815,25 @@ async function commandLwsServe (positional, options) {
|
|
|
1590
1815
|
const insecure = options['insecure-no-auth'] === true
|
|
1591
1816
|
const issuerOption = typeof options.issuer === 'string' ? options.issuer : null
|
|
1592
1817
|
const authFile = typeof options['auth-config'] === 'string' ? options['auth-config'] : null
|
|
1593
|
-
|
|
1594
|
-
|
|
1818
|
+
const authzDir = typeof options.authz === 'string' ? options.authz : null
|
|
1819
|
+
if (!insecure && issuerOption === null && authFile === null && authzDir === null) {
|
|
1820
|
+
throw new UsageError('lws-serve needs --authz DIR, --issuer IRI or --auth-config FILE, ' +
|
|
1595
1821
|
'or --insecure-no-auth for development')
|
|
1596
1822
|
}
|
|
1597
|
-
if (insecure && (issuerOption !== null || authFile !== null)) {
|
|
1598
|
-
throw new UsageError('--insecure-no-auth cannot be combined with --issuer or --auth-config')
|
|
1823
|
+
if (insecure && (issuerOption !== null || authFile !== null || authzDir !== null)) {
|
|
1824
|
+
throw new UsageError('--insecure-no-auth cannot be combined with --authz, --issuer or --auth-config')
|
|
1825
|
+
}
|
|
1826
|
+
// --authz DIR: an authorization server on the same port, its issuer the
|
|
1827
|
+
// storage root (L4Factoidal/Authz/Mount.lean). Its options are passed
|
|
1828
|
+
// unread; the engine checks them (Wasm/Ops/Authz.lean).
|
|
1829
|
+
const authz = authzDir === null ? undefined : {
|
|
1830
|
+
dir: authzDir,
|
|
1831
|
+
...(typeof options.alg === 'string' ? { alg: options.alg } : {}),
|
|
1832
|
+
...(options['token-lifetime'] !== undefined ? { tokenLifetime: natOption('token-lifetime', options['token-lifetime']) } : {}),
|
|
1833
|
+
...(options['rotate-after'] !== undefined ? { rotateAfter: natOption('rotate-after', options['rotate-after']) } : {}),
|
|
1834
|
+
...(options['allow-http'] === true ? { allowHttp: true } : {}),
|
|
1835
|
+
...(options['allow-private-fetch'] === true ? { allowPrivateFetch: true } : {}),
|
|
1836
|
+
...corsConfig(options)
|
|
1599
1837
|
}
|
|
1600
1838
|
let auth
|
|
1601
1839
|
if (authFile !== null) {
|
|
@@ -1628,6 +1866,7 @@ async function commandLwsServe (positional, options) {
|
|
|
1628
1866
|
...(typeof options.storage === 'string' ? { storage: options.storage } : {}),
|
|
1629
1867
|
...corsConfig(options),
|
|
1630
1868
|
...(insecure ? { insecureNoAuth: true } : { auth }),
|
|
1869
|
+
...(authz !== undefined ? { authz } : {}),
|
|
1631
1870
|
...(typeof options.owner === 'string' ? { owner: options.owner } : {}),
|
|
1632
1871
|
...(typeof options['root-acl'] === 'string'
|
|
1633
1872
|
? { rootAcl: new TextDecoder('utf-8', { fatal: true }).decode(readWhole(options['root-acl'])) }
|
|
@@ -1651,6 +1890,9 @@ async function commandLwsServe (positional, options) {
|
|
|
1651
1890
|
}
|
|
1652
1891
|
if (!quiet) {
|
|
1653
1892
|
err(`lws-serve: listening on ${running.origin} for ${positional[0]}`)
|
|
1893
|
+
if (running.authz() !== null) err(`lws-serve: authorization server ${running.authz().issuer} (keys in ${authzDir})`)
|
|
1894
|
+
if (options['allow-http'] === true) err('lws-serve: WARNING: --allow-http: http subject identifiers are accepted')
|
|
1895
|
+
if (options['allow-private-fetch'] === true) err('lws-serve: WARNING: --allow-private-fetch: private addresses may be fetched')
|
|
1654
1896
|
if (insecure) err('lws-serve: WARNING: --insecure-no-auth: every request is served without authorization')
|
|
1655
1897
|
}
|
|
1656
1898
|
out(running.origin)
|
package/bin/pack-host.mjs
CHANGED
|
@@ -42,11 +42,12 @@
|
|
|
42
42
|
// route rescues an in-page packer. See the closing section of
|
|
43
43
|
// `docs/designissues/2026-09-03-npm-pack-in-wasm.md`.
|
|
44
44
|
|
|
45
|
-
import { StoreHostError, listGeneration, readWhole } from '../store-host/index.mjs'
|
|
45
|
+
import { StoreHostError, listGeneration, readWhole, writeNew } from '../store-host/index.mjs'
|
|
46
46
|
import { fileUrlToPath, joinPath } from '../store-host/paths.mjs'
|
|
47
47
|
import { loadEngine } from './engine.mjs'
|
|
48
48
|
import { PackError, packFile, packSupported, verifyGeneration } from './pack.mjs'
|
|
49
49
|
import { StoreOperationError, openStore } from './store.mjs'
|
|
50
|
+
import { sbm11Api } from '../lib/sbm11.mjs'
|
|
50
51
|
|
|
51
52
|
const isDeno = typeof globalThis.Deno !== 'undefined'
|
|
52
53
|
|
|
@@ -207,6 +208,7 @@ export function isStackOverflow (error) {
|
|
|
207
208
|
*/
|
|
208
209
|
export async function packHere (task, onProgress) {
|
|
209
210
|
const engine = await loadEngine()
|
|
211
|
+
if (task.kind === 'packSbm11') return await packSbm11Here(engine, task)
|
|
210
212
|
if (!packSupported(engine)) return { notWired: true }
|
|
211
213
|
if (task.kind === 'activate') {
|
|
212
214
|
return { report: activateHere(engine, task) }
|
|
@@ -221,6 +223,34 @@ export async function packHere (task, onProgress) {
|
|
|
221
223
|
return { report }
|
|
222
224
|
}
|
|
223
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Pack one document into the files of one SBM11 generation (op
|
|
228
|
+
* sbm11PackFiles, through lib/sbm11.mjs packSbm11) and write each file
|
|
229
|
+
* under the name the engine gave it. The document is read whole: the op
|
|
230
|
+
* takes the whole document, and refuses one above its limit.
|
|
231
|
+
*
|
|
232
|
+
* @returns {{notWired: true}|{report: object}}
|
|
233
|
+
*/
|
|
234
|
+
async function packSbm11Here (engine, task) {
|
|
235
|
+
const { packSbm11 } = sbm11Api(async () => engine)
|
|
236
|
+
const bytes = readWhole(task.input)
|
|
237
|
+
let files
|
|
238
|
+
try {
|
|
239
|
+
files = await packSbm11(bytes, { syntax: task.syntax, base: task.base })
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error && error.code === 'SBM11_NO_PACK_OP') return { notWired: true }
|
|
242
|
+
throw error
|
|
243
|
+
}
|
|
244
|
+
const written = []
|
|
245
|
+
let bytesWritten = 0
|
|
246
|
+
for (const [name, fileBytes] of files) {
|
|
247
|
+
writeNew(joinPath(task.output, name), fileBytes)
|
|
248
|
+
written.push(name)
|
|
249
|
+
bytesWritten += fileBytes.length
|
|
250
|
+
}
|
|
251
|
+
return { report: { bytesRead: bytes.length, written, bytesWritten } }
|
|
252
|
+
}
|
|
253
|
+
|
|
224
254
|
/**
|
|
225
255
|
* Verify one generation and answer the engine's verdict.
|
|
226
256
|
*
|
package/index.d.ts
CHANGED
|
@@ -529,14 +529,15 @@ export function canonicalize(
|
|
|
529
529
|
export function graphs(dataset: Dataset): Array<[name: NamedNode | BlankNode, graph: Dataset]>;
|
|
530
530
|
|
|
531
531
|
/**
|
|
532
|
-
* RDFC-1.0 canonical
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
532
|
+
* The RDFC-1.0 canonical hash of a single graph: the SHA-256 of its
|
|
533
|
+
* canonical N-Quads document, as 64 lowercase hexadecimal digits (RDF
|
|
534
|
+
* Dataset Canonicalization 1.0, section 4.4 note 2). The engine computes
|
|
535
|
+
* the canonical form and the digest. Every quad's graph component is
|
|
536
|
+
* dropped first, so isomorphic graphs (including under blank-node
|
|
537
|
+
* relabeling) give the same hash whatever graph name they were extracted
|
|
538
|
+
* from. Typically called with one entry of graphs()'s output. For the
|
|
539
|
+
* canonical text, use canonicalize(). Rejects when the input holds an
|
|
540
|
+
* RDF 1.2 triple term or the section 4.4 call budget is exceeded.
|
|
540
541
|
*/
|
|
541
542
|
export function canonicalHash(datasetOrGraph: Dataset): Promise<string>;
|
|
542
543
|
|
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-10-
|
|
5
|
+
"release": "danbri/factoidal-builds build-fa72ba5bc828-web",
|
|
6
|
+
"releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/40439252690",
|
|
7
|
+
"gitSha": "fa72ba5bc828c1eb75572c3436e9d739f76fdd57",
|
|
8
|
+
"builtAt": "2026-10-27T03:13: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": "ebac38a482d03d4a6ce1cf4a59164288628fed6957483dd55180743e7a3daf40",
|
|
15
|
+
"wasmBytes": 17071472,
|
|
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/api.mjs
CHANGED
|
@@ -884,9 +884,12 @@ function buildApi(driver) {
|
|
|
884
884
|
}
|
|
885
885
|
|
|
886
886
|
/**
|
|
887
|
-
* RDFC-1.0 canonical
|
|
888
|
-
*
|
|
889
|
-
*
|
|
887
|
+
* The RDFC-1.0 canonical hash of one graph: the SHA-256, as 64 lowercase
|
|
888
|
+
* hexadecimal digits, of the canonical N-Quads document (RDF Dataset
|
|
889
|
+
* Canonicalization 1.0, section 4.4 note 2). Every quad's graph name is
|
|
890
|
+
* dropped first (op datasetGraphAsDefault with the union of the graphs);
|
|
891
|
+
* then op datasetCanonicalHash computes the canonical form and the digest
|
|
892
|
+
* in Lean. canonicalize() answers the canonical text.
|
|
890
893
|
* @param {Dataset} datasetOrGraph
|
|
891
894
|
* @returns {Promise<string>}
|
|
892
895
|
*/
|
|
@@ -897,10 +900,11 @@ function buildApi(driver) {
|
|
|
897
900
|
datasetOrGraph.size; // throws the disposed error
|
|
898
901
|
const c = datasetOrGraph._ctx;
|
|
899
902
|
requireEntryFn(c.e, 'datasetGraphAsDefault', 'canonicalHash');
|
|
903
|
+
requireEntryFn(c.e, 'datasetCanonicalHash', 'canonicalHash');
|
|
900
904
|
const merged = c.wrap(entryResult(c.e.datasetGraphAsDefault(datasetOrGraph.handle,
|
|
901
905
|
JSON.stringify({ type: 'union' })), 'canonicalHash'), {});
|
|
902
906
|
try {
|
|
903
|
-
return merged.
|
|
907
|
+
return entryResult(c.e.datasetCanonicalHash(merged.handle), 'canonicalHash').hash;
|
|
904
908
|
} finally {
|
|
905
909
|
merged.dispose();
|
|
906
910
|
}
|
package/lib/store.mjs
CHANGED
|
@@ -99,16 +99,18 @@ export function manifestKeys(inspectEnvelope) {
|
|
|
99
99
|
return keys;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
for (let b = 0; b < 256; b += 1) t[b] = b.toString(16).padStart(2, '0');
|
|
105
|
-
return t;
|
|
106
|
-
})();
|
|
102
|
+
const HEX_DIGIT = Uint8Array.from('0123456789abcdef', (c) => c.charCodeAt(0));
|
|
103
|
+
const HEX_DECODER = new TextDecoder();
|
|
107
104
|
|
|
108
105
|
/** Lowercase hexadecimal: the transport the manifest argument of the store
|
|
109
106
|
* ops takes. Artifact bytes never take this path. */
|
|
110
107
|
export function hexOfBytes(bytes) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
// One byte buffer of ASCII digits, decoded once (issue 1039; see
|
|
109
|
+
// bin/engine.mjs hexOfBytes).
|
|
110
|
+
const out = new Uint8Array(bytes.length * 2);
|
|
111
|
+
for (let i = 0, at = 0; i < bytes.length; i += 1, at += 2) {
|
|
112
|
+
out[at] = HEX_DIGIT[bytes[i] >> 4];
|
|
113
|
+
out[at + 1] = HEX_DIGIT[bytes[i] & 15];
|
|
114
|
+
}
|
|
115
|
+
return HEX_DECODER.decode(out);
|
|
114
116
|
}
|
package/lib/version.mjs
CHANGED
package/lws/server.mjs
CHANGED
|
@@ -584,11 +584,33 @@ export async function createLwsServer (options = {}) {
|
|
|
584
584
|
let settings = { ...options }
|
|
585
585
|
let opening = null
|
|
586
586
|
let opened = null
|
|
587
|
+
let authz = null
|
|
587
588
|
|
|
588
|
-
/** The storage handle, opened once.
|
|
589
|
+
/** The storage handle, opened once. With `authz` (an object of
|
|
590
|
+
* `openAuthorizationServer` options), an authorization server that shares
|
|
591
|
+
* the storage's issuer is opened first, on `mountedOn: baseIri`; the
|
|
592
|
+
* storage then trusts that issuer with the JWK Set the engine published
|
|
593
|
+
* (L4Factoidal/Authz/Mount.lean). */
|
|
589
594
|
function storage () {
|
|
590
595
|
if (opening === null) {
|
|
591
|
-
opening =
|
|
596
|
+
opening = (async () => {
|
|
597
|
+
let storageSettings = settings
|
|
598
|
+
if (settings.authz !== undefined && settings.authz !== null) {
|
|
599
|
+
if (typeof settings.baseIri !== 'string') throw new LwsHostError('authz needs baseIri')
|
|
600
|
+
const { openAuthorizationServer } = await import('../authz/server.mjs')
|
|
601
|
+
authz = await openAuthorizationServer({
|
|
602
|
+
...settings.authz,
|
|
603
|
+
...(settings.engine !== undefined ? { engine: settings.engine } : {}),
|
|
604
|
+
...(settings.auth && typeof settings.auth.audience === 'string' ? { audience: settings.auth.audience } : {}),
|
|
605
|
+
mountedOn: settings.baseIri
|
|
606
|
+
})
|
|
607
|
+
const issuers = [{ issuer: authz.issuer, jwks: authz.jwks }]
|
|
608
|
+
const prior = settings.auth && Array.isArray(settings.auth.issuers) ? settings.auth.issuers : []
|
|
609
|
+
storageSettings = { ...settings, auth: { ...(settings.auth || {}), issuers: issuers.concat(prior) } }
|
|
610
|
+
}
|
|
611
|
+
opened = await openLwsStorage(storageSettings)
|
|
612
|
+
return opened
|
|
613
|
+
})()
|
|
592
614
|
}
|
|
593
615
|
return opening
|
|
594
616
|
}
|
|
@@ -610,7 +632,13 @@ export async function createLwsServer (options = {}) {
|
|
|
610
632
|
}
|
|
611
633
|
let answer
|
|
612
634
|
try {
|
|
613
|
-
|
|
635
|
+
const store = await storage()
|
|
636
|
+
// One port for both servers: the engine names the authorization
|
|
637
|
+
// server's paths (L4Factoidal.Authz.owns); every other request is the
|
|
638
|
+
// storage's. The token request body is form text.
|
|
639
|
+
answer = authz !== null && authz.owns(record.target)
|
|
640
|
+
? await authz.step({ ...record, body: new TextDecoder('utf-8').decode(record.body) })
|
|
641
|
+
: await store.step(record)
|
|
614
642
|
} catch (error) {
|
|
615
643
|
// A failure of this host or of the engine, not a protocol answer.
|
|
616
644
|
// The engine's own words are kept; nothing is invented.
|
|
@@ -627,6 +655,8 @@ export async function createLwsServer (options = {}) {
|
|
|
627
655
|
return {
|
|
628
656
|
server,
|
|
629
657
|
storage,
|
|
658
|
+
/** The authorization server sharing the storage's issuer, or null. */
|
|
659
|
+
authz () { return authz },
|
|
630
660
|
/** Set the storage root's absolute IRI, before the first request. */
|
|
631
661
|
setBaseIri (value) {
|
|
632
662
|
if (opening !== null) {
|
|
@@ -646,6 +676,7 @@ export async function createLwsServer (options = {}) {
|
|
|
646
676
|
return new Promise((resolve) => {
|
|
647
677
|
server.close(() => {
|
|
648
678
|
if (opened !== null) opened.close()
|
|
679
|
+
if (authz !== null) authz.close()
|
|
649
680
|
resolve()
|
|
650
681
|
})
|
|
651
682
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@factoidal/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "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",
|
|
@@ -142,6 +142,7 @@
|
|
|
142
142
|
"scripts": {
|
|
143
143
|
"build": "bash ../../formal/lean4/Wasm/build-wasm.sh",
|
|
144
144
|
"test": "node --test 'test/*.test.js' 'test/*.test.mjs'",
|
|
145
|
+
"test:jena": "node --test 'test/jena/*.test.mjs'",
|
|
145
146
|
"test:speed": "node ../../tests/perf/npm_speed_gate.mjs",
|
|
146
147
|
"test:costs": "node ../../tests/perf/npm_costs.mjs",
|
|
147
148
|
"prepublishOnly": "npm test"
|