@factoidal/core 0.20.0 → 0.21.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 +49 -0
- package/README.md +38 -2
- package/authz/server.mjs +23 -2
- package/bin/engine.mjs +22 -1
- package/bin/factoidal.mjs +710 -38
- package/bin/pack-host.mjs +5 -0
- package/bin/serve.mjs +19 -3
- package/browser.d.ts +10 -0
- package/browser.js +12 -0
- package/index.d.ts +248 -9
- package/index.mjs +15 -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 +236 -12
- package/lib/browser-engine.mjs +28 -1
- package/lib/cid-node.mjs +77 -0
- package/lib/cid.mjs +146 -0
- package/lib/cipher-host.mjs +126 -0
- package/lib/engine-integrity.mjs +56 -0
- package/lib/nodes.mjs +234 -11
- package/lib/sbm11.mjs +14 -1
- package/lib/version.mjs +1 -1
- package/lws/README.md +9 -0
- package/lws/client.mjs +19 -2
- package/lws/persist.mjs +169 -0
- package/lws/server.mjs +82 -29
- package/package.json +2 -1
- package/solid/server/index.mjs +79 -13
- package/webdav/server.mjs +154 -25
- package/xmpp/client.mjs +332 -0
- package/xmpp/proxy.mjs +114 -0
package/lib/api.mjs
CHANGED
|
@@ -30,6 +30,8 @@ import { fullEngineMessage, encodeJsValue } from './lean-entry.mjs';
|
|
|
30
30
|
import { memoryDeltaLogStore } from './delta-log-hosts.mjs';
|
|
31
31
|
import { FactoidalError, errorFromEnvelope, envelopeResult } from './errors.mjs';
|
|
32
32
|
import { buildNodes } from './nodes.mjs';
|
|
33
|
+
import { runWithCipher } from './cipher-host.mjs';
|
|
34
|
+
import { cidFunctions } from './cid.mjs';
|
|
33
35
|
|
|
34
36
|
// The format names, regimes and query outputs are the engine's tables (op
|
|
35
37
|
// engineTables, formal/lean4/Wasm/Ops/Handles.lean), read once per engine:
|
|
@@ -265,6 +267,10 @@ class Store {
|
|
|
265
267
|
* Where the deltaLog* functions keep their records
|
|
266
268
|
* (lib/delta-log-hosts.mjs): IndexedDB in the browser entries. The
|
|
267
269
|
* default keeps them in the memory of this process.
|
|
270
|
+
* @param {object} [driver.cidHost]
|
|
271
|
+
* Name resolution and the GET of cidRetrieveVerificationMethod
|
|
272
|
+
* (lib/cid-node.mjs under Node and Deno). The default is the browser
|
|
273
|
+
* host of lib/cid.mjs: the page's fetch, no name resolution.
|
|
268
274
|
*/
|
|
269
275
|
function buildApi(driver) {
|
|
270
276
|
let entryPromise; // memoized; a failed load is retried on the next call
|
|
@@ -655,6 +661,17 @@ function buildApi(driver) {
|
|
|
655
661
|
const entail = opts.entail || 'none';
|
|
656
662
|
const sparql12 = opts.sparql12 === true || String(opts.version || '') === '1.2';
|
|
657
663
|
const e = c.e;
|
|
664
|
+
if (sparql12 && entail !== 'none' && opts.output === undefined) {
|
|
665
|
+
// SPARQL 1.2 under an entailment regime: the engine closes the stored
|
|
666
|
+
// dataset and runs the SPARQL 1.2 evaluator (op datasetQuery12Entail,
|
|
667
|
+
// https://github.com/danbri/factoidal/issues/1109).
|
|
668
|
+
requireEntryFn(e, 'datasetQuery12Entail', 'query {sparql12, entail}');
|
|
669
|
+
const r = await withExtensionRounds(() => entryResult(
|
|
670
|
+
e.datasetQuery12Entail(ds.handle, sparql, entail), 'query', sparql));
|
|
671
|
+
if (r.kind === 'ask') return r.boolean;
|
|
672
|
+
if (r.kind === 'construct' || r.kind === 'describe') return c.fromNQuads(r.nquads, ds.prefixes);
|
|
673
|
+
return bindingsFromSrj(r.srj);
|
|
674
|
+
}
|
|
658
675
|
if (entail !== 'none' || opts.output !== undefined) {
|
|
659
676
|
// queryRaw over the stored dataset: an entailment regime, or a
|
|
660
677
|
// results format (SPARQL 1.1 Query Results JSON, XML, CSV, TSV).
|
|
@@ -664,15 +681,18 @@ function buildApi(driver) {
|
|
|
664
681
|
requireEntryFn(e, 'datasetQueryRaw', `query {entail/output}`);
|
|
665
682
|
if (opts.output === undefined) {
|
|
666
683
|
// An entailment regime with the default answer shape.
|
|
667
|
-
|
|
668
|
-
|
|
684
|
+
// withExtensionRounds: a registered async extension function
|
|
685
|
+
// (SPARQL 1.1 §17.6) is resolved here as in datasetQuery
|
|
686
|
+
// (https://github.com/danbri/factoidal/issues/1090).
|
|
687
|
+
const r = await withExtensionRounds(() => entryResult(e.datasetQueryRaw(ds.handle, sparql,
|
|
688
|
+
JSON.stringify({ entail, output: graphForm ? 'ntriples' : 'json' })), 'query', sparql));
|
|
669
689
|
if (graphForm) return c.fromNQuads(r.text, ds.prefixes);
|
|
670
690
|
const srj = JSON.parse(r.text);
|
|
671
691
|
if (typeof srj.boolean === 'boolean') return srj.boolean;
|
|
672
692
|
return bindingsFromSrj(srj);
|
|
673
693
|
}
|
|
674
|
-
const r = entryResult(e.datasetQueryRaw(ds.handle, sparql,
|
|
675
|
-
JSON.stringify({ entail, output })), 'query', sparql);
|
|
694
|
+
const r = await withExtensionRounds(() => entryResult(e.datasetQueryRaw(ds.handle, sparql,
|
|
695
|
+
JSON.stringify({ entail, output })), 'query', sparql));
|
|
676
696
|
return output === 'json' && !graphForm ? JSON.parse(r.text) : r.text;
|
|
677
697
|
}
|
|
678
698
|
const fn = sparql12 ? e.datasetQuery12 : e.datasetQuery;
|
|
@@ -910,17 +930,28 @@ function buildApi(driver) {
|
|
|
910
930
|
* name first, so datasets that differ only in their graph names got the
|
|
911
931
|
* same hash. To hash a graph whatever its name, use
|
|
912
932
|
* canonicalHash(asDefaultGraph(g)).
|
|
913
|
-
*
|
|
933
|
+
*
|
|
934
|
+
* The input is what canonicalize() takes: a Dataset, or text (a string,
|
|
935
|
+
* or {text, format, baseIRI}), with the options {format, baseIRI}. Text is
|
|
936
|
+
* a JavaScript string: characters, not bytes. Decode bytes first (the RDF
|
|
937
|
+
* text syntaxes are UTF-8: `new TextDecoder('utf-8', { fatal: true })`);
|
|
938
|
+
* a Uint8Array is refused.
|
|
939
|
+
* @param {Dataset|string|{text: string, format?: string, baseIRI?: string}} data
|
|
940
|
+
* @param {{format?: string, baseIRI?: string}} [options]
|
|
914
941
|
* @returns {Promise<string>}
|
|
915
942
|
*/
|
|
916
|
-
async function canonicalHash(
|
|
917
|
-
|
|
918
|
-
|
|
943
|
+
async function canonicalHash(data, options) {
|
|
944
|
+
const hashOf = (e, ds) => {
|
|
945
|
+
requireEntryFn(e, 'datasetCanonicalHash', 'canonicalHash');
|
|
946
|
+
return entryResult(e.datasetCanonicalHash(ds.handle), 'canonicalHash').hash;
|
|
947
|
+
};
|
|
948
|
+
if (data instanceof Dataset && options === undefined) {
|
|
949
|
+
data.size; // throws the disposed error
|
|
950
|
+
return hashOf(data._ctx.e, data);
|
|
919
951
|
}
|
|
920
|
-
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
return entryResult(c.e.datasetCanonicalHash(dataset.handle), 'canonicalHash').hash;
|
|
952
|
+
// The options are canonicalize()'s, checked by the engine's table for it.
|
|
953
|
+
const opts = await checkedOptions('canonicalize', optionsOf('canonicalize', options));
|
|
954
|
+
return withDataset(data, opts, 'canonicalHash', (c, ds) => hashOf(c.e, ds));
|
|
924
955
|
}
|
|
925
956
|
|
|
926
957
|
/**
|
|
@@ -960,6 +991,30 @@ function buildApi(driver) {
|
|
|
960
991
|
}));
|
|
961
992
|
}
|
|
962
993
|
|
|
994
|
+
/**
|
|
995
|
+
* The RDF merge of two datasets (RDF 1.1 Semantics section 5.2): a new
|
|
996
|
+
* Dataset that holds the quads of both, with the blank nodes of each input
|
|
997
|
+
* kept apart, so `_:b` in `a` and `_:b` in `b` are two blank nodes. The
|
|
998
|
+
* engine computes it (op datasetMerge, formal/lean4/Wasm/Ops/GraphNodes.lean,
|
|
999
|
+
* theorem mergeDatasets_isMerge); the prefixes are those of `a`, then those
|
|
1000
|
+
* of `b` whose label `a` does not declare. A text argument is read as by
|
|
1001
|
+
* parse() (`options.format`, `options.baseIRI`), in its own blank-node
|
|
1002
|
+
* scope. Since 0.21.0 (https://github.com/danbri/factoidal/issues/1130).
|
|
1003
|
+
* @param {Dataset|string|Array} a
|
|
1004
|
+
* @param {Dataset|string|Array} b
|
|
1005
|
+
* @param {{format?: string, baseIRI?: string}} [options] read text arguments
|
|
1006
|
+
* @returns {Promise<Dataset>}
|
|
1007
|
+
*/
|
|
1008
|
+
async function datasetMerge(a, b, options) {
|
|
1009
|
+
// An engine built before the op refuses with op.unknown before the
|
|
1010
|
+
// options are read (its option table has no row for this function).
|
|
1011
|
+
requireEntryFn((await ctx()).e, 'datasetMerge', 'datasetMerge');
|
|
1012
|
+
const opts = await checkedOptions('datasetMerge', optionsOf('datasetMerge', options));
|
|
1013
|
+
return withDataset(a, opts, 'datasetMerge', (c, da) =>
|
|
1014
|
+
withDataset(b, opts, 'datasetMerge', (_c, db) =>
|
|
1015
|
+
c.wrap(entryResult(c.e.datasetMerge(da.handle, db.handle), 'datasetMerge'))));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
963
1018
|
/**
|
|
964
1019
|
* A Dataset holding RDF/JS quads (op datasetFromQuadsJson). Each term is
|
|
965
1020
|
* sent as its components; the engine reads no text.
|
|
@@ -1120,6 +1175,28 @@ function buildApi(driver) {
|
|
|
1120
1175
|
return withDataset(data, opts, 'closure', (c, ds) => closureOnHandle(c, ds, regime));
|
|
1121
1176
|
}
|
|
1122
1177
|
|
|
1178
|
+
/**
|
|
1179
|
+
* SPARQL 1.2 RL (SPARQL-RL) infer: apply a rule set to the default graph
|
|
1180
|
+
* of `data` and answer the inference graph (the triples the rules derive,
|
|
1181
|
+
* the rule set's DATA blocks included, that are not in `data`) as a new
|
|
1182
|
+
* Dataset. The rule set is SPARQL-RL text (https://w3c.github.io/data-shapes/sparql12-rl/).
|
|
1183
|
+
* A rule set that is not well-formed, not stratifiable, or has IMPORTS is
|
|
1184
|
+
* refused. Engine op: datasetSparqlRlInfer.
|
|
1185
|
+
* @param {Dataset|string|Array} data
|
|
1186
|
+
* @param {string} rules
|
|
1187
|
+
* @param {{format?: string, baseIRI?: string}} [options]
|
|
1188
|
+
* @returns {Promise<Dataset>}
|
|
1189
|
+
*/
|
|
1190
|
+
async function sparqlRlInfer(data, rules, options) {
|
|
1191
|
+
const opts = await checkedOptions('sparqlRlInfer', optionsOf('sparqlRlInfer', options));
|
|
1192
|
+
if (typeof rules !== 'string') throw new TypeError('sparqlRlInfer: rules must be a string');
|
|
1193
|
+
return withDataset(data, opts, 'sparqlRlInfer(data)', (c, ds) => {
|
|
1194
|
+
requireEntryFn(c.e, 'datasetSparqlRlInfer', 'sparqlRlInfer');
|
|
1195
|
+
const r = entryResult(c.e.datasetSparqlRlInfer(ds.handle, rules), 'sparqlRlInfer');
|
|
1196
|
+
return c.wrap(r, ds.prefixes);
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1123
1200
|
/**
|
|
1124
1201
|
* RDFS or OWL 2 RL closure of the default graph, as a new Dataset.
|
|
1125
1202
|
* @param {Dataset|string|Array} data
|
|
@@ -1652,6 +1729,136 @@ function buildApi(driver) {
|
|
|
1652
1729
|
e.vcEd25519Verify(publicKeyHex, messageHex, signatureHex), 'vcEd25519Verify').valid;
|
|
1653
1730
|
}
|
|
1654
1731
|
|
|
1732
|
+
// -----------------------------------------------------------------
|
|
1733
|
+
// XML Encryption 1.1 and JWE (https://github.com/danbri/factoidal/issues/1084,
|
|
1734
|
+
// https://github.com/danbri/factoidal/issues/1085). Every decision is in
|
|
1735
|
+
// Lean (formal/lean4/Wasm/Ops/XmlEnc.lean, Wasm/Ops/Jwe.lean); RSA, P-256
|
|
1736
|
+
// and X25519 run in the wasm (HACL*). AES runs in this host's WebCrypto:
|
|
1737
|
+
// runWithCipher (lib/cipher-host.mjs) answers each request the engine
|
|
1738
|
+
// makes and calls the op again. This code converts bytes to base64 and
|
|
1739
|
+
// draws random bytes; it reads no document and chooses nothing.
|
|
1740
|
+
// -----------------------------------------------------------------
|
|
1741
|
+
|
|
1742
|
+
// The most random octets any of these ops reads (Jwe.randomBytesNeeded is
|
|
1743
|
+
// at most 112; xmlEncrypt reads at most 76).
|
|
1744
|
+
const CIPHER_RANDOM_BYTES = 112;
|
|
1745
|
+
|
|
1746
|
+
function bytesToBase64(bytes) {
|
|
1747
|
+
let bin = '';
|
|
1748
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
1749
|
+
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
|
|
1750
|
+
}
|
|
1751
|
+
return btoa(bin);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function base64ToBytes(text) {
|
|
1755
|
+
const bin = atob(text);
|
|
1756
|
+
const out = new Uint8Array(bin.length);
|
|
1757
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
1758
|
+
return out;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
function randomBase64(n) {
|
|
1762
|
+
const b = new Uint8Array(n);
|
|
1763
|
+
globalThis.crypto.getRandomValues(b);
|
|
1764
|
+
return bytesToBase64(b);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// One op that may ask for AES: the engine's envelope after every request
|
|
1768
|
+
// is answered; a refusal throws a FactoidalError.
|
|
1769
|
+
async function cipherOp(fnName, what, args) {
|
|
1770
|
+
const e = await entry();
|
|
1771
|
+
requireEntryFn(e, fnName, what);
|
|
1772
|
+
const res = await runWithCipher((a) => JSON.parse(e[fnName](JSON.stringify(a))), args);
|
|
1773
|
+
return entryResult(JSON.stringify(res), fnName);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
function checkKeys(fnName, keys) {
|
|
1777
|
+
if (!Array.isArray(keys) || !keys.every((k) => k !== null && typeof k === 'object')) {
|
|
1778
|
+
throw new TypeError(`${fnName}: keys must be an array of JWK objects`);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
/**
|
|
1783
|
+
* Decrypt every xenc:EncryptedData of an XML document (XML Encryption 1.1).
|
|
1784
|
+
* Allowlist and key forms: API.md "XML Encryption and JWE".
|
|
1785
|
+
* @param {string} document the XML document
|
|
1786
|
+
* @param {{keys: object[], allowCbc?: boolean}} options private JWKs
|
|
1787
|
+
* (`kid` is the ds:KeyName); `allowCbc` admits aes128-cbc and aes256-cbc
|
|
1788
|
+
* @returns {Promise<{document: string, canonical: string}>}
|
|
1789
|
+
*/
|
|
1790
|
+
async function xmlDecrypt(document, options) {
|
|
1791
|
+
if (typeof document !== 'string') throw new TypeError('xmlDecrypt: document must be a string');
|
|
1792
|
+
const o = options || {};
|
|
1793
|
+
checkKeys('xmlDecrypt', o.keys);
|
|
1794
|
+
const r = await cipherOp('xmlDecrypt', 'XML decryption',
|
|
1795
|
+
{ document, keys: o.keys, allowCbc: o.allowCbc === true });
|
|
1796
|
+
return { document: r.document, canonical: r.canonical };
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
/**
|
|
1800
|
+
* Encrypt the document element (or, with `content`, its content) of an XML
|
|
1801
|
+
* document for one recipient (XML Encryption 1.1, AES-GCM content).
|
|
1802
|
+
* @param {string} document the XML document
|
|
1803
|
+
* @param {{recipient: object, algorithm?: 'aes128-gcm'|'aes256-gcm',
|
|
1804
|
+
* keyTransport?: 'rsa-oaep-mgf1p'|'rsa-oaep-sha256', keyWrap?: 'kw-aes128'|'kw-aes256',
|
|
1805
|
+
* content?: boolean, saml?: boolean}} options `recipient` is a public JWK
|
|
1806
|
+
* (RSA, EC P-256, OKP X25519) or an `oct` key; `saml` wraps the result in
|
|
1807
|
+
* saml:EncryptedAssertion
|
|
1808
|
+
* @returns {Promise<{document: string, canonical: string}>}
|
|
1809
|
+
*/
|
|
1810
|
+
async function xmlEncrypt(document, options) {
|
|
1811
|
+
if (typeof document !== 'string') throw new TypeError('xmlEncrypt: document must be a string');
|
|
1812
|
+
const o = options || {};
|
|
1813
|
+
if (o.recipient === null || typeof o.recipient !== 'object') {
|
|
1814
|
+
throw new TypeError('xmlEncrypt: options.recipient must be a JWK object');
|
|
1815
|
+
}
|
|
1816
|
+
const args = { document, recipient: o.recipient, random: randomBase64(CIPHER_RANDOM_BYTES) };
|
|
1817
|
+
for (const k of ['algorithm', 'keyTransport', 'keyWrap']) if (o[k] !== undefined) args[k] = o[k];
|
|
1818
|
+
if (o.content === true) args.content = true;
|
|
1819
|
+
if (o.saml === true) args.saml = true;
|
|
1820
|
+
const r = await cipherOp('xmlEncrypt', 'XML encryption', args);
|
|
1821
|
+
return { document: r.document, canonical: r.canonical };
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* Decrypt a JWE in the Compact Serialization (RFC 7516).
|
|
1826
|
+
* @param {string} token the compact JWE
|
|
1827
|
+
* @param {{jwk: object}} options the recipient's private JWK
|
|
1828
|
+
* @returns {Promise<{header: object, plaintext: Uint8Array}>}
|
|
1829
|
+
*/
|
|
1830
|
+
async function jweDecrypt(token, options) {
|
|
1831
|
+
if (typeof token !== 'string') throw new TypeError('jweDecrypt: token must be a string');
|
|
1832
|
+
const o = options || {};
|
|
1833
|
+
if (o.jwk === null || typeof o.jwk !== 'object') throw new TypeError('jweDecrypt: options.jwk must be a JWK object');
|
|
1834
|
+
const r = await cipherOp('jweDecrypt', 'JWE decryption', { token, jwk: o.jwk });
|
|
1835
|
+
return { header: r.header, plaintext: base64ToBytes(r.plaintext) };
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/**
|
|
1839
|
+
* Encrypt to a JWE in the Compact Serialization (RFC 7516).
|
|
1840
|
+
* @param {Uint8Array|string} plaintext bytes, or text written as UTF-8
|
|
1841
|
+
* @param {{alg: string, enc: string, jwk: object, header?: object}} options
|
|
1842
|
+
* the recipient's public JWK (or the `oct` key for A128KW, A256KW, dir);
|
|
1843
|
+
* `header`: more protected header members
|
|
1844
|
+
* @returns {Promise<string>} the compact JWE
|
|
1845
|
+
*/
|
|
1846
|
+
async function jweEncrypt(plaintext, options) {
|
|
1847
|
+
let bytes;
|
|
1848
|
+
if (typeof plaintext === 'string') bytes = new TextEncoder().encode(plaintext);
|
|
1849
|
+
else if (plaintext instanceof Uint8Array) bytes = plaintext;
|
|
1850
|
+
else throw new TypeError('jweEncrypt: plaintext must be a Uint8Array or a string');
|
|
1851
|
+
const o = options || {};
|
|
1852
|
+
if (typeof o.alg !== 'string' || typeof o.enc !== 'string') {
|
|
1853
|
+
throw new TypeError('jweEncrypt: options.alg and options.enc must be strings');
|
|
1854
|
+
}
|
|
1855
|
+
if (o.jwk === null || typeof o.jwk !== 'object') throw new TypeError('jweEncrypt: options.jwk must be a JWK object');
|
|
1856
|
+
const args = { alg: o.alg, enc: o.enc, jwk: o.jwk, plaintext: bytesToBase64(bytes),
|
|
1857
|
+
random: randomBase64(CIPHER_RANDOM_BYTES) };
|
|
1858
|
+
if (o.header !== undefined) args.header = o.header;
|
|
1859
|
+
return (await cipherOp('jweEncrypt', 'JWE encryption', args)).token;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1655
1862
|
/**
|
|
1656
1863
|
* Create an eddsa-rdfc-2022 Data Integrity proofValue over an
|
|
1657
1864
|
* already-canonicalized document + proof config (entry_jsoo.ml's
|
|
@@ -2282,11 +2489,14 @@ function buildApi(driver) {
|
|
|
2282
2489
|
// ops it actually has.
|
|
2283
2490
|
shacl: has('shaclValidate'),
|
|
2284
2491
|
shex: has('shexValidate'),
|
|
2492
|
+
sparqlRl: has('datasetSparqlRlInfer'),
|
|
2285
2493
|
owlClosure: has('owlClosure'),
|
|
2286
2494
|
explain: has('explainGraph'),
|
|
2287
2495
|
stages: has('stageList'),
|
|
2288
2496
|
pipelineCheck: has('pipelineCheck'),
|
|
2289
2497
|
pipelineCheckNodes: has('pipelineCheckNodes'),
|
|
2498
|
+
graphCheck: has('graphCheckNodes'),
|
|
2499
|
+
datasetMerge: has('datasetMerge'),
|
|
2290
2500
|
entailment: has('queryRaw'),
|
|
2291
2501
|
tableau: has('tableauMaterialise') && has('tableauDlInconsistent'),
|
|
2292
2502
|
rml: has('rmlMap'),
|
|
@@ -2318,6 +2528,7 @@ function buildApi(driver) {
|
|
|
2318
2528
|
store: has('storeOpen') && has('storeHandleQuery'),
|
|
2319
2529
|
deltaLog: has('deltaLogEncode') && has('deltaLogDecode') && has('deltaLogMerge'),
|
|
2320
2530
|
literalValues: has('literalValue') && has('literalFromValue'),
|
|
2531
|
+
cid: has('cidValidate') && has('cidRetrieve'),
|
|
2321
2532
|
};
|
|
2322
2533
|
}
|
|
2323
2534
|
|
|
@@ -2421,8 +2632,12 @@ function buildApi(driver) {
|
|
|
2421
2632
|
const nodeApi = buildNodes({
|
|
2422
2633
|
entry, entryResult, requireEntryFn, Dataset,
|
|
2423
2634
|
parse, closure, shaclValidate, query, serialize, canonicalize, canonicalHash, explain, csvwToRdf,
|
|
2635
|
+
datasetMerge,
|
|
2424
2636
|
});
|
|
2425
2637
|
|
|
2638
|
+
// Controlled Identifiers v1.0 (lib/cid.mjs, issue 1100 items 7 and 9).
|
|
2639
|
+
const cid = cidFunctions({ entry, entryResult, requireEntryFn, host: driver.cidHost });
|
|
2640
|
+
|
|
2426
2641
|
return {
|
|
2427
2642
|
parse,
|
|
2428
2643
|
query,
|
|
@@ -2443,6 +2658,7 @@ function buildApi(driver) {
|
|
|
2443
2658
|
asDefaultGraph,
|
|
2444
2659
|
shaclValidate,
|
|
2445
2660
|
shexValidate,
|
|
2661
|
+
sparqlRlInfer,
|
|
2446
2662
|
owlClosure,
|
|
2447
2663
|
coreRdfsClosure,
|
|
2448
2664
|
coreRdfsCheck,
|
|
@@ -2450,6 +2666,7 @@ function buildApi(driver) {
|
|
|
2450
2666
|
stages: nodeApi.stages,
|
|
2451
2667
|
node: nodeApi.node,
|
|
2452
2668
|
pipe: nodeApi.pipe,
|
|
2669
|
+
graph: nodeApi.graph,
|
|
2453
2670
|
run: nodeApi.run,
|
|
2454
2671
|
rdfsPlusClosure,
|
|
2455
2672
|
rhoDfClosure,
|
|
@@ -2484,12 +2701,18 @@ function buildApi(driver) {
|
|
|
2484
2701
|
vcEd25519SecretToPublic,
|
|
2485
2702
|
vcEd25519Sign,
|
|
2486
2703
|
vcEd25519Verify,
|
|
2704
|
+
xmlDecrypt,
|
|
2705
|
+
xmlEncrypt,
|
|
2706
|
+
jweDecrypt,
|
|
2707
|
+
jweEncrypt,
|
|
2487
2708
|
vcEddsaCreateFromCanonical,
|
|
2488
2709
|
vcEddsaVerifyFromCanonical,
|
|
2489
2710
|
vcCheckCredential,
|
|
2490
2711
|
vcCheckCredentialSubject,
|
|
2491
2712
|
vcCheckNoDataLoss,
|
|
2492
2713
|
vcCheckRelatedResourceDigests,
|
|
2714
|
+
cidValidate: cid.cidValidate,
|
|
2715
|
+
cidRetrieveVerificationMethod: cid.cidRetrieveVerificationMethod,
|
|
2493
2716
|
capabilities,
|
|
2494
2717
|
openStore,
|
|
2495
2718
|
queryStore,
|
|
@@ -2503,6 +2726,7 @@ function buildApi(driver) {
|
|
|
2503
2726
|
_deltaLogCorruptLastForTest,
|
|
2504
2727
|
queryRaw,
|
|
2505
2728
|
isomorphic,
|
|
2729
|
+
datasetMerge,
|
|
2506
2730
|
fromQuads,
|
|
2507
2731
|
literalValue,
|
|
2508
2732
|
isValidLiteral,
|
package/lib/browser-engine.mjs
CHANGED
|
@@ -13,6 +13,18 @@
|
|
|
13
13
|
//
|
|
14
14
|
// No Node built-ins: the browser imports this file as it is.
|
|
15
15
|
|
|
16
|
+
import { checkEngineStamp } from './engine-integrity.mjs';
|
|
17
|
+
|
|
18
|
+
// version.json next to the engine, or null (absent, unreadable, not JSON).
|
|
19
|
+
async function stampOf(url) {
|
|
20
|
+
try {
|
|
21
|
+
const r = await fetch(url);
|
|
22
|
+
return r.ok ? await r.json() : null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
async function instantiate(url, imports) {
|
|
17
29
|
try {
|
|
18
30
|
return await WebAssembly.instantiateStreaming(fetch(url), imports);
|
|
@@ -59,7 +71,22 @@ export function browserEngine(name, defaultAssetsUrl) {
|
|
|
59
71
|
// newer loader (the loader's own locateFile does the same).
|
|
60
72
|
const version = loader.wasmVersion ? `?v=${loader.wasmVersion}` : '';
|
|
61
73
|
const wasmUrl = new URL(`${name}.wasm`, assetsUrl).href + version;
|
|
62
|
-
|
|
74
|
+
// The release stamp check (engine-integrity.mjs, issue 1106): with a
|
|
75
|
+
// stamped version.json next to the full engine, its bytes are hashed
|
|
76
|
+
// before they are compiled (no streaming compile then). Without one,
|
|
77
|
+
// the streaming path as before.
|
|
78
|
+
const stamp = name === 'l4factoidal'
|
|
79
|
+
? await stampOf(new URL('version.json', assetsUrl).href + version) : null;
|
|
80
|
+
if (!stamp || stamp.stamped !== true) {
|
|
81
|
+
return loader.loadL4({ instantiateWasm: (imports) => instantiate(wasmUrl, imports) });
|
|
82
|
+
}
|
|
83
|
+
const response = await fetch(wasmUrl);
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`factoidal: fetching ${wasmUrl} failed: ${response.status} ${response.statusText}`);
|
|
86
|
+
}
|
|
87
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
88
|
+
await checkEngineStamp(bytes, stamp, { where: wasmUrl });
|
|
89
|
+
return loader.loadL4({ instantiateWasm: (imports) => WebAssembly.instantiate(bytes, imports) });
|
|
63
90
|
})();
|
|
64
91
|
enginePromise.catch(() => { enginePromise = null; });
|
|
65
92
|
return enginePromise;
|
package/lib/cid-node.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// The Node and Deno host of cidRetrieveVerificationMethod (lib/cid.mjs,
|
|
2
|
+
// https://github.com/danbri/factoidal/issues/1100 item 9): name resolution,
|
|
3
|
+
// and a GET that connects to the one address the engine chose, so a second
|
|
4
|
+
// answer of a name server cannot move the request (the authorization
|
|
5
|
+
// server's fetch, authz/server.mjs). Pure I/O: the engine decides which
|
|
6
|
+
// URL, which address, and what the answer means
|
|
7
|
+
// (formal/lean4/L4Factoidal/CID/Fetch.lean). No redirect is followed.
|
|
8
|
+
|
|
9
|
+
const FETCH_TIMEOUT_MS = 10000;
|
|
10
|
+
const MAX_DOCUMENT_BYTES = 1048576;
|
|
11
|
+
|
|
12
|
+
/** Every address of a name, as the system resolver gives them. */
|
|
13
|
+
async function resolve(host) {
|
|
14
|
+
try {
|
|
15
|
+
const dns = await import('node:dns');
|
|
16
|
+
const found = await dns.promises.lookup(host, { all: true, verbatim: true });
|
|
17
|
+
return found.map((a) => a.address);
|
|
18
|
+
} catch (_error) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A Response-like answer: status, headers.get, text().
|
|
24
|
+
function answer(status, contentType, body) {
|
|
25
|
+
return {
|
|
26
|
+
status,
|
|
27
|
+
headers: { get: (name) => (String(name).toLowerCase() === 'content-type' ? contentType : null) },
|
|
28
|
+
text: async () => body,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* GET `url` from `init.address`, sending `init.headers`. With no address
|
|
34
|
+
* (the engine named none), the system resolver connects.
|
|
35
|
+
*/
|
|
36
|
+
async function fetch(url, init) {
|
|
37
|
+
let target;
|
|
38
|
+
try {
|
|
39
|
+
target = new URL(url);
|
|
40
|
+
} catch (_error) {
|
|
41
|
+
return answer(0, '', '');
|
|
42
|
+
}
|
|
43
|
+
const mod = target.protocol === 'https:' ? await import('node:https') : await import('node:http');
|
|
44
|
+
const address = init && typeof init.address === 'string' ? init.address : null;
|
|
45
|
+
const family = address && address.indexOf(':') >= 0 ? 6 : 4;
|
|
46
|
+
const options = { method: 'GET', headers: (init && init.headers) || {}, timeout: FETCH_TIMEOUT_MS };
|
|
47
|
+
if (address) {
|
|
48
|
+
options.lookup = (_host, opts, callback) => {
|
|
49
|
+
if (opts && opts.all) callback(null, [{ address, family }]);
|
|
50
|
+
else callback(null, address, family);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return new Promise((done) => {
|
|
54
|
+
const request = mod.request(target, options, (response) => {
|
|
55
|
+
const chunks = [];
|
|
56
|
+
let size = 0;
|
|
57
|
+
response.on('data', (chunk) => {
|
|
58
|
+
size += chunk.length;
|
|
59
|
+
if (size > MAX_DOCUMENT_BYTES) {
|
|
60
|
+
response.destroy();
|
|
61
|
+
done(answer(0, '', ''));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
chunks.push(chunk);
|
|
65
|
+
});
|
|
66
|
+
response.on('end', () => done(answer(response.statusCode || 0,
|
|
67
|
+
String(response.headers['content-type'] || ''), Buffer.concat(chunks).toString('utf8'))));
|
|
68
|
+
response.on('error', () => done(answer(0, '', '')));
|
|
69
|
+
});
|
|
70
|
+
request.on('timeout', () => request.destroy(new Error('timeout')));
|
|
71
|
+
request.on('error', () => done(answer(0, '', '')));
|
|
72
|
+
request.end();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The host index.mjs passes to the API. */
|
|
77
|
+
export const nodeCidHost = Object.freeze({ resolve, fetch });
|
package/lib/cid.mjs
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Controlled Identifiers v1.0 (https://www.w3.org/TR/cid-1.0/):
|
|
2
|
+
// cidValidate and cidRetrieveVerificationMethod
|
|
3
|
+
// (https://github.com/danbri/factoidal/issues/1100, items 7 and 9).
|
|
4
|
+
//
|
|
5
|
+
// The engine decides everything (formal/lean4/L4Factoidal/CID/Validate.lean,
|
|
6
|
+
// CID/Fetch.lean, over the ops cidValidate and cidRetrieve of
|
|
7
|
+
// formal/lean4/Wasm/Ops/Cid.lean): the §2 checks; which URL to fetch; whether
|
|
8
|
+
// its scheme and addresses may be fetched (the private-address guard of the
|
|
9
|
+
// authorization server); whether the status and the Content-Type of the
|
|
10
|
+
// answer are accepted; §3.3; expiry and revocation with `now`. This file
|
|
11
|
+
// moves bytes: it resolves the host name the engine names, GETs the URL the
|
|
12
|
+
// engine names from the address the engine names, and passes the status,
|
|
13
|
+
// the Content-Type and the body back.
|
|
14
|
+
//
|
|
15
|
+
// ES module, no Node built-ins: the browser loads this file as it is. The
|
|
16
|
+
// Node and Deno host functions (name resolution, a GET pinned to one
|
|
17
|
+
// address) are lib/cid-node.mjs, which index.mjs passes in.
|
|
18
|
+
|
|
19
|
+
import { FactoidalError } from './errors.mjs';
|
|
20
|
+
|
|
21
|
+
// The engine names at most one resolution and one fetch; a longer loop is
|
|
22
|
+
// a defect, not a document.
|
|
23
|
+
const MAX_STEPS = 4;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The browser host: no name resolution (a page cannot resolve names; the
|
|
27
|
+
* engine then checks only IP literals and localhost names), and the page's
|
|
28
|
+
* fetch, which does not follow redirects and sends no credentials.
|
|
29
|
+
*/
|
|
30
|
+
export const browserCidHost = Object.freeze({
|
|
31
|
+
resolve: null,
|
|
32
|
+
fetch: (url, init) => globalThis.fetch(url, {
|
|
33
|
+
method: 'GET',
|
|
34
|
+
headers: init.headers,
|
|
35
|
+
redirect: 'manual',
|
|
36
|
+
credentials: 'omit',
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// The status, Content-Type and body of a fetch answer: a WHATWG Response,
|
|
41
|
+
// or an object of the same shape.
|
|
42
|
+
async function answerOf(url, response) {
|
|
43
|
+
const status = Number(response && response.status) || 0;
|
|
44
|
+
const headers = response && response.headers;
|
|
45
|
+
const contentType = headers && typeof headers.get === 'function'
|
|
46
|
+
? (headers.get('content-type') || '') : '';
|
|
47
|
+
const body = response && typeof response.text === 'function' ? String(await response.text()) : '';
|
|
48
|
+
return { url, status, contentType, body };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function nowSeconds(now) {
|
|
52
|
+
if (now === undefined || now === null) return undefined;
|
|
53
|
+
if (now instanceof Date) return Math.floor(now.getTime() / 1000);
|
|
54
|
+
if (typeof now === 'number' && Number.isFinite(now)) return Math.floor(now);
|
|
55
|
+
throw new TypeError('cidRetrieveVerificationMethod: options.now must be a Date or a number of seconds');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The two functions over an entry (lib/lean-entry.mjs) and a host.
|
|
60
|
+
* @param {object} deps
|
|
61
|
+
* @param {() => Promise<object>} deps.entry
|
|
62
|
+
* @param {(jsonText: string, what: string) => object} deps.entryResult
|
|
63
|
+
* @param {(e: object, fn: string, what: string) => void} deps.requireEntryFn
|
|
64
|
+
* @param {{resolve: ((host: string) => Promise<string[]>)|null, fetch: Function}} [deps.host]
|
|
65
|
+
*/
|
|
66
|
+
export function cidFunctions({ entry, entryResult, requireEntryFn, host }) {
|
|
67
|
+
const defaults = host || browserCidHost;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* CID 1.0 §2: check a controlled identifier document.
|
|
71
|
+
* @param {string|object} doc JSON text, or a value JSON.stringify writes
|
|
72
|
+
* @returns {Promise<{valid: boolean, errors: Array<{code: string, message: string, path: string}>}>}
|
|
73
|
+
*/
|
|
74
|
+
async function cidValidate(doc) {
|
|
75
|
+
const text = typeof doc === 'string' ? doc : JSON.stringify(doc);
|
|
76
|
+
if (typeof text !== 'string') throw new TypeError('cidValidate: doc must be JSON text or a JSON value');
|
|
77
|
+
const e = await entry();
|
|
78
|
+
requireEntryFn(e, 'cidValidate', 'CID 1.0 document check');
|
|
79
|
+
const r = entryResult(e.cidValidate(text), 'cidValidate');
|
|
80
|
+
return { valid: r.valid === true, errors: Array.isArray(r.errors) ? r.errors : [] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* CID 1.0 §3.3 Retrieve Verification Method, with the fetch done here.
|
|
85
|
+
* @param {string} url the verification method URL
|
|
86
|
+
* @param {string} relationship a verification relationship of §2.3
|
|
87
|
+
* @param {object} [options]
|
|
88
|
+
* @returns {Promise<{ok: true, verificationMethod: object, controllerDocument: string}
|
|
89
|
+
* | {ok: false, code: string, problem: {type: string, title: string, detail: string}}>}
|
|
90
|
+
*/
|
|
91
|
+
async function cidRetrieveVerificationMethod(url, relationship, options) {
|
|
92
|
+
if (typeof url !== 'string' || typeof relationship !== 'string') {
|
|
93
|
+
throw new TypeError('cidRetrieveVerificationMethod: url and relationship must be strings');
|
|
94
|
+
}
|
|
95
|
+
const o = options || {};
|
|
96
|
+
const fetchFn = typeof o.fetch === 'function' ? o.fetch : defaults.fetch;
|
|
97
|
+
// `resolve: null` says the caller cannot resolve names.
|
|
98
|
+
const resolveFn = 'resolve' in o ? o.resolve : defaults.resolve;
|
|
99
|
+
const request = {
|
|
100
|
+
vm: url,
|
|
101
|
+
relationship,
|
|
102
|
+
allowHttp: o.allowHttp === true,
|
|
103
|
+
allowPrivateFetch: o.allowPrivateFetch === true,
|
|
104
|
+
resolver: typeof resolveFn === 'function',
|
|
105
|
+
resolved: [],
|
|
106
|
+
fetched: [],
|
|
107
|
+
};
|
|
108
|
+
const now = nowSeconds(o.now);
|
|
109
|
+
if (now !== undefined) request.now = now;
|
|
110
|
+
const e = await entry();
|
|
111
|
+
requireEntryFn(e, 'cidRetrieve', 'CID 1.0 Retrieve Verification Method');
|
|
112
|
+
for (let i = 0; i < MAX_STEPS; i += 1) {
|
|
113
|
+
const r = entryResult(e.cidRetrieve(JSON.stringify(request)), 'cidRetrieve');
|
|
114
|
+
if (r.step === 'method') {
|
|
115
|
+
return { ok: true, verificationMethod: r.verificationMethod, controllerDocument: r.controllerDocument };
|
|
116
|
+
}
|
|
117
|
+
if (r.step === 'error') return { ok: false, code: r.code, problem: r.problem };
|
|
118
|
+
if (r.step === 'resolve') {
|
|
119
|
+
let addresses = [];
|
|
120
|
+
try {
|
|
121
|
+
const got = await resolveFn(r.host);
|
|
122
|
+
addresses = Array.isArray(got) ? got.map(String) : [];
|
|
123
|
+
} catch (_error) {
|
|
124
|
+
addresses = [];
|
|
125
|
+
}
|
|
126
|
+
request.resolved.push({ host: r.host, addresses });
|
|
127
|
+
} else if (r.step === 'fetch') {
|
|
128
|
+
let answer;
|
|
129
|
+
try {
|
|
130
|
+
const init = { method: 'GET', headers: { accept: r.accept }, redirect: 'manual' };
|
|
131
|
+
if (r.address !== null && r.address !== undefined) init.address = r.address;
|
|
132
|
+
answer = await answerOf(r.url, await fetchFn(r.url, init));
|
|
133
|
+
} catch (_error) {
|
|
134
|
+
answer = { url: r.url, status: 0, contentType: '', body: '' };
|
|
135
|
+
}
|
|
136
|
+
request.fetched.push(answer);
|
|
137
|
+
} else {
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw new FactoidalError('cidRetrieveVerificationMethod: the engine asked for more steps than one retrieval needs',
|
|
142
|
+
{ code: 'op.failed' });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { cidValidate, cidRetrieveVerificationMethod };
|
|
146
|
+
}
|