@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/CHANGELOG.md +65 -0
- package/README.md +59 -15
- 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 +23 -0
- package/browser.js +26 -0
- package/index.d.ts +463 -24
- package/index.mjs +29 -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 +480 -28
- 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/dataset.mjs +96 -12
- package/lib/engine-integrity.mjs +56 -0
- package/lib/lean-entry.mjs +30 -12
- package/lib/nodes.mjs +318 -17
- package/lib/sbm11-lws.mjs +4 -2
- package/lib/sbm11.mjs +29 -6
- 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
|
@@ -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
|
-
|
|
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;
|
|
@@ -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
|
|
889
|
-
* hexadecimal digits, of the canonical N-Quads document (
|
|
890
|
-
* Canonicalization 1.0, section 4.4 note 2)
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
* in
|
|
894
|
-
*
|
|
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(
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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,
|