@factoidal/core 0.12.0 → 0.13.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 +183 -0
- package/README.md +13 -1
- package/authz/server.mjs +331 -0
- package/bin/factoidal.mjs +372 -6
- package/index.d.ts +17 -7
- 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 +3 -2
- package/lib/version.mjs +1 -1
- package/lws/client.mjs +319 -0
- package/lws/persist.mjs +17 -1
- package/lws/server.mjs +25 -3
- package/package.json +9 -1
- package/xmpp/README.md +82 -4
- package/xmpp/server.mjs +549 -0
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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) {
|
|
@@ -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/index.d.ts
CHANGED
|
@@ -680,11 +680,16 @@ export function tableauDlInconsistent(
|
|
|
680
680
|
): Promise<{ inconsistent: boolean; rlAlone: boolean }>;
|
|
681
681
|
|
|
682
682
|
/**
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
683
|
+
* Checked consistency for the original default graph in a restricted
|
|
684
|
+
* atomic OWL fragment (named subclass/class assertions, Thing/Nothing,
|
|
685
|
+
* sameAs/differentFrom and limited declarations/annotations).
|
|
686
|
+
* false requires an accepted certificate; null means unsupported-syntax
|
|
687
|
+
* or incomplete-search. No model producer exists, so true is not returned.
|
|
688
|
+
* Named graphs do not contribute axioms. RDF 1.2 terms are refused.
|
|
689
|
+
* This narrows earlier unchecked coverage: restrictions, object/data
|
|
690
|
+
* property axioms, imports and equivalentClass are unsupported.
|
|
691
|
+
* fuel is retained for compatibility but does not change this fixed
|
|
692
|
+
* 64-round certificate check. This is not complete OWL DL support.
|
|
688
693
|
*/
|
|
689
694
|
export function owlIsConsistent(
|
|
690
695
|
data: DataInput,
|
|
@@ -694,8 +699,13 @@ export function owlIsConsistent(
|
|
|
694
699
|
/**
|
|
695
700
|
* OWL entailment check: does `premise` entail `conclusion`? Two paths: `via: "closure"` (the conclusion is in the OWL-RL
|
|
696
701
|
* closure of the premise) or `via: "refutation"` (the negated
|
|
697
|
-
* conclusion is refuted on every goal). Default graph only.
|
|
698
|
-
*
|
|
702
|
+
* conclusion is refuted on every goal). Default graph only. The current
|
|
703
|
+
* implementation returns `true` for those positive results and `null`
|
|
704
|
+
* when it cannot establish entailment. `reason` distinguishes budget-out,
|
|
705
|
+
* incomplete-search, unsupported-conclusion and unsupported-punning.
|
|
706
|
+
* No checked countermodel constructor exists, so it never returns `false`.
|
|
707
|
+
* The punning refusal is limited; full OWL DL recognition and universal
|
|
708
|
+
* Direct Semantics soundness are not established by this API.
|
|
699
709
|
*/
|
|
700
710
|
export function owlEntails(
|
|
701
711
|
premise: DataInput,
|
package/l4-assets/l4factoidal.js
CHANGED
|
Binary file
|
package/l4-assets/version.json
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
"engine": "lean4",
|
|
3
3
|
"note": "The Lean 4 (L4Factoidal) engine build provenance and claims: the only engine of @factoidal/core since 0.9.0 (issue 713). This committed file is a placeholder. tools/fetch-builds.sh --set web (through tools/stamp-l4-version.mjs) writes the provenance members from the danbri/factoidal-builds release that built the wasm; formal/lean4/Wasm/build-wasm.sh step 9 writes them for a local build. Do not commit a stamped copy.",
|
|
4
4
|
"stamped": true,
|
|
5
|
-
"release": "danbri/factoidal-builds build-
|
|
6
|
-
"releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/
|
|
7
|
-
"gitSha": "
|
|
8
|
-
"builtAt": "2026-10-
|
|
5
|
+
"release": "danbri/factoidal-builds build-6f5b96407a4c-web",
|
|
6
|
+
"releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/40076270080",
|
|
7
|
+
"gitSha": "6f5b96407a4cd95ac7452256ed2d4b9c938a7c49",
|
|
8
|
+
"builtAt": "2026-10-24T08:40:08Z",
|
|
9
9
|
"leanToolchain": "leanprover/lean4:v4.33.1",
|
|
10
10
|
"emscripten": "6.0.8",
|
|
11
11
|
"abiVersion": "1",
|
|
12
12
|
"engineVersion": "l4factoidal-wasm 0.2.0 (dispatch: RDF parse/serialize, SPARQL query+update, RDFS/OWL-RL closures, RDFC-1.0, CL/IKL)",
|
|
13
13
|
"version": "l4factoidal-wasm 0.2.0 (dispatch: RDF parse/serialize, SPARQL query+update, RDFS/OWL-RL closures, RDFC-1.0, CL/IKL)",
|
|
14
|
-
"wasmSha256": "
|
|
15
|
-
"wasmBytes":
|
|
14
|
+
"wasmSha256": "1b55f66d8e193c6a3d168838bd77e8a5b1accb94e0a7c84209b63429f4989396",
|
|
15
|
+
"wasmBytes": 14483288,
|
|
16
16
|
"claims": {
|
|
17
17
|
"source": "formal/lean4 (L4Factoidal): no sorry, no user axioms, no native_decide; W3C behaviour pinned by build-time #guard",
|
|
18
18
|
"suitesAtBuildSha": "see docs/test-results and formal/lean4/PORT_NOTES.md at gitSha"
|
package/lib/api.mjs
CHANGED
|
@@ -1071,8 +1071,9 @@ function buildApi(driver) {
|
|
|
1071
1071
|
}
|
|
1072
1072
|
|
|
1073
1073
|
/**
|
|
1074
|
-
* OWL
|
|
1075
|
-
*
|
|
1074
|
+
* Checked atomic OWL consistency of the default graph: false is a checked
|
|
1075
|
+
* contradiction; null means unsupported syntax or no accepted refutation.
|
|
1076
|
+
* No model producer exists. fuel does not change the fixed checker bound.
|
|
1076
1077
|
* @param {Dataset|string|Array} data
|
|
1077
1078
|
* @param {{format?: string, baseIRI?: string, fuel?: number|string}} [options]
|
|
1078
1079
|
* @returns {Promise<{consistent: boolean|null, reason?: string}>}
|
package/lib/version.mjs
CHANGED