@factoidal/core 0.19.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/lib/api.mjs CHANGED
@@ -26,10 +26,12 @@ import { dataFactory, termFromSrj, termToJson, quadToJson, _setTextEngine } from
26
26
  import { Dataset, INTERNAL, checkOptions, makeRegistry, WRITER_TAGS } from './dataset.mjs';
27
27
  import { version as packageVersion } from './version.mjs';
28
28
  import { openGeneration, readRegion, manifestKeys, hexOfBytes } from './store.mjs';
29
- import { fullEngineMessage } from './lean-entry.mjs';
29
+ 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
- const r = entryResult(e.datasetQueryRaw(ds.handle, sparql, JSON.stringify({
668
- entail, output: graphForm ? 'ntriples' : 'json' })), 'query', sparql);
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;
@@ -735,16 +755,33 @@ function buildApi(driver) {
735
755
  * skipped statement in `dataset.diagnostics`
736
756
  * (https://github.com/danbri/factoidal/issues/716); `maxErrors: n` stops
737
757
  * after n diagnostics. With no `format` the engine infers one
738
- * (`dataset.format`, `dataset.formatInferred`).
758
+ * (`dataset.format`, `dataset.formatInferred`). `{rdfstar:true}` reads
759
+ * JSON-LD as JSON-LD-star (`@triple`, `@reifies`, `@annotation`), as
760
+ * jsonldToRdf() does; it applies to format 'jsonld' only.
739
761
  * @param {string} text
740
762
  * @param {{format?: string, baseIRI?: string, lenient?: boolean,
741
- * maxErrors?: number}} [options]
763
+ * maxErrors?: number, rdfstar?: boolean}} [options]
742
764
  * @returns {Promise<Dataset>}
743
765
  */
744
766
  async function parse(text, options) {
745
767
  if (typeof text !== 'string') throw new TypeError('parse: text must be a string');
746
768
  const opts = await checkedOptions('parse', optionsOf('parse', options));
747
769
  const c = await ctx();
770
+ if (opts.rdfstar) {
771
+ const { tag, inferred } = formatTagOf(c, text, opts.format, 'parse');
772
+ if (tag !== 'jsonld') {
773
+ throw new FactoidalError(
774
+ `parse: rdfstar applies to format 'jsonld' only (format '${tag}'${inferred ? ', inferred' : ''})`,
775
+ { code: 'option.conflict' });
776
+ }
777
+ requireEntryFn(c.e, 'datasetFromJsonLd', 'parse {rdfstar:true} (datasetFromJsonLd)');
778
+ const optionsJson = JSON.stringify({
779
+ ...(opts.baseIRI ? { base: opts.baseIRI } : {}),
780
+ rdfstar: true,
781
+ });
782
+ return c.wrap(entryResult(c.e.datasetFromJsonLd(text, optionsJson), 'parse'), {},
783
+ { format: tag, formatInferred: inferred });
784
+ }
748
785
  if (opts.lenient) {
749
786
  const { tag, inferred } = formatTagOf(c, text, opts.format, 'parse');
750
787
  requireEntryFn(c.e, 'parseDocument', 'parse {lenient:true} (parseDocument)');
@@ -885,30 +922,54 @@ function buildApi(driver) {
885
922
  }
886
923
 
887
924
  /**
888
- * The RDFC-1.0 canonical hash of one graph: the SHA-256, as 64 lowercase
889
- * hexadecimal digits, of the canonical N-Quads document (RDF Dataset
890
- * Canonicalization 1.0, section 4.4 note 2). Every quad's graph name is
891
- * dropped first (op datasetGraphAsDefault with the union of the graphs);
892
- * then op datasetCanonicalHash computes the canonical form and the digest
893
- * in Lean. canonicalize() answers the canonical text.
894
- * @param {Dataset} datasetOrGraph
925
+ * The RDFC-1.0 canonical hash of a dataset: the SHA-256, as 64 lowercase
926
+ * hexadecimal digits, of the canonical N-Quads document that canonicalize()
927
+ * answers (RDF Dataset Canonicalization 1.0, section 4.4 note 2), graph
928
+ * names included. Op datasetCanonicalHash computes the canonical form and
929
+ * the digest in Lean. Since 0.20.0; 0.17.0 to 0.19.x dropped every graph
930
+ * name first, so datasets that differ only in their graph names got the
931
+ * same hash. To hash a graph whatever its name, use
932
+ * canonicalHash(asDefaultGraph(g)).
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]
895
941
  * @returns {Promise<string>}
896
942
  */
897
- async function canonicalHash(datasetOrGraph) {
898
- if (!(datasetOrGraph instanceof Dataset)) {
899
- throw new TypeError('canonicalHash: expected a Dataset (e.g. one entry of graphs())');
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);
900
951
  }
901
- datasetOrGraph.size; // throws the disposed error
902
- const c = datasetOrGraph._ctx;
903
- requireEntryFn(c.e, 'datasetGraphAsDefault', 'canonicalHash');
904
- requireEntryFn(c.e, 'datasetCanonicalHash', 'canonicalHash');
905
- const merged = c.wrap(entryResult(c.e.datasetGraphAsDefault(datasetOrGraph.handle,
906
- JSON.stringify({ type: 'union' })), 'canonicalHash'), {});
907
- try {
908
- return entryResult(c.e.datasetCanonicalHash(merged.handle), 'canonicalHash').hash;
909
- } finally {
910
- merged.dispose();
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));
955
+ }
956
+
957
+ /**
958
+ * A new Dataset that holds the triples of every graph of `dataset`, all in
959
+ * the default graph (op datasetGraphAsDefault with the union of the
960
+ * graphs). With one entry of graphs(), it is that graph without its name.
961
+ * @param {Dataset} dataset
962
+ * @returns {Dataset}
963
+ */
964
+ function asDefaultGraph(dataset) {
965
+ if (!(dataset instanceof Dataset)) {
966
+ throw new TypeError('asDefaultGraph: expected a Dataset');
911
967
  }
968
+ dataset.size; // throws the disposed error
969
+ const c = dataset._ctx;
970
+ requireEntryFn(c.e, 'datasetGraphAsDefault', 'asDefaultGraph');
971
+ return c.wrap(entryResult(c.e.datasetGraphAsDefault(dataset.handle,
972
+ JSON.stringify({ type: 'union' })), 'asDefaultGraph'), {});
912
973
  }
