@factoidal/core 0.16.0 → 0.18.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 +23 -0
- package/README.md +3 -2
- package/authz/server.mjs +17 -3
- package/bin/engine.mjs +12 -10
- package/bin/factoidal.mjs +339 -24
- package/bin/pack-host.mjs +31 -1
- package/bin/store.mjs +67 -11
- package/index.d.ts +18 -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 +34 -15
- package/lib/dataset.mjs +2 -1
- package/lib/lean-entry.mjs +40 -1
- package/lib/store.mjs +10 -8
- package/lib/version.mjs +1 -1
- package/lws/client.mjs +50 -8
- package/lws/server.mjs +34 -3
- package/package.json +6 -1
- package/webdav/server.mjs +293 -0
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.
|
|
@@ -106,6 +113,8 @@ commands:
|
|
|
106
113
|
(RFC 8414 metadata, JWK Set, RFC 8693 token exchange)
|
|
107
114
|
authz-keys DIR print the public JWK Set of an authorization server
|
|
108
115
|
xmpp-serve DIR serve XMPP (RFC 6120/6121) over TCP or direct TLS
|
|
116
|
+
webdav-serve CONFIGDIR serve local and remote LWS storages to file
|
|
117
|
+
browsers over WebDAV (RFC 4918)
|
|
109
118
|
|
|
110
119
|
global options:
|
|
111
120
|
-h, --help print this text, or a command's own help
|
|
@@ -226,8 +235,11 @@ normally STORE/gen-N. Packing does not activate: run activate next.
|
|
|
226
235
|
options:
|
|
227
236
|
--layout LAYOUT ibk5 (quads, wire version 10: RDF 1.2 terms,
|
|
228
237
|
out-of-line literals, zone maps; default), ibk4
|
|
229
|
-
(quads, wire version 9)
|
|
230
|
-
|
|
238
|
+
(quads, wire version 9), ibk3 (triples) or sbm11
|
|
239
|
+
(the files l4sbm11-pack writes; the document is read
|
|
240
|
+
whole, in memory; serve does not read it yet)
|
|
241
|
+
--syntax SYNTAX turtle, trig or nquads, and with sbm11 also rdfxml or
|
|
242
|
+
jsonld; default from the file extension
|
|
231
243
|
--chunk-bytes N Merkle chunk size; default is the engine's
|
|
232
244
|
--batch-bytes N ibk4 only: source bytes between publication batches;
|
|
233
245
|
default is the engine's. Blocks are published during
|
|
@@ -248,7 +260,10 @@ usage: factoidal activate STORE GENERATION [--json]
|
|
|
248
260
|
|
|
249
261
|
Verifies every artifact of GENERATION and every cross-artifact relation,
|
|
250
262
|
then atomically replaces STORE/CURRENT. A generation that fails
|
|
251
|
-
verification never becomes current
|
|
263
|
+
verification never becomes current. For an SBM11 generation the engine
|
|
264
|
+
checks the length, SHA-256 and Merkle root of every file and every
|
|
265
|
+
.merkle sidecar against the manifest (op sbm11Verify); it does not run
|
|
266
|
+
the decoder checks of l4sbm11 activate.`,
|
|
252
267
|
|
|
253
268
|
update: `factoidal update - apply a SPARQL Update through the delta log
|
|
254
269
|
|
|
@@ -379,8 +394,10 @@ private keys (with d): for a conformance harness that mints test tokens
|
|
|
379
394
|
|
|
380
395
|
'lws-serve': `factoidal lws-serve - serve Linked Web Storage 1.0 over HTTP
|
|
381
396
|
|
|
382
|
-
usage: factoidal lws-serve DIR (--issuer IRI | --auth-config FILE | --insecure-no-auth)
|
|
397
|
+
usage: factoidal lws-serve DIR (--authz DIR | --issuer IRI | --auth-config FILE | --insecure-no-auth)
|
|
383
398
|
[--owner WEBID] [--root-acl FILE] [--audience IRI]
|
|
399
|
+
[--alg ES256|EdDSA] [--token-lifetime SECONDS] [--rotate-after SECONDS]
|
|
400
|
+
[--allow-http] [--allow-private-fetch]
|
|
384
401
|
[--port N] [--host ADDRESS] [--base IRI] [--page-size N]
|
|
385
402
|
[--paginate-always] [--moves all|data|off]
|
|
386
403
|
[--no-linkset-put] [--unstable-search-results] [--merkle]
|
|
@@ -494,8 +511,21 @@ It protects the file on disk only (formal/lean4/L4Factoidal/LWS/SnapshotSeal.lea
|
|
|
494
511
|
--no-cors send no CORS fields; a page on another origin cannot read
|
|
495
512
|
any answer
|
|
496
513
|
|
|
497
|
-
Authorization (LWS editor's draft, section Authorization; one of the
|
|
514
|
+
Authorization (LWS editor's draft, section Authorization; one of the four
|
|
498
515
|
is required):
|
|
516
|
+
--authz DIR also serve an authorization server on this port, its
|
|
517
|
+
issuer the storage root without the trailing slash
|
|
518
|
+
(BASE less "/"): RFC 8414 metadata at
|
|
519
|
+
/.well-known/lws-configuration, the token endpoint at
|
|
520
|
+
BASE.well-known/lws-token, the JWK Set at
|
|
521
|
+
BASE.well-known/lws-jwks, signing keys in DIR (as
|
|
522
|
+
authz-serve; --alg, --token-lifetime, --rotate-after,
|
|
523
|
+
--allow-http and --allow-private-fetch as there). The
|
|
524
|
+
storage trusts it, and any --issuer too. Each request
|
|
525
|
+
is the authorization server's when the engine says so
|
|
526
|
+
(L4Factoidal/Authz/Mount.lean); every other is the
|
|
527
|
+
storage's. A proxy may serve BASE at a path prefix and
|
|
528
|
+
remove the prefix: both forms of every path are read.
|
|
499
529
|
--issuer IRI trust this authorization server; several may be
|
|
500
530
|
given separated by commas. Its metadata is read from
|
|
501
531
|
IRI/.well-known/lws-configuration and its keys from
|
|
@@ -616,6 +646,47 @@ MUC/MIX/GC3 rooms.
|
|
|
616
646
|
Specifications: https://www.rfc-editor.org/rfc/rfc6120 and
|
|
617
647
|
https://www.rfc-editor.org/rfc/rfc6121`,
|
|
618
648
|
|
|
649
|
+
'webdav-serve': `factoidal webdav-serve - serve LWS storages to file browsers over WebDAV
|
|
650
|
+
|
|
651
|
+
usage: factoidal webdav-serve CONFIGDIR [--port N] [--host ADDRESS]
|
|
652
|
+
|
|
653
|
+
Starts a WebDAV server (RFC 4918, classes 1 and 2) that macOS Finder,
|
|
654
|
+
Windows Explorer, GNOME Files and KDE Dolphin can connect to. Its root
|
|
655
|
+
collection holds one folder per mount; a mount is one Markdown file
|
|
656
|
+
CONFIGDIR/NAME.md (README.md is not a mount):
|
|
657
|
+
|
|
658
|
+
# Display name
|
|
659
|
+
- kind: local | lws
|
|
660
|
+
- path: ./alice-storage (local: an lws-serve storage directory)
|
|
661
|
+
- url: https://storage.example/alice/ (lws: the storage root)
|
|
662
|
+
- auth: none | did-key | bearer
|
|
663
|
+
- key-file: ./alice.jwk (did-key: a P-256 private JWK; made when absent)
|
|
664
|
+
- token-file: ./alice.token (bearer: an access token)
|
|
665
|
+
- writable: true | false (default true for local, false for lws)
|
|
666
|
+
- storage: snapshot | files (local: default what the directory holds)
|
|
667
|
+
- base: IRI (local: the storage root's IRI, default http://localhost/)
|
|
668
|
+
|
|
669
|
+
Only the first "# " heading and the first "- key: value" list are read; the
|
|
670
|
+
words in brackets above are notes, not part of a value. A malformed file
|
|
671
|
+
stops the start with its name and line number.
|
|
672
|
+
|
|
673
|
+
A local mount opens the directory with the operations lws-serve uses, as
|
|
674
|
+
the storage's owner (do not serve the same directory with lws-serve at the
|
|
675
|
+
same time). An lws mount reaches the storage through the LWS client.
|
|
676
|
+
|
|
677
|
+
--port N the port to bind (default 8090; 0 takes an ephemeral one)
|
|
678
|
+
--host ADDRESS the address to bind (default 127.0.0.1)
|
|
679
|
+
|
|
680
|
+
Connect: macOS Finder, Go > Connect to Server, http://127.0.0.1:8090/ ;
|
|
681
|
+
Windows, Map network drive, http://127.0.0.1:8090/ ; GNOME Files,
|
|
682
|
+
dav://127.0.0.1:8090/ .
|
|
683
|
+
|
|
684
|
+
The server makes no protocol decision: every method, status, property, lock
|
|
685
|
+
and mapping is decided by formal/lean4/L4Factoidal/WebDAV/. A WebAssembly
|
|
686
|
+
module built before the WebDAV operations landed makes this command exit 3
|
|
687
|
+
and say so. Design: docs/designissues/2026-10-27-webdav.md
|
|
688
|
+
(https://github.com/danbri/factoidal/issues/1066).`,
|
|
689
|
+
|
|
619
690
|
'solid-client': `factoidal solid-client - run one Solid client operation
|
|
620
691
|
|
|
621
692
|
usage: factoidal solid-client <get|put|post|delete|discover> URL [options]
|
|
@@ -750,7 +821,7 @@ const VALUE_OPTIONS = {
|
|
|
750
821
|
serve: new Set(['port', 'host', 'query-path', 'gsp-path', 'file', 'layout', 'tdb2']),
|
|
751
822
|
'lws-serve': new Set(['port', 'host', 'base', 'issuer', 'auth-config', 'owner', 'root-acl',
|
|
752
823
|
'audience', 'page-size', 'moves', 'controller-inbox', 'delivery-limit', 'delivery-window',
|
|
753
|
-
'redirects-max', 'storage', 'cors-origin']),
|
|
824
|
+
'redirects-max', 'storage', 'cors-origin', 'authz', 'alg', 'token-lifetime', 'rotate-after']),
|
|
754
825
|
'solid-serve': new Set(['port', 'host', 'base', 'owner', 'issuer', 'auth-config']),
|
|
755
826
|
'solid-client': new Set(['body', 'file', 'content-type', 'interpret']),
|
|
756
827
|
'lws-client': new Set(['body', 'file', 'content-type', 'slug', 'type', 'link', 'if-match',
|
|
@@ -760,7 +831,8 @@ const VALUE_OPTIONS = {
|
|
|
760
831
|
'authz-serve': new Set(['port', 'host', 'issuer', 'resource', 'alg', 'token-lifetime', 'rotate-after',
|
|
761
832
|
'cors-origin']),
|
|
762
833
|
'authz-keys': new Set([]),
|
|
763
|
-
'xmpp-serve': new Set(['port', 'host', 'domain', 'accounts', 'cert', 'key'])
|
|
834
|
+
'xmpp-serve': new Set(['port', 'host', 'domain', 'accounts', 'cert', 'key']),
|
|
835
|
+
'webdav-serve': new Set(['port', 'host'])
|
|
764
836
|
}
|
|
765
837
|
|
|
766
838
|
// ------------------------------------------------------------- commands
|
|
@@ -931,14 +1003,154 @@ function printTable (headers, rows) {
|
|
|
931
1003
|
for (const row of rows) out(line(row))
|
|
932
1004
|
}
|
|
933
1005
|
|
|
1006
|
+
// --------------------------------------------------------------- SBM11
|
|
1007
|
+
|
|
1008
|
+
let sbm11 = null
|
|
1009
|
+
|
|
1010
|
+
/** The SBM11 functions of lib/sbm11.mjs over this command's engine, with
|
|
1011
|
+
* the Node directory reader as the default reader (it moves bytes only). */
|
|
1012
|
+
function sbm11Functions () {
|
|
1013
|
+
if (sbm11 === null) {
|
|
1014
|
+
setDefaultSbm11Reader(nodeDefaultReaderFactory())
|
|
1015
|
+
sbm11 = sbm11Api(loadEngine)
|
|
1016
|
+
}
|
|
1017
|
+
return sbm11
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* The engine's answer (op storeLayout) when `source` holds an SBM11
|
|
1022
|
+
* generation, or names one through CURRENT: `{layout, generation,
|
|
1023
|
+
* manifest}`. null for an IBK store, for a directory the op does not
|
|
1024
|
+
* recognise, and for an engine built before the op; the IBK path then
|
|
1025
|
+
* runs, and reports a missing store in its own words.
|
|
1026
|
+
*/
|
|
1027
|
+
function sbm11Layout (engine, source) {
|
|
1028
|
+
sbm11Functions()
|
|
1029
|
+
let answer
|
|
1030
|
+
try {
|
|
1031
|
+
answer = engine.call('storeLayout', [source])
|
|
1032
|
+
} catch (_error) {
|
|
1033
|
+
return null
|
|
1034
|
+
}
|
|
1035
|
+
return answer && answer.layout === 'sbm11' ? answer : null
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/** The directory a command reads: STORE, or STORE/GENERATION when
|
|
1039
|
+
* --generation names one. */
|
|
1040
|
+
function storeSource (root, named) {
|
|
1041
|
+
return named === null ? root : joinPath(root, named)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/** The engine's words of a refusal, without the loader's prefix. */
|
|
1045
|
+
function engineWords (error) {
|
|
1046
|
+
const message = error && error.message ? String(error.message) : String(error)
|
|
1047
|
+
return message.replace(/^l4factoidal:\s*/, '')
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
async function inspectSbm11 (root, named, layout, options) {
|
|
1051
|
+
let store
|
|
1052
|
+
try {
|
|
1053
|
+
store = await sbm11Functions().openSbm11(storeSource(root, named))
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
err(`factoidal inspect: ${engineWords(error)}`)
|
|
1056
|
+
return EXIT_FAILURE
|
|
1057
|
+
}
|
|
1058
|
+
const terms = store.terms
|
|
1059
|
+
store.close()
|
|
1060
|
+
const generation = named !== null ? named : layout.generation
|
|
1061
|
+
const generationDir = named !== null || layout.generation === ''
|
|
1062
|
+
? storeSource(root, named)
|
|
1063
|
+
: joinPath(root, layout.generation)
|
|
1064
|
+
const files = listGeneration(generationDir)
|
|
1065
|
+
let directoryBytes = 0
|
|
1066
|
+
for (const file of files) directoryBytes += file.size
|
|
1067
|
+
if (options.json) {
|
|
1068
|
+
out(JSON.stringify({
|
|
1069
|
+
ok: true,
|
|
1070
|
+
layout: 'sbm11',
|
|
1071
|
+
generation,
|
|
1072
|
+
activated: named === null && layout.generation !== '',
|
|
1073
|
+
manifest: layout.manifest,
|
|
1074
|
+
terms,
|
|
1075
|
+
files: files.length,
|
|
1076
|
+
bytes: directoryBytes
|
|
1077
|
+
}, null, 2))
|
|
1078
|
+
return EXIT_OK
|
|
1079
|
+
}
|
|
1080
|
+
out(`store ${root}`)
|
|
1081
|
+
if (named !== null) out(`generation ${generation} (named on the command line)`)
|
|
1082
|
+
else if (layout.generation !== '') out(`generation ${generation} (activated through CURRENT)`)
|
|
1083
|
+
else out('generation directory (no CURRENT)')
|
|
1084
|
+
out(`manifest ${layout.manifest}`)
|
|
1085
|
+
out('layout sbm11')
|
|
1086
|
+
out(plural(terms, 'term'))
|
|
1087
|
+
out(`generation directory holds ${files.length} files, ${directoryBytes} bytes`)
|
|
1088
|
+
return EXIT_OK
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Answer a query on an SBM11 store (ops sbm11Open / sbm11Query /
|
|
1093
|
+
* sbm11Close) and print it as `query` prints an IBK answer. The engine
|
|
1094
|
+
* answers SPARQL Query Results JSON for SELECT and ASK and N-Triples for
|
|
1095
|
+
* CONSTRUCT and DESCRIBE; this only chooses which renderer prints it.
|
|
1096
|
+
*/
|
|
1097
|
+
async function querySbm11 (engine, root, named, sparql, format, limit, options) {
|
|
1098
|
+
const quiet = options.quiet === true
|
|
1099
|
+
if (options.explain === true) {
|
|
1100
|
+
err('factoidal query: --explain prints the plan of an IBK store; there is ' +
|
|
1101
|
+
'no plan operation for an SBM11 store yet ' +
|
|
1102
|
+
'(https://github.com/danbri/factoidal/issues/1009)')
|
|
1103
|
+
return EXIT_FAILURE
|
|
1104
|
+
}
|
|
1105
|
+
let answer
|
|
1106
|
+
try {
|
|
1107
|
+
const store = await sbm11Functions().openSbm11(storeSource(root, named))
|
|
1108
|
+
try {
|
|
1109
|
+
const got = store.query(sparql)
|
|
1110
|
+
// read the bytes while the handle is open
|
|
1111
|
+
answer = { path: got.path, format: got.format, reads: got.reads,
|
|
1112
|
+
bytesRead: got.bytesRead, text: got.result }
|
|
1113
|
+
} finally {
|
|
1114
|
+
store.close()
|
|
1115
|
+
}
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
err(`factoidal query: ${engineWords(error)}`)
|
|
1118
|
+
if (isStackOverflow(error)) {
|
|
1119
|
+
for (const line of stackLimitAdvice(STACK_REMEDY.query)) err(line)
|
|
1120
|
+
}
|
|
1121
|
+
return EXIT_FAILURE
|
|
1122
|
+
}
|
|
1123
|
+
if (!quiet) {
|
|
1124
|
+
err(`${answer.path}, ${plural(answer.reads, 'read')}, ${answer.bytesRead} bytes read`)
|
|
1125
|
+
}
|
|
1126
|
+
let result
|
|
1127
|
+
if (answer.format === 'srj') {
|
|
1128
|
+
const document = JSON.parse(answer.text)
|
|
1129
|
+
if (typeof document.boolean === 'boolean') {
|
|
1130
|
+
if (format === 'json') {
|
|
1131
|
+
out(JSON.stringify(document, null, 2))
|
|
1132
|
+
return EXIT_OK
|
|
1133
|
+
}
|
|
1134
|
+
result = { kind: 'ask', boolean: document.boolean }
|
|
1135
|
+
} else {
|
|
1136
|
+
result = { kind: 'select', srj: document }
|
|
1137
|
+
}
|
|
1138
|
+
} else {
|
|
1139
|
+
result = { kind: 'construct', nquads: answer.text }
|
|
1140
|
+
}
|
|
1141
|
+
return renderQueryResult(engine, result, format, limit, quiet)
|
|
1142
|
+
}
|
|
1143
|
+
|
|
934
1144
|
// ------------------------------------------------------------- inspect
|
|
935
1145
|
|
|
936
1146
|
async function commandInspect (positional, options) {
|
|
937
1147
|
if (positional.length !== 1) throw new UsageError('inspect needs exactly one STORE')
|
|
938
1148
|
const root = positional[0]
|
|
939
1149
|
const named = typeof options.generation === 'string' ? options.generation : null
|
|
940
|
-
const store = openStore(root, named)
|
|
941
1150
|
const engine = await loadEngine()
|
|
1151
|
+
const layout = sbm11Layout(engine, storeSource(root, named))
|
|
1152
|
+
if (layout !== null) return inspectSbm11(root, named, layout, options)
|
|
1153
|
+
const store = openStore(root, named)
|
|
942
1154
|
const envelope = inspectManifest(engine, store)
|
|
943
1155
|
|
|
944
1156
|
if (options.json) {
|
|
@@ -1102,8 +1314,11 @@ async function commandQuery (positional, options) {
|
|
|
1102
1314
|
const quiet = options.quiet === true
|
|
1103
1315
|
|
|
1104
1316
|
const named = typeof options.generation === 'string' ? options.generation : null
|
|
1105
|
-
const store = openStore(root, named)
|
|
1106
1317
|
const engine = await loadEngine()
|
|
1318
|
+
if (sbm11Layout(engine, storeSource(root, named)) !== null) {
|
|
1319
|
+
return querySbm11(engine, root, named, sparql, format, limit, options)
|
|
1320
|
+
}
|
|
1321
|
+
const store = openStore(root, named)
|
|
1107
1322
|
|
|
1108
1323
|
if (options.explain === true) {
|
|
1109
1324
|
let plan
|
|
@@ -1208,9 +1423,15 @@ function readHead (path, length) {
|
|
|
1208
1423
|
/** The syntax tag for an input: --syntax when given, else the engine's
|
|
1209
1424
|
* suffix table (op engineTables), else the engine's content detection
|
|
1210
1425
|
* (op detectFormat) on the first 64 KiB of the file. */
|
|
1211
|
-
function packSyntax (engine, input, options) {
|
|
1426
|
+
function packSyntax (engine, input, options, layout) {
|
|
1212
1427
|
const tables = engine.call('engineTables', []).tables
|
|
1213
|
-
|
|
1428
|
+
// The SBM11 pack reads the whole document (op sbm11PackFiles), so it also
|
|
1429
|
+
// reads the syntaxes of packWholeSuffixes (RDF/XML, JSON-LD).
|
|
1430
|
+
const whole = layout === 'sbm11' ? (tables.packWholeSuffixes ?? {}) : {}
|
|
1431
|
+
const syntaxes = tables.packSyntaxes.slice()
|
|
1432
|
+
for (const syntax of Object.values(whole)) {
|
|
1433
|
+
if (syntaxes.indexOf(syntax) < 0) syntaxes.push(syntax)
|
|
1434
|
+
}
|
|
1214
1435
|
if (typeof options.syntax === 'string') {
|
|
1215
1436
|
const syntax = options.syntax.toLowerCase()
|
|
1216
1437
|
if (syntaxes.indexOf(syntax) >= 0) return syntax
|
|
@@ -1220,11 +1441,14 @@ function packSyntax (engine, input, options) {
|
|
|
1220
1441
|
for (const [suffix, syntax] of Object.entries(tables.packSuffixes)) {
|
|
1221
1442
|
if (lower.endsWith(suffix)) return syntax
|
|
1222
1443
|
}
|
|
1444
|
+
for (const [suffix, syntax] of Object.entries(whole)) {
|
|
1445
|
+
if (lower.endsWith(suffix)) return syntax
|
|
1446
|
+
}
|
|
1223
1447
|
for (const [suffix, name] of Object.entries(tables.packUnsupportedSuffixes)) {
|
|
1224
1448
|
if (lower.endsWith(suffix)) {
|
|
1225
1449
|
throw new UsageError(
|
|
1226
|
-
`pack does not read ${name}. The packer
|
|
1227
|
-
`${syntaxes.join(', ')} only. Convert the file first, for ` +
|
|
1450
|
+
`pack does not read ${name}. The packer reads ` +
|
|
1451
|
+
`${syntaxes.join(', ')} only${layout === 'sbm11' ? '' : ' (--layout sbm11 also reads rdfxml and jsonld)'}. Convert the file first, for ` +
|
|
1228
1452
|
"example with: factoidal parse FILE --out nquads")
|
|
1229
1453
|
}
|
|
1230
1454
|
}
|
|
@@ -1253,8 +1477,11 @@ function packSyntax (engine, input, options) {
|
|
|
1253
1477
|
* `--base ''` asks for no base, which turns a relative IRI into a parse
|
|
1254
1478
|
* error rather than a silently different term.
|
|
1255
1479
|
*/
|
|
1256
|
-
function packBase (input, options) {
|
|
1480
|
+
function packBase (input, options, layout) {
|
|
1257
1481
|
if (typeof options.base === 'string') return options.base
|
|
1482
|
+
// `l4sbm11-pack` uses no base unless --base names one; the same default
|
|
1483
|
+
// gives the same bytes.
|
|
1484
|
+
if (layout === 'sbm11') return ''
|
|
1258
1485
|
const absolute = input.startsWith('/') ? input : joinPath(currentDirectory(), input)
|
|
1259
1486
|
return 'file://' + absolute
|
|
1260
1487
|
}
|
|
@@ -1275,8 +1502,8 @@ function packLayout (options) {
|
|
|
1275
1502
|
async function commandPack (positional, options) {
|
|
1276
1503
|
if (positional.length !== 2) throw new UsageError('pack needs INPUT and OUTPUT')
|
|
1277
1504
|
const [input, output] = positional
|
|
1278
|
-
const syntax = packSyntax(await loadEngine(), input, options)
|
|
1279
1505
|
const layout = packLayout(options)
|
|
1506
|
+
const syntax = packSyntax(await loadEngine(), input, options, layout)
|
|
1280
1507
|
// The pack fold needs a bigger call stack than either runtime gives by
|
|
1281
1508
|
// default (https://github.com/danbri/factoidal/issues/649). Under Node
|
|
1282
1509
|
// the work runs on a worker thread with a raised stack; under Deno the
|
|
@@ -1291,8 +1518,10 @@ async function commandPack (positional, options) {
|
|
|
1291
1518
|
let answer
|
|
1292
1519
|
try {
|
|
1293
1520
|
answer = await runPack(
|
|
1294
|
-
|
|
1295
|
-
|
|
1521
|
+
layout === 'sbm11'
|
|
1522
|
+
? { kind: 'packSbm11', input, output, syntax, base: packBase(input, options, layout) }
|
|
1523
|
+
: { kind: 'pack', input, output, syntax, layout, base: packBase(input, options, layout),
|
|
1524
|
+
batchBytes: options['batch-bytes'] },
|
|
1296
1525
|
quiet
|
|
1297
1526
|
? undefined
|
|
1298
1527
|
: (progress) => {
|
|
@@ -1331,7 +1560,7 @@ async function commandPack (positional, options) {
|
|
|
1331
1560
|
return EXIT_OK
|
|
1332
1561
|
}
|
|
1333
1562
|
out(`packed ${report.bytesRead} bytes of ${syntax} into ${output}`)
|
|
1334
|
-
out(`${report.written.length} artifacts, ${report.bytesWritten} bytes, layout ${layout}`)
|
|
1563
|
+
out(`${report.written.length} ${layout === 'sbm11' ? 'files' : 'artifacts'}, ${report.bytesWritten} bytes, layout ${layout}`)
|
|
1335
1564
|
if (typeof report.rows === 'number') out(`${plural(report.rows, 'row')}`)
|
|
1336
1565
|
out(`activate it with: factoidal activate ${dirOf(output)} ${nameOf(output)}`)
|
|
1337
1566
|
return EXIT_OK
|
|
@@ -1382,6 +1611,10 @@ async function commandActivate (positional, options) {
|
|
|
1382
1611
|
const host = { worker: options['no-worker'] !== true }
|
|
1383
1612
|
const reexec = await denoReexec(host)
|
|
1384
1613
|
if (reexec !== null) return reexec
|
|
1614
|
+
const engine = await loadEngine()
|
|
1615
|
+
if (sbm11Layout(engine, joinPath(root, generation)) !== null) {
|
|
1616
|
+
return activateSbm11(engine, root, generation, options)
|
|
1617
|
+
}
|
|
1385
1618
|
let answer
|
|
1386
1619
|
try {
|
|
1387
1620
|
answer = await runPack({ kind: 'activate', root, generation }, undefined, host)
|
|
@@ -1425,6 +1658,32 @@ async function commandActivate (positional, options) {
|
|
|
1425
1658
|
return EXIT_OK
|
|
1426
1659
|
}
|
|
1427
1660
|
|
|
1661
|
+
/**
|
|
1662
|
+
* Activate an SBM11 generation: the engine checks every file against the
|
|
1663
|
+
* manifest (op sbm11Verify), and only after its "ok" does CURRENT move.
|
|
1664
|
+
*/
|
|
1665
|
+
function activateSbm11 (engine, root, generation, options) {
|
|
1666
|
+
let verdict
|
|
1667
|
+
try {
|
|
1668
|
+
verdict = engine.call('sbm11Verify', [joinPath(root, generation)])
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
err(`factoidal activate: ${engineWords(error)}`)
|
|
1671
|
+
err('The generation is NOT activated; CURRENT is unchanged.')
|
|
1672
|
+
return EXIT_FAILURE
|
|
1673
|
+
}
|
|
1674
|
+
const synced = atomicReplace(joinPath(root, 'CURRENT'), new TextEncoder().encode(generation))
|
|
1675
|
+
if (options.json === true) {
|
|
1676
|
+
out(JSON.stringify({ ...verdict, layout: 'sbm11', generation, directorySynced: synced }, null, 2))
|
|
1677
|
+
return EXIT_OK
|
|
1678
|
+
}
|
|
1679
|
+
out(`activated ${generation}: ${verdict.artifacts} artifacts verified (${verdict.files} files, ${verdict.bytes} bytes), layout sbm11`)
|
|
1680
|
+
if (!synced) {
|
|
1681
|
+
err('CURRENT was replaced, but the directory entry was not synced; a ' +
|
|
1682
|
+
'crash now could lose the pointer update.')
|
|
1683
|
+
}
|
|
1684
|
+
return EXIT_OK
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1428
1687
|
function commandUpdate (positional, options) {
|
|
1429
1688
|
if (positional.length < 1) throw new UsageError('update needs a STORE')
|
|
1430
1689
|
const text = positional.length > 1
|
|
@@ -1529,6 +1788,10 @@ async function commandServe (positional, options) {
|
|
|
1529
1788
|
const { join } = await import('node:path')
|
|
1530
1789
|
const root = mkdtempSync(join(tmpdir(), 'factoidal-serve-'))
|
|
1531
1790
|
const packOptions = { quiet: true, layout: packLayout(options) }
|
|
1791
|
+
if (packOptions.layout === 'sbm11') {
|
|
1792
|
+
throw new UsageError('serve --file packs a store that accepts updates; --layout sbm11 ' +
|
|
1793
|
+
'packs a store that serve does not read yet. Use ibk3, ibk4 or ibk5.')
|
|
1794
|
+
}
|
|
1532
1795
|
let rc = await commandPack([options.file, join(root, 'gen-1')], packOptions)
|
|
1533
1796
|
if (rc !== 0) return rc
|
|
1534
1797
|
rc = await commandActivate([root, 'gen-1'], { quiet: true })
|
|
@@ -1549,6 +1812,12 @@ async function commandServe (positional, options) {
|
|
|
1549
1812
|
options = { ...options, 'query-path': `${dsName}/sparql`, 'gsp-path': `${dsName}/data` }
|
|
1550
1813
|
}
|
|
1551
1814
|
if (positional.length !== 1) throw new UsageError('serve needs one STORE')
|
|
1815
|
+
if (!tdb2 && sbm11Layout(await loadEngine(), positional[0]) !== null) {
|
|
1816
|
+
err(`factoidal serve: ${positional[0]} is an SBM11 store, and serve reads IBK ` +
|
|
1817
|
+
'stores only (the storeServe operations). Query it with: factoidal query ' +
|
|
1818
|
+
`${positional[0]} QUERY (https://github.com/danbri/factoidal/issues/1009)`)
|
|
1819
|
+
return EXIT_FAILURE
|
|
1820
|
+
}
|
|
1552
1821
|
const { listen, listenOnWorker, ServeHostError } = await import('./serve.mjs')
|
|
1553
1822
|
const serveOptions = {
|
|
1554
1823
|
port: servePort(options, 3030),
|
|
@@ -1590,12 +1859,25 @@ async function commandLwsServe (positional, options) {
|
|
|
1590
1859
|
const insecure = options['insecure-no-auth'] === true
|
|
1591
1860
|
const issuerOption = typeof options.issuer === 'string' ? options.issuer : null
|
|
1592
1861
|
const authFile = typeof options['auth-config'] === 'string' ? options['auth-config'] : null
|
|
1593
|
-
|
|
1594
|
-
|
|
1862
|
+
const authzDir = typeof options.authz === 'string' ? options.authz : null
|
|
1863
|
+
if (!insecure && issuerOption === null && authFile === null && authzDir === null) {
|
|
1864
|
+
throw new UsageError('lws-serve needs --authz DIR, --issuer IRI or --auth-config FILE, ' +
|
|
1595
1865
|
'or --insecure-no-auth for development')
|
|
1596
1866
|
}
|
|
1597
|
-
if (insecure && (issuerOption !== null || authFile !== null)) {
|
|
1598
|
-
throw new UsageError('--insecure-no-auth cannot be combined with --issuer or --auth-config')
|
|
1867
|
+
if (insecure && (issuerOption !== null || authFile !== null || authzDir !== null)) {
|
|
1868
|
+
throw new UsageError('--insecure-no-auth cannot be combined with --authz, --issuer or --auth-config')
|
|
1869
|
+
}
|
|
1870
|
+
// --authz DIR: an authorization server on the same port, its issuer the
|
|
1871
|
+
// storage root (L4Factoidal/Authz/Mount.lean). Its options are passed
|
|
1872
|
+
// unread; the engine checks them (Wasm/Ops/Authz.lean).
|
|
1873
|
+
const authz = authzDir === null ? undefined : {
|
|
1874
|
+
dir: authzDir,
|
|
1875
|
+
...(typeof options.alg === 'string' ? { alg: options.alg } : {}),
|
|
1876
|
+
...(options['token-lifetime'] !== undefined ? { tokenLifetime: natOption('token-lifetime', options['token-lifetime']) } : {}),
|
|
1877
|
+
...(options['rotate-after'] !== undefined ? { rotateAfter: natOption('rotate-after', options['rotate-after']) } : {}),
|
|
1878
|
+
...(options['allow-http'] === true ? { allowHttp: true } : {}),
|
|
1879
|
+
...(options['allow-private-fetch'] === true ? { allowPrivateFetch: true } : {}),
|
|
1880
|
+
...corsConfig(options)
|
|
1599
1881
|
}
|
|
1600
1882
|
let auth
|
|
1601
1883
|
if (authFile !== null) {
|
|
@@ -1628,6 +1910,7 @@ async function commandLwsServe (positional, options) {
|
|
|
1628
1910
|
...(typeof options.storage === 'string' ? { storage: options.storage } : {}),
|
|
1629
1911
|
...corsConfig(options),
|
|
1630
1912
|
...(insecure ? { insecureNoAuth: true } : { auth }),
|
|
1913
|
+
...(authz !== undefined ? { authz } : {}),
|
|
1631
1914
|
...(typeof options.owner === 'string' ? { owner: options.owner } : {}),
|
|
1632
1915
|
...(typeof options['root-acl'] === 'string'
|
|
1633
1916
|
? { rootAcl: new TextDecoder('utf-8', { fatal: true }).decode(readWhole(options['root-acl'])) }
|
|
@@ -1651,6 +1934,9 @@ async function commandLwsServe (positional, options) {
|
|
|
1651
1934
|
}
|
|
1652
1935
|
if (!quiet) {
|
|
1653
1936
|
err(`lws-serve: listening on ${running.origin} for ${positional[0]}`)
|
|
1937
|
+
if (running.authz() !== null) err(`lws-serve: authorization server ${running.authz().issuer} (keys in ${authzDir})`)
|
|
1938
|
+
if (options['allow-http'] === true) err('lws-serve: WARNING: --allow-http: http subject identifiers are accepted')
|
|
1939
|
+
if (options['allow-private-fetch'] === true) err('lws-serve: WARNING: --allow-private-fetch: private addresses may be fetched')
|
|
1654
1940
|
if (insecure) err('lws-serve: WARNING: --insecure-no-auth: every request is served without authorization')
|
|
1655
1941
|
}
|
|
1656
1942
|
out(running.origin)
|
|
@@ -1785,6 +2071,34 @@ async function commandSolidServe (positional, options) {
|
|
|
1785
2071
|
return untilInterrupted(running, quiet)
|
|
1786
2072
|
}
|
|
1787
2073
|
|
|
2074
|
+
// webdav-serve CONFIGDIR (https://github.com/danbri/factoidal/issues/1066):
|
|
2075
|
+
// the host carries sockets, files and fetches; the engine reads the mount
|
|
2076
|
+
// files and decides every answer (L4Factoidal/WebDAV/).
|
|
2077
|
+
async function commandWebdavServe (positional, options) {
|
|
2078
|
+
if (positional.length !== 1) throw new UsageError('webdav-serve needs one CONFIGDIR')
|
|
2079
|
+
const quiet = options.quiet === true
|
|
2080
|
+
const { listen, WebdavHostError } = await import('../webdav/server.mjs')
|
|
2081
|
+
let running
|
|
2082
|
+
try {
|
|
2083
|
+
running = await listen({ configDir: positional[0], port: servePort(options, 8090), host: serveHost(options) })
|
|
2084
|
+
} catch (error) {
|
|
2085
|
+
if (error instanceof WebdavHostError && error.unknownOp) return notWired('webdav-serve', error.message)
|
|
2086
|
+
if (error instanceof WebdavHostError || (error !== null && typeof error === 'object' && error.code === 'ENOENT')) {
|
|
2087
|
+
err(`factoidal webdav-serve: ${error.message}`)
|
|
2088
|
+
return EXIT_FAILURE
|
|
2089
|
+
}
|
|
2090
|
+
throw error
|
|
2091
|
+
}
|
|
2092
|
+
if (!quiet) {
|
|
2093
|
+
err(`webdav-serve: listening on ${running.origin} for ${positional[0]}`)
|
|
2094
|
+
for (const m of running.mounts) {
|
|
2095
|
+
err(`webdav-serve: ${running.origin}/${encodeURIComponent(m.name)}/ - ${m.title} (${m.kind}${m.writable ? '' : ', read-only'}${m.agent ? ', as ' + m.agent : ''})`)
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
out(running.origin)
|
|
2099
|
+
return untilInterrupted(running, quiet)
|
|
2100
|
+
}
|
|
2101
|
+
|
|
1788
2102
|
async function commandXmppServe (positional, options) {
|
|
1789
2103
|
if (positional.length !== 1) throw new UsageError('xmpp-serve needs one DIR')
|
|
1790
2104
|
const quiet = options.quiet === true
|
|
@@ -1965,7 +2279,8 @@ const COMMANDS = {
|
|
|
1965
2279
|
'lws-client': commandLwsClient,
|
|
1966
2280
|
'authz-serve': commandAuthzServe,
|
|
1967
2281
|
'authz-keys': commandAuthzKeys,
|
|
1968
|
-
'xmpp-serve': commandXmppServe
|
|
2282
|
+
'xmpp-serve': commandXmppServe,
|
|
2283
|
+
'webdav-serve': commandWebdavServe
|
|
1969
2284
|
}
|
|
1970
2285
|
|
|
1971
2286
|
// ----------------------------------------------------------------- main
|
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
|
*
|