@factoidal/core 0.12.0 → 0.14.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/bin/factoidal.mjs CHANGED
@@ -101,6 +101,11 @@ commands:
101
101
  lws-serve DIR serve Linked Web Storage 1.0 over HTTP
102
102
  solid-serve DIR serve the Solid Protocol over HTTP
103
103
  solid-client OP URL run one Solid client operation
104
+ lws-client OP URL run one Linked Web Storage client operation
105
+ authz-serve DIR serve an OAuth 2.0 authorization server for LWS
106
+ (RFC 8414 metadata, JWK Set, RFC 8693 token exchange)
107
+ authz-keys DIR print the public JWK Set of an authorization server
108
+ xmpp-serve DIR serve XMPP (RFC 6120/6121) over TCP or direct TLS
104
109
 
105
110
  global options:
106
111
  -h, --help print this text, or a command's own help
@@ -219,9 +224,9 @@ INPUT is an RDF file. OUTPUT is the generation directory to create; it is
219
224
  normally STORE/gen-N. Packing does not activate: run activate next.
220
225
 
221
226
  options:
222
- --layout LAYOUT ibk3 (triples, default), ibk4 (quads, wire version 9)
223
- or ibk5 (quads, wire version 10: RDF 1.2 terms,
224
- out-of-line literals, zone maps)
227
+ --layout LAYOUT ibk5 (quads, wire version 10: RDF 1.2 terms,
228
+ out-of-line literals, zone maps; default), ibk4
229
+ (quads, wire version 9) or ibk3 (triples)
225
230
  --syntax SYNTAX turtle, trig or nquads; default from the file extension
226
231
  --chunk-bytes N Merkle chunk size; default is the engine's
227
232
  --batch-bytes N ibk4 only: source bytes between publication batches;
@@ -314,7 +319,7 @@ activated generation's delta log as one batch before the response is sent,
314
319
  and is visible to the next request. An IBK3 (triples) generation accepts
315
320
  changes to the default graph only, because its delta log is replayed and
316
321
  compacted for that graph only; a change to a named graph answers 501. A
317
- quad generation (ibk4, ibk5) accepts every graph. Fold the log into a new
322
+ quad generation (ibk4, ibk5, the default) accepts every graph. Fold the log into a new
318
323
  generation with l4block-shard-compact, activate it, and restart the server.
319
324
 
320
325
  The server makes no protocol decision: the request framing, routing,
@@ -328,6 +333,50 @@ Specifications: https://www.w3.org/TR/sparql11-protocol/
328
333
  https://www.w3.org/TR/sparql11-http-rdf-update/
329
334
  https://www.w3.org/TR/sparql11-service-description/`,
330
335
 
