@factoidal/core 0.10.0 → 0.12.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 +496 -1
- package/README.md +70 -4
- package/api.d.ts +2 -3
- package/api.mjs +2 -2
- package/bin/ext.mjs +16 -20
- package/bin/factoidal.mjs +308 -69
- package/browser.d.ts +7 -2
- package/browser.js +5 -2
- package/fn.d.ts +1 -1
- package/fn.js +33 -100
- package/index.d.ts +105 -29
- package/index.mjs +5 -2
- 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 +214 -287
- package/lib/dataset.mjs +164 -75
- package/lib/delta-log-hosts.mjs +9 -8
- package/lib/errors.mjs +97 -0
- package/lib/lean-entry.mjs +36 -22
- package/lib/store.mjs +4 -5
- package/lib/version.mjs +1 -1
- package/lws/README.md +114 -7
- package/lws/persist.mjs +309 -0
- package/lws/server.mjs +364 -17
- package/package.json +2 -1
- package/rdfjs.mjs +151 -226
- package/solid/client/index.mjs +25 -9
- package/solid/server/index.mjs +99 -15
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,184 @@ 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] [--page-size N]
|
|
336
|
+
[--paginate-always] [--moves all|data|off]
|
|
337
|
+
[--no-linkset-put] [--unstable-search-results] [--merkle]
|
|
338
|
+
[--controller-inbox URL] [--delivery-limit N]
|
|
339
|
+
[--delivery-window SECONDS] [--redirects-max N]
|
|
340
|
+
[--storage snapshot|files]
|
|
348
341
|
|
|
349
342
|
Starts a Node HTTP server that answers every request through the engine's
|
|
350
343
|
lwsStep operation.
|
|
351
344
|
|
|
352
|
-
DIR names the storage directory. The
|
|
353
|
-
|
|
354
|
-
|
|
345
|
+
DIR names the storage directory. The storage is kept in the engine
|
|
346
|
+
handle and written to DIR/lws-state.json after every request that
|
|
347
|
+
changed it (temporary file, fsync, rename), and read back on the next
|
|
348
|
+
start. The engine decides the snapshot format and refuses a snapshot
|
|
349
|
+
path with a dot segment or other traversal form
|
|
350
|
+
(formal/lean4/L4Factoidal/LWS/Snapshot.lean).
|
|
351
|
+
|
|
352
|
+
With --storage files (off by default) a write costs the same whatever the
|
|
353
|
+
size of the storage: DIR holds one file per resource body (DIR/bodies/),
|
|
354
|
+
a journal of metadata changes (DIR/lws-journal) and a metadata file
|
|
355
|
+
(DIR/lws-files.json) that the journal is folded into from time to time.
|
|
356
|
+
A journal record is valid only when it is complete and its SHA-256
|
|
357
|
+
matches; a torn last record is ignored at start. An existing
|
|
358
|
+
DIR/lws-state.json is migrated at the first start and renamed to
|
|
359
|
+
lws-state.json.migrated. The engine decides every file name and every
|
|
360
|
+
byte (formal/lean4/L4Factoidal/LWS/FileLayout.lean); the storage is still
|
|
361
|
+
held in memory while the server runs
|
|
362
|
+
(docs/designissues/2026-10-22-lws-storage-on-disk.md).
|
|
363
|
+
|
|
364
|
+
Encryption at rest (off by default): when the environment variable
|
|
365
|
+
FACTOIDAL_LWS_KEY holds a 32-byte key (64 hex digits, or 43 characters of
|
|
366
|
+
base64url), the snapshot is sealed with ChaCha20-Poly1305 (HACL*) under a
|
|
367
|
+
new random nonce for every write, and authenticated when the storage is
|
|
368
|
+
opened. A wrong key, a changed or truncated file, a clear-text file with the
|
|
369
|
+
variable set, or a sealed file without it stops the start with a message.
|
|
370
|
+
It protects the file on disk only (formal/lean4/L4Factoidal/LWS/SnapshotSeal.lean).
|
|
355
371
|
|
|
356
372
|
--port N the port to bind; 0 takes an ephemeral one (default 3000)
|
|
357
373
|
--host ADDRESS the address to bind (default 127.0.0.1)
|
|
358
374
|
--base IRI the absolute IRI the storage root maps to
|
|
359
375
|
(default: the bound origin)
|
|
376
|
+
--page-size N the engine's container page size (config member pageSize;
|
|
377
|
+
default 100, 0 turns pagination off)
|
|
378
|
+
--paginate-always
|
|
379
|
+
present every container listing as paginated, also one
|
|
380
|
+
that fits on one page (config member paginateAlways;
|
|
381
|
+
default off): rel="first" and rel="last", no rel="next"
|
|
382
|
+
--moves all|data|off
|
|
383
|
+
which resources a client may move by changing their up
|
|
384
|
+
link (config member moves; default all): data resources
|
|
385
|
+
and containers, data resources only (the MoveResource
|
|
386
|
+
capability's appliesTo is [DataResource]), or none (no
|
|
387
|
+
MoveResource capability; a change of up answers 409)
|
|
388
|
+
--no-linkset-put
|
|
389
|
+
a linkset resource does not accept PUT (config member
|
|
390
|
+
linksetPut false): its Allow leaves PUT out and a PUT
|
|
391
|
+
answers 405 Method Not Allowed; PATCH still applies
|
|
392
|
+
--unstable-search-results
|
|
393
|
+
ANTICIPATORY AND UNSTABLE, off by default: a QUERY answer
|
|
394
|
+
of the type search names a results resource with
|
|
395
|
+
Content-Location (RFC 10008 section 2.3). lws10-index
|
|
396
|
+
defines no such resource; this is likely to change.
|
|
397
|
+
https://github.com/danbri/factoidal/issues/922
|
|
398
|
+
--merkle off by default: keep a SHA-256 digest of every resource
|
|
399
|
+
and a Merkle root of every container, and show them: an
|
|
400
|
+
RFC 9530 Repr-Digest field on GET of a resource, a
|
|
401
|
+
Factoidal-Merkle-Root field and listing member on a
|
|
402
|
+
container, and the storage root's digest in the storage
|
|
403
|
+
description (roots to the storage controller only under
|
|
404
|
+
authorization). Entity tags do not change. The hashes are
|
|
405
|
+
rebuilt when the storage is opened, not stored.
|
|
406
|
+
https://github.com/danbri/factoidal/issues/919
|
|
407
|
+
--controller-inbox URL
|
|
408
|
+
the storage controller's inbox (config member
|
|
409
|
+
controllerInbox; default none): each new access request
|
|
410
|
+
is announced there with a Create notification. The LWS
|
|
411
|
+
draft names no such inbox; this is a server choice.
|
|
412
|
+
https://github.com/danbri/factoidal/issues/920
|
|
413
|
+
--delivery-limit N
|
|
414
|
+
the most webhook deliveries one subscription gets in one
|
|
415
|
+
window (config member deliveryLimit; default 10, 0 turns
|
|
416
|
+
the limit off); the changes over it wait and go in one
|
|
417
|
+
batched notification when the window ends
|
|
418
|
+
https://github.com/danbri/factoidal/issues/915
|
|
419
|
+
--delivery-window SECONDS
|
|
420
|
+
the length of that window (config member deliveryWindow;
|
|
421
|
+
default 60)
|
|
422
|
+
--redirects-max N
|
|
423
|
+
the most previous identifiers of moved resources kept to
|
|
424
|
+
answer 301 Moved Permanently (config member redirectsMax;
|
|
425
|
+
default 256)
|
|
426
|
+
https://github.com/danbri/factoidal/issues/914
|
|
427
|
+
--storage snapshot|files
|
|
428
|
+
how DIR keeps the storage (config member storage;
|
|
429
|
+
default the environment variable FACTOIDAL_LWS_STORAGE,
|
|
430
|
+
else snapshot): one snapshot file written whole after
|
|
431
|
+
each change, or one file per body with a journal, so a
|
|
432
|
+
write costs the same whatever the storage size
|
|
433
|
+
https://github.com/danbri/factoidal/issues/918
|
|
434
|
+
|
|
435
|
+
Authorization (LWS editor's draft, section Authorization; one of the three
|
|
436
|
+
is required):
|
|
437
|
+
--issuer IRI trust this authorization server; several may be
|
|
438
|
+
given separated by commas. Its metadata is read from
|
|
439
|
+
IRI/.well-known/lws-configuration and its keys from
|
|
440
|
+
the jwks_uri there, when a token first needs them.
|
|
441
|
+
--auth-config FILE the engine's "auth" configuration as JSON:
|
|
442
|
+
{"issuers":[{"issuer":IRI,"jwks":{...}?}],
|
|
443
|
+
"audience":IRI?, "leeway":SECONDS?,
|
|
444
|
+
"refreshInterval":SECONDS?}
|
|
445
|
+
--insecure-no-auth development only: no authorization at all; every
|
|
446
|
+
request is served. Never the default.
|
|
447
|
+
--owner WEBID the storage owner: a new storage gets a root ACL
|
|
448
|
+
that gives this agent Read, Write and Control
|
|
449
|
+
--root-acl FILE a Turtle root ACL for a new storage instead
|
|
450
|
+
--audience IRI the aud value tokens must carry (default: the
|
|
451
|
+
storage IRI, BASE.well-known/lws-storage, which
|
|
452
|
+
every lws#storage link names)
|
|
453
|
+
|
|
454
|
+
Requests carry an access token as "Authorization: Bearer TOKEN" (RFC 6750).
|
|
455
|
+
The engine verifies its JWS signature (ES256, RS256 or EdDSA, with the
|
|
456
|
+
vendored HACL* code), its iss, aud, exp, nbf and iat, and then decides
|
|
457
|
+
access with Web Access Control over the storage's ACL resources. A
|
|
458
|
+
request without a valid token answers 401 with
|
|
459
|
+
WWW-Authenticate: Bearer resource_metadata="..." (RFC 9728), and the
|
|
460
|
+
metadata document is served at /.well-known/oauth-protected-resource.
|
|
360
461
|
|
|
361
462
|
The server makes no protocol decision: status codes, Link relations,
|
|
362
|
-
Last-Modified
|
|
363
|
-
formal/lean4/L4Factoidal/LWS/. A WebAssembly module built before those
|
|
463
|
+
Last-Modified, the PATCH blank-node refusal and every authorization
|
|
464
|
+
decision are answered by formal/lean4/L4Factoidal/LWS/. A WebAssembly module built before those
|
|
364
465
|
operations landed makes this command exit 3 and say so.
|
|
365
466
|
|
|
366
467
|
Specification: https://w3c.github.io/lws-protocol/lws10-core/`,
|
|
367
468
|
|
|
368
469
|
'solid-serve': `factoidal solid-serve - serve the Solid Protocol over HTTP
|
|
369
470
|
|
|
370
|
-
usage: factoidal solid-serve DIR
|
|
471
|
+
usage: factoidal solid-serve DIR (--issuer IRI | --auth-config FILE | --insecure-no-auth)
|
|
472
|
+
[--owner WEBID] [--port N] [--host ADDRESS] [--base IRI]
|
|
371
473
|
|
|
372
474
|
Starts a Node HTTP server that answers every request through the engine's
|
|
373
475
|
solidStep operation.
|
|
374
476
|
|
|
375
|
-
DIR names the storage directory. The
|
|
376
|
-
|
|
377
|
-
|
|
477
|
+
DIR names the storage directory. The storage is kept in the engine
|
|
478
|
+
handle and written to DIR/lws-state.json after every request that
|
|
479
|
+
changed it (temporary file, fsync, rename), and read back on the next
|
|
480
|
+
start. The engine decides the snapshot format and refuses a snapshot
|
|
481
|
+
path with a dot segment or other traversal form
|
|
482
|
+
(formal/lean4/L4Factoidal/LWS/Snapshot.lean).
|
|
483
|
+
|
|
484
|
+
Encryption at rest (off by default): when the environment variable
|
|
485
|
+
FACTOIDAL_LWS_KEY holds a 32-byte key (64 hex digits, or 43 characters of
|
|
486
|
+
base64url), the snapshot is sealed with ChaCha20-Poly1305 (HACL*) under a
|
|
487
|
+
new random nonce for every write, and authenticated when the storage is
|
|
488
|
+
opened. A wrong key, a changed or truncated file, a clear-text file with the
|
|
489
|
+
variable set, or a sealed file without it stops the start with a message.
|
|
490
|
+
It protects the file on disk only (formal/lean4/L4Factoidal/LWS/SnapshotSeal.lean).
|
|
378
491
|
|
|
379
492
|
--port N the port to bind; 0 takes an ephemeral one (default 3000)
|
|
380
493
|
--host ADDRESS the address to bind (default 127.0.0.1)
|
|
381
494
|
--base IRI the absolute IRI the storage root maps to
|
|
382
495
|
(default: the bound origin)
|
|
383
|
-
--owner WEBID the WebID the storage advertises as its owner
|
|
496
|
+
--owner WEBID the WebID the storage advertises as its owner; under
|
|
497
|
+
authentication it gets acl:Read, acl:Write and
|
|
498
|
+
acl:Control on the root (Web Access Control §3.2)
|
|
499
|
+
--issuer IRI trust this Solid-OIDC issuer (comma separated for more).
|
|
500
|
+
The engine verifies each DPoP-bound access token
|
|
501
|
+
(Solid-OIDC §8, §9.3; RFC 9449): signature under the
|
|
502
|
+
issuer's keys found by OpenID Connect Discovery, aud
|
|
503
|
+
"solid", webid, exp, cnf.jkt bound to the DPoP key,
|
|
504
|
+
htm, htu, iat and a jti replay cache, and that the
|
|
505
|
+
WebID profile lists the issuer (solid:oidcIssuer).
|
|
506
|
+
--auth-config FILE the same as JSON: {"issuers":[{"issuer":IRI,
|
|
507
|
+
"jwks":{...}?}], "leeway"?, "iatWindow"?,
|
|
508
|
+
"refreshInterval"?, "checkProfileIssuer"?}
|
|
509
|
+
--insecure-no-auth development only: no authentication; Web Access
|
|
510
|
+
Control is not enforced
|
|
384
511
|
|
|
385
512
|
The server makes no protocol decision. Storage discovery, containment
|
|
386
513
|
triples, slash semantics, the Allow and Accept-* headers, the PUT/POST/
|
|
@@ -469,8 +596,10 @@ const VALUE_OPTIONS = {
|
|
|
469
596
|
update: new Set(['update', 'file']),
|
|
470
597
|
compact: new Set([]),
|
|
471
598
|
serve: new Set(['port', 'host', 'query-path', 'gsp-path', 'file', 'layout', 'tdb2']),
|
|
472
|
-
'lws-serve': new Set(['port', 'host', 'base'
|
|
473
|
-
|
|
599
|
+
'lws-serve': new Set(['port', 'host', 'base', 'issuer', 'auth-config', 'owner', 'root-acl',
|
|
600
|
+
'audience', 'page-size', 'moves', 'controller-inbox', 'delivery-limit', 'delivery-window',
|
|
601
|
+
'redirects-max', 'storage']),
|
|
602
|
+
'solid-serve': new Set(['port', 'host', 'base', 'owner', 'issuer', 'auth-config']),
|
|
474
603
|
'solid-client': new Set(['body', 'file', 'content-type', 'interpret'])
|
|
475
604
|
}
|
|
476
605
|
|
|
@@ -599,31 +728,28 @@ async function commandVersion (options) {
|
|
|
599
728
|
/**
|
|
600
729
|
* One graph name as the manifest reports it. `{"kind":"default"}` is the
|
|
601
730
|
* default graph; the operation marks it that way so a host never has to
|
|
602
|
-
* recognise a reserved IRI.
|
|
731
|
+
* recognise a reserved IRI. The engine writes the N-Triples form of a name
|
|
732
|
+
* (op termsToNTriples).
|
|
603
733
|
*/
|
|
604
|
-
function
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
734
|
+
function graphLabels (engine, graphs) {
|
|
735
|
+
const terms = graphs.filter((g) => g.kind !== 'default')
|
|
736
|
+
.map((g) => ({ type: g.kind === 'iri' ? 'uri' : 'bnode', value: g.value }))
|
|
737
|
+
const tokens = terms.length === 0 ? [] : engine.call('termsToNTriples', [JSON.stringify(terms)]).tokens
|
|
738
|
+
let k = 0
|
|
739
|
+
return graphs.map((g) => (g.kind === 'default' ? 'default' : tokens[k++]))
|
|
609
740
|
}
|
|
610
741
|
|
|
611
742
|
/**
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
743
|
+
* The SPARQL Query Results JSON terms of a table, as display cells: the
|
|
744
|
+
* engine writes each term's canonical N-Triples form (op termsToNTriples).
|
|
745
|
+
* `--format json` prints the engine's own document.
|
|
615
746
|
*/
|
|
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)
|
|
747
|
+
function cellsOfRows (engine, rows) {
|
|
748
|
+
const terms = []
|
|
749
|
+
for (const row of rows) for (const t of row) if (t !== undefined && t !== null) terms.push(t)
|
|
750
|
+
const tokens = terms.length === 0 ? [] : engine.call('termsToNTriples', [JSON.stringify(terms)]).tokens
|
|
751
|
+
let k = 0
|
|
752
|
+
return rows.map((row) => row.map((t) => (t === undefined || t === null ? '' : tokens[k++])))
|
|
627
753
|
}
|
|
628
754
|
|
|
629
755
|
function plural (count, noun) {
|
|
@@ -699,7 +825,7 @@ async function commandInspect (positional, options) {
|
|
|
699
825
|
String(entry.rows),
|
|
700
826
|
String(entry.bytes),
|
|
701
827
|
entry.blockKind,
|
|
702
|
-
entry.graphs.length === 0 ? '-' : entry.graphs
|
|
828
|
+
entry.graphs.length === 0 ? '-' : graphLabels(engine, entry.graphs).join(' '),
|
|
703
829
|
entry.predicate
|
|
704
830
|
].concat(zoned
|
|
705
831
|
? [String((entry.blobRefs ?? []).length), zoneCell(entry.subjectZone)]
|
|
@@ -875,7 +1001,7 @@ function renderQueryResult (engine, result, format, limit, quiet) {
|
|
|
875
1001
|
return EXIT_FAILURE
|
|
876
1002
|
}
|
|
877
1003
|
const shown = limit === null ? bindings : bindings.slice(0, limit)
|
|
878
|
-
printTable(vars, shown.map((row) => vars.map((name) =>
|
|
1004
|
+
printTable(vars, cellsOfRows(engine, shown.map((row) => vars.map((name) => row[name]))))
|
|
879
1005
|
if (shown.length < bindings.length) {
|
|
880
1006
|
err(`showing ${shown.length} of ${bindings.length} rows (--limit ${limit})`)
|
|
881
1007
|
} else if (!quiet) {
|
|
@@ -914,32 +1040,51 @@ function renderQueryResult (engine, result, format, limit, quiet) {
|
|
|
914
1040
|
return EXIT_FAILURE
|
|
915
1041
|
}
|
|
916
1042
|
|
|
917
|
-
/** The
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1043
|
+
/** The first `length` bytes of a file, as text, for content detection. */
|
|
1044
|
+
function readHead (path, length) {
|
|
1045
|
+
return new TextDecoder('utf-8', { fatal: false }).decode(readChunk(path, 0, length))
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/** The syntax tag for an input: --syntax when given, else the engine's
|
|
1049
|
+
* suffix table (op engineTables), else the engine's content detection
|
|
1050
|
+
* (op detectFormat) on the first 64 KiB of the file. */
|
|
1051
|
+
function packSyntax (engine, input, options) {
|
|
1052
|
+
const tables = engine.call('engineTables', []).tables
|
|
1053
|
+
const syntaxes = tables.packSyntaxes
|
|
921
1054
|
if (typeof options.syntax === 'string') {
|
|
922
1055
|
const syntax = options.syntax.toLowerCase()
|
|
923
|
-
if (
|
|
924
|
-
throw new UsageError(`--syntax ${options.syntax} is not one of ${
|
|
1056
|
+
if (syntaxes.indexOf(syntax) >= 0) return syntax
|
|
1057
|
+
throw new UsageError(`--syntax ${options.syntax} is not one of ${syntaxes.join(', ')}`)
|
|
925
1058
|
}
|
|
926
1059
|
const lower = input.toLowerCase()
|
|
927
|
-
for (const [suffix, syntax] of
|
|
1060
|
+
for (const [suffix, syntax] of Object.entries(tables.packSuffixes)) {
|
|
928
1061
|
if (lower.endsWith(suffix)) return syntax
|
|
929
1062
|
}
|
|
930
|
-
for (const [suffix, name] of
|
|
1063
|
+
for (const [suffix, name] of Object.entries(tables.packUnsupportedSuffixes)) {
|
|
931
1064
|
if (lower.endsWith(suffix)) {
|
|
932
1065
|
throw new UsageError(
|
|
933
1066
|
`pack does not read ${name}. The packer's streaming fold reads ` +
|
|
934
|
-
`${
|
|
1067
|
+
`${syntaxes.join(', ')} only. Convert the file first, for ` +
|
|
935
1068
|
"example with: factoidal parse FILE --out nquads")
|
|
936
1069
|
}
|
|
937
1070
|
}
|
|
1071
|
+
let head = ''
|
|
1072
|
+
try {
|
|
1073
|
+
head = readHead(input, 65536)
|
|
1074
|
+
} catch (_error) {
|
|
1075
|
+
head = ''
|
|
1076
|
+
}
|
|
1077
|
+
if (head !== '') {
|
|
1078
|
+
const tag = engine.call('detectFormat', [head]).format
|
|
1079
|
+
const syntax = tag.replace(/12$/, '')
|
|
1080
|
+
if (syntaxes.indexOf(syntax) >= 0) return syntax
|
|
1081
|
+
}
|
|
938
1082
|
throw new UsageError(
|
|
939
|
-
`cannot tell the syntax of ${input} from its name; give --syntax ` +
|
|
940
|
-
`(${
|
|
1083
|
+
`cannot tell the syntax of ${input} from its name or its content; give --syntax ` +
|
|
1084
|
+
`(${syntaxes.join(', ')})`)
|
|
941
1085
|
}
|
|
942
1086
|
|
|
1087
|
+
|
|
943
1088
|
/**
|
|
944
1089
|
* The base IRI relative IRIs in the source resolve against.
|
|
945
1090
|
*
|
|
@@ -970,7 +1115,7 @@ function packLayout (options) {
|
|
|
970
1115
|
async function commandPack (positional, options) {
|
|
971
1116
|
if (positional.length !== 2) throw new UsageError('pack needs INPUT and OUTPUT')
|
|
972
1117
|
const [input, output] = positional
|
|
973
|
-
const syntax = packSyntax(input, options)
|
|
1118
|
+
const syntax = packSyntax(await loadEngine(), input, options)
|
|
974
1119
|
const layout = packLayout(options)
|
|
975
1120
|
// The pack fold needs a bigger call stack than either runtime gives by
|
|
976
1121
|
// default (https://github.com/danbri/factoidal/issues/649). Under Node
|
|
@@ -1146,6 +1291,20 @@ function servePort (options, fallback) {
|
|
|
1146
1291
|
return port
|
|
1147
1292
|
}
|
|
1148
1293
|
|
|
1294
|
+
// --page-size N: a CLI number, passed unchanged as the engine config member
|
|
1295
|
+
// `pageSize`; what a page holds is decided in L4Factoidal/LWS/Pagination.lean.
|
|
1296
|
+
function pageSizeOption (value) {
|
|
1297
|
+
return natOption('page-size', value)
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function natOption (flag, value) {
|
|
1301
|
+
const n = Number(value)
|
|
1302
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
1303
|
+
throw new UsageError(`--${flag} must be a non-negative integer, not "${value}"`)
|
|
1304
|
+
}
|
|
1305
|
+
return n
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1149
1308
|
function serveHost (options) {
|
|
1150
1309
|
if (options.host === undefined) return '127.0.0.1'
|
|
1151
1310
|
if (typeof options.host !== 'string' || options.host.length === 0) {
|
|
@@ -1248,6 +1407,25 @@ async function commandServe (positional, options) {
|
|
|
1248
1407
|
async function commandLwsServe (positional, options) {
|
|
1249
1408
|
if (positional.length !== 1) throw new UsageError('lws-serve needs one DIR')
|
|
1250
1409
|
const quiet = options.quiet === true
|
|
1410
|
+
const insecure = options['insecure-no-auth'] === true
|
|
1411
|
+
const issuerOption = typeof options.issuer === 'string' ? options.issuer : null
|
|
1412
|
+
const authFile = typeof options['auth-config'] === 'string' ? options['auth-config'] : null
|
|
1413
|
+
if (!insecure && issuerOption === null && authFile === null) {
|
|
1414
|
+
throw new UsageError('lws-serve needs --issuer IRI or --auth-config FILE, ' +
|
|
1415
|
+
'or --insecure-no-auth for development')
|
|
1416
|
+
}
|
|
1417
|
+
if (insecure && (issuerOption !== null || authFile !== null)) {
|
|
1418
|
+
throw new UsageError('--insecure-no-auth cannot be combined with --issuer or --auth-config')
|
|
1419
|
+
}
|
|
1420
|
+
let auth
|
|
1421
|
+
if (authFile !== null) {
|
|
1422
|
+
auth = readJsonFile(authFile)
|
|
1423
|
+
} else if (issuerOption !== null) {
|
|
1424
|
+
auth = { issuers: issuerOption.split(',').filter((v) => v !== '').map((issuer) => ({ issuer })) }
|
|
1425
|
+
}
|
|
1426
|
+
if (auth !== undefined && typeof options.audience === 'string') {
|
|
1427
|
+
auth = { ...auth, audience: options.audience }
|
|
1428
|
+
}
|
|
1251
1429
|
const { listen, LwsHostError } = await import('../lws/server.mjs')
|
|
1252
1430
|
let running
|
|
1253
1431
|
try {
|
|
@@ -1255,15 +1433,45 @@ async function commandLwsServe (positional, options) {
|
|
|
1255
1433
|
port: servePort(options, 3000),
|
|
1256
1434
|
host: serveHost(options),
|
|
1257
1435
|
...(typeof options.base === 'string' ? { baseIri: options.base } : {}),
|
|
1258
|
-
|
|
1436
|
+
...(options['page-size'] !== undefined ? { pageSize: pageSizeOption(options['page-size']) } : {}),
|
|
1437
|
+
...(options['unstable-search-results'] === true ? { searchResults: true } : {}),
|
|
1438
|
+
...(options.merkle === true ? { merkle: true } : {}),
|
|
1439
|
+
// Passed unread as config members; the engine decides what they mean
|
|
1440
|
+
// and refuses a value it does not know (Wasm/Ops/Lws.lean).
|
|
1441
|
+
...(options['paginate-always'] === true ? { paginateAlways: true } : {}),
|
|
1442
|
+
...(typeof options.moves === 'string' ? { moves: options.moves } : {}),
|
|
1443
|
+
...(options['no-linkset-put'] === true ? { linksetPut: false } : {}),
|
|
1444
|
+
...(typeof options['controller-inbox'] === 'string' ? { controllerInbox: options['controller-inbox'] } : {}),
|
|
1445
|
+
...(options['delivery-limit'] !== undefined ? { deliveryLimit: natOption('delivery-limit', options['delivery-limit']) } : {}),
|
|
1446
|
+
...(options['delivery-window'] !== undefined ? { deliveryWindow: natOption('delivery-window', options['delivery-window']) } : {}),
|
|
1447
|
+
...(options['redirects-max'] !== undefined ? { redirectsMax: natOption('redirects-max', options['redirects-max']) } : {}),
|
|
1448
|
+
...(typeof options.storage === 'string' ? { storage: options.storage } : {}),
|
|
1449
|
+
...(insecure ? { insecureNoAuth: true } : { auth }),
|
|
1450
|
+
...(typeof options.owner === 'string' ? { owner: options.owner } : {}),
|
|
1451
|
+
...(typeof options['root-acl'] === 'string'
|
|
1452
|
+
? { rootAcl: new TextDecoder('utf-8', { fatal: true }).decode(readWhole(options['root-acl'])) }
|
|
1453
|
+
: {}),
|
|
1454
|
+
realClock: true,
|
|
1455
|
+
dir: positional[0]
|
|
1259
1456
|
})
|
|
1260
1457
|
} catch (error) {
|
|
1261
1458
|
if (error instanceof LwsHostError && error.unknownOp) {
|
|
1262
1459
|
return notWired('lws-serve', error.message)
|
|
1263
1460
|
}
|
|
1461
|
+
// The engine refused to open the storage (a bad configuration, or a
|
|
1462
|
+
// snapshot it could not read or authenticate, issue 921), or the state
|
|
1463
|
+
// file is not JSON: the message is the whole answer.
|
|
1464
|
+
if (error !== null && typeof error === 'object' &&
|
|
1465
|
+
(error.envelope !== undefined || error.stateFile === true)) {
|
|
1466
|
+
err(`factoidal lws-serve: ${error.envelope !== undefined ? error.envelope.error : error.message}`)
|
|
1467
|
+
return EXIT_FAILURE
|
|
1468
|
+
}
|
|
1264
1469
|
throw error
|
|
1265
1470
|
}
|
|
1266
|
-
if (!quiet)
|
|
1471
|
+
if (!quiet) {
|
|
1472
|
+
err(`lws-serve: listening on ${running.origin} for ${positional[0]}`)
|
|
1473
|
+
if (insecure) err('lws-serve: WARNING: --insecure-no-auth: every request is served without authorization')
|
|
1474
|
+
}
|
|
1267
1475
|
out(running.origin)
|
|
1268
1476
|
return untilInterrupted(running, quiet)
|
|
1269
1477
|
}
|
|
@@ -1271,6 +1479,24 @@ async function commandLwsServe (positional, options) {
|
|
|
1271
1479
|
async function commandSolidServe (positional, options) {
|
|
1272
1480
|
if (positional.length !== 1) throw new UsageError('solid-serve needs one DIR')
|
|
1273
1481
|
const quiet = options.quiet === true
|
|
1482
|
+
// The same rule as lws-serve: no start without a trusted issuer unless
|
|
1483
|
+
// the development mode is asked for by name.
|
|
1484
|
+
const insecure = options['insecure-no-auth'] === true
|
|
1485
|
+
const issuerOption = typeof options.issuer === 'string' ? options.issuer : null
|
|
1486
|
+
const authFile = typeof options['auth-config'] === 'string' ? options['auth-config'] : null
|
|
1487
|
+
if (!insecure && issuerOption === null && authFile === null) {
|
|
1488
|
+
throw new UsageError('solid-serve needs --issuer IRI or --auth-config FILE, ' +
|
|
1489
|
+
'or --insecure-no-auth for development')
|
|
1490
|
+
}
|
|
1491
|
+
if (insecure && (issuerOption !== null || authFile !== null)) {
|
|
1492
|
+
throw new UsageError('--insecure-no-auth cannot be combined with --issuer or --auth-config')
|
|
1493
|
+
}
|
|
1494
|
+
let auth
|
|
1495
|
+
if (authFile !== null) {
|
|
1496
|
+
auth = readJsonFile(authFile)
|
|
1497
|
+
} else if (issuerOption !== null) {
|
|
1498
|
+
auth = { issuers: issuerOption.split(',').filter((v) => v !== '').map((issuer) => ({ issuer })) }
|
|
1499
|
+
}
|
|
1274
1500
|
const { listen, SolidServerHostError } = await import('../solid/server/index.mjs')
|
|
1275
1501
|
let running
|
|
1276
1502
|
try {
|
|
@@ -1278,16 +1504,29 @@ async function commandSolidServe (positional, options) {
|
|
|
1278
1504
|
port: servePort(options, 3000),
|
|
1279
1505
|
host: serveHost(options),
|
|
1280
1506
|
...(typeof options.base === 'string' ? { baseIri: options.base } : {}),
|
|
1507
|
+
...(insecure ? { insecureNoAuth: true } : { auth }),
|
|
1281
1508
|
...(typeof options.owner === 'string' ? { owner: options.owner } : {}),
|
|
1282
|
-
realClock: true
|
|
1509
|
+
realClock: true,
|
|
1510
|
+
dir: positional[0]
|
|
1283
1511
|
})
|
|
1284
1512
|
} catch (error) {
|
|
1285
1513
|
if (error instanceof SolidServerHostError && error.unknownOp) {
|
|
1286
1514
|
return notWired('solid-serve', error.message)
|
|
1287
1515
|
}
|
|
1516
|
+
// The engine refused to open the storage (a bad configuration, or a
|
|
1517
|
+
// snapshot it could not read or authenticate, issue 921), or the state
|
|
1518
|
+
// file is not JSON: the message is the whole answer.
|
|
1519
|
+
if (error !== null && typeof error === 'object' &&
|
|
1520
|
+
(error.envelope !== undefined || error.stateFile === true)) {
|
|
1521
|
+
err(`factoidal solid-serve: ${error.envelope !== undefined ? error.envelope.error : error.message}`)
|
|
1522
|
+
return EXIT_FAILURE
|
|
1523
|
+
}
|
|
1288
1524
|
throw error
|
|
1289
1525
|
}
|
|
1290
|
-
if (!quiet)
|
|
1526
|
+
if (!quiet) {
|
|
1527
|
+
err(`solid-serve: listening on ${running.origin} for ${positional[0]}`)
|
|
1528
|
+
if (insecure) err('solid-serve: WARNING: --insecure-no-auth: every request is served without authentication')
|
|
1529
|
+
}
|
|
1291
1530
|
out(running.origin)
|
|
1292
1531
|
return untilInterrupted(running, quiet)
|
|
1293
1532
|
}
|
package/browser.d.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// system, so a path string rejects.
|
|
6
6
|
|
|
7
7
|
export type {
|
|
8
|
+
FactoidalErrorCode,
|
|
9
|
+
FactoidalErrorPosition,
|
|
8
10
|
NamedNode, BlankNode, Literal, Variable, DefaultGraph, Quad, Term, DataFactory,
|
|
9
11
|
DataFormat, EntailRegime, Bindings, DataInput, QueryInput, ParseOptions,
|
|
10
12
|
QueryOptions, SerializeOptions, StoreReader, OpenStoreOptions,
|
|
@@ -19,6 +21,10 @@ export {
|
|
|
19
21
|
canonicalize,
|
|
20
22
|
graphs,
|
|
21
23
|
canonicalHash,
|
|
24
|
+
isomorphic,
|
|
25
|
+
fromQuads,
|
|
26
|
+
ready,
|
|
27
|
+
engineTables,
|
|
22
28
|
openDataset,
|
|
23
29
|
closure,
|
|
24
30
|
formats,
|
|
@@ -85,9 +91,8 @@ export {
|
|
|
85
91
|
didKeyResolve,
|
|
86
92
|
capabilities,
|
|
87
93
|
Dataset,
|
|
88
|
-
DatasetHandle,
|
|
89
94
|
Store,
|
|
90
|
-
|
|
95
|
+
FactoidalError,
|
|
91
96
|
dataFactory,
|
|
92
97
|
version,
|
|
93
98
|
engine,
|
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;
|
|
@@ -149,9 +153,8 @@ export const didKeyResolve = api.didKeyResolve;
|
|
|
149
153
|
// Types, probes and metadata
|
|
150
154
|
export const capabilities = api.capabilities;
|
|
151
155
|
export const Dataset = api.Dataset;
|
|
152
|
-
export const DatasetHandle = api.DatasetHandle;
|
|
153
156
|
export const Store = api.Store;
|
|
154
|
-
export const
|
|
157
|
+
export const FactoidalError = api.FactoidalError;
|
|
155
158
|
export const dataFactory = api.dataFactory;
|
|
156
159
|
export const version = api.version;
|
|
157
160
|
export const engine = api.engine;
|
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> {
|