913
974
 
914
975
  /**
@@ -930,6 +991,30 @@ function buildApi(driver) {
930
991
  }));
931
992
  }
932
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
+
933
1018
  /**
934
1019
  * A Dataset holding RDF/JS quads (op datasetFromQuadsJson). Each term is
935
1020
  * sent as its components; the engine reads no text.
@@ -949,6 +1034,95 @@ function buildApi(driver) {
949
1034
  await ctx();
950
1035
  }
951
1036
 
1037
+ // The synchronous term functions (sameValue, syntaxFor, splitIri, ...) call
1038
+ // the loaded engine: `await ready()`, or any other call, loads it first.
1039
+ function loadedCtx(who, op = who) {
1040
+ if (!ctxCache) {
1041
+ throw new FactoidalError(
1042
+ `${who}: the engine is not loaded yet. Await any @factoidal/core call first ` +
1043
+ '(for example `await ready()`).', { code: 'op.unknown' });
1044
+ }
1045
+ requireEntryFn(ctxCache.e, op, who);
1046
+ return ctxCache;
1047
+ }
1048
+
1049
+ /**
1050
+ * SPARQL 1.1 `=` on two RDF terms (sections 17.3 and 17.4.1.7), decided by
1051
+ * the engine (L4Factoidal.SPARQL.sameValue;
1052
+ * https://github.com/danbri/factoidal/issues/1052).
1053
+ * @param {object|string} a an RDF/JS term or an N-Triples term string
1054
+ * @param {object|string} b
1055
+ * @returns {boolean|null} true: the same value; false: different values;
1056
+ * null: incomparable (the operator's type error)
1057
+ */
1058
+ function sameValue(a, b) {
1059
+ const c = loadedCtx('sameValue');
1060
+ const r = entryResult(c.e.sameValue(JSON.stringify(termToJson(a)),
1061
+ JSON.stringify(termToJson(b))), 'sameValue');
1062
+ return r.sameValue === 'true' ? true : (r.sameValue === 'false' ? false : null);
1063
+ }
1064
+
1065
+ const iriText = (iri) => (typeof iri === 'string' ? iri : (iri && iri.value) || '');
1066
+
1067
+ /**
1068
+ * The syntax of a document by its file name (or IRI path) and its media
1069
+ * type, from the engine's table of W3C registrations
1070
+ * (L4Factoidal.RDF.syntaxOfNames; https://github.com/danbri/factoidal/issues/1053).
1071
+ * The media type wins when it is given; an unknown media type does not fall
1072
+ * back to the name.
1073
+ * @param {string|{fileName?: string, mediaType?: string}} what a file name or IRI,
1074
+ * or both names
1075
+ * @returns {string|null} a syntax name of formats(), or null
1076
+ */
1077
+ function syntaxFor(what) {
1078
+ const c = loadedCtx('syntaxFor');
1079
+ const o = (typeof what === 'string') ? { fileName: what } : (what || {});
1080
+ const r = entryResult(c.e.syntaxFor(String(o.fileName ?? ''), String(o.mediaType ?? '')), 'syntaxFor');
1081
+ return r.syntax ?? null;
1082
+ }
1083
+
1084
+ /**
1085
+ * An IRI as a namespace and a local name: the local name is the longest
1086
+ * suffix that is an XML NCName, '' when there is none
1087
+ * (L4Factoidal.RDF.splitIriXml; https://github.com/danbri/factoidal/issues/1051 item 4).
1088
+ * @param {string|{value: string}} iri
1089
+ * @returns {{namespace: string, localName: string}}
1090
+ */
1091
+ function splitIri(iri) {
1092
+ const c = loadedCtx('splitIri');
1093
+ const r = entryResult(c.e.splitIri(iriText(iri)), 'splitIri');
1094
+ return { namespace: r.namespace, localName: r.localName };
1095
+ }
1096
+
1097
+ /** Is the text an XML NCName (Namespaces in XML 1.0, production [4])? */
1098
+ function isNCName(text) {
1099
+ const c = loadedCtx('isNCName');
1100
+ return entryResult(c.e.isNCName(String(text)), 'isNCName').ncName === true;
1101
+ }
1102
+
1103
+ /**
1104
+ * The engine's vocabulary table: prefix, namespace and term local names of
1105
+ * rdf, rdfs, xsd and owl, and the namespaces of foaf, dcterms, dc and schema
1106
+ * (L4Factoidal.RDF.vocabularyTable; https://github.com/danbri/factoidal/issues/1051 item 5).
1107
+ * @returns {Array<{prefix: string, namespace: string, terms: string[]}>}
1108
+ */
1109
+ function vocabularies() {
1110
+ const c = loadedCtx('vocabularies');
1111
+ return entryResult(c.e.vocabularies(), 'vocabularies').vocabularies;
1112
+ }
1113
+
1114
+ /** The container membership property rdf:_n, n >= 1, as a NamedNode. */
1115
+ function memberIri(n) {
1116
+ const c = loadedCtx('memberIri');
1117
+ return dataFactory.namedNode(entryResult(c.e.memberIri(String(n)), 'memberIri').iri);
1118
+ }
1119
+
1120
+ /** The n of rdf:_n, or null for any other IRI. */
1121
+ function memberOrdinal(iri) {
1122
+ const c = loadedCtx('memberOrdinal');
1123
+ return entryResult(c.e.memberOrdinal(iriText(iri)), 'memberOrdinal').ordinal ?? null;
1124
+ }
1125
+
952
1126
  /** The engine's name tables (formats, regimes, query outputs, pack syntaxes). */