336
+ 'authz-serve': `factoidal authz-serve - serve an OAuth 2.0 authorization server
337
+
338
+ usage: factoidal authz-serve DIR --resource IRI[,IRI...] [--issuer IRI]
339
+ [--port N] [--host ADDRESS] [--alg ES256|EdDSA]
340
+ [--token-lifetime SECONDS] [--rotate-after SECONDS]
341
+ [--allow-http] [--allow-private-fetch]
342
+ [--cors-origin ORIGIN]... [--no-cors]
343
+
344
+ An authorization server as a component (https://github.com/danbri/factoidal/issues/916),
345
+ for lws-serve and any resource server that reads RFC 8414 metadata:
346
+ GET /.well-known/lws-configuration (also /.well-known/oauth-authorization-server)
347
+ GET /jwks the JWK Set (public keys only)
348
+ POST /token RFC 8693 token exchange for an RFC 9068
349
+ access token whose aud is the resource
350
+ Subject tokens: OpenID Connect ID tokens (lws10-authn-openid) and
351
+ self-issued JWTs for https and did:key subjects (lws10-authn-ssi-cid). SAML
352
+ 2.0 is not accepted (https://github.com/danbri/factoidal/issues/926).
353
+
354
+ --resource names the resource IRIs (for lws-serve: BASE.well-known/lws-storage)
355
+ tokens may be issued for; any other is refused with invalid_target.
356
+ --issuer defaults to the bound origin. Give lws-serve --issuer the same IRI.
357
+ DIR holds the signing keys (DIR/authz-keys.json, mode 0600), sealed with
358
+ FACTOIDAL_LWS_KEY when it is set. --rotate-after makes a new key when the
359
+ current one is that old; the old key stays published until its tokens expire.
360
+ --allow-http accepts http subject identifiers; --allow-private-fetch lets the
361
+ server fetch documents from loopback, link-local and private addresses. Both
362
+ are off by default and are for tests.
363
+ CORS (https://github.com/danbri/factoidal/issues/923): by default every answer
364
+ echoes a valid request Origin in Access-Control-Allow-Origin, so a page on
365
+ another origin can read the metadata and keys and exchange a token; a
366
+ preflight is answered 204 first. --cors-origin ORIGIN (repeatable) answers
367
+ only those origins; --no-cors sends no CORS fields.
368
+
369
+ Every decision is made in formal/lean4/L4Factoidal/Authz/; this command only
370
+ carries sockets, the clock, random bytes, DNS answers and files.`,
371
+
372
+ 'authz-keys': `factoidal authz-keys - print the JWK Set of an authorization server
373
+
374
+ usage: factoidal authz-keys DIR [--private]
375
+
376
+ Prints the published JWK Set of the key file in DIR. --private prints the
377
+ private keys (with d): for a conformance harness that mints test tokens
378
+ (tools/lws-touchstone.sh --authz factoidal). Never publish that output.`,
379
+
331
380
  'lws-serve': `factoidal lws-serve - serve Linked Web Storage 1.0 over HTTP
332
381
 
333
382
  usage: factoidal lws-serve DIR (--issuer IRI | --auth-config FILE | --insecure-no-auth)
@@ -338,6 +387,7 @@ usage: factoidal lws-serve DIR (--issuer IRI | --auth-config FILE | --insecure-n
338
387
  [--controller-inbox URL] [--delivery-limit N]
339
388
  [--delivery-window SECONDS] [--redirects-max N]
340
389
  [--storage snapshot|files]
390
+ [--cors-origin ORIGIN]... [--no-cors]
341
391
 
342
392
  Starts a Node HTTP server that answers every request through the engine's
343
393
  lwsStep operation.
@@ -431,6 +481,18 @@ It protects the file on disk only (formal/lean4/L4Factoidal/LWS/SnapshotSeal.lea
431
481
  each change, or one file per body with a journal, so a
432
482
  write costs the same whatever the storage size
433
483
  https://github.com/danbri/factoidal/issues/918
484
+ --cors-origin ORIGIN
485
+ answer CORS (Fetch Standard) only for this origin, written
486
+ scheme://host[:port] (for example https://danbri.github.io);
487
+ give it once per origin. Default: echo any valid request
488
+ Origin, as the Solid Protocol section 8.1 requires of Solid
489
+ servers. Never Access-Control-Allow-Credentials: the token
490
+ travels in Authorization. A preflight (OPTIONS with
491
+ Access-Control-Request-Method) is answered 204 before any
492
+ authorization decision.
493
+ https://github.com/danbri/factoidal/issues/923
494
+ --no-cors send no CORS fields; a page on another origin cannot read
495
+ any answer
434
496
 
435
497
  Authorization (LWS editor's draft, section Authorization; one of the three
436
498
  is required):
@@ -518,6 +580,42 @@ those operations landed makes this command exit 3 and say so.
518
580
 
519
581
  Specification: https://solidproject.org/TR/protocol`,
520
582
 
