@factoidal/core 0.10.0 → 0.11.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 +114 -1
- package/README.md +7 -1
- package/bin/ext.mjs +16 -20
- package/bin/factoidal.mjs +172 -69
- package/browser.d.ts +4 -0
- package/browser.js +4 -0
- package/fn.d.ts +1 -1
- package/fn.js +34 -97
- package/index.d.ts +22 -1
- package/index.mjs +4 -0
- package/l4-assets/l4factoidal.js +1 -1
- package/l4-assets/l4factoidal.mjs +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +6 -6
- package/lib/api.mjs +155 -118
- package/lib/dataset.mjs +128 -34
- package/lib/lean-entry.mjs +23 -14
- package/lib/version.mjs +1 -1
- package/lws/README.md +46 -6
- package/lws/persist.mjs +78 -0
- package/lws/server.mjs +101 -16
- package/package.json +1 -1
- package/rdfjs.mjs +149 -226
- package/solid/client/index.mjs +25 -9
- package/solid/server/index.mjs +93 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,119 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.11.0 — 2026-10-21
|
|
4
|
+
|
|
5
|
+
No RDF, SPARQL, datatype or IRI decision is made in JavaScript any more
|
|
6
|
+
(https://github.com/danbri/factoidal/issues/894; owner, 2026-10-21: "The ja
|
|
7
|
+
leak fix is p0"). The engine parses, writes, escapes, compares and chooses
|
|
8
|
+
datatypes; the JavaScript copies term components and calls ops.
|
|
9
|
+
`tools/host-purity-lint.sh` group 3 scans the npm and hub JavaScript for RDF
|
|
10
|
+
syntax and runs in `npm test`.
|
|
11
|
+
|
|
12
|
+
### Breaking changes
|
|
13
|
+
|
|
14
|
+
These shapes could not be kept:
|
|
15
|
+
|
|
16
|
+
- `shexValidate` and `Dataset.validateShex`: a focus or shape given as a
|
|
17
|
+
string is read as one RDF 1.2 N-Triples term (`'<http://…>'` or `'_:b'`);
|
|
18
|
+
a bare IRI string is refused. Every API that takes a term as a string
|
|
19
|
+
now reads it through one engine reader (`TermJson.readTermString`), so
|
|
20
|
+
`_:x` is always a blank node and `<…>` always an IRI.
|
|
21
|
+
- `graphs()` returns `[NamedNode|BlankNode, Dataset]` pairs, not
|
|
22
|
+
`[string, Dataset]`, so an IRI graph and a blank-node graph with the same
|
|
23
|
+
text are never confused.
|
|
24
|
+
- A literal with a language and a direction has datatype
|
|
25
|
+
`rdf:dirLangString` (RDF 1.2 Concepts; RDF/JS Data Model); before, the
|
|
26
|
+
RDF/JS default gave `rdf:langString`.
|
|
27
|
+
|
|
28
|
+
- `factoidal lws-serve` and `@factoidal/core/lws` enforce storage-server
|
|
29
|
+
authorization from the LWS editor's draft (401 with
|
|
30
|
+
`WWW-Authenticate: Bearer resource_metadata`, the RFC 9728 metadata
|
|
31
|
+
document, Bearer JWT access tokens checked for signature, `iss`, `aud`,
|
|
32
|
+
`exp`, `nbf` and `iat`, then Web Access Control). `lws-serve` needs
|
|
33
|
+
`--issuer IRI`, `--auth-config FILE` or, for development only,
|
|
34
|
+
`--insecure-no-auth`; `listen()` with neither `auth` nor
|
|
35
|
+
`insecureNoAuth` serves only what the ACLs give to the public. New op
|
|
36
|
+
`lwsFetched`. https://github.com/danbri/factoidal/issues/886
|
|
37
|
+
|
|
38
|
+
- The N-Quads text functions of `@factoidal/core/rdfjs` (`nquadsToQuads`,
|
|
39
|
+
`nquadsLineToQuad`, `quadsToNQuads`, `quadToNQuads`, `termToNQuads`,
|
|
40
|
+
`DatasetCore.fromNQuads`, `DatasetCore.prototype.toNQuads`) call the engine.
|
|
41
|
+
They throw until the engine is loaded: `await ready()` (new) or any other
|
|
42
|
+
`@factoidal/core` call first. `nquadsToQuads` now rejects a line the
|
|
43
|
+
N-Quads grammar rejects (before, `<http://e/a b>` was read as an IRI), and
|
|
44
|
+
the writers escape as the engine's canonical N-Triples form does (before,
|
|
45
|
+
`\b`, `\f` and other control characters were written raw).
|
|
46
|
+
- A JavaScript number answered by a registered extension function becomes
|
|
47
|
+
`xsd:integer` only for an integer within ±(2^53 − 1); every other number is
|
|
48
|
+
an `xsd:double` in the XSD 1.1 canonical form (`1e21` is `"1.0E21"`, before
|
|
49
|
+
`"1e+21"^^xsd:integer`; `0.1` is `"1.0E-1"`, before `"0.1"`). `NaN`,
|
|
50
|
+
`Infinity` and `-Infinity` answer `"NaN"`, `"INF"`, `"-INF"`; before they
|
|
51
|
+
were the §17.6 error. A `bigint` answers `xsd:integer`
|
|
52
|
+
(`L4Factoidal.XSD.JsValue.jsLiteral`).
|
|
53
|
+
- `queryHdt` answers a Dataset for CONSTRUCT and DESCRIBE; before it threw a
|
|
54
|
+
TypeError chosen by a regular expression over the query text.
|
|
55
|
+
- `Literal` has a `direction` member (`''`, `'ltr'`, `'rtl'`), and a
|
|
56
|
+
directional language-tagged string from the engine keeps its direction and
|
|
57
|
+
its `rdf:dirLangString` datatype; `equals` compares it.
|
|
58
|
+
|
|
59
|
+
### New
|
|
60
|
+
|
|
61
|
+
- `isomorphic(a, b)`: dataset isomorphism by the engine
|
|
62
|
+
(`Dataset.isomorphicOutcome`, blank nodes in triple terms included).
|
|
63
|
+
`@factoidal/core/fn`'s `equals` uses it.
|
|
64
|
+
- `fromQuads(quads)`: a Dataset from RDF/JS quads, sent as term components.
|
|
65
|
+
- `ready()`: load the engine. `engineTables()`: the engine's format,
|
|
66
|
+
regime, query-output and pack-syntax name tables.
|
|
67
|
+
- Ops (formal/lean4/Wasm/Ops/Handles.lean): `datasetQuadsJson`,
|
|
68
|
+
`datasetAddQuadsJson`, `datasetDeleteQuadsJson`, `datasetFromQuadsJson`,
|
|
69
|
+
`datasetGraphNames`, `datasetGraphAsDefault`, `datasetIsomorphic`,
|
|
70
|
+
`parseQuadsJson`, `quadsToNQuads`, `termsToNTriples`, `resolveIri`,
|
|
71
|
+
`queryForm`, `engineTables`; `datasetMatch`/`datasetMatchCount` take term
|
|
72
|
+
objects. Theorems: `TermJson.decodeTerm_encodeTerm`,
|
|
73
|
+
`TermJson.decodeQuad_encodeQuad`, `TermJson.mem_quadList_default`,
|
|
74
|
+
`TermJson.mem_quadList_named`, `TermJson.toCanonicalNQuads_eq_quadList`,
|
|
75
|
+
`TermJson.quadPage_append`, `XSD.JsValue.doubleLexical_inLexicalSpace`,
|
|
76
|
+
`XSD.JsValue.integerLexical_inLexicalSpace`,
|
|
77
|
+
`XSD.JsValue.jsLiteral_number_double`, `XSD.JsValue.jsLiteral_number_integer`.
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
- An extension function may answer an exact term: an RDF/JS term
|
|
82
|
+
(Literal, NamedNode, BlankNode, or a Quad as an RDF 1.2 triple term), or
|
|
83
|
+
`{value, datatype}`, or `{value, language, direction?}`. The lexical form
|
|
84
|
+
and the datatype IRI are kept as given, ill-typed literals included (RDF
|
|
85
|
+
1.1 Concepts §3.3). The engine checks only what RDF requires: absolute
|
|
86
|
+
IRIs, a well-formed BCP 47 language tag, direction `ltr` or `rtl`; a term
|
|
87
|
+
that fails is the §17.6 error for that call. A plain JS number still gets
|
|
88
|
+
its datatype from `jsLiteral`. Theorem `TermJson.extAnswerJson_encodeTerm`:
|
|
89
|
+
an app-supplied term is the answer, unchanged.
|
|
90
|
+
|
|
91
|
+
### Performance
|
|
92
|
+
|
|
93
|
+
- Dataset iteration reads the quads from the engine as one byte region (op
|
|
94
|
+
`datasetQuadsBin`: a term table in the delta-log v2 term codec and four
|
|
95
|
+
u32 indices per quad), decoded in JavaScript with `DataView` and
|
|
96
|
+
`TextDecoder`. 60,000 quads: 178–205 ms under load, against 214–280 ms in
|
|
97
|
+
0.10.0 and about the time of the N-Quads writer op in the same run.
|
|
98
|
+
Theorems `decodeBin_quadsBin` and `quadsBin_binSpec`
|
|
99
|
+
(`formal/lean4/L4Factoidal/RDF/QuadsBinTheorems.lean`) prove the region
|
|
100
|
+
reads back as exactly the dataset's quads, so no run-time check is made.
|
|
101
|
+
https://github.com/danbri/factoidal/issues/900
|
|
102
|
+
|
|
103
|
+
### Fixed
|
|
104
|
+
|
|
105
|
+
- `graphs()` (both APIs) no longer merges an IRI graph and a blank-node graph
|
|
106
|
+
whose strings are equal.
|
|
107
|
+
- `Dataset` iteration builds its quads from the engine's term objects; it no
|
|
108
|
+
longer parses N-Quads text in JavaScript.
|
|
109
|
+
- The Solid client resolves a relative target against `baseIri` by RFC 3986
|
|
110
|
+
§5.2 (op `resolveIri`); before it concatenated strings.
|
|
111
|
+
- `factoidal query` table cells and `factoidal inspect` graph names are the
|
|
112
|
+
engine's canonical N-Triples forms (escaped); `factoidal pack` reads its
|
|
113
|
+
suffix table from the engine and detects the syntax of a file with an
|
|
114
|
+
unknown suffix from its content.
|
|
115
|
+
|
|
116
|
+
## 0.10.0 — 2026-10-21
|
|
4
117
|
|
|
5
118
|
### Breaking changes
|
|
6
119
|
|
package/README.md
CHANGED
|
@@ -43,6 +43,12 @@ RDF 1.2 input uses `turtle12`, `ntriples12`, `nquads12` or `trig12`.
|
|
|
43
43
|
- Servers: SPARQL 1.1 Protocol and Graph Store (Fuseki-compatible paths,
|
|
44
44
|
Jena TDB2 read only), Linked Web Storage 1.0, Solid
|
|
45
45
|
- A persisted store with a delta log for updates (early stage)
|
|
46
|
+
- Terms: a JavaScript extension function can answer with an exact term
|
|
47
|
+
(`{value: "007", datatype: XSD + "integer"}`, an RDF/JS term, a triple
|
|
48
|
+
term); the engine keeps the lexical form and the datatype IRI as given.
|
|
49
|
+
A term given as a string is one N-Triples term everywhere (`<http://…>`
|
|
50
|
+
is an IRI, `_:b` a blank node). See API.md, "Custom extension functions" and
|
|
51
|
+
"Terms given as strings".
|
|
46
52
|
- RDF/JS DataFactory and DatasetCore (`/rdfjs`), a functional API (`/fn`)
|
|
47
53
|
|
|
48
54
|
Formats (generated from `formats()`):
|
|
@@ -75,7 +81,7 @@ $ npx factoidal serve --tdb2 DIR /ds # serve a Jena TDB2 database
|
|
|
75
81
|
$ npx factoidal pack data.ttl store/gen-1 && npx factoidal activate store gen-1
|
|
76
82
|
$ npx factoidal query store 'SELECT * WHERE { ?s ?p ?o } LIMIT 5'
|
|
77
83
|
$ npx factoidal query "$(npx factoidal sample-store)" 'SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }'
|
|
78
|
-
$ npx factoidal lws-serve DIR
|
|
84
|
+
$ npx factoidal lws-serve DIR --issuer IRI # Linked Web Storage
|
|
79
85
|
$ npx factoidal solid-serve DIR # Solid Protocol
|
|
80
86
|
```
|
|
81
87
|
|
package/bin/ext.mjs
CHANGED
|
@@ -33,11 +33,12 @@
|
|
|
33
33
|
// times the physical plan evaluates the expression, and across
|
|
34
34
|
// re-evaluation rounds.
|
|
35
35
|
|
|
36
|
-
const XSD = 'http://www.w3.org/2001/XMLSchema#'
|
|
37
36
|
|
|
38
37
|
/** How many evaluation rounds an async resolution may take. */
|
|
39
38
|
export const EXT_MAX_ROUNDS = 25
|
|
40
39
|
|
|
40
|
+
import { appTermToJson } from '../rdfjs.mjs'
|
|
41
|
+
|
|
41
42
|
const extFunctions = new Map()
|
|
42
43
|
const extRegistered = new Set()
|
|
43
44
|
let extCache = new Map()
|
|
@@ -45,29 +46,24 @@ let extPending = []
|
|
|
45
46
|
let bridgeInstalled = false
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
|
-
* Encode one JavaScript answer
|
|
49
|
-
*
|
|
49
|
+
* Encode one JavaScript answer for Wasm/Ops/ExtFns.lean, as a JSON string:
|
|
50
|
+
* the value tagged with its JavaScript type ({js, v}; a number as its
|
|
51
|
+
* Number::toString text), or a term object. The engine chooses the datatype
|
|
52
|
+
* and the lexical form (L4Factoidal.XSD.JsValue.jsLiteral,
|
|
53
|
+
* https://github.com/danbri/factoidal/issues/894 item 3). `''` means
|
|
54
|
+
* "no value", the section 17.6 error.
|
|
50
55
|
*/
|
|
51
56
|
function encodeAnswer (out) {
|
|
52
57
|
if (out === null || out === undefined) return ''
|
|
53
|
-
|
|
54
|
-
return JSON.stringify({
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
if (typeof out === 'string') {
|
|
63
|
-
return JSON.stringify({ type: 'literal', value: out })
|
|
64
|
-
}
|
|
65
|
-
if (typeof out === 'object' && typeof out.type === 'string') {
|
|
66
|
-
// Already an SRJ binding-value object (or an RDF/JS-shaped term the
|
|
67
|
-
// caller built): pass it through and let the Lean decoder judge it.
|
|
68
|
-
return JSON.stringify(out)
|
|
58
|
+
switch (typeof out) {
|
|
59
|
+
case 'boolean': return JSON.stringify({ js: 'boolean', v: out })
|
|
60
|
+
case 'number': return JSON.stringify({ js: 'number', v: Object.is(out, -0) ? '-0' : String(out) })
|
|
61
|
+
case 'bigint': return JSON.stringify({ js: 'bigint', v: out.toString() })
|
|
62
|
+
case 'string': return JSON.stringify({ js: 'string', v: out })
|
|
63
|
+
case 'object':
|
|
64
|
+
try { return JSON.stringify(appTermToJson(out)) } catch (_error) { return '' }
|
|
65
|
+
default: return ''
|
|
69
66
|
}
|
|
70
|
-
return ''
|
|
71
67
|
}
|
|
72
68
|
|
|
73
69
|
/** The synchronous bridge the wasm module calls. */
|
package/bin/factoidal.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// `--format turtle` all print documents the engine produced.
|
|
21
21
|
|
|
22
22
|
import {
|
|
23
|
-
StoreHostError, atomicReplace, listGeneration, makeDirectory, readWhole,
|
|
23
|
+
StoreHostError, atomicReplace, listGeneration, makeDirectory, readChunk, readWhole,
|
|
24
24
|
runtime
|
|
25
25
|
} from '../store-host/index.mjs'
|
|
26
26
|
import { fileUrlToPath, joinPath } from '../store-host/paths.mjs'
|
|
@@ -46,23 +46,9 @@ const EXIT_NOT_WIRED = 3
|
|
|
46
46
|
const FEED_PROGRESS = 65536
|
|
47
47
|
|
|
48
48
|
const PACK_LAYOUTS = ['ibk3', 'ibk4', 'ibk5']
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
['.trig', 'trig'],
|
|
53
|
-
['.nq', 'nquads'], ['.nquads', 'nquads'],
|
|
54
|
-
['.nt', 'ntriples'], ['.ntriples', 'ntriples']
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
// Suffixes the engine parses elsewhere but the packer's streaming fold does
|
|
58
|
-
// not read. Naming them is better than "cannot tell the syntax from its
|
|
59
|
-
// name", which sends the reader looking for a --syntax value that does not
|
|
60
|
-
// exist.
|
|
61
|
-
const PACK_UNSUPPORTED_SUFFIXES = [
|
|
62
|
-
['.rdf', 'RDF/XML'], ['.owl', 'RDF/XML'], ['.xml', 'RDF/XML'],
|
|
63
|
-
['.jsonld', 'JSON-LD'], ['.json', 'JSON-LD'], ['.n3', 'Notation3'],
|
|
64
|
-
['.csv', 'CSV'], ['.tsv', 'TSV'], ['.hdt', 'HDT']
|
|
65
|
-
]
|
|
49
|
+
// The syntaxes the packer reads and the file-name suffixes of each are the
|
|
50
|
+
// engine's tables (op engineTables: packSyntaxes, packSuffixes,
|
|
51
|
+
// packUnsupportedSuffixes), https://github.com/danbri/factoidal/issues/894.
|
|
66
52
|
|
|
67
53
|
/** The parent of a path, and its last component. The `activate` hint
|
|
68
54
|
* printed after a pack needs both; neither is a format decision. */
|
|
@@ -344,43 +330,92 @@ https://www.w3.org/TR/sparql11-service-description/`,
|
|
|
344
330
|
|
|
345
331
|
'lws-serve': `factoidal lws-serve - serve Linked Web Storage 1.0 over HTTP
|
|
346
332
|
|
|
347
|
-
usage: factoidal lws-serve DIR
|
|
333
|
+
usage: factoidal lws-serve DIR (--issuer IRI | --auth-config FILE | --insecure-no-auth)
|
|
334
|
+
[--owner WEBID] [--root-acl FILE] [--audience IRI]
|
|
335
|
+
[--port N] [--host ADDRESS] [--base IRI]
|
|
348
336
|
|
|
349
337
|
Starts a Node HTTP server that answers every request through the engine's
|
|
350
338
|
lwsStep operation.
|
|
351
339
|
|
|
352
|
-
DIR names the storage directory. The
|
|
353
|
-
|
|
354
|
-
|
|
340
|
+
DIR names the storage directory. The storage is kept in the engine
|
|
341
|
+
handle and written to DIR/lws-state.json after every request that
|
|
342
|
+
changed it (temporary file, fsync, rename), and read back on the next
|
|
343
|
+
start. The engine decides the snapshot format and refuses a snapshot
|
|
344
|
+
path with a dot segment or other traversal form
|
|
345
|
+
(formal/lean4/L4Factoidal/LWS/Snapshot.lean).
|
|
355
346
|
|
|
356
347
|
--port N the port to bind; 0 takes an ephemeral one (default 3000)
|
|
357
348
|
--host ADDRESS the address to bind (default 127.0.0.1)
|
|
358
349
|
--base IRI the absolute IRI the storage root maps to
|
|
359
350
|
(default: the bound origin)
|
|
360
351
|
|
|
352
|
+
Authorization (LWS editor's draft, section Authorization; one of the three
|
|
353
|
+
is required):
|
|
354
|
+
--issuer IRI trust this authorization server; several may be
|
|
355
|
+
given separated by commas. Its metadata is read from
|
|
356
|
+
IRI/.well-known/lws-configuration and its keys from
|
|
357
|
+
the jwks_uri there, when a token first needs them.
|
|
358
|
+
--auth-config FILE the engine's "auth" configuration as JSON:
|
|
359
|
+
{"issuers":[{"issuer":IRI,"jwks":{...}?}],
|
|
360
|
+
"audience":IRI?, "leeway":SECONDS?,
|
|
361
|
+
"refreshInterval":SECONDS?}
|
|
362
|
+
--insecure-no-auth development only: no authorization at all; every
|
|
363
|
+
request is served. Never the default.
|
|
364
|
+
--owner WEBID the storage owner: a new storage gets a root ACL
|
|
365
|
+
that gives this agent Read, Write and Control
|
|
366
|
+
--root-acl FILE a Turtle root ACL for a new storage instead
|
|
367
|
+
--audience IRI the aud value tokens must carry (default: the
|
|
368
|
+
storage root IRI)
|
|
369
|
+
|
|
370
|
+
Requests carry an access token as "Authorization: Bearer TOKEN" (RFC 6750).
|
|
371
|
+
The engine verifies its JWS signature (ES256, RS256 or EdDSA, with the
|
|
372
|
+
vendored HACL* code), its iss, aud, exp, nbf and iat, and then decides
|
|
373
|
+
access with Web Access Control over the storage's ACL resources. A
|
|
374
|
+
request without a valid token answers 401 with
|
|
375
|
+
WWW-Authenticate: Bearer resource_metadata="..." (RFC 9728), and the
|
|
376
|
+
metadata document is served at /.well-known/oauth-protected-resource.
|
|
377
|
+
|
|
361
378
|
The server makes no protocol decision: status codes, Link relations,
|
|
362
|
-
Last-Modified
|
|
363
|
-
formal/lean4/L4Factoidal/LWS/. A WebAssembly module built before those
|
|
379
|
+
Last-Modified, the PATCH blank-node refusal and every authorization
|
|
380
|
+
decision are answered by formal/lean4/L4Factoidal/LWS/. A WebAssembly module built before those
|
|
364
381
|
operations landed makes this command exit 3 and say so.
|
|
365
382
|
|
|
366
383
|
Specification: https://w3c.github.io/lws-protocol/lws10-core/`,
|
|
367
384
|
|
|
368
385
|
'solid-serve': `factoidal solid-serve - serve the Solid Protocol over HTTP
|
|
369
386
|
|
|
370
|
-
usage: factoidal solid-serve DIR
|
|
387
|
+
usage: factoidal solid-serve DIR (--issuer IRI | --auth-config FILE | --insecure-no-auth)
|
|
388
|
+
[--owner WEBID] [--port N] [--host ADDRESS] [--base IRI]
|
|
371
389
|
|
|
372
390
|
Starts a Node HTTP server that answers every request through the engine's
|
|
373
391
|
solidStep operation.
|
|
374
392
|
|
|
375
|
-
DIR names the storage directory. The
|
|
376
|
-
|
|
377
|
-
|
|
393
|
+
DIR names the storage directory. The storage is kept in the engine
|
|
394
|
+
handle and written to DIR/lws-state.json after every request that
|
|
395
|
+
changed it (temporary file, fsync, rename), and read back on the next
|
|
396
|
+
start. The engine decides the snapshot format and refuses a snapshot
|
|
397
|
+
path with a dot segment or other traversal form
|
|
398
|
+
(formal/lean4/L4Factoidal/LWS/Snapshot.lean).
|
|
378
399
|
|
|
379
400
|
--port N the port to bind; 0 takes an ephemeral one (default 3000)
|
|
380
401
|
--host ADDRESS the address to bind (default 127.0.0.1)
|
|
381
402
|
--base IRI the absolute IRI the storage root maps to
|
|
382
403
|
(default: the bound origin)
|
|
383
|
-
--owner WEBID the WebID the storage advertises as its owner
|
|
404
|
+
--owner WEBID the WebID the storage advertises as its owner; under
|
|
405
|
+
authentication it gets acl:Read, acl:Write and
|
|
406
|
+
acl:Control on the root (Web Access Control §3.2)
|
|
407
|
+
--issuer IRI trust this Solid-OIDC issuer (comma separated for more).
|
|
408
|
+
The engine verifies each DPoP-bound access token
|
|
409
|
+
(Solid-OIDC §8, §9.3; RFC 9449): signature under the
|
|
410
|
+
issuer's keys found by OpenID Connect Discovery, aud
|
|
411
|
+
"solid", webid, exp, cnf.jkt bound to the DPoP key,
|
|
412
|
+
htm, htu, iat and a jti replay cache, and that the
|
|
413
|
+
WebID profile lists the issuer (solid:oidcIssuer).
|
|
414
|
+
--auth-config FILE the same as JSON: {"issuers":[{"issuer":IRI,
|
|
415
|
+
"jwks":{...}?}], "leeway"?, "iatWindow"?,
|
|
416
|
+
"refreshInterval"?, "checkProfileIssuer"?}
|
|
417
|
+
--insecure-no-auth development only: no authentication; Web Access
|
|
418
|
+
Control is not enforced
|
|
384
419
|
|
|
385
420
|
The server makes no protocol decision. Storage discovery, containment
|
|
386
421
|
triples, slash semantics, the Allow and Accept-* headers, the PUT/POST/
|
|
@@ -469,8 +504,9 @@ const VALUE_OPTIONS = {
|
|
|
469
504
|
update: new Set(['update', 'file']),
|
|
470
505
|
compact: new Set([]),
|
|
471
506
|
serve: new Set(['port', 'host', 'query-path', 'gsp-path', 'file', 'layout', 'tdb2']),
|
|
472
|
-
'lws-serve': new Set(['port', 'host', 'base'
|
|
473
|
-
|
|
507
|
+
'lws-serve': new Set(['port', 'host', 'base', 'issuer', 'auth-config', 'owner', 'root-acl',
|
|
508
|
+
'audience']),
|
|
509
|
+
'solid-serve': new Set(['port', 'host', 'base', 'owner', 'issuer', 'auth-config']),
|
|
474
510
|
'solid-client': new Set(['body', 'file', 'content-type', 'interpret'])
|
|
475
511
|
}
|
|
476
512
|
|
|
@@ -599,31 +635,28 @@ async function commandVersion (options) {
|
|
|
599
635
|
/**
|
|
600
636
|
* One graph name as the manifest reports it. `{"kind":"default"}` is the
|
|
601
637
|
* default graph; the operation marks it that way so a host never has to
|
|
602
|
-
* recognise a reserved IRI.
|
|
638
|
+
* recognise a reserved IRI. The engine writes the N-Triples form of a name
|
|
639
|
+
* (op termsToNTriples).
|
|
603
640
|
*/
|
|
604
|
-
function
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
641
|
+
function graphLabels (engine, graphs) {
|
|
642
|
+
const terms = graphs.filter((g) => g.kind !== 'default')
|
|
643
|
+
.map((g) => ({ type: g.kind === 'iri' ? 'uri' : 'bnode', value: g.value }))
|
|
644
|
+
const tokens = terms.length === 0 ? [] : engine.call('termsToNTriples', [JSON.stringify(terms)]).tokens
|
|
645
|
+
let k = 0
|
|
646
|
+
return graphs.map((g) => (g.kind === 'default' ? 'default' : tokens[k++]))
|
|
609
647
|
}
|
|
610
648
|
|
|
611
649
|
/**
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
650
|
+
* The SPARQL Query Results JSON terms of a table, as display cells: the
|
|
651
|
+
* engine writes each term's canonical N-Triples form (op termsToNTriples).
|
|
652
|
+
* `--format json` prints the engine's own document.
|
|
615
653
|
*/
|
|
616
|
-
function
|
|
617
|
-
|
|
618
|
-
if (
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if (typeof lang === 'string' && lang.length > 0) return `"${term.value}"@${lang}`
|
|
623
|
-
if (typeof term.datatype === 'string') return `"${term.value}"^^<${term.datatype}>`
|
|
624
|
-
return `"${term.value}"`
|
|
625
|
-
}
|
|
626
|
-
return JSON.stringify(term)
|
|
654
|
+
function cellsOfRows (engine, rows) {
|
|
655
|
+
const terms = []
|
|
656
|
+
for (const row of rows) for (const t of row) if (t !== undefined && t !== null) terms.push(t)
|
|
657
|
+
const tokens = terms.length === 0 ? [] : engine.call('termsToNTriples', [JSON.stringify(terms)]).tokens
|
|
658
|
+
let k = 0
|
|
659
|
+
return rows.map((row) => row.map((t) => (t === undefined || t === null ? '' : tokens[k++])))
|
|
627
660
|
}
|
|
628
661
|
|
|
629
662
|
function plural (count, noun) {
|
|
@@ -699,7 +732,7 @@ async function commandInspect (positional, options) {
|
|
|
699
732
|
String(entry.rows),
|
|
700
733
|
String(entry.bytes),
|
|
701
734
|
entry.blockKind,
|
|
702
|
-
entry.graphs.length === 0 ? '-' : entry.graphs
|
|
735
|
+
entry.graphs.length === 0 ? '-' : graphLabels(engine, entry.graphs).join(' '),
|
|
703
736
|
entry.predicate
|
|
704
737
|
].concat(zoned
|
|
705
738
|
? [String((entry.blobRefs ?? []).length), zoneCell(entry.subjectZone)]
|
|
@@ -875,7 +908,7 @@ function renderQueryResult (engine, result, format, limit, quiet) {
|
|
|
875
908
|
return EXIT_FAILURE
|
|
876
909
|
}
|
|
877
910
|
const shown = limit === null ? bindings : bindings.slice(0, limit)
|
|
878
|
-
printTable(vars, shown.map((row) => vars.map((name) =>
|
|
911
|
+
printTable(vars, cellsOfRows(engine, shown.map((row) => vars.map((name) => row[name]))))
|
|
879
912
|
if (shown.length < bindings.length) {
|
|
880
913
|
err(`showing ${shown.length} of ${bindings.length} rows (--limit ${limit})`)
|
|
881
914
|
} else if (!quiet) {
|
|
@@ -914,32 +947,51 @@ function renderQueryResult (engine, result, format, limit, quiet) {
|
|
|
914
947
|
return EXIT_FAILURE
|
|
915
948
|
}
|
|
916
949
|
|
|
917
|
-
/** The
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
950
|
+
/** The first `length` bytes of a file, as text, for content detection. */
|
|
951
|
+
function readHead (path, length) {
|
|
952
|
+
return new TextDecoder('utf-8', { fatal: false }).decode(readChunk(path, 0, length))
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/** The syntax tag for an input: --syntax when given, else the engine's
|
|
956
|
+
* suffix table (op engineTables), else the engine's content detection
|
|
957
|
+
* (op detectFormat) on the first 64 KiB of the file. */
|
|
958
|
+
function packSyntax (engine, input, options) {
|
|
959
|
+
const tables = engine.call('engineTables', []).tables
|
|
960
|
+
const syntaxes = tables.packSyntaxes
|
|
921
961
|
if (typeof options.syntax === 'string') {
|
|
922
962
|
const syntax = options.syntax.toLowerCase()
|
|
923
|
-
if (
|
|
924
|
-
throw new UsageError(`--syntax ${options.syntax} is not one of ${
|
|
963
|
+
if (syntaxes.indexOf(syntax) >= 0) return syntax
|
|
964
|
+
throw new UsageError(`--syntax ${options.syntax} is not one of ${syntaxes.join(', ')}`)
|
|
925
965
|
}
|
|
926
966
|
const lower = input.toLowerCase()
|
|
927
|
-
for (const [suffix, syntax] of
|
|
967
|
+
for (const [suffix, syntax] of Object.entries(tables.packSuffixes)) {
|
|
928
968
|
if (lower.endsWith(suffix)) return syntax
|
|
929
969
|
}
|
|
930
|
-
for (const [suffix, name] of
|
|
970
|
+
for (const [suffix, name] of Object.entries(tables.packUnsupportedSuffixes)) {
|
|
931
971
|
if (lower.endsWith(suffix)) {
|
|
932
972
|
throw new UsageError(
|
|
933
973
|
`pack does not read ${name}. The packer's streaming fold reads ` +
|
|
934
|
-
`${
|
|
974
|
+
`${syntaxes.join(', ')} only. Convert the file first, for ` +
|
|
935
975
|
"example with: factoidal parse FILE --out nquads")
|
|
936
976
|
}
|
|
937
977
|
}
|
|
978
|
+
let head = ''
|
|
979
|
+
try {
|
|
980
|
+
head = readHead(input, 65536)
|
|
981
|
+
} catch (_error) {
|
|
982
|
+
head = ''
|
|
983
|
+
}
|
|
984
|
+
if (head !== '') {
|
|
985
|
+
const tag = engine.call('detectFormat', [head]).format
|
|
986
|
+
const syntax = tag.replace(/12$/, '')
|
|
987
|
+
if (syntaxes.indexOf(syntax) >= 0) return syntax
|
|
988
|
+
}
|
|
938
989
|
throw new UsageError(
|
|
939
|
-
`cannot tell the syntax of ${input} from its name; give --syntax ` +
|
|
940
|
-
`(${
|
|
990
|
+
`cannot tell the syntax of ${input} from its name or its content; give --syntax ` +
|
|
991
|
+
`(${syntaxes.join(', ')})`)
|
|
941
992
|
}
|
|
942
993
|
|
|
994
|
+
|
|
943
995
|
/**
|
|
944
996
|
* The base IRI relative IRIs in the source resolve against.
|
|
945
997
|
*
|
|
@@ -970,7 +1022,7 @@ function packLayout (options) {
|
|
|
970
1022
|
async function commandPack (positional, options) {
|
|
971
1023
|
if (positional.length !== 2) throw new UsageError('pack needs INPUT and OUTPUT')
|
|
972
1024
|
const [input, output] = positional
|
|
973
|
-
const syntax = packSyntax(input, options)
|
|
1025
|
+
const syntax = packSyntax(await loadEngine(), input, options)
|
|
974
1026
|
const layout = packLayout(options)
|
|
975
1027
|
// The pack fold needs a bigger call stack than either runtime gives by
|
|
976
1028
|
// default (https://github.com/danbri/factoidal/issues/649). Under Node
|
|
@@ -1248,6 +1300,25 @@ async function commandServe (positional, options) {
|
|
|
1248
1300
|
async function commandLwsServe (positional, options) {
|
|
1249
1301
|
if (positional.length !== 1) throw new UsageError('lws-serve needs one DIR')
|
|
1250
1302
|
const quiet = options.quiet === true
|
|
1303
|
+
const insecure = options['insecure-no-auth'] === true
|
|
1304
|
+
const issuerOption = typeof options.issuer === 'string' ? options.issuer : null
|
|
1305
|
+
const authFile = typeof options['auth-config'] === 'string' ? options['auth-config'] : null
|
|
1306
|
+
if (!insecure && issuerOption === null && authFile === null) {
|
|
1307
|
+
throw new UsageError('lws-serve needs --issuer IRI or --auth-config FILE, ' +
|
|
1308
|
+
'or --insecure-no-auth for development')
|
|
1309
|
+
}
|
|
1310
|
+
if (insecure && (issuerOption !== null || authFile !== null)) {
|
|
1311
|
+
throw new UsageError('--insecure-no-auth cannot be combined with --issuer or --auth-config')
|
|
1312
|
+
}
|
|
1313
|
+
let auth
|
|
1314
|
+
if (authFile !== null) {
|
|
1315
|
+
auth = readJsonFile(authFile)
|
|
1316
|
+
} else if (issuerOption !== null) {
|
|
1317
|
+
auth = { issuers: issuerOption.split(',').filter((v) => v !== '').map((issuer) => ({ issuer })) }
|
|
1318
|
+
}
|
|
1319
|
+
if (auth !== undefined && typeof options.audience === 'string') {
|
|
1320
|
+
auth = { ...auth, audience: options.audience }
|
|
1321
|
+
}
|
|
1251
1322
|
const { listen, LwsHostError } = await import('../lws/server.mjs')
|
|
1252
1323
|
let running
|
|
1253
1324
|
try {
|
|
@@ -1255,7 +1326,13 @@ async function commandLwsServe (positional, options) {
|
|
|
1255
1326
|
port: servePort(options, 3000),
|
|
1256
1327
|
host: serveHost(options),
|
|
1257
1328
|
...(typeof options.base === 'string' ? { baseIri: options.base } : {}),
|
|
1258
|
-
|
|
1329
|
+
...(insecure ? { insecureNoAuth: true } : { auth }),
|
|
1330
|
+
...(typeof options.owner === 'string' ? { owner: options.owner } : {}),
|
|
1331
|
+
...(typeof options['root-acl'] === 'string'
|
|
1332
|
+
? { rootAcl: new TextDecoder('utf-8', { fatal: true }).decode(readWhole(options['root-acl'])) }
|
|
1333
|
+
: {}),
|
|
1334
|
+
realClock: true,
|
|
1335
|
+
dir: positional[0]
|
|
1259
1336
|
})
|
|
1260
1337
|
} catch (error) {
|
|
1261
1338
|
if (error instanceof LwsHostError && error.unknownOp) {
|
|
@@ -1263,7 +1340,10 @@ async function commandLwsServe (positional, options) {
|
|
|
1263
1340
|
}
|
|
1264
1341
|
throw error
|
|
1265
1342
|
}
|
|
1266
|
-
if (!quiet)
|
|
1343
|
+
if (!quiet) {
|
|
1344
|
+
err(`lws-serve: listening on ${running.origin} for ${positional[0]}`)
|
|
1345
|
+
if (insecure) err('lws-serve: WARNING: --insecure-no-auth: every request is served without authorization')
|
|
1346
|
+
}
|
|
1267
1347
|
out(running.origin)
|
|
1268
1348
|
return untilInterrupted(running, quiet)
|
|
1269
1349
|
}
|
|
@@ -1271,6 +1351,24 @@ async function commandLwsServe (positional, options) {
|
|
|
1271
1351
|
async function commandSolidServe (positional, options) {
|
|
1272
1352
|
if (positional.length !== 1) throw new UsageError('solid-serve needs one DIR')
|
|
1273
1353
|
const quiet = options.quiet === true
|
|
1354
|
+
// The same rule as lws-serve: no start without a trusted issuer unless
|
|
1355
|
+
// the development mode is asked for by name.
|
|
1356
|
+
const insecure = options['insecure-no-auth'] === true
|
|
1357
|
+
const issuerOption = typeof options.issuer === 'string' ? options.issuer : null
|
|
1358
|
+
const authFile = typeof options['auth-config'] === 'string' ? options['auth-config'] : null
|
|
1359
|
+
if (!insecure && issuerOption === null && authFile === null) {
|
|
1360
|
+
throw new UsageError('solid-serve needs --issuer IRI or --auth-config FILE, ' +
|
|
1361
|
+
'or --insecure-no-auth for development')
|
|
1362
|
+
}
|
|
1363
|
+
if (insecure && (issuerOption !== null || authFile !== null)) {
|
|
1364
|
+
throw new UsageError('--insecure-no-auth cannot be combined with --issuer or --auth-config')
|
|
1365
|
+
}
|
|
1366
|
+
let auth
|
|
1367
|
+
if (authFile !== null) {
|
|
1368
|
+
auth = readJsonFile(authFile)
|
|
1369
|
+
} else if (issuerOption !== null) {
|
|
1370
|
+
auth = { issuers: issuerOption.split(',').filter((v) => v !== '').map((issuer) => ({ issuer })) }
|
|
1371
|
+
}
|
|
1274
1372
|
const { listen, SolidServerHostError } = await import('../solid/server/index.mjs')
|
|
1275
1373
|
let running
|
|
1276
1374
|
try {
|
|
@@ -1278,8 +1376,10 @@ async function commandSolidServe (positional, options) {
|
|
|
1278
1376
|
port: servePort(options, 3000),
|
|
1279
1377
|
host: serveHost(options),
|
|
1280
1378
|
...(typeof options.base === 'string' ? { baseIri: options.base } : {}),
|
|
1379
|
+
...(insecure ? { insecureNoAuth: true } : { auth }),
|
|
1281
1380
|
...(typeof options.owner === 'string' ? { owner: options.owner } : {}),
|
|
1282
|
-
realClock: true
|
|
1381
|
+
realClock: true,
|
|
1382
|
+
dir: positional[0]
|
|
1283
1383
|
})
|
|
1284
1384
|
} catch (error) {
|
|
1285
1385
|
if (error instanceof SolidServerHostError && error.unknownOp) {
|
|
@@ -1287,7 +1387,10 @@ async function commandSolidServe (positional, options) {
|
|
|
1287
1387
|
}
|
|
1288
1388
|
throw error
|
|
1289
1389
|
}
|
|
1290
|
-
if (!quiet)
|
|
1390
|
+
if (!quiet) {
|
|
1391
|
+
err(`solid-serve: listening on ${running.origin} for ${positional[0]}`)
|
|
1392
|
+
if (insecure) err('solid-serve: WARNING: --insecure-no-auth: every request is served without authentication')
|
|
1393
|
+
}
|
|
1291
1394
|
out(running.origin)
|
|
1292
1395
|
return untilInterrupted(running, quiet)
|
|
1293
1396
|
}
|
package/browser.d.ts
CHANGED
package/browser.js
CHANGED
|
@@ -67,6 +67,10 @@ export const serialize = api.serialize;
|
|
|
67
67
|
export const canonicalize = api.canonicalize;
|
|
68
68
|
export const graphs = api.graphs;
|
|
69
69
|
export const canonicalHash = api.canonicalHash;
|
|
70
|
+
export const isomorphic = api.isomorphic;
|
|
71
|
+
export const fromQuads = api.fromQuads;
|
|
72
|
+
export const ready = api.ready;
|
|
73
|
+
export const engineTables = api.engineTables;
|
|
70
74
|
export const openDataset = api.openDataset;
|
|
71
75
|
export const closure = api.closure;
|
|
72
76
|
export const formats = api.formats;
|
package/fn.d.ts
CHANGED
|
@@ -306,7 +306,7 @@ export function hash(ds: FnDataset): Promise<string>;
|
|
|
306
306
|
export function equals(a: FnDataset, b: FnDataset): Promise<boolean>;
|
|
307
307
|
|
|
308
308
|
/** Enumerate named graphs (default graph excluded), first-seen order. */
|
|
309
|
-
export function graphs(ds: FnDataset): Array<[
|
|
309
|
+
export function graphs(ds: FnDataset): Array<[name: NamedNode | BlankNode, graph: FnDataset]>;
|
|
310
310
|
|
|
311
311
|
/** A minimal mutable box holding one dataflow input value. */
|
|
312
312
|
export interface Cell<T> {
|