@factoidal/core 0.28.0 → 0.30.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 CHANGED
@@ -1,6 +1,32 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.30.0 (2026-11-04)
4
+
5
+ - The Lean XMPP server now has an in-page host (`@factoidal/core/xmpp/inpage`): `openInPageXmppServer` exposes ordered message connections and `inPageWebSocket` adapts them for RFC 7395 clients. Protocol decisions remain in Lean, with shared routing for direct messages, MUC rooms, MIX channels and experimental GC3 groups. This provides an app-to-app transport; it does not include a chat UI or WebMCP bridge.
6
+ - The Node XMPP host adds STARTTLS and WebSocket transport, IQ routing and sealed LWS-backed state for restart persistence. LWS writes enforce the configured content limit with a 413 response; component requests carry their principal through authorization.
7
+
8
+ - `openInPageLwsStorage({ engine, baseIri, principal, rootAcl?, storageKey?, snapshot?, onSnapshot? })` (`@factoidal/core/lws/inpage`): one Lean LWS storage with no network, in a page, a worker or Node, reached through a `fetch`-shaped function that answers real `Response`s. Every request is the component principal's (issue 1238) and Web Access Control decides it; notifications are off. The storage is kept as one snapshot, sealed when a `storageKey` is given: after each change it is handed to `onSnapshot`, and the caller passes it back as `snapshot` to reopen (the engine refuses one sealed under another key). No Node API. Test: `test/lws-inpage.test.mjs`.
9
+ - `openLwsDataset(storageRootUrl, { fetch, auth })` (`@factoidal/core/lws/dataset`): an LWS storage as an RDF dataset (https://github.com/danbri/factoidal/issues/1011, version 2 of the view). The default graph is empty. The first named graph is the table of contents, named by the storage description IRI (the target of the root's `https://www.w3.org/ns/lws#storage` link; the result member `toc`): the containment as `lws:items`, each listing member's `rdf:type` (`lws:Container` or `lws:DataResource`) with its `dcterms:format`, `schema:size` and `dcterms:modified`, and `<resource> <http://www.iana.org/assignments/relation/linkset> <linkset>` for each linkset read. Then one named graph per RDF resource (named by its IRI) and one per linkset (named by the linkset IRI, one triple per link; relation types become IRIs from the LWS vocabulary, else `http://www.iana.org/assignments/relation/<name>`). `query()` answers `{result, evidence}`; the evidence is each document read with its ETag and each linkset read with its linkset IRI and ETag. A full crawl on every open (one HEAD and one GET per resource for its linkset), no search index, no caching. The definition is Lean (`LWS/DatasetView.lean`, theorems `view_names_nodup`, `view_toc_first`, `view_replace`); the source theorem `lwsSource_query_sound` rests on the named assumption `lwsReadsAtSnapshot`. Needs a wasm build with version 2 of the ops `lwsViewPlan` (with `metadata`) and `lwsDatasetView`.
10
+ - The experimental code notice now says "EXPERIMENTAL UNDER-REVIEWED CODE" (was "UNREVIEWED"), in the README, the package description, every help text, the warning on standard error when a server, client or key command starts, and the hub pages. Owner decision of 2026-10-28, confirmed 2026-11-03.
11
+
12
+ ## 0.29.0 (2026-11-02)
13
+
14
+ - The Data Compositor can open and save versioned pipeline JSON files. Import validates the recipe and never runs it automatically.
15
+ - The Data Compositor can discover storage declared by an explicitly selected FOAF profile, browse and load pipelines, and create or update files with LWS preconditions. Remembered data contains pod metadata only; credentials stay in the dialog session. The server still controls write access.
16
+ - The browser LWS client no longer eagerly imports its Node-only transport loader. Browser applications can import the public client and supply a browser transport without loading Node built-ins.
17
+ - Package functions `xmlVerify` and `xmlSign` (XML Signature 1.1, all hosts) over the wasm ops of the same names. `xmlVerify(document, {key?, hmacKey?, keyFromKeyInfo?, certificates?, documents?})` gives `{valid: true, signedInfo, references: [{uri, octets}], key, certificate}` or `{valid: false, reason}`; wrong arguments throw a `FactoidalError`. `xmlSign(document, {key, signatureMethod?, canonicalizationMethod?, inclusivePrefixes?, references?, enveloping?, keyValue?})` gives `{document}`. Keys are JWKs, octets are `Uint8Array`; the ECDSA nonce comes from `crypto.getRandomValues`. Test: `test/xmldsig.test.mjs` (RSA, ECDSA and HMAC round trips; node:crypto checks each signature value over the reported `ds:SignedInfo` and each `DigestValue`).
18
+ - XML Signature 1.1 on the wasm dispatch surface (`formal/lean4/Wasm/Ops/XmlDsig.lean`): new wasm ops `xmlVerify` (`{document, key?, hmacKey?, keyFromKeyInfo?, certificates?, documents?}` -> `{valid, signedInfo, references, key, certificate}` or `{valid: false, reason}`) and `xmlSign` (`{document, key, signatureMethod?, canonicalizationMethod?, inclusivePrefixes?, references?, enveloping?, keyValue?, random?}` -> `{document}`). Keys are JWKs; RSA and P-256 by HACL\*, HMAC in Lean; a private key goes only to the custodians of `XML/DSig11Native.lean`, and every signed document passes core validation before it is returned.
19
+ - XML Signature 1.1 `ds:RetrievalMethod` (§4.5.3) in the Lean engine: the `URI` is dereferenced as a reference's (same-document, or the caller's resolver; the engine fetches nothing), its `ds:Transforms` are applied, and the result is read as its `Type` says: `rawX509Certificate` octets, or one element (`ds:X509Data`, `ds:KeyValue`, `ds:RSAKeyValue`, `dsig11:ECKeyValue`, `dsig11:DEREncodedKeyValue`) whose name is the `Type`. One level only. Theorems `dsig11_2013_retrieved_x509Data_eq_direct`, `dsig11_2013_raw_eq_x509Certificate`, `dsig11_2013_retrieved_type`. `lake exe l4xmldsig`: retrieval fixtures 17 pass, 0 fail (out of 17); certificate selection in the 2001 vectors 7 pass, 0 fail (out of 7). Closes https://github.com/danbri/factoidal/issues/1228. No npm or wasm op yet.
20
+ - XML Signature 1.1 `ds:X509Data` keys (§4.5.4) in the Lean engine: `X509Certificate`, `X509IssuerSerial`, `X509SubjectName` (RFC 4514 names), `X509SKI` and `dsig11:X509Digest` select one certificate among those of the element and those the caller gives (`Config.certificates`); with no identifier, the one end-entity certificate of the element. The key is the certificate's SubjectPublicKeyInfo, read as `dsig11:DEREncodedKeyValue` is (theorem `dsig11_2013_x509_key_eq_derEncoded`); the selected certificate matches every identifier (theorem `x509SelectCore_2013_identified`). Certificate path validation stays the caller's (`signingCertificate?`). New DER reader `L4Factoidal/XML/X509.lean`. `lake exe l4xmldsig`: X509Data fixtures made by signxml 16 pass, 0 fail (out of 16); certificate selection in the 6 X.509 vectors of `merlin-xmldsig-twenty-three` 6 pass, 0 fail (out of 6; their DSA keys stay refused, https://github.com/danbri/factoidal/issues/1224). Closes https://github.com/danbri/factoidal/issues/1226. `ds:RetrievalMethod` is not followed (https://github.com/danbri/factoidal/issues/1228); the certificate of the 2012 `X509Digest` vector is not published (https://github.com/danbri/factoidal/issues/1229). No npm or wasm op yet.
21
+ - The XSLT transform of XML Signature 1.1 (§6.6.5) in the Lean engine (`L4Factoidal/XML/DSigXslt.lean`): the stylesheet in `ds:Transform` runs on the XSLT 4.0 processor over the input octets, and the result is written with the stylesheet's `xsl:output`. Theorem `dsig11_xslt_1999_output`. `lake exe l4xmldsig` on Merlin Hughes's 2001 interop vectors `merlin-xmldsig-twenty-three` (now vendored): 38 pass, 0 fail, 14 expected failures (out of 52; every reference digest matches, the XSLT reference included; the 14 are DSA, https://github.com/danbri/factoidal/issues/1224, and one 1024-bit RSA key, https://github.com/danbri/factoidal/issues/1227). Closes the XSLT part of https://github.com/danbri/factoidal/issues/1225. No npm or wasm op yet.
22
+ - XML Signature XPath Filter 2.0 (W3C Recommendation, 2002) in the Lean engine (`L4Factoidal/XML/DSigFilter2.lean`), as a transform of XML Signature 1.1 core validation and generation: intersect, subtract and union over subtree-expanded node-sets, evaluated by the XPath 4.0 engine; `here()` for this transform and for the XPath filtering transform. Theorem `filter2_2002_single_pass`: the filter set equals the single-pass flag of §3.4. `lake exe l4xmldsig` on Merlin Hughes's `merlin-xpath-filter2-three` vectors: 4 pass, 0 fail, 2 expected failures (out of 6; DSA signatures, https://github.com/danbri/factoidal/issues/1224). The XSLT transform is still refused (https://github.com/danbri/factoidal/issues/1225). No npm or wasm op yet.
23
+ - XML Signature 1.1 ECDSA P-256 with each digest of §6.2 (SHA-1, SHA-224, SHA-256, SHA-384, SHA-512) in the Lean engine, for core validation and signature generation: the digest is computed in Lean and HACL\* checks or makes the signature over its FIPS 186-4 §6.4 representative (`Crypto.P256.p256MsgRep`, `Hacl_P256_ecdsa_verif_without_hash`, `Hacl_P256_ecdsa_sign_p256_without_hash`). P-384 and P-521 are still refused (https://github.com/danbri/factoidal/issues/1223). No npm or wasm op yet.
24
+ - XML Signature 1.1 signature generation (§3.1) in the Lean engine (`L4Factoidal/XML/DSig11Sign.lean`): references are digested and `ds:SignedInfo` is canonicalized by the same functions as core validation, in the document that is returned; enveloped and enveloping placement; `ds:KeyValue` with `ds:RSAKeyValue` or `dsig11:ECKeyValue`. Keys stay in one signer component (`Signer`: `hmacSigner`; `rsaSigner` with RSASSA-PKCS1-v1_5 and the HACL\* private operation, the value checked before it is given out; `p256Signer` with a nonce per call). Every signed document is checked by core validation before it is returned (theorem `dsig11_sign_validates`). `lake exe l4xmldsig`: signature generation 15 pass, 0 fail (out of 15); signxml 5.2 verifies the same 15 documents (`tests/xmldsig/verify_lean_signed.py`). No npm or wasm op yet.
25
+ - XML Signature 1.1 core validation in the Lean engine (`L4Factoidal/XML/DSig11.lean`): reference processing (§4.4.3) over the node-sets of `XML.C14n`, with same-document references, the enveloped-signature, XPath filtering, base64 and the six canonicalization transforms; SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512 digests; HMAC with `HMACOutputLength` (§6.3.1), RSASSA-PKCS1-v1_5 and ECDSA P-256 SHA-256 signatures; keys from `ds:KeyValue`, `dsig11:DEREncodedKeyValue` and `dsig11:KeyInfoReference`. The SAML profile verifier is unchanged and is a case of it. `lake exe l4xmldsig`: W3C 1.1 interop vectors 9 pass, 0 fail, 36 expected failures (out of 45; https://github.com/danbri/factoidal/issues/1223 (ECDSA other than P-256 SHA-256), https://github.com/danbri/factoidal/issues/1227 (1024-bit RSA keys), https://github.com/danbri/factoidal/issues/1226 (X509Data)); signxml fixtures 22 pass, 0 fail (out of 22). New: SHA-224 (`Crypto.sha224`), HMAC over any hash (`Crypto.hmacWith`), EMSA-PKCS1-v1_5 for every DigestInfo of RFC 8017 (`JOSE.Pkcs1.rsaPkcs1Verify`). No npm or wasm op.
26
+ - The Direct Mapping of relational data to RDF (W3C Recommendation, 2012) in the Lean engine, `L4Factoidal/RDB2RDF/`, over a SQL subset in Lean (`L4Factoidal/SQL/`: CREATE TABLE with keys, ALTER TABLE, INSERT, and the SELECT forms of the W3C RDB2RDF test scripts), with the natural RDF literals of R2RML §10.2. R2RML (W3C Recommendation, 2012) in the Lean engine, `L4Factoidal/RDB2RDF/R2RML.lean`: an R2RML mapping graph is read into the RML model by a term table and runs through the existing RML validator and evaluator; logical tables are queries of the Lean SQL subset, cells are the natural RDF literals of §10.2, and the R2RML-only rules (`rr:sqlVersion`, one logical table per triples map, duplicate result columns, SQL identifier case, valid `rr:language`) are checked in Lean. `bash tests/rdb2rdf/run.sh`: W3C Direct Mapping cases 24 pass, 0 fail (out of 24); W3C R2RML cases 63 pass, 0 fail (out of 63). No npm or wasm op yet.
27
+ - XInclude with XPointer in the Lean engine (`XInclude.expandX`, `L4Factoidal/XPath4/XInclude.lean`): the `xpointer` attribute by the XPointer Framework, element(), xmlns() and xpointer(), on the acquired infoset; intra-document references; namespace, base URI and language fixup; the XInclude 1.1 loop rule and fatal errors; `parse="text"` decoding by `encoding` or detection. `lake exe l4xinclude` on the W3C XInclude test suite: 166 pass, 0 fail, 8 expected failures (out of 174; https://github.com/danbri/factoidal/issues/1218 (unexpanded and unparsed entities), https://github.com/danbri/factoidal/issues/1219 (EBCDIC text, HTTP resources)). The `xinclude` option of `fn:doc` does not evaluate `xpointer` yet: https://github.com/danbri/factoidal/issues/1220. No npm or wasm op.
28
+ - RELAX NG in the Lean engine (`L4Factoidal/RelaxNG/`): the XML syntax of RELAX NG (OASIS 2001, ISO/IEC 19757-2), with simplification, the §7 restrictions, validation by derivatives, and the XSD datatype library through `XSD.SimpleType`. Also the RELAX NG compact syntax (OASIS 2002), translated to the XML syntax as its Appendix A specifies. `lake exe l4relaxng`: James Clark's spectest 965 pass, 0 fail (out of 965); compacttest 87 pass, 0 fail (out of 87). No npm or wasm op yet (https://github.com/danbri/factoidal/issues/1200).
29
+ - Web Annotation Data Model and Vocabulary (W3C Recommendations, 2017) in the Lean engine, `L4Factoidal/WebAnnotation/`: the procedure of the W3C annotation-model tests (`JsonTest.lean`) over the JSON Schema validator, which gains a draft-04 dialect (`draft04Ajv4`: unknown keywords ignored, boolean `exclusiveMinimum`/`exclusiveMaximum`, `format` checked as ajv 4.0.4 "fast" mode checks `uri` and `date-time`); the vocabulary checks over the existing JSON-LD 1.1, N-Triples, Turtle and RDF/XML code (`Vocab.lean`). `bash tests/webannotation/run.sh`: model tests on the 44 Recommendation examples and 41 protocol samples 246 pass, 0 fail (out of 246 runs), 6 excluded (defective test data, https://github.com/danbri/factoidal/issues/1193); 19 negative documents 19 pass, 0 fail (out of 19); vocabulary 342 pass, 0 fail (out of 342). The Web Annotation Protocol (2017) as a Lean request handler over the LWS store (`Protocol.lean`): the `server-manual.html` assertions run against it (`ProtocolSuite.lean`), MUST 35 pass, 0 fail (out of 35), SHOULD 10 pass, 0 fail (out of 10). No npm or wasm op and no HTTP host yet, and no Turtle representation (https://github.com/danbri/factoidal/issues/1197).
4
30
 
5
31
  ## 0.28.0 (2026-10-31)
6
32
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @factoidal/core
2
2
 
3
- > ⚠️ **EXPERIMENTAL UNREVIEWED CODE - USE WITH CARE - DO NOT CREATE PUBLIC SERVICES USING THIS WITHOUT CAREFUL INDEPENDENT EXPERT REVIEW.**
3
+ > ⚠️ **EXPERIMENTAL UNDER-REVIEWED CODE - USE WITH CARE - DO NOT CREATE PUBLIC SERVICES USING THIS WITHOUT CAREFUL INDEPENDENT EXPERT REVIEW.**
4
4
  >
5
5
  > This covers the whole core library and every command line tool, and in
6
6
  > particular all protocol, authentication and security code (the LWS, Solid,
@@ -58,9 +58,10 @@ and `dataset.serialize({ format: 'jsonld', rdfstar: true })`.
58
58
  Jena TDB2 read only), Linked Web Storage 1.0, Solid, an OAuth 2.0
59
59
  authorization server (token exchange for OpenID Connect ID tokens,
60
60
  self-issued CID credentials and SAML 2.0 assertions)
61
- - Security: JWS/JWT (ES256, RS256, EdDSA), XML Signature verification in the
62
- SAML Core profile (Exclusive XML Canonicalization 1.0, rsa-sha256), with
63
- HACL\* primitives. XML Encryption 1.1 and JWE (RFC 7516), decryption and
61
+ - Security: JWS/JWT (ES256, RS256, EdDSA), XML Signature 1.1 verification
62
+ and signing (`xmlVerify`, `xmlSign`; RSA, ECDSA P-256, HMAC; not DSA,
63
+ https://github.com/danbri/factoidal/issues/1224, nor ECDSA P-384 and P-521,
64
+ https://github.com/danbri/factoidal/issues/1223), with HACL\* primitives. XML Encryption 1.1 and JWE (RFC 7516), decryption and
64
65
  encryption (`xmlDecrypt`, `xmlEncrypt`, `jweDecrypt`, `jweEncrypt`):
65
66
  RSA-OAEP, P-256 and X25519 run in the wasm (HACL\*), AES runs in the host's
66
67
  WebCrypto. The authorization server decrypts encrypted SAML assertions
package/bin/factoidal.mjs CHANGED
@@ -94,7 +94,7 @@ function err (line) { console.error(line) }
94
94
  // Owner, 2026-10-27: every protocol, authentication and security component,
95
95
  // the CLIs and the core library carry this notice. It is printed with every
96
96
  // help text and on standard error when a server, client or key command starts.
97
- const REVIEW_WARNING = 'EXPERIMENTAL UNREVIEWED CODE - USE WITH CARE - DO NOT CREATE PUBLIC SERVICES USING THIS WITHOUT CAREFUL INDEPENDENT EXPERT REVIEW.'
97
+ const REVIEW_WARNING = 'EXPERIMENTAL UNDER-REVIEWED CODE - USE WITH CARE - DO NOT CREATE PUBLIC SERVICES USING THIS WITHOUT CAREFUL INDEPENDENT EXPERT REVIEW.'
98
98
 
99
99
  /** The commands that serve, speak a protocol, or handle keys and tokens. */
100
100
  const WARNED_COMMANDS = new Set(['serve', 'lws-serve', 'solid-serve', 'solid-client', 'lws-client',
@@ -128,7 +128,7 @@ commands:
128
128
  authz-serve DIR serve an OAuth 2.0 authorization server for LWS
129
129
  (RFC 8414 metadata, JWK Set, RFC 8693 token exchange)
130
130
  authz-keys DIR print the public JWK Set of an authorization server
131
- xmpp-serve DIR serve XMPP (RFC 6120/6121) over TCP or direct TLS
131
+ xmpp-serve DIR serve XMPP (RFC 6120/6121): TCP, STARTTLS or direct TLS
132
132
  webdav-serve CONFIGDIR serve local and remote LWS storages to file
133
133
  browsers over WebDAV (RFC 4918)
134
134
  webdav-user FILE NAME add or replace a user in a webdav-serve users
@@ -481,14 +481,8 @@ usage: factoidal lws-serve DIR (--authz DIR | --issuer IRI | --auth-config FILE
481
481
  Starts a Node HTTP server that answers every request through the engine's
482
482
  lwsStep operation.
483
483
 
484
- DIR names the storage directory. The storage is kept in the engine
485
- handle and written to DIR/lws-state.json after every request that
486
- changed it (temporary file, fsync, rename), and read back on the next
487
- start. The engine decides the snapshot format and refuses a snapshot
488
- path with a dot segment or other traversal form
489
- (formal/lean4/L4Factoidal/LWS/Snapshot.lean).
490
-
491
- With --storage files (off by default) a write costs the same whatever the
484
+ DIR names the storage directory. By default (--storage files, since
485
+ 2026-11-03) a write costs the same whatever the
492
486
  size of the storage: DIR holds one file per resource body (DIR/bodies/),
493
487
  a journal of metadata changes (DIR/lws-journal) and a metadata file
494
488
  (DIR/lws-files.json) that the journal is folded into from time to time.
@@ -500,6 +494,12 @@ byte (formal/lean4/L4Factoidal/LWS/FileLayout.lean); the storage is still
500
494
  held in memory while the server runs
501
495
  (docs/designissues/2026-10-22-lws-storage-on-disk.md).
502
496
 
497
+ With --storage snapshot the storage is written whole to DIR/lws-state.json
498
+ after every request that changed it (temporary file, fsync, rename), and
499
+ read back on the next start. The engine decides the snapshot format and
500
+ refuses a snapshot path with a dot segment or other traversal form
501
+ (formal/lean4/L4Factoidal/LWS/Snapshot.lean).
502
+
503
503
  Encryption at rest (off by default): when the environment variable
504
504
  FACTOIDAL_LWS_KEY holds a 32-byte key (64 hex digits, or 43 characters of
505
505
  base64url), the snapshot is sealed with ChaCha20-Poly1305 (HACL*) under a
@@ -577,9 +577,9 @@ The engine decides the file, its contents and every refusal
577
577
  --storage snapshot|files
578
578
  how DIR keeps the storage (config member storage;
579
579
  default the environment variable FACTOIDAL_LWS_STORAGE,
580
- else snapshot): one snapshot file written whole after
581
- each change, or one file per body with a journal, so a
582
- write costs the same whatever the storage size
580
+ else files): one file per body with a journal, so a
581
+ write costs the same whatever the storage size, or one
582
+ snapshot file written whole after each change
583
583
  https://github.com/danbri/factoidal/issues/918
584
584
  --cors-origin ORIGIN
585
585
  answer CORS (Fetch Standard) only for this origin, written
@@ -744,10 +744,13 @@ those operations landed makes this command exit 3 and say so.
744
744
 
745
745
  Specification: https://solidproject.org/TR/protocol`,
746
746
 
747
- 'xmpp-serve': `factoidal xmpp-serve - serve XMPP (RFC 6120 and RFC 6121) over TCP or direct TLS
747
+ 'xmpp-serve': `factoidal xmpp-serve - serve XMPP (RFC 6120 and RFC 6121) over TCP, STARTTLS or direct TLS
748
748
 
749
749
  usage: factoidal xmpp-serve DIR [--domain NAME] [--port N] [--host ADDRESS]
750
- [--accounts FILE] [--cert FILE --key FILE]
750
+ [--accounts FILE] [--cert FILE --key FILE [--starttls]]
751
+ [--ws-port N [--ws-path PATH] [--ws-origin ORIGIN]...]
752
+ [--muc DOMAIN] [--mix DOMAIN] [--gc3 DOMAIN]
753
+ [--state-key FILE]
751
754
 
752
755
  Starts a Node TCP server that runs one engine session per connection and
753
756
  answers every byte through the engine's xmppSessionFeed operation.
@@ -761,11 +764,41 @@ DIR/spool/<bare jid>/ is delivered to that account's bound session.
761
764
  --domain NAME the domain the server is authoritative for (default localhost);
762
765
  a stream for any other domain is answered host-unknown
763
766
  --port N the port to bind; 0 takes an ephemeral one
764
- (default 5222, or 5223 with --cert and --key)
765
- --host ADDRESS the address to bind (default 127.0.0.1)
767
+ (default 5222; 5223 with --cert and --key and no --starttls)
768
+ --host ADDRESS the address to bind (default 127.0.0.1); without
769
+ --cert and --key only a loopback address is accepted
766
770
  --accounts FILE the accounts file (default DIR/accounts)
767
- --cert FILE PEM certificate chain: XEP-0368 direct TLS, no STARTTLS
771
+ --cert FILE PEM certificate chain: XEP-0368 direct TLS, or STARTTLS with --starttls
768
772
  --key FILE PEM private key for --cert
773
+ --starttls listen in clear and REQUIRE STARTTLS (RFC 6120 section 5)
774
+ before anything else; no SASL mechanism is offered and no
775
+ credential is accepted until the TLS handshake finishes
776
+ --ws-port N also serve XMPP over WebSocket (RFC 7395, the "xmpp"
777
+ subprotocol of RFC 6455) for browser clients such as
778
+ Strophe.js; wss: with --cert and --key, else loopback only
779
+ --ws-path PATH the WebSocket endpoint path (default /xmpp-websocket)
780
+ --ws-origin ORIGIN a browser Origin allowed to connect, e.g.
781
+ http://localhost:5173 (repeatable); a request from any
782
+ other Origin is refused 403, and one with no Origin
783
+ (not a browser) is accepted only when no --ws-origin is given
784
+ --muc DOMAIN also serve XEP-0045 multi-user chat rooms at DOMAIN, e.g.
785
+ conference.example.com: temporary rooms, made by the first
786
+ join and gone with the last occupant; no history, no
787
+ configuration, no persistence (L4Factoidal/XMPP/Muc.lean)
788
+ --mix DOMAIN also serve XEP-0369 MIX channels at DOMAIN, e.g. mix.example.com,
789
+ joined through this server (XEP-0405 MIX-PAM); channels are
790
+ made by an IQ <create/>, kept in memory only, JID-hidden; no
791
+ PubSub nodes, no archive (L4Factoidal/XMPP/Mix.lean)
792
+ --state-key FILE keep the server's state across restarts: rosters, messages held
793
+ for offline accounts, MIX channels and GC3 groups, in an LWS
794
+ storage in DIR/state, sealed (ChaCha20-Poly1305) under the key
795
+ in FILE (64 hex digits; keep FILE outside DIR). A wrong key
796
+ stops the start. Plaintext roster files are migrated, then
797
+ deleted. Without it, that state is lost at a restart.
798
+ --gc3 DOMAIN also serve PROTOTYPE "Groupchat 3.0" groups at DOMAIN: persistent
799
+ membership, stable participant ids, optional events. There is
800
+ no published GC3 spec; every wire format is invented and listed
801
+ in L4Factoidal/XMPP/Gc3.lean and danbri/factoidal issue 1236
769
802
 
770
803
  The server makes no protocol decision. Stream negotiation, SASL PLAIN and
771
804
  SCRAM-SHA-256, resource binding, the roster, presence subscription and
@@ -774,7 +807,7 @@ disco#info are answered by formal/lean4/L4Factoidal/XMPP/. A WebAssembly
774
807
  module built before the session operations landed makes this command
775
808
  exit 3 and say so.
776
809
 
777
- Not implemented: STARTTLS (use --cert/--key), server-to-server federation,
810
+ Not implemented: SCRAM channel binding, server-to-server federation,
778
811
  MUC/MIX/GC3 rooms.
779
812
 
780
813
  Specifications: https://www.rfc-editor.org/rfc/rfc6120 and
@@ -1150,7 +1183,7 @@ function parseArguments (args, valueOptions) {
1150
1183
  class UsageError extends Error {}
1151
1184
 
1152
1185
  /** Options that may be given more than once; their value is the list. */
1153
- const REPEATABLE_OPTIONS = new Set(['cors-origin', 'saml-idp', 'saml-decrypt-key', 'allow-host', 'allow-origin'])
1186
+ const REPEATABLE_OPTIONS = new Set(['cors-origin', 'saml-idp', 'saml-decrypt-key', 'allow-host', 'allow-origin', 'ws-origin'])
1154
1187
 
1155
1188
  const VALUE_OPTIONS = {
1156
1189
  xpath4: new Set(['expression', 'file', 'input', 'format']),
@@ -1178,7 +1211,7 @@ const VALUE_OPTIONS = {
1178
1211
  'authz-serve': new Set(['port', 'host', 'issuer', 'resource', 'alg', 'token-lifetime', 'rotate-after',
1179
1212
  'cors-origin', 'saml-idp', 'saml-decrypt-key']),
1180
1213
  'authz-keys': new Set([]),
1181
- 'xmpp-serve': new Set(['port', 'host', 'domain', 'accounts', 'cert', 'key']),
1214
+ 'xmpp-serve': new Set(['port', 'host', 'domain', 'accounts', 'cert', 'key', 'ws-port', 'ws-path', 'ws-origin', 'muc', 'mix', 'gc3', 'state-key']),
1182
1215
  'webdav-serve': new Set(['port', 'host', 'users', 'guest', 'cert', 'key', 'login-limit', 'login-window', 'xmpp', 'xmpp-password-file',
1183
1216
  'xmpp-host', 'xmpp-port', 'xmpp-allow', 'xmpp-resource', 'xmpp-ca', 'allow-host', 'allow-origin']),
1184
1217
  'webdav-user': new Set(['iterations']),
@@ -2825,9 +2858,11 @@ async function commandXmppServe (positional, options) {
2825
2858
  if (useTls && (typeof options.cert !== 'string' || typeof options.key !== 'string')) {
2826
2859
  throw new UsageError('--cert and --key go together')
2827
2860
  }
2861
+ const startTls = options.starttls === true
2862
+ if (startTls && !useTls) throw new UsageError('--starttls needs --cert and --key')
2828
2863
  const settings = {
2829
2864
  dir: positional[0],
2830
- port: servePort(options, useTls ? 5223 : 5222),
2865
+ port: servePort(options, useTls && !startTls ? 5223 : 5222),
2831
2866
  host: serveHost(options),
2832
2867
  onError: (error) => { if (!quiet) err(`xmpp-serve: ${error.message}`) }
2833
2868
  }
@@ -2837,6 +2872,51 @@ async function commandXmppServe (positional, options) {
2837
2872
  settings.cert = await fs.readFile(options.cert)
2838
2873
  settings.key = await fs.readFile(options.key)
2839
2874
  }
2875
+ if (startTls) settings.startTls = true
2876
+ if (options['state-key'] !== undefined) {
2877
+ if (typeof options['state-key'] !== 'string') throw new UsageError('--state-key needs a FILE')
2878
+ // 64 hex digits or base64url of 32 bytes; the engine checks it.
2879
+ settings.stateKey = (await fs.readFile(options['state-key'], 'utf8')).trim()
2880
+ }
2881
+ if (options.muc !== undefined) {
2882
+ if (typeof options.muc !== 'string' || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i.test(options.muc)) {
2883
+ throw new UsageError('--muc needs a domain name, e.g. conference.example.com')
2884
+ }
2885
+ settings.mucDomain = options.muc
2886
+ }
2887
+ if (options.mix !== undefined) {
2888
+ 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)) {
2889
+ throw new UsageError('--mix needs a domain name, e.g. mix.example.com')
2890
+ }
2891
+ if (options.mix === options.muc) throw new UsageError('--mix and --muc need different domains')
2892
+ settings.mixDomain = options.mix
2893
+ }
2894
+ if (options.gc3 !== undefined) {
2895
+ if (typeof options.gc3 !== 'string' || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i.test(options.gc3)) {
2896
+ throw new UsageError('--gc3 needs a domain name, e.g. gc3.example.com')
2897
+ }
2898
+ if (options.gc3 === options.muc || options.gc3 === options.mix) throw new UsageError('--gc3 needs a domain of its own')
2899
+ settings.gc3Domain = options.gc3
2900
+ }
2901
+ if (options['ws-port'] !== undefined) {
2902
+ const wsPort = Number(options['ws-port'])
2903
+ if (!Number.isInteger(wsPort) || wsPort < 0 || wsPort > 65535) throw new UsageError('--ws-port needs a port number')
2904
+ const origins = options['ws-origin'] === undefined ? []
2905
+ : Array.isArray(options['ws-origin']) ? options['ws-origin'] : [options['ws-origin']]
2906
+ settings.websocket = {
2907
+ port: wsPort,
2908
+ path: typeof options['ws-path'] === 'string' ? options['ws-path'] : '/xmpp-websocket',
2909
+ allowedOrigins: origins,
2910
+ allowNoOrigin: origins.length === 0
2911
+ }
2912
+ if (useTls) { settings.websocket.cert = settings.cert; settings.websocket.key = settings.key }
2913
+ }
2914
+ // Invariant I4 (docs/designissues/2026-10-28-feature-combination-safety.md,
2915
+ // C4): with no TLS this listener offers PLAIN, which sends the password in
2916
+ // clear, so it binds to loopback only.
2917
+ if (!useTls && !/^(127\.\d+\.\d+\.\d+|::1|localhost)$/.test(settings.host)) {
2918
+ throw new UsageError(`xmpp-serve: no TLS on ${settings.host}: give --cert and --key (with --starttls for port 5222), or bind to loopback`)
2919
+ }
2840
2920
  let running
2841
2921
  try {
2842
2922
  running = await listen(settings)
@@ -2849,9 +2929,19 @@ async function commandXmppServe (positional, options) {
2849
2929
  if (!quiet) {
2850
2930
  err(`xmpp-serve: listening on ${running.origin} for domain ${running.xmpp.domain}` +
2851
2931
  ` (${running.xmpp.accounts} accounts, state ${positional[0]}` +
2852
- (useTls ? ', direct TLS)' : ', PLAINTEXT: no TLS on this listener)'))
2932
+ (startTls ? ', STARTTLS required)' : useTls ? ', direct TLS)' : ', PLAINTEXT: no TLS on this listener)'))
2933
+ if (settings.mucDomain) err(`xmpp-serve: multi-user chat (XEP-0045) at ${settings.mucDomain}`)
2934
+ if (settings.mixDomain) err(`xmpp-serve: MIX channels (XEP-0369, joined by XEP-0405) at ${settings.mixDomain}`)
2935
+ if (settings.gc3Domain) err(`xmpp-serve: GC3 PROTOTYPE groups at ${settings.gc3Domain} (no published spec; invented wire format)`)
2936
+ if (settings.stateKey) err(`xmpp-serve: state kept across restarts in ${positional[0]}/state, sealed under the state key`)
2937
+ else err('xmpp-serve: WARNING: no --state-key: held messages, MIX channels and GC3 groups are lost at a restart, and rosters are written in clear')
2938
+ }
2939
+ if (running.wsOrigin && !quiet) {
2940
+ err(`xmpp-serve: XMPP over WebSocket on ${running.wsOrigin}` +
2941
+ (settings.websocket.allowedOrigins.length ? ` for Origin ${settings.websocket.allowedOrigins.join(', ')}` : ' (no browser Origin allowed)'))
2853
2942
  }
2854
2943
  out(running.origin)
2944
+ if (running.wsOrigin) out(running.wsOrigin)
2855
2945
  return untilInterrupted(running, quiet)
2856
2946
  }
2857
2947
 
package/browser.d.ts CHANGED
@@ -117,6 +117,8 @@ export {
117
117
  vcEd25519Verify,
118
118
  xmlDecrypt,
119
119
  xmlEncrypt,
120
+ xmlVerify,
121
+ xmlSign,
120
122
  jweDecrypt,
121
123
  jweEncrypt,
122
124
  vcEddsaCreateFromCanonical,
package/browser.js CHANGED
@@ -188,6 +188,8 @@ export const vcEd25519Verify = api.vcEd25519Verify;
188
188
  // XML Encryption 1.1 and JWE; AES through WebCrypto (lib/cipher-host.mjs)
189
189
  export const xmlDecrypt = api.xmlDecrypt;
190
190
  export const xmlEncrypt = api.xmlEncrypt;
191
+ export const xmlVerify = api.xmlVerify;
192
+ export const xmlSign = api.xmlSign;
191
193
  export const jweDecrypt = api.jweDecrypt;
192
194
  export const jweEncrypt = api.jweEncrypt;
193
195
  export const vcEddsaCreateFromCanonical = api.vcEddsaCreateFromCanonical;
package/index.d.ts CHANGED
@@ -1978,6 +1978,50 @@ export function xmlEncrypt(
1978
1978
  }
1979
1979
  ): Promise<{ document: string; canonical: string }>;
1980
1980
 
1981
+ /**
1982
+ * XML Signature 1.1 core validation (§3.2). A signature that does not
1983
+ * validate resolves to `{valid: false, reason}`; wrong arguments reject with
1984
+ * a FactoidalError. A key read from ds:KeyInfo is returned, not trusted:
1985
+ * the caller decides whether to trust `key` and `certificate`.
1986
+ */
1987
+ export function xmlVerify(
1988
+ document: string,
1989
+ options?: {
1990
+ key?: object;
1991
+ hmacKey?: Uint8Array;
1992
+ keyFromKeyInfo?: boolean;
1993
+ certificates?: Uint8Array[];
1994
+ documents?: Record<string, Uint8Array | string>;
1995
+ }
1996
+ ): Promise<
1997
+ | {
1998
+ valid: true;
1999
+ signedInfo: string;
2000
+ references: { uri: string; octets: Uint8Array }[];
2001
+ key: object | null;
2002
+ certificate: Uint8Array | null;
2003
+ }
2004
+ | { valid: false; reason: string }
2005
+ >;
2006
+
2007
+ /**
2008
+ * XML Signature 1.1 signature generation (§3.1). `key` is a private JWK
2009
+ * (RSA 2048/3072/4096, EC P-256, or oct for HMAC). The returned document
2010
+ * has passed core validation in the engine.
2011
+ */
2012
+ export function xmlSign(
2013
+ document: string,
2014
+ options: {
2015
+ key: object;
2016
+ signatureMethod?: string;
2017
+ canonicalizationMethod?: string;
2018
+ inclusivePrefixes?: string[];
2019
+ references?: { uri: string; transforms?: string[]; digestMethod?: string }[];
2020
+ enveloping?: { id: string; content: string };
2021
+ keyValue?: boolean;
2022
+ }
2023
+ ): Promise<{ document: string }>;
2024
+
1981
2025
  /**
1982
2026
  * Decrypt a compact JWE (RFC 7516). Every failure after the allowlist is
1983
2027
  * the one error "decryption failed".
@@ -2330,6 +2374,8 @@ declare const _default: {
2330
2374
  vcEd25519Verify: typeof vcEd25519Verify;
2331
2375
  xmlDecrypt: typeof xmlDecrypt;
2332
2376
  xmlEncrypt: typeof xmlEncrypt;
2377
+ xmlVerify: typeof xmlVerify;
2378
+ xmlSign: typeof xmlSign;
2333
2379
  jweDecrypt: typeof jweDecrypt;
2334
2380
  jweEncrypt: typeof jweEncrypt;
2335
2381
  vcEddsaCreateFromCanonical: typeof vcEddsaCreateFromCanonical;
package/index.mjs CHANGED
@@ -189,6 +189,8 @@ export const vcEd25519Verify = api.vcEd25519Verify;
189
189
  // XML Encryption 1.1 and JWE; AES through WebCrypto (lib/cipher-host.mjs)
190
190
  export const xmlDecrypt = api.xmlDecrypt;
191
191
  export const xmlEncrypt = api.xmlEncrypt;
192
+ export const xmlVerify = api.xmlVerify;
193
+ export const xmlSign = api.xmlSign;
192
194
  export const jweDecrypt = api.jweDecrypt;
193
195
  export const jweEncrypt = api.jweEncrypt;
194
196
  export const vcEddsaCreateFromCanonical = api.vcEddsaCreateFromCanonical;
@@ -37,7 +37,7 @@
37
37
  // bytes change.
38
38
 
39
39
  // Stamped by formal/lean4/Wasm/build-wasm.sh step 9 -- do not hand-edit.
40
- const WASM_VERSION = "5ca09ab0343e";
40
+ const WASM_VERSION = "0b80e57ea8e5";
41
41
 
42
42
  import createModule from './l4factoidal.mjs';
43
43
 
Binary file
@@ -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-e5057984e388-web",
6
- "releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/41154673265",
7
- "gitSha": "e5057984e388be6534fef19cdbb869b5b5652a17",
8
- "builtAt": "2026-10-31T02:40:08Z",
5
+ "release": "danbri/factoidal-builds build-084162b863c1-web",
6
+ "releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/41860977405",
7
+ "gitSha": "084162b863c1f3a28d6dddab32511caa2d6e7c30",
8
+ "builtAt": "2026-11-04T16:27:40Z",
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": "5ca09ab0343eac5cfea0295461af8c4ea963a8bf083afc6f3aeadf1f92ef4804",
15
- "wasmBytes": 30629431,
14
+ "wasmSha256": "0b80e57ea8e53299640cc8b1d75bfd5974ea91963988ab91d3aa4f5d2c0d7778",
15
+ "wasmBytes": 31363760,
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
@@ -1821,6 +1821,138 @@ function buildApi(driver) {
1821
1821
  return { document: r.document, canonical: r.canonical };
1822
1822
  }
1823
1823
 
1824
+ // -----------------------------------------------------------------
1825
+ // XML Signature 1.1 (https://github.com/danbri/factoidal/issues/1165,
1826
+ // track B, item 4). Every decision is in Lean (formal/lean4/Wasm/Ops/
1827
+ // XmlDsig.lean over XML/DSig11.lean and XML/DSig11Sign.lean); RSA and
1828
+ // P-256 run in the wasm (HACL*), HMAC in Lean. This code converts octets
1829
+ // to base64 and draws the ECDSA nonce; it reads no document and chooses
1830
+ // no algorithm or key.
1831
+ // -----------------------------------------------------------------
1832
+
1833
+ // Octets given as a Uint8Array (or an ArrayBuffer view), as base64.
1834
+ function octetsToBase64(fnName, name, value) {
1835
+ if (value instanceof Uint8Array) return bytesToBase64(value);
1836
+ if (ArrayBuffer.isView(value)) {
1837
+ return bytesToBase64(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
1838
+ }
1839
+ if (value instanceof ArrayBuffer) return bytesToBase64(new Uint8Array(value));
1840
+ throw new TypeError(`${fnName}: ${name} must be a Uint8Array`);
1841
+ }
1842
+
1843
+ // A document for `documents`: a string is written as UTF-8.
1844
+ function documentOctetsToBase64(fnName, uri, value) {
1845
+ if (typeof value === 'string') return bytesToBase64(new TextEncoder().encode(value));
1846
+ return octetsToBase64(fnName, `documents[${JSON.stringify(uri)}]`, value);
1847
+ }
1848
+
1849
+ // One XML Signature op: the engine's envelope, or a FactoidalError when
1850
+ // the arguments are wrong (`{ok: false}`).
1851
+ async function dsigOp(fnName, what, args) {
1852
+ const e = await entry();
1853
+ requireEntryFn(e, fnName, what);
1854
+ return entryResult(e[fnName](JSON.stringify(args)), fnName);
1855
+ }
1856
+
1857
+ /**
1858
+ * XML Signature 1.1 core validation (§3.2) of the one ds:Signature of an
1859
+ * XML document. A signature that does not validate is a result
1860
+ * (`valid: false` with the engine's reason), not an exception; wrong
1861
+ * arguments throw a FactoidalError.
1862
+ *
1863
+ * With no `key` and no `hmacKey`, the key of ds:KeyInfo is used (unless
1864
+ * `keyFromKeyInfo` is false). Such a key only says that the signature
1865
+ * verifies under it: the result gives the key (`key`, a JWK) and the
1866
+ * certificate that ds:X509Data names (`certificate`, DER), and the caller
1867
+ * decides whether to trust them. The engine fetches nothing: `documents`
1868
+ * gives the octets of each URI that is not a same-document reference.
1869
+ *
1870
+ * @param {string} document the XML document
1871
+ * @param {{key?: object, hmacKey?: Uint8Array, keyFromKeyInfo?: boolean,
1872
+ * certificates?: Uint8Array[], documents?: Object<string, Uint8Array|string>}} [options]
1873
+ * `key` is a public JWK (RSA, or EC on P-256, P-384, P-521); `hmacKey`
1874
+ * the HMAC key octets; `certificates` DER certificates to select from
1875
+ * @returns {Promise<{valid: true, signedInfo: string,
1876
+ * references: {uri: string, octets: Uint8Array}[], key: object|null,
1877
+ * certificate: Uint8Array|null} | {valid: false, reason: string}>}
1878
+ */
1879
+ async function xmlVerify(document, options) {
1880
+ if (typeof document !== 'string') throw new TypeError('xmlVerify: document must be a string');
1881
+ const o = options || {};
1882
+ const args = { document };
1883
+ if (o.key !== undefined) {
1884
+ if (o.key === null || typeof o.key !== 'object') {
1885
+ throw new TypeError('xmlVerify: options.key must be a JWK object');
1886
+ }
1887
+ args.key = o.key;
1888
+ }
1889
+ if (o.hmacKey !== undefined) args.hmacKey = octetsToBase64('xmlVerify', 'options.hmacKey', o.hmacKey);
1890
+ if (o.keyFromKeyInfo !== undefined) args.keyFromKeyInfo = o.keyFromKeyInfo === true;
1891
+ if (o.certificates !== undefined) {
1892
+ if (!Array.isArray(o.certificates)) {
1893
+ throw new TypeError('xmlVerify: options.certificates must be an array of Uint8Array');
1894
+ }
1895
+ args.certificates = o.certificates.map((c, i) =>
1896
+ octetsToBase64('xmlVerify', `options.certificates[${i}]`, c));
1897
+ }
1898
+ if (o.documents !== undefined) {
1899
+ if (o.documents === null || typeof o.documents !== 'object' || Array.isArray(o.documents)) {
1900
+ throw new TypeError('xmlVerify: options.documents must be an object from URI to octets');
1901
+ }
1902
+ args.documents = {};
1903
+ for (const [uri, v] of Object.entries(o.documents)) {
1904
+ args.documents[uri] = documentOctetsToBase64('xmlVerify', uri, v);
1905
+ }
1906
+ }
1907
+ const r = await dsigOp('xmlVerify', 'XML Signature verification', args);
1908
+ if (r.valid !== true) return { valid: false, reason: String(r.reason) };
1909
+ return {
1910
+ valid: true,
1911
+ signedInfo: r.signedInfo,
1912
+ references: (r.references || []).map((x) => ({ uri: x.uri, octets: base64ToBytes(x.octets) })),
1913
+ key: r.key ?? null,
1914
+ certificate: typeof r.certificate === 'string' ? base64ToBytes(r.certificate) : null
1915
+ };
1916
+ }
1917
+
1918
+ /**
1919
+ * XML Signature 1.1 signature generation (§3.1). The returned document
1920
+ * has passed core validation in the engine before it is returned.
1921
+ *
1922
+ * `key` is a private JWK: RSA (`n`, `e`, `d`; 2048, 3072 or 4096 bits),
1923
+ * EC on P-256 (`d`), or `oct` (`k`) for HMAC. The engine chooses the
1924
+ * default method by the key kind (rsa-sha256, ecdsa-sha256, hmac-sha256)
1925
+ * and refuses a method of another key kind. The default reference is the
1926
+ * whole document with the enveloped-signature transform; with
1927
+ * `enveloping`, the signature is the document and holds `content` in a
1928
+ * ds:Object. The ECDSA nonce is 32 octets of `crypto.getRandomValues`.
1929
+ *
1930
+ * @param {string} document the XML document
1931
+ * @param {{key: object, signatureMethod?: string, canonicalizationMethod?: string,
1932
+ * inclusivePrefixes?: string[],
1933
+ * references?: {uri: string, transforms?: string[], digestMethod?: string}[],
1934
+ * enveloping?: {id: string, content: string}, keyValue?: boolean}} options
1935
+ * algorithm identifiers are the URIs of XML Signature 1.1 §6
1936
+ * @returns {Promise<{document: string}>}
1937
+ */
1938
+ async function xmlSign(document, options) {
1939
+ if (typeof document !== 'string') throw new TypeError('xmlSign: document must be a string');
1940
+ const o = options || {};
1941
+ if (o.key === null || typeof o.key !== 'object') {
1942
+ throw new TypeError('xmlSign: options.key must be a private JWK object');
1943
+ }
1944
+ const args = { document, key: o.key };
1945
+ for (const k of ['signatureMethod', 'canonicalizationMethod', 'inclusivePrefixes',
1946
+ 'references', 'enveloping']) {
1947
+ if (o[k] !== undefined) args[k] = o[k];
1948
+ }
1949
+ if (o.keyValue !== undefined) args.keyValue = o.keyValue === true;
1950
+ // The nonce is given for every key; the engine reads it only for ECDSA.
1951
+ args.random = randomBase64(32);
1952
+ const r = await dsigOp('xmlSign', 'XML Signature generation', args);
1953
+ return { document: r.document };
1954
+ }
1955
+
1824
1956
  /**
1825
1957
  * Decrypt a JWE in the Compact Serialization (RFC 7516).
1826
1958
  * @param {string} token the compact JWE
@@ -2770,6 +2902,8 @@ function buildApi(driver) {
2770
2902
  vcEd25519Verify,
2771
2903
  xmlDecrypt,
2772
2904
  xmlEncrypt,
2905
+ xmlVerify,
2906
+ xmlSign,
2773
2907
  jweDecrypt,
2774
2908
  jweEncrypt,
2775
2909
  vcEddsaCreateFromCanonical,
package/lib/version.mjs CHANGED
@@ -2,4 +2,4 @@
2
2
  // needs import attributes, which not every runtime this package targets
3
3
  // reads). test/dts-drift.test.js checks that it equals package.json's
4
4
  // `version`.
5
- export const version = '0.28.0';
5
+ export const version = '0.30.0';
package/lws/client.mjs CHANGED
@@ -34,7 +34,6 @@
34
34
  // browser's, or one that carries each request as a message over another
35
35
  // transport (XMPP, a worker port). The client needs no global fetch.
36
36
 
37
- import { loadEngine } from '../bin/engine.mjs'
38
37
  import { FactoidalError } from '../lib/errors.mjs'
39
38
 
40
39
  /** The ops this host needs from the engine. */
@@ -159,7 +158,9 @@ export async function lwsClientOpsAvailable (engine) {
159
158
  * @returns {Promise<object>} the client
160
159
  */
161
160
  export async function createLwsClient (options = {}) {
162
- const engine = options.engine !== undefined ? options.engine : await loadEngine()
161
+ // Browsers inject their already loaded engine. Only the Node/Deno default
162
+ // loader imports the file-backed store host; keep it out of that import graph.
163
+ const engine = options.engine !== undefined ? options.engine : await (await import('../bin/engine.mjs')).loadEngine()
163
164
  const wire = typeof options.fetch === 'function' ? options.fetch : globalThis.fetch
164
165
  if (typeof wire !== 'function') throw new TypeError('no fetch is available in this runtime')
165
166
  const maxSteps = typeof options.maxSteps === 'number' ? options.maxSteps : 1024
@@ -310,7 +311,7 @@ export async function createLwsClient (options = {}) {
310
311
  * `selfIssued` is the `createLwsClient` option of that name
311
312
  */
312
313
  export async function createDidKey (options = {}) {
313
- const engine = options.engine !== undefined ? options.engine : await loadEngine()
314
+ const engine = options.engine !== undefined ? options.engine : await (await import('../bin/engine.mjs')).loadEngine()
314
315
  const algorithm = typeof options.algorithm === 'string' ? options.algorithm : 'ES256'
315
316
  const random = new Uint8Array(64)
316
317
  globalThis.crypto.getRandomValues(random)