583
+ 'xmpp-serve': `factoidal xmpp-serve - serve XMPP (RFC 6120 and RFC 6121) over TCP or direct TLS
584
+
585
+ usage: factoidal xmpp-serve DIR [--domain NAME] [--port N] [--host ADDRESS]
586
+ [--accounts FILE] [--cert FILE --key FILE]
587
+
588
+ Starts a Node TCP server that runs one engine session per connection and
589
+ answers every byte through the engine's xmppSessionFeed operation.
590
+
591
+ DIR is the state directory, laid out as the native carrier's
592
+ (formal/lean4/Harness/XmppServe.lean): DIR/accounts holds one
593
+ user:password per line (unless --accounts names another file), rosters
594
+ are persisted under DIR/roster/, and a stanza written as a file under
595
+ DIR/spool/<bare jid>/ is delivered to that account's bound session.
596
+
597
+ --domain NAME the domain the server is authoritative for (default localhost);
598
+ a stream for any other domain is answered host-unknown
599
+ --port N the port to bind; 0 takes an ephemeral one
600
+ (default 5222, or 5223 with --cert and --key)
601
+ --host ADDRESS the address to bind (default 127.0.0.1)
602
+ --accounts FILE the accounts file (default DIR/accounts)
603
+ --cert FILE PEM certificate chain: XEP-0368 direct TLS, no STARTTLS
604
+ --key FILE PEM private key for --cert
605
+
606
+ The server makes no protocol decision. Stream negotiation, SASL PLAIN and
607
+ SCRAM-SHA-256, resource binding, the roster, presence subscription and
608
+ broadcast, message routing with the server-stamped from, stream errors and
609
+ disco#info are answered by formal/lean4/L4Factoidal/XMPP/. A WebAssembly
610
+ module built before the session operations landed makes this command
611
+ exit 3 and say so.
612
+
613
+ Not implemented: STARTTLS (use --cert/--key), server-to-server federation,
614
+ MUC/MIX/GC3 rooms.
615
+
616
+ Specifications: https://www.rfc-editor.org/rfc/rfc6120 and
617
+ https://www.rfc-editor.org/rfc/rfc6121`,
618
+
521
619
  'solid-client': `factoidal solid-client - run one Solid client operation
522
620
 
523
621
  usage: factoidal solid-client <get|put|post|delete|discover> URL [options]
@@ -539,7 +637,50 @@ kind names are accepted directly as well.
539
637
  --interpret KIND one of storage, containment, auxiliaries, profile,
540
638
  wacAllow, used when the engine names none
541
639
 
542
- Specification: https://solidproject.org/TR/protocol`
640
+ Specification: https://solidproject.org/TR/protocol`,
641
+
642
+ 'lws-client': `factoidal lws-client - run one Linked Web Storage client operation
643
+
644
+ usage: factoidal lws-client <op> URL [options]
645
+
646
+ Every request is chosen and built by the engine (lwsClientStart,
647
+ lwsClientStep; formal/lean4/L4Factoidal/LWS/Client/): the authorization
648
+ flow with its discovery checks, preconditions, retries, paging and linksets.
649
+ This command supplies a socket and prints the engine's result as JSON. It
650
+ exits 1 when the operation fails, and prints the failure document.
651
+
652
+ operations:
653
+ discover URL read the storage description of the storage of URL
654
+ read URL | head URL | getAccess URL
655
+ create CONTAINER --body/--file, --content-type, --slug, --container,
656
+ --type IRI, --link VALUE
657
+ update URL --body/--file, --content-type, --link VALUE
658
+ patch URL --body/--file (JSON Patch unless --content-type)
659
+ delete URL --recursive
660
+ readLinkset URL | updateLinkset URL (--body/--file, --content-type)
661
+ move URL --destination CONTAINER
662
+ list CONTAINER every page, following rel="next"
663
+ typeIndex URL | search URL --query JSON
664
+ subscribe URL --inbox URL [--topic URL]
665
+ unsubscribe URL | revokeAccess URL
666
+ requestAccess URL | grantAccess URL
667
+ --action A[,A] --assignee URI [--target URL[,URL]]
668
+ [--inbox URL]
669
+
670
+ preconditions (update, patch, delete, updateLinkset, move):
671
+ --if-match TAG | --if-match-current | --if-none-match-star |
672
+ --if-unmodified-since DATE | --no-precondition
673
+ (default: --if-match-current for update, patch, updateLinkset and move)
674
+ --retry-412 N read the tag again and resend, at most N times
675
+
676
+ authorization:
677
+ --subject-token-file FILE the credential to exchange (RFC 8693)
678
+ --subject-token-type URI its type, for example
679
+ urn:ietf:params:oauth:token-type:saml2
680
+ --access-token TOKEN a token to present to the storage of URL
681
+ (with --storage IRI and --root URL)
682
+
683
+ Draft: https://w3c.github.io/lws-protocol/lws10-core/`
543
684
  }
544
685
 
545
686
  // ------------------------------------------------------------- arguments
@@ -567,8 +708,16 @@ function parseArguments (args, valueOptions) {
567
708
  }
568
709
  const equals = argument.indexOf('=')
569
710
  const name = equals < 0 ? argument.slice(2) : argument.slice(2, equals)
