@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 +27 -1
- package/README.md +5 -4
- package/bin/factoidal.mjs +113 -23
- package/browser.d.ts +2 -0
- package/browser.js +2 -0
- package/index.d.ts +46 -0
- package/index.mjs +2 -0
- 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 +134 -0
- package/lib/version.mjs +1 -1
- package/lws/client.mjs +4 -3
- package/lws/dataset.mjs +160 -0
- package/lws/inpage.mjs +135 -0
- package/lws/persist.mjs +1 -1
- package/lws/server.mjs +55 -9
- package/package.json +9 -2
- package/xmpp/README.md +9 -1
- package/xmpp/inpage.mjs +197 -0
- package/xmpp/router.mjs +96 -0
- package/xmpp/server.mjs +280 -58
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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)
|
|
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.
|
|
485
|
-
|
|
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
|
|
581
|
-
|
|
582
|
-
|
|
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
|
|
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,
|
|
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:
|
|
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
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;
|
package/l4-assets/l4factoidal.js
CHANGED
|
Binary file
|
package/l4-assets/version.json
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
"engine": "lean4",
|
|
3
3
|
"note": "The Lean 4 (L4Factoidal) engine build provenance and claims: the only engine of @factoidal/core since 0.9.0 (issue 713). This committed file is a placeholder. tools/fetch-builds.sh --set web (through tools/stamp-l4-version.mjs) writes the provenance members from the danbri/factoidal-builds release that built the wasm; formal/lean4/Wasm/build-wasm.sh step 9 writes them for a local build. Do not commit a stamped copy.",
|
|
4
4
|
"stamped": true,
|
|
5
|
-
"release": "danbri/factoidal-builds build-
|
|
6
|
-
"releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/
|
|
7
|
-
"gitSha": "
|
|
8
|
-
"builtAt": "2026-
|
|
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": "
|
|
15
|
-
"wasmBytes":
|
|
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
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
|
-
|
|
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)
|