@factoidal/core 0.32.0 → 0.34.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 +21 -2
- package/bin/factoidal.mjs +184 -23
- package/bin/foafnaut.mjs +29 -7
- package/browser.js +5 -2
- package/index.d.ts +20 -7
- package/index.mjs +7 -4
- 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 +5 -3
- package/lib/sbm11-lws.mjs +25 -35
- package/lib/version.mjs +1 -1
- package/lws/server.mjs +82 -14
- package/package.json +1 -1
- package/webdav/server.mjs +12 -0
- package/xmpp/README.md +58 -4
- package/xmpp/client.mjs +18 -3
- package/xmpp/inpage.mjs +7 -1
- package/xmpp/server.mjs +314 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
4
|
-
|
|
3
|
+
## 0.34.0 (2026-11-08)
|
|
4
|
+
|
|
5
|
+
- `foafnaut` command: `--url` and `--html` open a page or a file (`file:` URLs too), and other options are passed to the app.
|
|
6
|
+
- XMPP: `xmpp-serve --proxy-protocol` (`proxyProtocol` in `createXmppServer`): every connection, on every listener, must start with a PROXY protocol v2 header, read before TLS, and the failed-login limit counts the client address it names. So the limit per client address can stay on behind Fly's proxy, where `--no-address-limit` was needed. The header is decided in Lean (`L4Factoidal/Net/ProxyProtocol.lean`, new wasm op `proxyHeaderParse`, theorems `parse_signature`, `parse_more_bounded`, `parse_proxy_le`); a connection without a valid header in 10 seconds (`proxyHeaderTimeoutMs`) is closed. Without the flag no header is read.
|
|
7
|
+
- XMPP: the server keeps no client address. The limit counts an HMAC-SHA-256 tag of the address (IPv6 by its /64) under a random key that is in memory only and new at each start (`LoginLimit.addressTag`; `xmppOpen` takes `addressKey`; `xmppSessionOpen` and `xmppWsOpen` answer `addressTag`, its first 12 digits). The "connection from" log line shows the tag, not the address and port.
|
|
8
|
+
- XMPP: the server pings a bound session that has been silent, and closes it if it stays silent, so a dead connection frees its resource. A ping is XEP-0199 on TCP and a ping frame on a WebSocket; any byte from the client counts as an answer. `xmpp-serve --ping-interval SECONDS` (default 30, 0 disables) and `--ping-timeout SECONDS` (default 20); `pingIntervalMs` and `pingTimeoutMs` in `createXmppServer`. New ops `xmppSessionPing` and `xmppWsPing`.
|
|
9
|
+
- XMPP: a client that binds a resource a live session holds gets a server-generated one (RFC 6120 section 7.7.2.2), in the bind result. The live session stays. A client must use the JID in the bind result. A resource with no live session is free, and a room seat goes when its stream ends.
|
|
10
|
+
- XMPP: room moderation (XEP-0045 sections 8 to 10): kick (status 307), ban (status 301), grant and revoke voice, moderator, member, admin and owner, the admin and owner lists, and status 321. `xmpp-serve --muc-owners FILE` names the owners, as `room: a@x` or `*: a@x` for every room. With that flag, also with an empty file, the first occupant of a room gets no rights, XMPP cannot change a configured owner, and with `--state-key` bans, grants and revoked voices survive a restart. Without the flag the first occupant is the owner as before, and a room's bans go with the room.
|
|
11
|
+
|
|
12
|
+
## 0.33.0 (2026-11-06)
|
|
13
|
+
|
|
14
|
+
- XMPP: held stanzas have caps per recipient and per server and a rate per sending account. A stanza for a name with no account is not held. Rosters have a cap on items and a rate on writes.
|
|
15
|
+
- XMPP: SASL closes a stream after three failed attempts, adds a slower limit per client address, and uses one SCRAM exchange for every name. `--no-address-limit` is for a listener behind a proxy.
|
|
16
|
+
- XMPP: accounts have a salt per name and more SCRAM iterations by default. An accounts line can hold keys in place of a password (`xmpp-serve --verifier NAME`). A malformed accounts file stops the start.
|
|
17
|
+
- XMPP: a stanza has at most 64 levels and 8192 elements. Rooms have reserved nicks and nick checks, and a session leaves its rooms at a clean stream end.
|
|
18
|
+
- XMPP: XEP-0421 occupant ids in rooms. A room seat stays until the last session of a full JID goes.
|
|
5
19
|
- XMPP: `xmpp-serve --starttls --tls-port N` serves STARTTLS and XEP-0368 direct TLS from one server.
|
|
6
20
|
- XMPP: members-only rooms (XEP-0045 section 7.2.6). `xmpp-serve --muc-members FILE` lists who may join each room; anyone else gets `registration-required` and cannot see the room.
|
|
7
21
|
- XMPP: disco#info and disco#items results carry `from`. Without it, Profanity 0.19.0 stopped with a segmentation fault after login.
|
|
22
|
+
- The URL decisions of the LWS SBM11 reader and of the compositor's pod storage are now in Lean (`L4Factoidal/LWS/ContainerPath.lean`, new pure wasm op `containerPath`), not in JavaScript: the container URL, each resource URL from a raw relative path, one new file name, a URL inside a pod root, and the folder above (https://github.com/danbri/factoidal/issues/1127). Theorems `resourceIn_under`, `segmentIn_under`, `within_under`, `parentIn_under` (an accepted URL has the container's origin and the container URL, which ends with `/`, is its prefix) and `within_sibling_refused` (`https://s.example/alice-other/` is not within `https://s.example/alice/`). `lwsSbm11Reader` is now asynchronous (`await lwsSbm11Reader(url, opts)`): it loads the engine first; `opts.engine` gives one. An empty `?` or `#` in a container URL is now refused too. The LWS client's token scope (`TokenEntry.covers`) has the same path-segment boundary: a token for the root `https://s.example/alice` is no longer sent to `https://s.example/alice-other/x` (theorem `covers_sibling_refused`). The hub compositor's pod storage needs an engine with `containerPath`; with an older engine it refuses every pod operation.
|
|
23
|
+
- Requests to private addresses (https://github.com/danbri/factoidal/issues/1126). One Lean decision for every outbound request whose URL a remote party chooses, `L4Factoidal/Net/Egress.lean`: it refuses every address of the IANA IPv4 and IPv6 special-purpose registries (now also 100.64.0.0/10, 198.18.0.0/15, 192.0.0.0/24, the documentation ranges, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255, fec0::/10, ff00::/8, every IPv6 address outside 2000::/3, and NAT64 64:ff9b::/96 or 6to4 2002::/16 with such an IPv4 address inside) and localhost names, unless the operator opts in with `--allow-private-fetch`. The authorization server and CID retrieval use it (theorem `egress_refuses_special`). `lws-serve`: a webhook inbox at such an address answers 400 at subscription; every webhook, access-request, access-grant and controller-inbox delivery is checked again just before it is made, against the addresses the host resolves then, and the host connects to the address the engine chose (new wasm op `lwsEgress`; theorems `deliveries_pinned`, `egressOf_refuses_special`), so a name that resolves to a private address only at delivery time is refused. A refused delivery is not retried. Deliveries to one destination are limited for all subscriptions together: `--delivery-dest-limit N` (default 10 per window, theorem `admit_per_destination`). `lws-serve --allow-private-fetch` now also lets inboxes be private addresses, and its warning is printed also with `--quiet`. Hosts and tests that deliver to 127.0.0.1 must opt in.
|
|
24
|
+
- `webdav-serve --xmpp` accepts `--users`, `--login-limit`, `--login-window` and `--dangerously-disable-ratelimits` (https://github.com/danbri/factoidal/issues/1125). Each request over XMPP passes the JID allow list and then the same Lean login as on TCP (`Login.admit`); HTTP Basic credentials in the XEP-0332 headers log in. `--users` over XMPP needs `--xmpp-tls` or a loopback `--xmpp-host`. `--cert` and `--key` are refused with `--xmpp` (no port opens).
|
|
25
|
+
- The failed-login limit counts per requester (https://github.com/danbri/factoidal/issues/1128): the client address on TCP, the sender's bare JID over XMPP (`L4Factoidal/Net/LoginLimit.lean`, `requesterKey`), never an empty key; one JID's failures do not block another JID. Over XMPP, requests that wait for `chunkedBase64` chunks are limited to 4 per sender and 16 in total, and wait at most 60 seconds (`resource-constraint`).
|
|
26
|
+
- `xmpp-serve` limits failed SASL logins by default: 5 in 60 seconds per account name and per client address, then `temporary-auth-failure` with no password check until the window ends. `--dangerously-disable-ratelimits` turns it off and prints a warning at start. Wasm: `xmppOpen` takes `disableRateLimits` and answers `warnings`; `xmppSessionOpen` takes `remoteAddress` and `now`; `xmppClientTick` also serves the serving mode; `xmppClientFeed` request records carry `requester`; `webdavOpen` takes `login.transport: "xmpp"` and `login.jid`; `ready.login`.
|
|
8
27
|
|
|
9
28
|
## 0.32.0 (2026-11-05)
|
|
10
29
|
|
package/bin/factoidal.mjs
CHANGED
|
@@ -445,8 +445,11 @@ DIR holds the signing keys (DIR/authz-keys.json, mode 0600), sealed with
|
|
|
445
445
|
FACTOIDAL_LWS_KEY when it is set. --rotate-after makes a new key when the
|
|
446
446
|
current one is that old; the old key stays published until its tokens expire.
|
|
447
447
|
--allow-http accepts http subject identifiers; --allow-private-fetch lets the
|
|
448
|
-
server fetch documents from loopback,
|
|
449
|
-
|
|
448
|
+
server fetch documents from special-purpose addresses: loopback, private,
|
|
449
|
+
link-local, shared (100.64.0.0/10), multicast, reserved and the other ranges
|
|
450
|
+
of the IANA special-purpose registries, and localhost names
|
|
451
|
+
(https://github.com/danbri/factoidal/issues/1126). Both are off by default
|
|
452
|
+
and are for tests.
|
|
450
453
|
CORS (https://github.com/danbri/factoidal/issues/923): by default every answer
|
|
451
454
|
echoes a valid request Origin in Access-Control-Allow-Origin, so a page on
|
|
452
455
|
another origin can read the metadata and keys and exchange a token; a
|
|
@@ -474,7 +477,8 @@ usage: factoidal lws-serve DIR (--authz DIR | --issuer IRI | --auth-config FILE
|
|
|
474
477
|
[--paginate-always] [--moves all|data|off]
|
|
475
478
|
[--no-linkset-put] [--unstable-search-results] [--merkle]
|
|
476
479
|
[--controller-inbox URL] [--delivery-limit N]
|
|
477
|
-
[--delivery-window SECONDS] [--
|
|
480
|
+
[--delivery-window SECONDS] [--delivery-dest-limit N]
|
|
481
|
+
[--redirects-max N]
|
|
478
482
|
[--storage snapshot|files]
|
|
479
483
|
[--cors-origin ORIGIN]... [--no-cors]
|
|
480
484
|
|
|
@@ -569,6 +573,25 @@ The engine decides the file, its contents and every refusal
|
|
|
569
573
|
--delivery-window SECONDS
|
|
570
574
|
the length of that window (config member deliveryWindow;
|
|
571
575
|
default 60)
|
|
576
|
+
--delivery-dest-limit N
|
|
577
|
+
the most deliveries one destination (host and port of the
|
|
578
|
+
inbox URL) gets in one window, for all subscriptions and
|
|
579
|
+
access records together (config member deliveryDestLimit;
|
|
580
|
+
default 10, 0 turns the limit off)
|
|
581
|
+
https://github.com/danbri/factoidal/issues/1126
|
|
582
|
+
--allow-private-fetch
|
|
583
|
+
let webhook, access-request, access-grant and controller
|
|
584
|
+
inboxes be special-purpose addresses (loopback, private,
|
|
585
|
+
link-local, shared 100.64.0.0/10, multicast, reserved and
|
|
586
|
+
the other IANA special-purpose ranges) or localhost names,
|
|
587
|
+
and let the --authz server fetch from them (config member
|
|
588
|
+
allowPrivateFetch; default off). Without it such an inbox
|
|
589
|
+
is refused with 400 when its address is known at
|
|
590
|
+
subscription, and every delivery is checked again just
|
|
591
|
+
before it is made, against the addresses resolved then;
|
|
592
|
+
the server connects to the address it checked. For tests
|
|
593
|
+
and closed networks only.
|
|
594
|
+
https://github.com/danbri/factoidal/issues/1126
|
|
572
595
|
--redirects-max N
|
|
573
596
|
the most previous identifiers of moved resources kept to
|
|
574
597
|
answer 301 Moved Permanently (config member redirectsMax;
|
|
@@ -749,9 +772,11 @@ Specification: https://solidproject.org/TR/protocol`,
|
|
|
749
772
|
usage: factoidal xmpp-serve DIR [--domain NAME] [--port N] [--host ADDRESS]
|
|
750
773
|
[--accounts FILE] [--cert FILE --key FILE [--starttls [--tls-port N]]]
|
|
751
774
|
[--ws-port N [--ws-path PATH] [--ws-origin ORIGIN]...]
|
|
752
|
-
[--muc DOMAIN [--muc-members FILE]] [--mix DOMAIN] [--gc3 DOMAIN]
|
|
775
|
+
[--muc DOMAIN [--muc-members FILE] [--muc-owners FILE]] [--mix DOMAIN] [--gc3 DOMAIN]
|
|
753
776
|
[--state-key FILE] [--max-connections N]
|
|
754
|
-
[--login-timeout SECONDS]
|
|
777
|
+
[--login-timeout SECONDS] [--ping-interval SECONDS]
|
|
778
|
+
[--ping-timeout SECONDS] [--proxy-protocol] [--no-address-limit]
|
|
779
|
+
[--dangerously-disable-ratelimits]
|
|
755
780
|
|
|
756
781
|
Starts a Node TCP server that runs one engine session per connection and
|
|
757
782
|
answers every byte through the engine's xmppSessionFeed operation.
|
|
@@ -768,7 +793,13 @@ DIR/spool/<bare jid>/ is delivered to that account's bound session.
|
|
|
768
793
|
(default 5222; 5223 with --cert and --key and no --starttls)
|
|
769
794
|
--host ADDRESS the address to bind (default 127.0.0.1); without
|
|
770
795
|
--cert and --key only a loopback address is accepted
|
|
771
|
-
--accounts FILE the accounts file (default DIR/accounts)
|
|
796
|
+
--accounts FILE the accounts file (default DIR/accounts). One account per
|
|
797
|
+
line: name:password, or a line from --verifier, which holds
|
|
798
|
+
no password (SCRAM only). Any other line stops the start. A
|
|
799
|
+
file others can read gives a warning
|
|
800
|
+
--verifier NAME read a password from standard input, print the accounts line
|
|
801
|
+
for NAME that holds no password, and exit; --iterations N
|
|
802
|
+
sets the SCRAM count (default 10000)
|
|
772
803
|
--cert FILE PEM certificate chain: XEP-0368 direct TLS, or STARTTLS with --starttls
|
|
773
804
|
--key FILE PEM private key for --cert
|
|
774
805
|
--starttls listen in clear and REQUIRE STARTTLS (RFC 6120 section 5)
|
|
@@ -787,11 +818,19 @@ DIR/spool/<bare jid>/ is delivered to that account's bound session.
|
|
|
787
818
|
--muc DOMAIN also serve XEP-0045 multi-user chat rooms at DOMAIN, e.g.
|
|
788
819
|
conference.example.com: temporary rooms, made by the first
|
|
789
820
|
join and gone with the last occupant; no history, no
|
|
790
|
-
configuration
|
|
821
|
+
configuration form (L4Factoidal/XMPP/Muc.lean). Kick, ban, voice
|
|
822
|
+
and the admin and owner lists follow XEP-0045 sections 8 to 10
|
|
791
823
|
--muc-members FILE members-only rooms: one room per line, as
|
|
792
824
|
"room: alice@example.com bob@example.com". Only those accounts
|
|
793
825
|
may join or see that room; a room not in the file is open to
|
|
794
826
|
every account (L4Factoidal/XMPP/Muc.lean)
|
|
827
|
+
--muc-owners FILE room owners: one room per line, as "room: alice@example.com",
|
|
828
|
+
and "*: alice@example.com" for every room. XMPP cannot change
|
|
829
|
+
these names. With this flag, also with an empty file, nobody
|
|
830
|
+
becomes an owner by being first in a room, and with --state-key
|
|
831
|
+
bans and affiliations survive a restart. Without this flag, the
|
|
832
|
+
first occupant of a room with no owner is its owner, and a room's
|
|
833
|
+
bans and affiliations go with the room
|
|
795
834
|
--mix DOMAIN also serve XEP-0369 MIX channels at DOMAIN, e.g. mix.example.com,
|
|
796
835
|
joined through this server (XEP-0405 MIX-PAM); channels are
|
|
797
836
|
made by an IQ <create/>, kept in memory only, JID-hidden; no
|
|
@@ -801,6 +840,11 @@ DIR/spool/<bare jid>/ is delivered to that account's bound session.
|
|
|
801
840
|
behind a proxy, every client has the same one
|
|
802
841
|
--login-timeout SECONDS how long a connection may stay without a bound
|
|
803
842
|
resource before it is closed (default 30; 0 disables)
|
|
843
|
+
--ping-interval SECONDS how long a bound session may be silent before the
|
|
844
|
+
server pings it: XEP-0199 on TCP, a ping frame on a WebSocket
|
|
845
|
+
(default 30; 0 disables pings)
|
|
846
|
+
--ping-timeout SECONDS how long after a ping a session that sends no byte is
|
|
847
|
+
closed, which frees its resource (default 20)
|
|
804
848
|
--state-key FILE keep the server's state across restarts: rosters, messages held
|
|
805
849
|
for offline accounts, MIX channels and GC3 groups, in an LWS
|
|
806
850
|
storage in DIR/state, sealed (ChaCha20-Poly1305) under the key
|
|
@@ -811,6 +855,37 @@ DIR/spool/<bare jid>/ is delivered to that account's bound session.
|
|
|
811
855
|
membership, stable participant ids, optional events. There is
|
|
812
856
|
no published GC3 spec; every wire format is invented and listed
|
|
813
857
|
in L4Factoidal/XMPP/Gc3.lean and danbri/factoidal issue 1236
|
|
858
|
+
--proxy-protocol every connection, on every listener (--port, --tls-port,
|
|
859
|
+
--ws-port), must start with a PROXY protocol version 2 header
|
|
860
|
+
(HAProxy's spec), read before TLS. The client address in it is
|
|
861
|
+
the one the failed-login limit counts. A connection with no
|
|
862
|
+
valid header in 10 seconds is closed; a version 1 (text)
|
|
863
|
+
header is refused. A LOCAL header (a health check of the
|
|
864
|
+
proxy) is accepted with the socket's address. Use it only
|
|
865
|
+
behind a proxy that always sends the header, such as Fly with
|
|
866
|
+
handlers = ["proxy_proto"] and proxy_proto_options =
|
|
867
|
+
{ version = "v2" }; without it no header is ever read, so a
|
|
868
|
+
client cannot claim an address
|
|
869
|
+
--no-address-limit
|
|
870
|
+
count failed logins per account name only. For a listener
|
|
871
|
+
behind a proxy that sends no PROXY header, where every client
|
|
872
|
+
has the proxy's address and a limit on it would stop them all
|
|
873
|
+
--dangerously-disable-ratelimits
|
|
874
|
+
no failed SASL login limit and no sender rate on held
|
|
875
|
+
messages (a warning is printed at start); any client can
|
|
876
|
+
then try passwords without end
|
|
877
|
+
|
|
878
|
+
Failed SASL logins are limited by default
|
|
879
|
+
(https://github.com/danbri/factoidal/issues/1128): after 5 failed logins in
|
|
880
|
+
60 seconds for one account name (known or not) or from one client address
|
|
881
|
+
(an IPv6 client by its /64), on one stream or on new ones, an attempt gets
|
|
882
|
+
the SASL failure temporary-auth-failure, and no password is checked, also
|
|
883
|
+
the right one, until that window ends. A client address also has a slower
|
|
884
|
+
limit of 20 failed logins in an hour. A malformed SASL payload checks no
|
|
885
|
+
password and does not count. A stream closes after 3 failed attempts.
|
|
886
|
+
The server keeps no client address: the counters hold an HMAC-SHA-256
|
|
887
|
+
tag of it (of its /64 for IPv6) under a random key that is in memory
|
|
888
|
+
only and new at each start, and logs show the first 12 digits of the tag.
|
|
814
889
|
|
|
815
890
|
The server makes no protocol decision. Stream negotiation, SASL PLAIN and
|
|
816
891
|
SCRAM-SHA-256, resource binding, the roster, presence subscription and
|
|
@@ -885,7 +960,9 @@ usage: factoidal webdav-serve CONFIGDIR [--port N] [--host ADDRESS]
|
|
|
885
960
|
[--login-limit N] [--login-window SECONDS]
|
|
886
961
|
[--dangerously-disable-ratelimits]
|
|
887
962
|
factoidal webdav-serve CONFIGDIR --xmpp JID --xmpp-password-file FILE
|
|
888
|
-
--xmpp-allow JID[,JID...] [--guest NAME] [--
|
|
963
|
+
--xmpp-allow JID[,JID...] [--guest NAME] [--users FILE]
|
|
964
|
+
[--login-limit N] [--login-window SECONDS] [--dangerously-disable-ratelimits]
|
|
965
|
+
[--xmpp-host H] [--xmpp-port P] [--xmpp-tls]
|
|
889
966
|
|
|
890
967
|
EMERGENCY MODE (https://github.com/danbri/factoidal/issues/1147): the server
|
|
891
968
|
is READ ONLY. Every write method (PUT, DELETE, MKCOL, COPY, MOVE, PROPPATCH,
|
|
@@ -994,13 +1071,24 @@ NAT is reached this way with no inbound port.
|
|
|
994
1071
|
--xmpp-host (127.0.0.0/8, ::1) is accepted.
|
|
995
1072
|
--xmpp-ca FILE PEM certificates to trust for --xmpp-tls
|
|
996
1073
|
--dangerously-allow-writable-without-login
|
|
997
|
-
start although a local mount
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
Without it such a start is refused.
|
|
1074
|
+
with no --users: start although a local mount
|
|
1075
|
+
is writable, so every allowed JID writes as the
|
|
1076
|
+
owner. Without it such a start is refused.
|
|
1077
|
+
With --users it is not needed.
|
|
1002
1078
|
--xmpp-resource R the resource to bind (default dav)
|
|
1003
1079
|
|
|
1080
|
+
Both checks apply to every request over XMPP
|
|
1081
|
+
(https://github.com/danbri/factoidal/issues/1125): the sender's bare JID must
|
|
1082
|
+
be in --xmpp-allow, and then the request acts as a user exactly as on TCP:
|
|
1083
|
+
the --guest user with no credentials, a user of --users with HTTP Basic
|
|
1084
|
+
credentials in the request's headers (xmpp-http-proxy passes Authorization
|
|
1085
|
+
unchanged). --users over XMPP needs --xmpp-tls or a loopback --xmpp-host.
|
|
1086
|
+
The failed-login limit over XMPP counts per user name and per bare JID of
|
|
1087
|
+
the sender (https://github.com/danbri/factoidal/issues/1128): one JID's
|
|
1088
|
+
failures do not block another JID. A request whose chunked body is not
|
|
1089
|
+
complete waits at most 60 seconds, and at most 4 such requests per sender
|
|
1090
|
+
(16 in total) wait at once; others get resource-constraint.
|
|
1091
|
+
|
|
1004
1092
|
It prints the bound JID (xmpp:user@domain/resource) on standard output.
|
|
1005
1093
|
XEP-0332 is Deferred, and no WebDAV client speaks it: a reader runs
|
|
1006
1094
|
"factoidal xmpp-http-proxy --target JID/RESOURCE" (as an allowed JID) and
|
|
@@ -1211,7 +1299,7 @@ const VALUE_OPTIONS = {
|
|
|
1211
1299
|
compact: new Set([]),
|
|
1212
1300
|
serve: new Set(['port', 'host', 'query-path', 'gsp-path', 'file', 'layout', 'tdb2', 'allow-host', 'allow-origin']),
|
|
1213
1301
|
'lws-serve': new Set(['port', 'host', 'base', 'issuer', 'auth-config', 'owner', 'root-acl',
|
|
1214
|
-
'audience', 'page-size', 'moves', 'controller-inbox', 'delivery-limit', 'delivery-window',
|
|
1302
|
+
'audience', 'page-size', 'moves', 'controller-inbox', 'delivery-limit', 'delivery-window', 'delivery-dest-limit',
|
|
1215
1303
|
'redirects-max', 'storage', 'cors-origin', 'authz', 'alg', 'token-lifetime', 'rotate-after',
|
|
1216
1304
|
'saml-idp', 'saml-decrypt-key', 'allow-host', 'allow-origin']),
|
|
1217
1305
|
'solid-serve': new Set(['port', 'host', 'base', 'owner', 'issuer', 'auth-config', 'allow-host', 'allow-origin']),
|
|
@@ -1223,7 +1311,7 @@ const VALUE_OPTIONS = {
|
|
|
1223
1311
|
'authz-serve': new Set(['port', 'host', 'issuer', 'resource', 'alg', 'token-lifetime', 'rotate-after',
|
|
1224
1312
|
'cors-origin', 'saml-idp', 'saml-decrypt-key']),
|
|
1225
1313
|
'authz-keys': new Set([]),
|
|
1226
|
-
'xmpp-serve': new Set(['port', 'host', 'domain', 'accounts', 'cert', 'key', 'tls-port', 'ws-port', 'ws-path', 'ws-origin', 'muc', 'muc-members', 'mix', 'gc3', 'state-key', 'max-connections', 'login-timeout']),
|
|
1314
|
+
'xmpp-serve': new Set(['port', 'host', 'domain', 'accounts', 'cert', 'key', 'tls-port', 'ws-port', 'ws-path', 'ws-origin', 'muc', 'muc-members', 'muc-owners', 'mix', 'gc3', 'state-key', 'max-connections', 'login-timeout', 'ping-interval', 'ping-timeout', 'verifier', 'iterations']),
|
|
1227
1315
|
'webdav-serve': new Set(['port', 'host', 'users', 'guest', 'cert', 'key', 'login-limit', 'login-window', 'xmpp', 'xmpp-password-file',
|
|
1228
1316
|
'xmpp-host', 'xmpp-port', 'xmpp-allow', 'xmpp-resource', 'xmpp-ca', 'allow-host', 'allow-origin']),
|
|
1229
1317
|
'webdav-user': new Set(['iterations']),
|
|
@@ -2339,6 +2427,10 @@ async function commandLwsServe (positional, options) {
|
|
|
2339
2427
|
...(typeof options['controller-inbox'] === 'string' ? { controllerInbox: options['controller-inbox'] } : {}),
|
|
2340
2428
|
...(options['delivery-limit'] !== undefined ? { deliveryLimit: natOption('delivery-limit', options['delivery-limit']) } : {}),
|
|
2341
2429
|
...(options['delivery-window'] !== undefined ? { deliveryWindow: natOption('delivery-window', options['delivery-window']) } : {}),
|
|
2430
|
+
...(options['delivery-dest-limit'] !== undefined ? { deliveryDestLimit: natOption('delivery-dest-limit', options['delivery-dest-limit']) } : {}),
|
|
2431
|
+
// Inboxes at special-purpose addresses (issue 1126): passed unread;
|
|
2432
|
+
// L4Factoidal/Net/Egress.lean decides.
|
|
2433
|
+
...(options['allow-private-fetch'] === true ? { allowPrivateFetch: true } : {}),
|
|
2342
2434
|
...(options['redirects-max'] !== undefined ? { redirectsMax: natOption('redirects-max', options['redirects-max']) } : {}),
|
|
2343
2435
|
...(typeof options.storage === 'string' ? { storage: options.storage } : {}),
|
|
2344
2436
|
...corsConfig(options),
|
|
@@ -2368,11 +2460,16 @@ async function commandLwsServe (positional, options) {
|
|
|
2368
2460
|
// The engine's start notices (a stale lock taken over; encryption at rest
|
|
2369
2461
|
// on or off), printed also with --quiet.
|
|
2370
2462
|
for (const n of (await running.storage()).notices || []) err(`lws-serve: ${n}`)
|
|
2463
|
+
// Printed also with --quiet: this option opens the local network to
|
|
2464
|
+
// requests that remote parties choose (issue 1126).
|
|
2465
|
+
if (options['allow-private-fetch'] === true) {
|
|
2466
|
+
err('lws-serve: WARNING: --allow-private-fetch: webhook and inbox deliveries and authorization ' +
|
|
2467
|
+
'server fetches may go to loopback, private, link-local and other special-purpose addresses')
|
|
2468
|
+
}
|
|
2371
2469
|
if (!quiet) {
|
|
2372
2470
|
err(`lws-serve: listening on ${running.origin} for ${positional[0]}`)
|
|
2373
2471
|
if (running.authz() !== null) err(`lws-serve: authorization server ${running.authz().issuer} (keys in ${authzDir})`)
|
|
2374
2472
|
if (options['allow-http'] === true) err('lws-serve: WARNING: --allow-http: http subject identifiers are accepted')
|
|
2375
|
-
if (options['allow-private-fetch'] === true) err('lws-serve: WARNING: --allow-private-fetch: private addresses may be fetched')
|
|
2376
2473
|
if (insecure) {
|
|
2377
2474
|
err('lws-serve: WARNING: --insecure-no-auth: every request is served without authorization')
|
|
2378
2475
|
err('lws-serve: --insecure-no-auth: only the Host names of this listener are answered, and a ' +
|
|
@@ -2641,20 +2738,32 @@ async function webdavOverXmpp (configDir, options, quiet) {
|
|
|
2641
2738
|
if (typeof options.xmpp !== 'string') throw new UsageError('--xmpp needs a JID')
|
|
2642
2739
|
if (typeof options['xmpp-password-file'] !== 'string') throw new UsageError('--xmpp needs --xmpp-password-file FILE')
|
|
2643
2740
|
if (typeof options['xmpp-allow'] !== 'string') throw new UsageError('--xmpp needs --xmpp-allow JID[,JID...]')
|
|
2644
|
-
// No
|
|
2645
|
-
// (https://github.com/danbri/factoidal/issues/1125).
|
|
2646
|
-
if (['
|
|
2647
|
-
throw new UsageError('--
|
|
2741
|
+
// No port is open with --xmpp, so the HTTPS options do not apply. --users
|
|
2742
|
+
// and the login-limit options do (https://github.com/danbri/factoidal/issues/1125).
|
|
2743
|
+
if (['cert', 'key'].some((k) => options[k] !== undefined)) {
|
|
2744
|
+
throw new UsageError('--cert and --key do not apply with --xmpp: no port is open; give --xmpp-tls for the XMPP stream')
|
|
2648
2745
|
}
|
|
2649
2746
|
const fs = await import('node:fs/promises')
|
|
2747
|
+
const { resolve: resolvePath } = await import('node:path')
|
|
2650
2748
|
const password = (await fs.readFile(options['xmpp-password-file'], 'utf8')).split(/\r?\n/)[0]
|
|
2651
2749
|
const allow = options['xmpp-allow'].split(',').map((x) => x.trim()).filter((x) => x.length > 0)
|
|
2652
2750
|
const { openWebdav, WebdavHostError } = await import('../webdav/server.mjs')
|
|
2653
2751
|
const { connectHttpTarget, XmppClientError } = await import('../xmpp/client.mjs')
|
|
2654
2752
|
if (options.guest !== undefined && typeof options.guest !== 'string') throw new UsageError('--guest needs a user name')
|
|
2753
|
+
// The login (issue 1125): the same users file and limit options as on
|
|
2754
|
+
// TCP, and the XMPP stream as the transport; the engine checks them all.
|
|
2755
|
+
const davOptions = { configDir, transport: 'xmpp', jid: options.xmpp, tls: options['xmpp-tls'] === true }
|
|
2756
|
+
if (typeof options['xmpp-host'] === 'string') davOptions.host = options['xmpp-host']
|
|
2757
|
+
if (typeof options.guest === 'string') davOptions.guest = options.guest
|
|
2758
|
+
if (typeof options.users === 'string') davOptions.users = resolvePath(options.users)
|
|
2759
|
+
if (options['login-limit'] !== undefined) davOptions.loginLimit = String(options['login-limit'])
|
|
2760
|
+
if (options['login-window'] !== undefined) davOptions.loginWindow = String(options['login-window'])
|
|
2761
|
+
if (options['dangerously-disable-ratelimits'] !== undefined) {
|
|
2762
|
+
davOptions.disableRateLimits = options['dangerously-disable-ratelimits'] === true
|
|
2763
|
+
}
|
|
2655
2764
|
let dav
|
|
2656
2765
|
try {
|
|
2657
|
-
dav = await openWebdav(
|
|
2766
|
+
dav = await openWebdav(davOptions)
|
|
2658
2767
|
} catch (error) {
|
|
2659
2768
|
if (error instanceof WebdavHostError && error.unknownOp) return notWired('webdav-serve', error.message)
|
|
2660
2769
|
if (error instanceof WebdavHostError || (error !== null && typeof error === 'object' && error.code === 'ENOENT')) {
|
|
@@ -2674,6 +2783,7 @@ async function webdavOverXmpp (configDir, options, quiet) {
|
|
|
2674
2783
|
tls: options['xmpp-tls'] === true,
|
|
2675
2784
|
mounts: dav.mounts,
|
|
2676
2785
|
dangerouslyAllowWritableWithoutLogin: options['dangerously-allow-writable-without-login'] === true,
|
|
2786
|
+
login: dav.login,
|
|
2677
2787
|
step: (request) => dav.step(request),
|
|
2678
2788
|
onError: (error) => { if (!quiet) err(`webdav-serve: xmpp: ${error.message}`) }
|
|
2679
2789
|
}
|
|
@@ -2697,7 +2807,8 @@ async function webdavOverXmpp (configDir, options, quiet) {
|
|
|
2697
2807
|
throw error
|
|
2698
2808
|
}
|
|
2699
2809
|
if (!quiet) {
|
|
2700
|
-
err(`webdav-serve: connected to XMPP as ${conn.jid}; no port is open; requests from ${allow.join(', ')}`
|
|
2810
|
+
err(`webdav-serve: connected to XMPP as ${conn.jid}; no port is open; requests from ${allow.join(', ')}` +
|
|
2811
|
+
(dav.login ? `, login from ${options.users} (failed logins limited per user name and per bare JID)` : ', no --users: credentials get 401'))
|
|
2701
2812
|
if (conn.writableWithoutLogin.length > 0) {
|
|
2702
2813
|
err(`webdav-serve: WARNING: --dangerously-allow-writable-without-login: every allowed JID can write ${conn.writableWithoutLogin.join(', ')} as the storage owner, with no login`)
|
|
2703
2814
|
}
|
|
@@ -2862,6 +2973,17 @@ async function commandWebdavUser (positional, options) {
|
|
|
2862
2973
|
}
|
|
2863
2974
|
|
|
2864
2975
|
async function commandXmppServe (positional, options) {
|
|
2976
|
+
// One accounts line with no password in it: the password comes from stdin.
|
|
2977
|
+
if (typeof options.verifier === 'string') {
|
|
2978
|
+
const { randomBytes } = await import('node:crypto')
|
|
2979
|
+
const { readFileSync } = await import('node:fs')
|
|
2980
|
+
const { loadEngine } = await import('./engine.mjs')
|
|
2981
|
+
const password = readFileSync(0, 'utf8').replace(/\r?\n$/, '')
|
|
2982
|
+
const engine = await loadEngine()
|
|
2983
|
+
const made = engine.call('xmppVerifier', [options.verifier, password, randomBytes(16).toString('base64'), String(options.iterations ?? 10000)])
|
|
2984
|
+
out(made.line)
|
|
2985
|
+
return 0
|
|
2986
|
+
}
|
|
2865
2987
|
if (positional.length !== 1) throw new UsageError('xmpp-serve needs one DIR')
|
|
2866
2988
|
const quiet = options.quiet === true
|
|
2867
2989
|
const { listen, XmppHostError } = await import('../xmpp/server.mjs')
|
|
@@ -2879,7 +3001,17 @@ async function commandXmppServe (positional, options) {
|
|
|
2879
3001
|
onError: (error) => { if (!quiet) err(`xmpp-serve: ${error.message}`) }
|
|
2880
3002
|
}
|
|
2881
3003
|
if (typeof options.domain === 'string') settings.domain = options.domain
|
|
2882
|
-
if (typeof options.accounts === 'string')
|
|
3004
|
+
if (typeof options.accounts === 'string') {
|
|
3005
|
+
settings.accountsText = await fs.readFile(options.accounts, 'utf8')
|
|
3006
|
+
settings.accountsFile = options.accounts
|
|
3007
|
+
}
|
|
3008
|
+
// The failed SASL login limit (https://github.com/danbri/factoidal/issues/1128):
|
|
3009
|
+
// on by default; the engine decides and words the warning.
|
|
3010
|
+
if (options['dangerously-disable-ratelimits'] !== undefined) {
|
|
3011
|
+
settings.disableRateLimits = options['dangerously-disable-ratelimits'] === true
|
|
3012
|
+
}
|
|
3013
|
+
if (options['no-address-limit'] === true) settings.addressLimit = false
|
|
3014
|
+
if (options['proxy-protocol'] === true) settings.proxyProtocol = true
|
|
2883
3015
|
if (useTls) {
|
|
2884
3016
|
settings.cert = await fs.readFile(options.cert)
|
|
2885
3017
|
settings.key = await fs.readFile(options.key)
|
|
@@ -2916,6 +3048,20 @@ async function commandXmppServe (positional, options) {
|
|
|
2916
3048
|
settings.mucMembers.push({ room, members })
|
|
2917
3049
|
}
|
|
2918
3050
|
}
|
|
3051
|
+
if (options['muc-owners'] !== undefined) {
|
|
3052
|
+
if (typeof options['muc-owners'] !== 'string') throw new UsageError('--muc-owners needs a FILE')
|
|
3053
|
+
if (settings.mucDomain === undefined) throw new UsageError('--muc-owners needs --muc')
|
|
3054
|
+
settings.mucOwners = []
|
|
3055
|
+
for (const line of (await fs.readFile(options['muc-owners'], 'utf8')).split('\n')) {
|
|
3056
|
+
if (line.trim() === '') continue
|
|
3057
|
+
const [, room, list] = /^([^:\s@/]+):(.*)$/.exec(line.trim()) ?? []
|
|
3058
|
+
const owners = (list ?? '').split(/\s+/).filter(Boolean)
|
|
3059
|
+
if (room === undefined || owners.length === 0 || owners.some((m) => !/^[^\s@/]+@[^\s@/]+$/.test(m))) {
|
|
3060
|
+
throw new UsageError(`--muc-owners: not "room: bare@jid ...": ${line}`)
|
|
3061
|
+
}
|
|
3062
|
+
settings.mucOwners.push({ room, owners })
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
2919
3065
|
if (options.mix !== undefined) {
|
|
2920
3066
|
if (typeof options.mix !== 'string' || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i.test(options.mix)) {
|
|
2921
3067
|
throw new UsageError('--mix needs a domain name, e.g. mix.example.com')
|
|
@@ -2940,6 +3086,16 @@ async function commandXmppServe (positional, options) {
|
|
|
2940
3086
|
if (!Number.isSafeInteger(n) || n < 0) throw new UsageError('--login-timeout needs a number of seconds')
|
|
2941
3087
|
settings.loginTimeoutMs = n * 1000
|
|
2942
3088
|
}
|
|
3089
|
+
if (options['ping-interval'] !== undefined) {
|
|
3090
|
+
const n = Number(options['ping-interval'])
|
|
3091
|
+
if (!Number.isSafeInteger(n) || n < 0) throw new UsageError('--ping-interval needs a number of seconds')
|
|
3092
|
+
settings.pingIntervalMs = n * 1000
|
|
3093
|
+
}
|
|
3094
|
+
if (options['ping-timeout'] !== undefined) {
|
|
3095
|
+
const n = Number(options['ping-timeout'])
|
|
3096
|
+
if (!Number.isSafeInteger(n) || n < 1) throw new UsageError('--ping-timeout needs a positive number of seconds')
|
|
3097
|
+
settings.pingTimeoutMs = n * 1000
|
|
3098
|
+
}
|
|
2943
3099
|
if (options['ws-port'] !== undefined) {
|
|
2944
3100
|
const wsPort = Number(options['ws-port'])
|
|
2945
3101
|
if (!Number.isInteger(wsPort) || wsPort < 0 || wsPort > 65535) throw new UsageError('--ws-port needs a port number')
|
|
@@ -2968,10 +3124,15 @@ async function commandXmppServe (positional, options) {
|
|
|
2968
3124
|
}
|
|
2969
3125
|
throw error
|
|
2970
3126
|
}
|
|
3127
|
+
// The engine's start warnings (--dangerously-disable-ratelimits), printed
|
|
3128
|
+
// also with --quiet.
|
|
3129
|
+
for (const w of running.xmpp.warnings || []) err(`factoidal xmpp-serve: WARNING: ${w}`)
|
|
2971
3130
|
if (!quiet) {
|
|
2972
3131
|
err(`xmpp-serve: listening on ${running.origin} for domain ${running.xmpp.domain}` +
|
|
2973
3132
|
` (${running.xmpp.accounts} accounts, state ${positional[0]}` +
|
|
2974
3133
|
(startTls ? ', STARTTLS required)' : useTls ? ', direct TLS)' : ', PLAINTEXT: no TLS on this listener)'))
|
|
3134
|
+
if (settings.proxyProtocol) err('xmpp-serve: every connection must start with a PROXY protocol v2 header')
|
|
3135
|
+
else if (settings.addressLimit === false) err('xmpp-serve: failed logins count per account name only (--no-address-limit)')
|
|
2975
3136
|
if (settings.mucDomain) err(`xmpp-serve: multi-user chat (XEP-0045) at ${settings.mucDomain}`)
|
|
2976
3137
|
if (settings.mixDomain) err(`xmpp-serve: MIX channels (XEP-0369, joined by XEP-0405) at ${settings.mixDomain}`)
|
|
2977
3138
|
if (settings.gc3Domain) err(`xmpp-serve: GC3 PROTOTYPE groups at ${settings.gc3Domain} (no published spec; invented wire format)`)
|
package/bin/foafnaut.mjs
CHANGED
|
@@ -10,9 +10,11 @@ import { homedir } from 'node:os';
|
|
|
10
10
|
import { basename, join, resolve } from 'node:path';
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
|
|
13
|
-
const HELP = `foafnaut [--latest|--bleeding-edge] [PAGE] [-- APP_ARGS]
|
|
13
|
+
const HELP = `foafnaut [--latest|--bleeding-edge] [PAGE | --url URL | --html FILE] [APP_OPTIONS] [-- APP_ARGS]
|
|
14
14
|
|
|
15
|
-
PAGE an http(s) URL or an HTML file; none opens the default page
|
|
15
|
+
PAGE an http(s) or file: URL, or an HTML file; none opens the default page
|
|
16
|
+
--url URL the page as a URL (http, https or file)
|
|
17
|
+
--html FILE the page as a local HTML file
|
|
16
18
|
--latest install the newest published macOS build, then use it
|
|
17
19
|
--bleeding-edge use the local build (config.json "bleedingEdge", or
|
|
18
20
|
~/Applications/foafnaut.app)
|
|
@@ -23,6 +25,15 @@ const HELP = `foafnaut [--latest|--bleeding-edge] [PAGE] [-- APP_ARGS]
|
|
|
23
25
|
--which print the app that would run
|
|
24
26
|
--help this text
|
|
25
27
|
|
|
28
|
+
App options, passed to the app as they are:
|
|
29
|
+
--perf-summary print frame and input latency figures when the window closes
|
|
30
|
+
--check-pixels read back and check every frame (slow)
|
|
31
|
+
--wasm-engine use the Wasm engine, not the native library
|
|
32
|
+
--native-engine PATH use this native engine library
|
|
33
|
+
--font FILE load this font file
|
|
34
|
+
--bitmap-font use the baked-in ASCII font only
|
|
35
|
+
--follow-links open links that a page asks for at load
|
|
36
|
+
|
|
26
37
|
Also: foafnaut-latest, foafnaut-bleedingedge. Environment: FOAFNAUT_HOME, and
|
|
27
38
|
FOAFNAUT_APP, which chooses the app outright. Page data goes to the app. Its trace
|
|
28
39
|
is in ~/Library/Logs/foafnaut/trace.jsonl. The command stays open until the window
|
|
@@ -96,9 +107,14 @@ async function installLatest() {
|
|
|
96
107
|
return versionApp(dir);
|
|
97
108
|
}
|
|
98
109
|
|
|
110
|
+
// App options that take no value, and those that take one.
|
|
111
|
+
const APP_FLAGS = ['--perf-summary', '--check-pixels', '--wasm-engine', '--bitmap-font', '--follow-links', '--idle-checks-only', '--trace-input'];
|
|
112
|
+
const APP_VALUES = ['--native-engine', '--font'];
|
|
113
|
+
|
|
99
114
|
function pageArgs(page) {
|
|
100
115
|
if (page === undefined) return [];
|
|
101
116
|
if (/^https?:\/\//i.test(page)) return ['--url', page];
|
|
117
|
+
if (/^file:/i.test(page)) page = new URL(page).pathname;
|
|
102
118
|
const path = resolve(page);
|
|
103
119
|
if (!existsSync(path)) fail(`no such file: ${page}`);
|
|
104
120
|
return ['--html', path];
|
|
@@ -109,10 +125,13 @@ export async function main(argv, channel = 'current') {
|
|
|
109
125
|
const options = split < 0 ? argv : argv.slice(0, split);
|
|
110
126
|
const extra = split < 0 ? [] : argv.slice(split + 1);
|
|
111
127
|
let page, which = false, verbose = false, detach = false;
|
|
128
|
+
const app_ = [];
|
|
129
|
+
const setPage = (value) => { if (page !== undefined) fail('give one page'); if (value === undefined) fail('a page option needs a value'); page = value; };
|
|
112
130
|
// Each of these picks the app or the action. Two of them have no one meaning.
|
|
113
131
|
const exclusive = options.filter((arg) => ['--latest', '--bleeding-edge', '--install-latest', '--list'].includes(arg));
|
|
114
132
|
if (new Set(exclusive).size > 1) fail(`${exclusive.join(' and ')} can not be used together; see --help`);
|
|
115
|
-
for (
|
|
133
|
+
for (let i = 0; i < options.length; i++) {
|
|
134
|
+
const arg = options[i];
|
|
116
135
|
if (arg === '--help' || arg === '-h') { process.stdout.write(HELP); return; }
|
|
117
136
|
else if (arg === '--latest') channel = 'latest';
|
|
118
137
|
else if (arg === '--bleeding-edge') channel = 'bleeding-edge';
|
|
@@ -127,9 +146,12 @@ export async function main(argv, channel = 'current') {
|
|
|
127
146
|
console.log(`${name === current ? '*' : ' '} ${name}`);
|
|
128
147
|
console.log(` bleeding edge: ${bleedingEdgeApp()}${existsSync(bleedingEdgeApp()) ? '' : ' (not found)'}`);
|
|
129
148
|
return;
|
|
130
|
-
} else if (arg
|
|
131
|
-
else if (
|
|
132
|
-
else
|
|
149
|
+
} else if (arg === '--url' || arg === '--html') setPage(options[++i]);
|
|
150
|
+
else if (arg.startsWith('--url=') || arg.startsWith('--html=')) setPage(arg.slice(arg.indexOf('=') + 1));
|
|
151
|
+
else if (APP_FLAGS.includes(arg)) app_.push(arg);
|
|
152
|
+
else if (APP_VALUES.includes(arg)) { if (options[i + 1] === undefined) fail(`${arg} needs a value`); app_.push(arg, options[++i]); }
|
|
153
|
+
else if (arg.startsWith('-')) fail(`unknown option ${arg}; see --help`);
|
|
154
|
+
else setPage(arg);
|
|
133
155
|
}
|
|
134
156
|
if (process.platform !== 'darwin') fail('the native window is macOS only; use the Hub page in a WebGPU browser');
|
|
135
157
|
let app = process.env.FOAFNAUT_APP;
|
|
@@ -138,7 +160,7 @@ export async function main(argv, channel = 'current') {
|
|
|
138
160
|
if (!app) app = currentApp() ?? bleedingEdgeApp();
|
|
139
161
|
if (!existsSync(app)) fail(`no app at ${app}. Run foafnaut --install-latest.`);
|
|
140
162
|
if (which) { console.log(app); return; }
|
|
141
|
-
const args = ['-n', ...(detach ? [] : ['-W']), app, '--args', ...pageArgs(page), ...(verbose ? ['--trace-input'] : []), ...extra];
|
|
163
|
+
const args = ['-n', ...(detach ? [] : ['-W']), app, '--args', ...pageArgs(page), ...(verbose ? ['--trace-input'] : []), ...app_, ...extra];
|
|
142
164
|
const log = join(homedir(), 'Library', 'Logs', 'foafnaut', 'trace.jsonl');
|
|
143
165
|
if (verbose) console.error(`foafnaut: app ${app}\nfoafnaut: open ${args.join(' ')}\nfoafnaut: trace ${log}`);
|
|
144
166
|
// Follow only what this run appends.
|
package/browser.js
CHANGED
|
@@ -27,7 +27,7 @@ import { entryFromEngine } from './lib/lean-entry.mjs';
|
|
|
27
27
|
import { browserEngine } from './lib/browser-engine.mjs';
|
|
28
28
|
import { indexedDbDeltaLogStore } from './lib/delta-log-hosts.mjs';
|
|
29
29
|
import { sbm11Api, registerSbm11Source, mapReader } from './lib/sbm11.mjs';
|
|
30
|
-
import { lwsSbm11Reader } from './lib/sbm11-lws.mjs';
|
|
30
|
+
import { lwsSbm11Reader as lwsSbm11ReaderWith } from './lib/sbm11-lws.mjs';
|
|
31
31
|
|
|
32
32
|
const l4Engine = browserEngine('l4factoidal', new URL('./l4-assets/', import.meta.url).href);
|
|
33
33
|
|
|
@@ -73,7 +73,10 @@ export const packSbm11 = sbm11.packSbm11;
|
|
|
73
73
|
export { registerSbm11Source, mapReader };
|
|
74
74
|
// A reader of an SBM11 generation held as LWS resources (issue 1010 KR3). In
|
|
75
75
|
// a browser worker its default transport is a synchronous XMLHttpRequest.
|
|
76
|
-
|
|
76
|
+
// Asynchronous: it loads the engine, whose op containerPath decides every URL.
|
|
77
|
+
export async function lwsSbm11Reader(url, opts = {}) {
|
|
78
|
+
return lwsSbm11ReaderWith(url, { ...opts, engine: opts.engine ?? await loadEngine() });
|
|
79
|
+
}
|
|
77
80
|
|
|
78
81
|
// RDF and SPARQL
|
|
79
82
|
export const parse = api.parse;
|
package/index.d.ts
CHANGED
|
@@ -240,7 +240,7 @@ export type WriteFormat =
|
|
|
240
240
|
| 'rdfxml' | 'rdf-xml' | 'rdf' | 'jsonld' | 'json-ld' | 'rdfc' | 'canonical';
|
|
241
241
|
|
|
242
242
|
/** Results formats of query() with `output`. */
|
|
243
|
-
export type ResultsFormat = 'json' | 'xml' | 'csv' | 'tsv' | 'table' | 'ntriples';
|
|
243
|
+
export type ResultsFormat = 'json' | 'xml' | 'xml-report' | 'csv' | 'tsv' | 'table' | 'ntriples';
|
|
244
244
|
|
|
245
245
|
/** One row of formats(). */
|
|
246
246
|
export interface FormatCapability {
|
|
@@ -1570,10 +1570,14 @@ export type Sbm11Transport = (url: string, headers: Record<string, string>) =>
|
|
|
1570
1570
|
* null before transport and appear in errors().
|
|
1571
1571
|
* Browser worker default XHR accepts no caller auth/headers; provide an explicit
|
|
1572
1572
|
* trusted transport for those. Its final-URL check cannot prevent an already
|
|
1573
|
-
* sent anonymous request or ambient same-origin browser cookies.
|
|
1573
|
+
* sent anonymous request or ambient same-origin browser cookies.
|
|
1574
|
+
* Asynchronous: the engine's op containerPath (Lean, L4Factoidal.LWS.ContainerPath)
|
|
1575
|
+
* decides the container URL and every resource URL. */
|
|
1574
1576
|
export function lwsSbm11Reader(url: string, opts?: {
|
|
1575
|
-
auth?: string; headers?: Record<string, string>; transport?: Sbm11Transport; timeoutMs?: number
|
|
1576
|
-
|
|
1577
|
+
auth?: string; headers?: Record<string, string>; transport?: Sbm11Transport; timeoutMs?: number;
|
|
1578
|
+
/** a loaded engine handle (default: loadEngine()); its op containerPath decides every URL */
|
|
1579
|
+
engine?: object
|
|
1580
|
+
}): Promise<Sbm11Reader & { evidence(): Sbm11ReadEvidence[]; errors(): string[]; close(): void }>;
|
|
1577
1581
|
|
|
1578
1582
|
/** One query against a store: open with the query's plan, query, close. */
|
|
1579
1583
|
export function queryStore(
|
|
@@ -2260,13 +2264,22 @@ export interface SparqlResultsJson {
|
|
|
2260
2264
|
boolean?: boolean;
|
|
2261
2265
|
}
|
|
2262
2266
|
|
|
2267
|
+
/** The exact serialized result is checked by the Lean XML parser. This is
|
|
2268
|
+
* parser admission, not a proof of SPARQL serialization fidelity or XSLT success.
|
|
2269
|
+
* Invalid RDF/XML characters are refused, never removed or replaced.
|
|
2270
|
+
*/
|
|
2271
|
+
export type SparqlXmlReport =
|
|
2272
|
+
| { kind: 'xml'; xml: string }
|
|
2273
|
+
| { kind: 'diagnostic'; code: 'XML_ADMISSION_REFUSED'; message: string };
|
|
2274
|
+
|
|
2263
2275
|
export type OutputFormat =
|
|
2264
|
-
| 'json' | 'csv' | 'tsv' | 'xml' | 'table' | 'ntriples';
|
|
2276
|
+
| 'json' | 'csv' | 'tsv' | 'xml' | 'xml-report' | 'table' | 'ntriples';
|
|
2265
2277
|
|
|
2266
2278
|
/**
|
|
2267
2279
|
* One call, serialised answer: parse `dataString`, close the default
|
|
2268
2280
|
* graph under `entail`, run the query. Resolves to parsed SPARQL Results
|
|
2269
|
-
* JSON when output is 'json' (default),
|
|
2281
|
+
* JSON when output is 'json' (default), an XML/diagnostic object for
|
|
2282
|
+
* 'xml-report', and text otherwise. 'csv', 'tsv'
|
|
2270
2283
|
* and 'xml' are the W3C SPARQL results formats (CSV and TSV have no ASK
|
|
2271
2284
|
* encoding, so an ASK with those rejects); 'table' and 'ntriples' are
|
|
2272
2285
|
* display formats. A CONSTRUCT or DESCRIBE answer is a graph and takes
|
|
@@ -2280,7 +2293,7 @@ export function queryRaw(
|
|
|
2280
2293
|
entail?: EntailRegime;
|
|
2281
2294
|
output?: OutputFormat;
|
|
2282
2295
|
}
|
|
2283
|
-
): Promise<SparqlResultsJson | string>;
|
|
2296
|
+
): Promise<SparqlResultsJson | SparqlXmlReport | string>;
|
|
2284
2297
|
|
|
2285
2298
|
/** Package version string, e.g. '0.9.0'. */
|
|
2286
2299
|
export const version: string;
|
package/index.mjs
CHANGED
|
@@ -103,12 +103,15 @@ export { registerSbm11Source, mapReader };
|
|
|
103
103
|
// A reader of an SBM11 generation held as LWS resources (lib/sbm11-lws.mjs;
|
|
104
104
|
// https://github.com/danbri/factoidal/issues/1010 KR3). In Node the default
|
|
105
105
|
// synchronous transport is a fetch in a worker thread (lib/sync-http-node.mjs),
|
|
106
|
-
// stopped by the reader's close().
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
// stopped by the reader's close(). Asynchronous: it loads the engine, whose
|
|
107
|
+
// op containerPath decides the container URL and every resource URL
|
|
108
|
+
// (formal/lean4/L4Factoidal/LWS/ContainerPath.lean).
|
|
109
|
+
export async function lwsSbm11Reader(url, opts = {}) {
|
|
110
|
+
const engine = opts.engine ?? await loadEngine();
|
|
111
|
+
if (typeof opts.transport === 'function') return lwsSbm11ReaderWith(url, { ...opts, engine });
|
|
109
112
|
const transport = nodeSyncTransport({ timeoutMs: opts.timeoutMs });
|
|
110
113
|
try {
|
|
111
|
-
return lwsSbm11ReaderWith(url, { ...opts, transport, onClose: () => transport.close() });
|
|
114
|
+
return lwsSbm11ReaderWith(url, { ...opts, engine, transport, onClose: () => transport.close() });
|
|
112
115
|
} catch (error) {
|
|
113
116
|
transport.close();
|
|
114
117
|
throw error;
|
package/l4-assets/l4factoidal.js
CHANGED
|
Binary file
|