953
1127
  async function engineTables() {
954
1128
  return (await ctx()).tables();
@@ -1001,6 +1175,28 @@ function buildApi(driver) {
1001
1175
  return withDataset(data, opts, 'closure', (c, ds) => closureOnHandle(c, ds, regime));
1002
1176
  }
1003
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
+
1004
1200
  /**
1005
1201
  * RDFS or OWL 2 RL closure of the default graph, as a new Dataset.
1006
1202
  * @param {Dataset|string|Array} data
@@ -1533,6 +1729,136 @@ function buildApi(driver) {
1533
1729
  e.vcEd25519Verify(publicKeyHex, messageHex, signatureHex), 'vcEd25519Verify').valid;
1534
1730
  }
1535
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
+
1536
1862
  /**
1537
1863
  * Create an eddsa-rdfc-2022 Data Integrity proofValue over an
1538
1864
  * already-canonicalized document + proof config (entry_jsoo.ml's
@@ -2163,11 +2489,14 @@ function buildApi(driver) {
2163
2489
  // ops it actually has.
2164
2490
  shacl: has('shaclValidate'),
2165
2491
  shex: has('shexValidate'),
2492
+ sparqlRl: has('datasetSparqlRlInfer'),
2166
2493
  owlClosure: has('owlClosure'),
2167
2494
  explain: has('explainGraph'),
2168
2495
  stages: has('stageList'),
2169
2496
  pipelineCheck: has('pipelineCheck'),
2170
2497
  pipelineCheckNodes: has('pipelineCheckNodes'),
2498
+ graphCheck: has('graphCheckNodes'),
2499
+ datasetMerge: has('datasetMerge'),
2171
2500
  entailment: has('queryRaw'),
2172
2501
  tableau: has('tableauMaterialise') && has('tableauDlInconsistent'),
2173
2502
  rml: has('rmlMap'),
@@ -2198,15 +2527,117 @@ function buildApi(driver) {
2198
2527
  turtlePrefixes: has('serializeTurtleWith'),
2199
2528
  store: has('storeOpen') && has('storeHandleQuery'),
2200
2529
  deltaLog: has('deltaLogEncode') && has('deltaLogDecode') && has('deltaLogMerge'),
2530
+ literalValues: has('literalValue') && has('literalFromValue'),
2531
+ cid: has('cidValidate') && has('cidRetrieve'),
2201
2532
  };
2202
2533
  }
2203
2534
 
2535
+ // ---------------------------------------------------------------
2536
+ // Literal values (https://github.com/danbri/factoidal/issues/1051 items 1-3).
2537
+ // The ops literalValue and literalFromValue (formal/lean4/Wasm/Ops/
2538
+ // LiteralValue.lean, over L4Factoidal.RDF.LiteralValue) make every
2539
+ // decision: the XSD lexical-to-value mapping, validity, the canonical form,
2540
+ // the datatype of a JavaScript value. These functions are SYNCHRONOUS, so a
2541
+ // library-style API can call them in a getter; the engine must be loaded
2542
+ // first (await ready(), or any other call).
2543
+ // ---------------------------------------------------------------
2544
+ function loadedEntry(who) {
2545
+ if (!ctxCache) {
2546
+ throw new FactoidalError(
2547
+ `${who}: the engine is not loaded yet. Await any @factoidal/core call ` +
2548
+ 'first (for example `await ready()`); after that this function is synchronous.',
2549
+ { code: 'op.unknown' });
2550
+ }
2551
+ return ctxCache.e;
2552
+ }
2553
+
2554
+ function literalAnswer(term, who) {
2555
+ if (!term || typeof term !== 'object' || term.termType !== 'Literal') {
2556
+ throw new TypeError(`${who}: expected an RDF/JS Literal term`);
2557
+ }
2558
+ const e = loadedEntry(who);
2559
+ requireEntryFn(e, 'literalValue', who);
2560
+ return entryResult(e.literalValue(JSON.stringify(termToJson(term))), who);
2561
+ }
2562
+
2563
+ // NaN and the infinities have no JSON number; the engine names them in
2564
+ // `special`, and this copies the matching JavaScript number in.
2565
+ const SPECIAL_NUMBERS = { NaN: NaN, INF: Infinity, '-INF': -Infinity };
2566
+ function valueOut(v) {
2567
+ if (v && typeof v.special === 'string' && v.special in SPECIAL_NUMBERS) {
2568
+ return { ...v, number: SPECIAL_NUMBERS[v.special] };
2569
+ }
2570
+ // A JSON text -0 may reach this layer as 0; negativeZero is the engine's
2571
+ // statement of the sign.
2572
+ if (v && v.negativeZero === true) return { ...v, number: -0 };
2573
+ return v;
2574
+ }
2575
+
2576
+ /**
2577
+ * The value of a literal: {datatype, recognized, valid, value?, canonical?}.
2578
+ * `recognized` false: the engine has no lexical space for the datatype, and
2579
+ * `valid` is null. `value` and `canonical` are present for a valid literal
2580
+ * of a datatype with a value model (the shapes are in API.md).
2581
+ */
2582
+ function literalValue(term) {
2583
+ const r = literalAnswer(term, 'literalValue');
2584
+ const out = { datatype: r.datatype, recognized: r.recognized,
2585
+ valid: r.recognized ? r.valid : null };
2586
+ if (r.value !== undefined) out.value = valueOut(r.value);
2587
+ if (r.canonical !== undefined) out.canonical = r.canonical;
2588
+ return out;
2589
+ }
2590
+
2591
+ /** true or false: the lexical form is in the datatype's lexical space;
2592
+ * null: the engine has no lexical space for the datatype. */
2593
+ function isValidLiteral(term) {
2594
+ const r = literalAnswer(term, 'isValidLiteral');
2595
+ return r.recognized ? r.valid : null;
2596
+ }
2597
+
2598
+ /** The literal with the canonical lexical form of its value (XSD 1.1 Part 2
2599
+ * section 2.3.1), the same datatype and language; null when the literal is
2600
+ * not valid or the engine has no canonical mapping for its datatype. */
2601
+ function canonicalLiteral(term) {
2602
+ const r = literalAnswer(term, 'canonicalLiteral');
2603
+ if (r.canonical === undefined) return null;
2604
+ if (term.language) return dataFactory.literal(r.canonical, term.direction
2605
+ ? { language: term.language, direction: term.direction } : term.language);
2606
+ return dataFactory.literal(r.canonical, dataFactory.namedNode(r.datatype));
2607
+ }
2608
+
2609
+ /**
2610
+ * The literal of a JavaScript value. The engine chooses the datatype (table
2611
+ * in API.md) unless `options.datatype` names one (an IRI, a NamedNode, or
2612
+ * an XSD built-in local name such as 'int'); the value must then be in that
2613
+ * datatype's value space, else a FactoidalError with code literal.value.
2614
+ */
2615
+ function literalFromValue(value, options) {
2616
+ const who = 'literalFromValue';
2617
+ const tagged = encodeJsValue(value);
2618
+ if (!tagged) {
2619
+ throw new TypeError(`${who}: expected a boolean, number, bigint, string or Date`);
2620
+ }
2621
+ const dt = options && options.datatype;
2622
+ const hint = dt === undefined || dt === null ? ''
2623
+ : (typeof dt === 'string' ? dt : (dt && typeof dt.value === 'string' ? dt.value : null));
2624
+ if (hint === null) throw new TypeError(`${who}: options.datatype must be a string or a NamedNode`);
2625
+ const e = loadedEntry(who);
2626
+ requireEntryFn(e, 'literalFromValue', who);
2627
+ const r = entryResult(e.literalFromValue(JSON.stringify(tagged), hint), who);
2628
+ return dataFactory.literal(r.value, dataFactory.namedNode(r.datatype));
2629
+ }
2630
+
2204
2631
  // The node-composition API (lib/nodes.mjs, issue 1002 step 2).
2205
2632
  const nodeApi = buildNodes({
2206
2633
  entry, entryResult, requireEntryFn, Dataset,
2207
- parse, closure, shaclValidate, query, serialize, canonicalize, explain, csvwToRdf,
2634
+ parse, closure, shaclValidate, query, serialize, canonicalize, canonicalHash, explain, csvwToRdf,
2635
+ datasetMerge,
2208
2636
  });
2209
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
+
2210
2641
  return {
2211
2642
  parse,
2212
2643
  query,
@@ -2224,8 +2655,10 @@ function buildApi(driver) {
2224
2655
  canonicalize,
2225
2656
  graphs,
2226
2657
  canonicalHash,
2658
+ asDefaultGraph,
2227
2659
  shaclValidate,
2228
2660
  shexValidate,
2661
+ sparqlRlInfer,
2229
2662
  owlClosure,
2230
2663
  coreRdfsClosure,
2231
2664
  coreRdfsCheck,
@@ -2233,6 +2666,7 @@ function buildApi(driver) {
2233
2666
  stages: nodeApi.stages,
2234
2667
  node: nodeApi.node,
2235
2668
  pipe: nodeApi.pipe,
2669
+ graph: nodeApi.graph,
2236
2670
  run: nodeApi.run,
2237
2671
  rdfsPlusClosure,
2238
2672
  rhoDfClosure,
@@ -2267,12 +2701,18 @@ function buildApi(driver) {
2267
2701
  vcEd25519SecretToPublic,
2268
2702
  vcEd25519Sign,
2269
2703
  vcEd25519Verify,
2704
+ xmlDecrypt,
2705
+ xmlEncrypt,
2706
+ jweDecrypt,
2707
+ jweEncrypt,
2270
2708
  vcEddsaCreateFromCanonical,
2271
2709
  vcEddsaVerifyFromCanonical,
2272
2710
  vcCheckCredential,
2273
2711
  vcCheckCredentialSubject,
2274
2712
  vcCheckNoDataLoss,
2275
2713
  vcCheckRelatedResourceDigests,
2714
+ cidValidate: cid.cidValidate,
2715
+ cidRetrieveVerificationMethod: cid.cidRetrieveVerificationMethod,
2276
2716
  capabilities,
2277
2717
  openStore,
2278
2718
  queryStore,
@@ -2286,9 +2726,21 @@ function buildApi(driver) {
2286
2726
  _deltaLogCorruptLastForTest,
2287
2727
  queryRaw,
2288
2728
  isomorphic,
2729
+ datasetMerge,
2289
2730
  fromQuads,
2731
+ literalValue,
2732
+ isValidLiteral,
2733
+ canonicalLiteral,
2734
+ literalFromValue,
2290
2735
  ready,
2291
2736
  engineTables,
2737
+ sameValue,
2738
+ syntaxFor,
2739
+ splitIri,
2740
+ isNCName,
2741
+ vocabularies,
2742
+ memberIri,
2743
+ memberOrdinal,
2292
2744
  Dataset,
2293
2745
  Store,
2294
2746
  FactoidalError,