711
+ // An option named in REPEATABLE_OPTIONS keeps every value, in order.
712
+ const keep = (value) => {
713
+ if (REPEATABLE_OPTIONS.has(name)) {
714
+ options[name] = Array.isArray(options[name]) ? [...options[name], value] : [value]
715
+ } else {
716
+ options[name] = value
717
+ }
718
+ }
570
719
  if (equals >= 0) {
571
- options[name] = argument.slice(equals + 1)
720
+ keep(argument.slice(equals + 1))
572
721
  continue
573
722
  }
574
723
  if (valueOptions.has(name)) {
@@ -576,7 +725,7 @@ function parseArguments (args, valueOptions) {
576
725
  if (index >= args.length) {
577
726
  throw new UsageError(`option --${name} needs a value`)
578
727
  }
579
- options[name] = args[index]
728
+ keep(args[index])
580
729
  continue
581
730
  }
582
731
  options[name] = true
@@ -586,6 +735,9 @@ function parseArguments (args, valueOptions) {
586
735
 
587
736
  class UsageError extends Error {}
588
737
 
738
+ /** Options that may be given more than once; their value is the list. */
739
+ const REPEATABLE_OPTIONS = new Set(['cors-origin'])
740
+
589
741
  const VALUE_OPTIONS = {
590
742
  version: new Set([]),
591
743
  'sample-store': new Set([]),
@@ -598,9 +750,17 @@ const VALUE_OPTIONS = {
598
750
  serve: new Set(['port', 'host', 'query-path', 'gsp-path', 'file', 'layout', 'tdb2']),
599
751
  'lws-serve': new Set(['port', 'host', 'base', 'issuer', 'auth-config', 'owner', 'root-acl',
600
752
  'audience', 'page-size', 'moves', 'controller-inbox', 'delivery-limit', 'delivery-window',
601
- 'redirects-max', 'storage']),
753
+ 'redirects-max', 'storage', 'cors-origin']),
602
754
  'solid-serve': new Set(['port', 'host', 'base', 'owner', 'issuer', 'auth-config']),
603
- 'solid-client': new Set(['body', 'file', 'content-type', 'interpret'])
755
+ 'solid-client': new Set(['body', 'file', 'content-type', 'interpret']),
756
+ 'lws-client': new Set(['body', 'file', 'content-type', 'slug', 'type', 'link', 'if-match',
757
+ 'if-unmodified-since', 'retry-412', 'destination', 'query', 'topic', 'action', 'assignee',
758
+ 'target', 'subject-token-file', 'subject-token-type', 'access-token', 'storage', 'root',
759
+ 'inbox']),
760
+ 'authz-serve': new Set(['port', 'host', 'issuer', 'resource', 'alg', 'token-lifetime', 'rotate-after',
761
+ 'cors-origin']),
762
+ 'authz-keys': new Set([]),
763
+ 'xmpp-serve': new Set(['port', 'host', 'domain', 'accounts', 'cert', 'key'])
604
764
  }
605
765
 
606
766
  // ------------------------------------------------------------- commands
@@ -1106,7 +1266,7 @@ function currentDirectory () {
1106
1266
  }
1107
1267
 
1108
1268
  function packLayout (options) {
1109
- if (typeof options.layout !== 'string') return 'ibk3'
1269
+ if (typeof options.layout !== 'string') return 'ibk5'
1110
1270
  const layout = options.layout.toLowerCase()
1111
1271
  if (PACK_LAYOUTS.indexOf(layout) >= 0) return layout
1112
1272
  throw new UsageError(`--layout ${options.layout} is not one of ${PACK_LAYOUTS.join(', ')}`)
@@ -1179,6 +1339,11 @@ async function commandPack (positional, options) {
1179
1339
 
1180
1340
  async function commandSampleStore (positional, options) {
1181
1341
  if (positional.length > 1) throw new UsageError('sample-store takes at most one DIR')
1342
+ // Re-execute here, once, before any work. The pack and the activate
1343
+ // below would each re-execute the whole command line under Deno, and
1344
+ // each child printed the path again (three lines, 2026-10-24).
1345
+ const reexec = await denoReexec({ worker: options['no-worker'] !== true })
1346
+ if (reexec !== null) return reexec
1182
1347
  const { mkdtempSync, writeFileSync, existsSync } = await import('node:fs')
1183
1348
  const { tmpdir } = await import('node:os')
1184
1349
  const root = positional.length === 1
@@ -1305,6 +1470,21 @@ function natOption (flag, value) {
1305
1470
  return n
1306
1471
  }
1307
1472
 
1473
+ // --cors-origin ORIGIN (repeatable) and --no-cors, as the config member
1474
+ // `cors`: a list of origins, or false. Passed unread; the engine checks each
1475
+ // origin and decides every CORS field (L4Factoidal/HTTP/Cors.lean). With
1476
+ // neither option the member is absent and the engine echoes the request
1477
+ // Origin (Solid Protocol section 8.1).
1478
+ function corsConfig (options) {
1479
+ const origins = options['cors-origin']
1480
+ if (options['no-cors'] === true && origins !== undefined) {
1481
+ throw new UsageError('--no-cors cannot be combined with --cors-origin')
1482
+ }
1483
+ if (options['no-cors'] === true) return { cors: false }
1484
+ if (Array.isArray(origins)) return { cors: origins }
1485
+ return {}
1486
+ }
1487
+
1308
1488
  function serveHost (options) {
1309
1489
  if (options.host === undefined) return '127.0.0.1'
1310
1490
  if (typeof options.host !== 'string' || options.host.length === 0) {
@@ -1348,7 +1528,7 @@ async function commandServe (positional, options) {
1348
1528
  const { tmpdir } = await import('node:os')
1349
1529
  const { join } = await import('node:path')
1350
1530
  const root = mkdtempSync(join(tmpdir(), 'factoidal-serve-'))
1351
- const packOptions = { quiet: true, layout: typeof options.layout === 'string' ? options.layout : 'ibk5' }
1531
+ const packOptions = { quiet: true, layout: packLayout(options) }
1352
1532
  let rc = await commandPack([options.file, join(root, 'gen-1')], packOptions)
1353
1533
  if (rc !== 0) return rc
1354
1534
  rc = await commandActivate([root, 'gen-1'], { quiet: true })
@@ -1446,6 +1626,7 @@ async function commandLwsServe (positional, options) {
1446
1626
  ...(options['delivery-window'] !== undefined ? { deliveryWindow: natOption('delivery-window', options['delivery-window']) } : {}),
1447
1627
  ...(options['redirects-max'] !== undefined ? { redirectsMax: natOption('redirects-max', options['redirects-max']) } : {}),
1448
1628
  ...(typeof options.storage === 'string' ? { storage: options.storage } : {}),
1629
+ ...corsConfig(options),
1449
1630
  ...(insecure ? { insecureNoAuth: true } : { auth }),
1450
1631
  ...(typeof options.owner === 'string' ? { owner: options.owner } : {}),
1451
1632
  ...(typeof options['root-acl'] === 'string'
@@ -1476,6 +1657,79 @@ async function commandLwsServe (positional, options) {
1476
1657
  return untilInterrupted(running, quiet)
1477
1658
  }
1478
1659
 
1660
+ async function commandAuthzServe (positional, options) {
1661
+ if (positional.length !== 1) throw new UsageError('authz-serve needs one DIR')
1662
+ if (typeof options.resource !== 'string') {
1663
+ throw new UsageError('authz-serve needs --resource IRI[,IRI...]: the resources tokens may be issued for')
1664
+ }
1665
+ const quiet = options.quiet === true
1666
+ const { listen } = await import('../authz/server.mjs')
1667
+ let running
1668
+ try {
1669
+ running = await listen({
1670
+ port: servePort(options, 3001),
1671
+ host: serveHost(options),
1672
+ dir: positional[0],
1673
+ resources: options.resource.split(',').filter((v) => v !== ''),
1674
+ ...(typeof options.issuer === 'string' ? { issuer: options.issuer } : {}),
1675
+ // Passed unread; the engine checks them (Wasm/Ops/Authz.lean).
1676
+ ...(typeof options.alg === 'string' ? { alg: options.alg } : {}),
1677
+ ...(options['token-lifetime'] !== undefined ? { tokenLifetime: natOption('token-lifetime', options['token-lifetime']) } : {}),
1678
+ ...(options['rotate-after'] !== undefined ? { rotateAfter: natOption('rotate-after', options['rotate-after']) } : {}),
1679
+ ...(options['allow-http'] === true ? { allowHttp: true } : {}),
1680
+ ...(options['allow-private-fetch'] === true ? { allowPrivateFetch: true } : {}),
1681
+ ...corsConfig(options)
1682
+ })
1683
+ } catch (error) {
1684
+ if (error !== null && typeof error === 'object' && error.envelope !== undefined) {
1685
+ err(`factoidal authz-serve: ${error.envelope.error}`)
1686
+ return EXIT_FAILURE
1687
+ }
1688
+ if (error !== null && typeof error === 'object' && /unknown op/.test(String(error.message))) {
1689
+ return notWired('authz-serve', error.message)
1690
+ }
1691
+ throw error
1692
+ }
1693
+ if (!quiet) {
1694
+ err(`authz-serve: listening on ${running.origin}, issuer ${running.issuer}`)
1695
+ if (options['allow-http'] === true) err('authz-serve: WARNING: --allow-http: http subject identifiers are accepted')
1696
+ if (options['allow-private-fetch'] === true) err('authz-serve: WARNING: --allow-private-fetch: private addresses may be fetched')
1697
+ }
1698
+ out(running.origin)
1699
+ return untilInterrupted(running, quiet)
1700
+ }
1701
+
1702
+ async function commandAuthzKeys (positional, options) {
1703
+ if (positional.length !== 1) throw new UsageError('authz-keys needs one DIR')
1704
+ const { openAuthorizationServer, readKeyFile } = await import('../authz/server.mjs')
1705
+ if (readKeyFile(positional[0]) === null) {
1706
+ err(`factoidal authz-keys: ${positional[0]} holds no key file`)
1707
+ return EXIT_FAILURE
1708
+ }
1709
+ let authz
1710
+ try {
1711
+ // The issuer and resources do not matter for reading the keys.
1712
+ authz = await openAuthorizationServer({ dir: positional[0], issuer: 'https://authz.invalid', resources: [] })
1713
+ } catch (error) {
1714
+ if (error !== null && typeof error === 'object' && error.envelope !== undefined) {
1715
+ err(`factoidal authz-keys: ${error.envelope.error}`)
1716
+ return EXIT_FAILURE
1717
+ }
1718
+ throw error
1719
+ }
1720
+ try {
1721
+ if (options.private === true) {
1722
+ out(JSON.stringify(authz.privateJwks()))
1723
+ } else {
1724
+ const answer = await authz.step({ method: 'GET', target: '/jwks', headers: [], body: '' })
1725
+ out(answer.body)
1726
+ }
1727
+ } finally {
1728
+ authz.close()
1729
+ }
1730
+ return EXIT_OK
1731
+ }
1732
+
1479
1733
  async function commandSolidServe (positional, options) {
1480
1734
  if (positional.length !== 1) throw new UsageError('solid-serve needs one DIR')
1481
1735
  const quiet = options.quiet === true
@@ -1531,6 +1785,45 @@ async function commandSolidServe (positional, options) {
1531
1785
  return untilInterrupted(running, quiet)
1532
1786
  }
1533
1787
 
1788
+ async function commandXmppServe (positional, options) {
1789
+ if (positional.length !== 1) throw new UsageError('xmpp-serve needs one DIR')
1790
+ const quiet = options.quiet === true
1791
+ const { listen, XmppHostError } = await import('../xmpp/server.mjs')
1792
+ const fs = await import('node:fs/promises')
1793
+ const useTls = options.cert !== undefined || options.key !== undefined
1794
+ if (useTls && (typeof options.cert !== 'string' || typeof options.key !== 'string')) {
1795
+ throw new UsageError('--cert and --key go together')
1796
+ }
1797
+ const settings = {
1798
+ dir: positional[0],
1799
+ port: servePort(options, useTls ? 5223 : 5222),
1800
+ host: serveHost(options),
1801
+ onError: (error) => { if (!quiet) err(`xmpp-serve: ${error.message}`) }
1802
+ }
1803
+ if (typeof options.domain === 'string') settings.domain = options.domain
1804
+ if (typeof options.accounts === 'string') settings.accountsText = await fs.readFile(options.accounts, 'utf8')
1805
+ if (useTls) {
1806
+ settings.cert = await fs.readFile(options.cert)
1807
+ settings.key = await fs.readFile(options.key)
1808
+ }
1809
+ let running
1810
+ try {
1811
+ running = await listen(settings)
1812
+ } catch (error) {
1813
+ if (error instanceof XmppHostError && error.unknownOp) {
1814
+ return notWired('xmpp-serve', error.message)
1815
+ }
1816
+ throw error
1817
+ }
1818
+ if (!quiet) {
1819
+ err(`xmpp-serve: listening on ${running.origin} for domain ${running.xmpp.domain}` +
1820
+ ` (${running.xmpp.accounts} accounts, state ${positional[0]}` +
1821
+ (useTls ? ', direct TLS)' : ', PLAINTEXT: no TLS on this listener)'))
1822
+ }
1823
+ out(running.origin)
1824
+ return untilInterrupted(running, quiet)
1825
+ }
1826
+
1534
1827
  // The command's verbs, as aliases for the wasm ABI's request kinds. A
1535
1828
  // kind name is accepted directly too, so this table adds a spelling and
1536
1829
  // hides nothing.
@@ -1587,6 +1880,75 @@ async function commandSolidClient (positional, options) {
1587
1880
  return EXIT_OK
1588
1881
  }
1589
1882
 
1883
+ // `factoidal lws-client`: the caller's options become the members of one
1884
+ // operation document; the engine decides everything else.
1885
+ async function commandLwsClient (positional, options) {
1886
+ if (positional.length !== 2) {
1887
+ throw new UsageError('lws-client needs an operation and a URL')
1888
+ }
1889
+ const [kind, url] = positional
1890
+ const { createLwsClient, LwsClientError, LWS_CLIENT_KINDS } = await import('../lws/client.mjs')
1891
+ if (!LWS_CLIENT_KINDS.includes(kind) || kind === 'verifyNotification') {
1892
+ throw new UsageError(`unknown operation "${kind}"; one of ` +
1893
+ LWS_CLIENT_KINDS.filter((k) => k !== 'verifyNotification').join(', '))
1894
+ }
1895
+ const list = (v) => String(v).split(',').filter((x) => x !== '')
1896
+ const op = { kind, url }
1897
+ if (typeof options.file === 'string') op.body = readWhole(options.file)
1898
+ else if (typeof options.body === 'string') op.body = options.body
1899
+ if (typeof options['content-type'] === 'string') op.mediaType = options['content-type']
1900
+ if (typeof options.slug === 'string') op.slug = options.slug
1901
+ if (options.container === true) op.container = true
1902
+ if (options.recursive === true) op.recursive = true
1903
+ if (typeof options.type === 'string') op.types = list(options.type)
1904
+ if (typeof options.link === 'string') op.links = [options.link]
1905
+ if (typeof options['if-match'] === 'string') { op.precondition = 'ifMatch'; op.tag = options['if-match'] }
1906
+ if (options['if-match-current'] === true) op.precondition = 'ifMatchCurrent'
1907
+ if (options['if-none-match-star'] === true) op.precondition = 'ifNoneMatchStar'
1908
+ if (typeof options['if-unmodified-since'] === 'string') {
1909
+ op.precondition = 'ifUnmodifiedSince'; op.tag = options['if-unmodified-since']
1910
+ }
1911
+ if (options['no-precondition'] === true) op.precondition = 'none'
1912
+ if (typeof options['retry-412'] === 'string') op.retry412 = Number(options['retry-412'])
1913
+ if (typeof options.destination === 'string') op.destination = options.destination
1914
+ if (typeof options.query === 'string') op.query = JSON.parse(options.query)
1915
+ if (typeof options.topic === 'string') op.topics = list(options.topic)
1916
+ if (typeof options.inbox === 'string') op.inbox = options.inbox
1917
+ if (typeof options.action === 'string') op.actions = list(options.action)
1918
+ if (typeof options.assignee === 'string') op.assignee = options.assignee
1919
+ if (typeof options.target === 'string') op.targets = list(options.target)
1920
+ const clientOptions = {}
1921
+ if (typeof options['subject-token-file'] === 'string') {
1922
+ clientOptions.subjectToken = new TextDecoder('utf-8', { fatal: true })
1923
+ .decode(readWhole(options['subject-token-file'])).trim()
1924
+ }
1925
+ if (typeof options['subject-token-type'] === 'string') {
1926
+ clientOptions.subjectTokenType = options['subject-token-type']
1927
+ }
1928
+ if (typeof options['access-token'] === 'string') {
1929
+ if (typeof options.storage !== 'string' || typeof options.root !== 'string') {
1930
+ throw new UsageError('--access-token needs --storage IRI and --root URL')
1931
+ }
1932
+ clientOptions.session = {
1933
+ tokens: [{ storage: options.storage, root: options.root, token: options['access-token'] }]
1934
+ }
1935
+ }
1936
+ let answer
1937
+ try {
1938
+ const client = await createLwsClient(clientOptions)
1939
+ answer = await client.run(op)
1940
+ } catch (error) {
1941
+ if (error instanceof LwsClientError) {
1942
+ if (error.unknownOp) return notWired('lws-client', error.message)
1943
+ out(JSON.stringify({ fail: error.detail }, null, 2))
1944
+ return EXIT_FAILURE
1945
+ }
1946
+ throw error
1947
+ }
1948
+ out(JSON.stringify(answer, null, 2))
1949
+ return EXIT_OK
1950
+ }
1951
+
1590
1952
  const COMMANDS = {
1591
1953
  version: (positional, options) => commandVersion(options),
1592
1954
  'sample-store': (positional, options) => commandSampleStore(positional, options),
@@ -1599,7 +1961,11 @@ const COMMANDS = {
1599
1961
  serve: commandServe,
1600
1962
  'lws-serve': commandLwsServe,
1601
1963
  'solid-serve': commandSolidServe,
1602
- 'solid-client': commandSolidClient
1964
+ 'solid-client': commandSolidClient,
1965
+ 'lws-client': commandLwsClient,
1966
+ 'authz-serve': commandAuthzServe,
1967
+ 'authz-keys': commandAuthzKeys,
1968
+ 'xmpp-serve': commandXmppServe
1603
1969
  }
1604
1970
 
1605
1971
  // ----------------------------------------------------------------- main
package/browser.d.ts CHANGED
@@ -11,6 +11,8 @@ export type {
11
11
  DataFormat, EntailRegime, Bindings, DataInput, QueryInput, ParseOptions,
12
12
  QueryOptions, SerializeOptions, StoreReader, OpenStoreOptions,
13
13
  DeltaLogHandle, DeltaLogInspection,
14
+ Sbm11Reader, Sbm11Answer, Sbm11Store,
15
+ ExplainResult, StageInfo, EvidenceRecord, Wire, FactoidalNode, Pipeline, RunResult, RunOptions,
14
16
  } from './index';
15
17
 
16
18
  export {
@@ -50,6 +52,11 @@ export {
50
52
  owlClosure,
51
53
  coreRdfsClosure,
52
54
  coreRdfsCheck,
55
+ explain,
56
+ stages,
57
+ node,
58
+ pipe,
59
+ run,
53
60
  rdfsPlusClosure,
54
61
  rhoDfClosure,
55
62
  rhoDfFragmentCheck,
package/browser.js CHANGED
@@ -59,6 +59,9 @@ const api = buildApi({
59
59
  deltaLogStore: indexedDbDeltaLogStore(),
60
60
  });
61
61
 
62
+ // SBM11 (openSbm11, registerSbm11Source) is not exported in 0.14.0: the op
63
+ // sbm11Open traps in the wasm build (https://github.com/danbri/factoidal/issues/1009).
64
+
62
65
  // RDF and SPARQL
63
66
  export const parse = api.parse;
64
67
  export const query = api.query;
@@ -102,6 +105,11 @@ export const shexValidate = api.shexValidate;
102
105
  export const owlClosure = api.owlClosure;
103
106
  export const coreRdfsClosure = api.coreRdfsClosure;
104
107
  export const coreRdfsCheck = api.coreRdfsCheck;
108
+ export const explain = api.explain;
109
+ export const stages = api.stages;
110
+ export const node = api.node;
111
+ export const pipe = api.pipe;
112
+ export const run = api.run;
105
113
  export const rdfsPlusClosure = api.rdfsPlusClosure;
106
114
  export const rhoDfClosure = api.rhoDfClosure;
107
115
  export const rhoDfFragmentCheck = api.rhoDfFragmentCheck;