@factoidal/core 0.7.0 → 0.10.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 +510 -1
- package/NOTICE +2 -21
- package/README.md +117 -993
- package/api.d.ts +53 -0
- package/api.js +5 -0
- package/api.mjs +56 -0
- package/bin/engine.mjs +38 -19
- package/bin/factoidal.mjs +513 -67
- package/bin/pack.mjs +37 -0
- package/bin/serve-worker.mjs +32 -0
- package/bin/serve.mjs +423 -0
- package/bin/store-worker-host.mjs +1 -1
- package/bin/store.mjs +1 -1
- package/browser.d.ts +110 -507
- package/browser.js +151 -2257
- package/docs/fuseki-replacement.md +108 -0
- package/fn.d.ts +24 -95
- package/fn.js +104 -150
- package/index.d.ts +598 -259
- package/index.js +4 -234
- package/index.mjs +176 -84
- package/l4-assets/l4factoidal.js +47 -7
- package/l4-assets/l4factoidal.mjs +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +11 -7
- package/lib/api.mjs +2300 -0
- package/lib/browser-engine.mjs +69 -0
- package/lib/dataset.mjs +377 -0
- package/lib/delta-log-hosts.mjs +191 -0
- package/lib/lean-entry.mjs +204 -0
- package/lib/pure.mjs +98 -0
- package/lib/store.mjs +115 -0
- package/lib/version.mjs +5 -0
- package/lws/README.md +97 -0
- package/lws/server.mjs +331 -0
- package/package.json +59 -61
- package/rdfjs.js +5 -536
- package/rdfjs.mjs +594 -0
- package/sample-store.d.ts +8 -16
- package/sample-store.mjs +39 -29
- package/solid/client/README.md +100 -0
- package/solid/client/index.mjs +277 -0
- package/solid/server/README.md +96 -0
- package/solid/server/index.mjs +317 -0
- package/store-host/deno.mjs +8 -0
- package/store-host/index.mjs +12 -0
- package/store-host/node.mjs +8 -0
- package/xmpp/README.md +107 -0
- package/xmpp/index.mjs +229 -0
- package/browser-wasm.js +0 -872
- package/factoidal-npm-entry.js +0 -15477
- package/factoidal-npm-entry.wasm.assets/code-5a7fc68f2ab1323718b8.wasm +0 -0
- package/factoidal-npm-entry.wasm.js +0 -455
- package/factoidal.js +0 -13118
- package/factoidal.wasm.assets/code-bbe6099bfb5b10c4c3ab.wasm +0 -0
- package/factoidal.wasm.js +0 -457
- package/hacl-init.js +0 -92
- package/hacl-wasm/FStar.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum25519_51.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum_Base.wasm +0 -0
- package/hacl-wasm/Hacl_Curve25519_51.wasm +0 -0
- package/hacl-wasm/Hacl_Ed25519.wasm +0 -0
- package/hacl-wasm/Hacl_Ed25519_PrecompTable.wasm +0 -0
- package/hacl-wasm/Hacl_Hash_Base.wasm +0 -0
- package/hacl-wasm/Hacl_Hash_SHA2.wasm +0 -0
- package/hacl-wasm/Hacl_IntTypes_Intrinsics.wasm +0 -0
- package/hacl-wasm/LowStar_Endianness.wasm +0 -0
- package/hacl-wasm/WasmSupport.wasm +0 -0
- package/hacl-wasm/api.js +0 -775
- package/hacl-wasm/api.json +0 -3787
- package/hacl-wasm/layouts.json +0 -1
- package/hacl-wasm/loader.js +0 -568
- package/hacl-wasm/shell.js +0 -12
- package/l4-core.d.ts +0 -159
- package/l4-core.js +0 -188
- package/l4.d.ts +0 -50
- package/l4.js +0 -104
- package/lib/api.js +0 -2300
- package/lib/engine-js.js +0 -165
- package/lib/engine-wasm.js +0 -300
- package/sample-store/CURRENT +0 -1
- package/sample-store/gen-1/manifest.sbm2 +0 -0
- package/sample-store/gen-1/manifest.tsv +0 -14
- package/sample-store/gen-1/predicate-0.ibk3 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-0.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-0.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-0.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.oli2.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-1.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-10.ibk3 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-10.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.oli2.merkle +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-10.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-11.ibk3 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-11.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-11.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-2.ibk3 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-2.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-2.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.sri2.merkle +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-3.ibk3 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-3.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-3.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-3.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-4.ibk3 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-4.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-4.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.tli1.merkle +0 -2
- package/sample-store/gen-1/predicate-5.ibk3 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-5.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-5.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-5.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-7.ibk3 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.oli2.merkle +0 -2
- package/sample-store/gen-1/predicate-7.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-7.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-9.ibk3 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-9.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-9.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.sri2.merkle +0 -3
- package/sample-store/gen-1/predicate-9.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.tli1.merkle +0 -2
- package/select.d.ts +0 -116
- package/select.js +0 -492
- package/version.json +0 -102
- package/wasm.d.ts +0 -130
- package/wasm.js +0 -158
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// The entry object over a loaded Lean engine handle.
|
|
2
|
+
//
|
|
3
|
+
// A Lean engine handle is what `loadL4()` of l4-assets/l4factoidal.js
|
|
4
|
+
// answers: `call(op, args)`, `callBlob(op, args, bytes)`,
|
|
5
|
+
// `callBlobIO(op, args, bytes)` over the dispatch ABI of
|
|
6
|
+
// formal/lean4/Wasm/Dispatch.lean. Those methods parse the envelope and
|
|
7
|
+
// THROW on {"ok":false,...}. lib/api.mjs wants the other shape: one method
|
|
8
|
+
// per op, string arguments, the envelope as a JSON string, errors as
|
|
9
|
+
// {"ok":false} envelopes. entryFromEngine() builds that object.
|
|
10
|
+
//
|
|
11
|
+
// Which ops exist is the engine's own answer: the `ops` reflection op
|
|
12
|
+
// lists them, and the entry object has a method for each name in that list
|
|
13
|
+
// and for no other. capabilities() in lib/api.mjs probes the methods, so a
|
|
14
|
+
// wasm module older than this package reports what it serves.
|
|
15
|
+
//
|
|
16
|
+
// Four ops need more than strings in and an envelope out, and get their
|
|
17
|
+
// own adapters below:
|
|
18
|
+
// - queryHdt and storeOpen read a byte region (the dispatch ABI's blob);
|
|
19
|
+
// - registerExtensionFunction / unregisterExtensionFunction /
|
|
20
|
+
// clearExtensionFunctions install the JavaScript call-out of SPARQL 1.1
|
|
21
|
+
// section 17.6 (the ops extRegister, extUnregister, extClear).
|
|
22
|
+
// None of them makes a semantic decision: which IRIs reach the host, what
|
|
23
|
+
// an absent answer means, which artifact a query reads, are decided in Lean.
|
|
24
|
+
|
|
25
|
+
const XSD = 'http://www.w3.org/2001/XMLSchema#';
|
|
26
|
+
|
|
27
|
+
// The two npm entries and the engine each loads (issue 713 stage 4). The
|
|
28
|
+
// core engine's `ops` reflection lists what only the full engine serves
|
|
29
|
+
// (`fullOnlyOps`, `fullOnlyFormats`); a call to one of those names the
|
|
30
|
+
// entry that has it.
|
|
31
|
+
export const FULL_ENTRY = '@factoidal/core';
|
|
32
|
+
// The core profile is built for the hub pages only; the package ships the
|
|
33
|
+
// full engine (owner, 2026-10-20).
|
|
34
|
+
export const CORE_ENTRY = 'l4factoidal-core.js (the hub build of the core profile)';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The message of a refusal by the core engine of something only the full
|
|
38
|
+
* engine serves.
|
|
39
|
+
* @param {string} what the function, op or format refused
|
|
40
|
+
*/
|
|
41
|
+
export function fullEngineMessage(what) {
|
|
42
|
+
return `${what} is not in the core engine (${CORE_ENTRY}, ` +
|
|
43
|
+
`l4factoidal-core.wasm); import '${FULL_ENTRY}', whose full engine ` +
|
|
44
|
+
'(l4factoidal.wasm) serves it';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The ops whose second argument is a format tag of a document they read.
|
|
48
|
+
const DOCUMENT_FORMAT_OPS = new Set(['parseToDatasetJson', 'datasetOpen', 'parseDocument']);
|
|
49
|
+
const EXT_PENDING_MARKER = '__FACTOIDAL_EXT_PENDING__';
|
|
50
|
+
|
|
51
|
+
function okEnvelope(extra) {
|
|
52
|
+
return JSON.stringify({ ok: true, ...(extra || {}) });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// An engine refusal arrives as an Error whose `.envelope` is the whole
|
|
56
|
+
// {"ok":false,...} document (the loader keeps it, so a parse failure's
|
|
57
|
+
// line, column, offset and format reach lib/api.mjs's ParseError). The
|
|
58
|
+
// message stays the loader's own, "l4factoidal: <error>".
|
|
59
|
+
function failEnvelope(err) {
|
|
60
|
+
const env = err && err.envelope;
|
|
61
|
+
if (env && typeof env === 'object' && env.ok === false) {
|
|
62
|
+
return JSON.stringify({ ...env, error: String(err.message) });
|
|
63
|
+
}
|
|
64
|
+
return JSON.stringify({ ok: false, error: String((err && err.message) || err) });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// One SPARQL Results JSON term object per answer, the wire form
|
|
68
|
+
// Wasm/ExtHost.lean reads; '' means "no value" (the section 17.6 error).
|
|
69
|
+
function encodeExtAnswer(out) {
|
|
70
|
+
if (out === null || out === undefined || out === EXT_PENDING_MARKER) return '';
|
|
71
|
+
if (typeof out === 'boolean') {
|
|
72
|
+
return JSON.stringify({ type: 'literal', value: out ? 'true' : 'false', datatype: XSD + 'boolean' });
|
|
73
|
+
}
|
|
74
|
+
if (typeof out === 'number') {
|
|
75
|
+
if (!Number.isFinite(out)) return '';
|
|
76
|
+
return Number.isInteger(out)
|
|
77
|
+
? JSON.stringify({ type: 'literal', value: String(out), datatype: XSD + 'integer' })
|
|
78
|
+
: JSON.stringify({ type: 'literal', value: String(out), datatype: XSD + 'double' });
|
|
79
|
+
}
|
|
80
|
+
if (typeof out === 'string') return JSON.stringify({ type: 'literal', value: out });
|
|
81
|
+
if (typeof out === 'object' && typeof out.type === 'string') return JSON.stringify(out);
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The Lean module asks its host through ONE global,
|
|
86
|
+
// `globalThis.__factoidalExtCall(iri, argsJson)` (Wasm/ExtHost.lean). The
|
|
87
|
+
// bridges lib/api.mjs registers are keyed by IRI here.
|
|
88
|
+
function installExtensionBridge(eng, entry) {
|
|
89
|
+
const bridges = new Map();
|
|
90
|
+
const hostCall = (iri, argsJson) => {
|
|
91
|
+
const bridge = bridges.get(String(iri));
|
|
92
|
+
if (!bridge) return '';
|
|
93
|
+
try {
|
|
94
|
+
return encodeExtAnswer(bridge(String(iri), String(argsJson)));
|
|
95
|
+
} catch (_err) {
|
|
96
|
+
return '';
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
entry.registerExtensionFunction = (iri, bridge) => {
|
|
100
|
+
try {
|
|
101
|
+
eng.call('extRegister', [String(iri)]);
|
|
102
|
+
bridges.set(String(iri), bridge);
|
|
103
|
+
globalThis.__factoidalExtCall = hostCall;
|
|
104
|
+
return okEnvelope();
|
|
105
|
+
} catch (err) {
|
|
106
|
+
return failEnvelope(err);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
entry.unregisterExtensionFunction = (iri) => {
|
|
110
|
+
try {
|
|
111
|
+
eng.call('extUnregister', [String(iri)]);
|
|
112
|
+
bridges.delete(String(iri));
|
|
113
|
+
return okEnvelope();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return failEnvelope(err);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
entry.clearExtensionFunctions = () => {
|
|
119
|
+
try {
|
|
120
|
+
eng.call('extClear', []);
|
|
121
|
+
bridges.clear();
|
|
122
|
+
return okEnvelope();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return failEnvelope(err);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Build the entry object lib/api.mjs drives, from a loaded engine handle.
|
|
131
|
+
*
|
|
132
|
+
* @param {{call: Function, callBlob?: Function, callBlobIO?: Function}} eng
|
|
133
|
+
* @returns {Record<string, Function> & {abiVersion: string, engine: object}}
|
|
134
|
+
*/
|
|
135
|
+
export function entryFromEngine(eng) {
|
|
136
|
+
if (!eng || typeof eng.call !== 'function') {
|
|
137
|
+
throw new TypeError(
|
|
138
|
+
'factoidal: expected a Lean engine handle with a call(op, args) method ' +
|
|
139
|
+
'(the value loadL4() of l4-assets/l4factoidal.js resolves to)');
|
|
140
|
+
}
|
|
141
|
+
const reflection = eng.call('ops', []);
|
|
142
|
+
// `engine` is "core" for l4factoidal-core.wasm and "full" for
|
|
143
|
+
// l4factoidal.wasm; a module older than issue 713 stage 4 has no member and
|
|
144
|
+
// is the full engine.
|
|
145
|
+
const profile = reflection.engine === 'core' ? 'core' : 'full';
|
|
146
|
+
const entry = {
|
|
147
|
+
abiVersion: String(reflection.abiVersion),
|
|
148
|
+
engine: eng,
|
|
149
|
+
profile,
|
|
150
|
+
fullOnlyOps: Array.isArray(reflection.fullOnlyOps) ? reflection.fullOnlyOps.map(String) : [],
|
|
151
|
+
fullOnlyFormats: Array.isArray(reflection.fullOnlyFormats)
|
|
152
|
+
? reflection.fullOnlyFormats.map((f) => String(f).toLowerCase()) : [],
|
|
153
|
+
};
|
|
154
|
+
const ops = Array.isArray(reflection.ops) ? reflection.ops : [];
|
|
155
|
+
for (const op of ops) {
|
|
156
|
+
if (op === 'ops') continue;
|
|
157
|
+
const formatChecked = DOCUMENT_FORMAT_OPS.has(op) && entry.fullOnlyFormats.length > 0;
|
|
158
|
+
entry[op] = (...args) => {
|
|
159
|
+
if (formatChecked && entry.fullOnlyFormats.includes(String(args[1] ?? '').toLowerCase())) {
|
|
160
|
+
return JSON.stringify({
|
|
161
|
+
ok: false, servedBy: 'full',
|
|
162
|
+
error: fullEngineMessage(`the format '${args[1]}'`),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
return JSON.stringify(eng.call(op, args.map(String)));
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return failEnvelope(err);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (ops.includes('extRegister')) installExtensionBridge(eng, entry);
|
|
173
|
+
// The byte-region ops. `bytes` is a Uint8Array; it crosses into the wasm
|
|
174
|
+
// heap with no encoding.
|
|
175
|
+
// queryHdt goes through the IO form of the blob entry when the engine has it:
|
|
176
|
+
// that entry reads the system clock once for the query, so NOW() (SPARQL 1.1
|
|
177
|
+
// section 17.4.5.1) is bound. The pure `callBlob` has no clock. An engine
|
|
178
|
+
// without `callBlobIO` serves the op through `callBlob`, as before
|
|
179
|
+
// (https://github.com/danbri/factoidal/issues/743).
|
|
180
|
+
if ((typeof eng.callBlobIO === 'function' || typeof eng.callBlob === 'function') &&
|
|
181
|
+
ops.includes('queryHdt')) {
|
|
182
|
+
entry.queryHdt = (bytes, sparql) => {
|
|
183
|
+
try {
|
|
184
|
+
const args = [String(sparql)];
|
|
185
|
+
return JSON.stringify(typeof eng.callBlobIO === 'function'
|
|
186
|
+
? eng.callBlobIO('queryHdt', args, bytes).envelope
|
|
187
|
+
: eng.callBlob('queryHdt', args, bytes));
|
|
188
|
+
} catch (err) {
|
|
189
|
+
return failEnvelope(err);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (typeof eng.callBlobIO === 'function' && ops.includes('storeOpen')) {
|
|
194
|
+
entry.storeOpen = (manifestHex, artifactsJson, bytes) => {
|
|
195
|
+
try {
|
|
196
|
+
return JSON.stringify(
|
|
197
|
+
eng.callBlobIO('storeOpen', [String(manifestHex), String(artifactsJson)], bytes).envelope);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return failEnvelope(err);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return entry;
|
|
204
|
+
}
|
package/lib/pure.mjs
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Pure functions over engine ops that had no JavaScript function before
|
|
2
|
+
// 0.10.0. Each one takes values and returns a value: no handle, no hidden
|
|
3
|
+
// state. Every decision is made by the Lean op it calls
|
|
4
|
+
// (formal/lean4/Wasm/Ops/Crypto.lean, Wasm/Ops/Proof.lean,
|
|
5
|
+
// Wasm/CoreDispatch.lean). This file converts bytes to hex and back.
|
|
6
|
+
// No Node built-ins: the browser imports it as it is.
|
|
7
|
+
|
|
8
|
+
function hexOf(bytes) {
|
|
9
|
+
if (!(bytes instanceof Uint8Array)) throw new TypeError('expected a Uint8Array');
|
|
10
|
+
let s = '';
|
|
11
|
+
for (const b of bytes) s += b.toString(16).padStart(2, '0');
|
|
12
|
+
return s;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function bytesOf(hex) {
|
|
16
|
+
const out = new Uint8Array(hex.length / 2);
|
|
17
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.substr(2 * i, 2), 16);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function optHex(v) {
|
|
22
|
+
return v === undefined ? undefined : hexOf(v);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function str(name, v) {
|
|
26
|
+
if (typeof v !== 'string') throw new TypeError(`${name}: argument must be a string`);
|
|
27
|
+
return v;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {() => Promise<object>} loadEngine resolves to an engine handle
|
|
32
|
+
* with `call(op, args)` (bin/engine.mjs, lib/browser-engine.mjs).
|
|
33
|
+
*/
|
|
34
|
+
export function buildPure(loadEngine) {
|
|
35
|
+
const call = async (op, args) => (await loadEngine()).call(op, args);
|
|
36
|
+
|
|
37
|
+
/** ChaCha20-Poly1305 seal (RFC 8439). key 32 bytes, nonce 12 bytes. */
|
|
38
|
+
async function aeadSeal({ key, nonce, plaintext, aad }) {
|
|
39
|
+
const r = await call('aeadSeal', [JSON.stringify({
|
|
40
|
+
alg: 'chacha20-poly1305', key: hexOf(key), nonce: hexOf(nonce),
|
|
41
|
+
aad: optHex(aad), plaintext: optHex(plaintext),
|
|
42
|
+
})]);
|
|
43
|
+
return bytesOf(r.ciphertext);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** ChaCha20-Poly1305 open. Rejects when the tag does not authenticate. */
|
|
47
|
+
async function aeadOpen({ key, nonce, ciphertext, aad }) {
|
|
48
|
+
const r = await call('aeadOpen', [JSON.stringify({
|
|
49
|
+
alg: 'chacha20-poly1305', key: hexOf(key), nonce: hexOf(nonce),
|
|
50
|
+
aad: optHex(aad), ciphertext: hexOf(ciphertext),
|
|
51
|
+
})]);
|
|
52
|
+
return bytesOf(r.plaintext);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** HPKE base mode seal, RFC 9180 DHKEM(X25519)/HKDF-SHA256/ChaCha20-Poly1305. */
|
|
56
|
+
async function hpkeSeal({ skE, pkR, plaintext, info, aad }) {
|
|
57
|
+
const r = await call('hpkeSeal', [JSON.stringify({
|
|
58
|
+
skE: hexOf(skE), pkR: hexOf(pkR), info: optHex(info),
|
|
59
|
+
aad: optHex(aad), plaintext: optHex(plaintext),
|
|
60
|
+
})]);
|
|
61
|
+
return { message: bytesOf(r.message), enc: bytesOf(r.enc), ciphertext: bytesOf(r.ciphertext) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** HPKE base mode open over the `message` that hpkeSeal returned. */
|
|
65
|
+
async function hpkeOpen({ skR, message, info, aad }) {
|
|
66
|
+
const r = await call('hpkeOpen', [JSON.stringify({
|
|
67
|
+
skR: hexOf(skR), info: optHex(info), aad: optHex(aad), message: hexOf(message),
|
|
68
|
+
})]);
|
|
69
|
+
return bytesOf(r.plaintext);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Check an FPP0 proof bundle (JSON text). Returns the engine's report. */
|
|
73
|
+
async function proofCheck(bundleJson) {
|
|
74
|
+
const { ok, ...rest } = await call('proofCheck', [str('proofCheck', bundleJson)]);
|
|
75
|
+
return rest;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Describe the shape of an FPP0 proof bundle without checking it. */
|
|
79
|
+
async function proofInspect(bundleJson) {
|
|
80
|
+
const { ok, ...rest } = await call('proofInspect', [str('proofInspect', bundleJson)]);
|
|
81
|
+
return rest;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** RDFC-1.0 over N-Quads text, N-Quads text out. */
|
|
85
|
+
async function canonicalizeNQuads(nquads) {
|
|
86
|
+
return (await call('canonicalizeToNQuads', [str('canonicalizeNQuads', nquads)])).nquads;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Parse N-Quads (or N-Triples) text and write it back as N-Quads. */
|
|
90
|
+
async function normalizeNQuads(nquads) {
|
|
91
|
+
return (await call('serializeNQuads', [str('normalizeNQuads', nquads)])).nquads;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
aeadSeal, aeadOpen, hpkeSeal, hpkeOpen,
|
|
96
|
+
proofCheck, proofInspect, canonicalizeNQuads, normalizeNQuads,
|
|
97
|
+
};
|
|
98
|
+
}
|
package/lib/store.mjs
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Host plumbing for the Lean persisted store (Shardborough IBK3/IBK4/IBK5
|
|
2
|
+
// generations), shared by Node and the browser.
|
|
3
|
+
//
|
|
4
|
+
// A store is read through a READER: a function from a path relative to the
|
|
5
|
+
// collection root ('CURRENT', 'gen-1/manifest.sbm2',
|
|
6
|
+
// 'gen-1/predicate-0.ibk3', ...) to the bytes of that file (a Uint8Array or
|
|
7
|
+
// an ArrayBuffer, or a Promise of either). index.mjs builds one over the
|
|
8
|
+
// file system; a browser page builds one over fetch().
|
|
9
|
+
//
|
|
10
|
+
// WHAT THIS FILE IS ALLOWED TO DO
|
|
11
|
+
// Read CURRENT, read a manifest by name, read the artifact files the
|
|
12
|
+
// engine named, concatenate their bytes and say where each one starts.
|
|
13
|
+
// It never parses a manifest, verifies a digest, decodes a block or
|
|
14
|
+
// chooses an artifact: the ops storeManifestInspect, storeQueryPlan and
|
|
15
|
+
// storeOpen (formal/lean4/Wasm/Ops/Store.lean, StoreHandles.lean) decide
|
|
16
|
+
// all of that. bin/store.mjs is the same plumbing over store-host/ for the
|
|
17
|
+
// `factoidal` command.
|
|
18
|
+
|
|
19
|
+
import { requireChildName } from '../store-host/paths.mjs';
|
|
20
|
+
|
|
21
|
+
/** The manifest file names a generation directory can carry, in the order
|
|
22
|
+
* the native tools try them. */
|
|
23
|
+
export const MANIFEST_NAMES = ['manifest.sbm2', 'manifest.sbm1'];
|
|
24
|
+
|
|
25
|
+
async function readBytes(reader, path) {
|
|
26
|
+
const out = await reader(path);
|
|
27
|
+
if (out instanceof Uint8Array) return out;
|
|
28
|
+
if (out instanceof ArrayBuffer) return new Uint8Array(out);
|
|
29
|
+
if (ArrayBuffer.isView(out)) return new Uint8Array(out.buffer, out.byteOffset, out.byteLength);
|
|
30
|
+
throw new TypeError(
|
|
31
|
+
`store reader: '${path}' answered ${out === null ? 'null' : typeof out}, ` +
|
|
32
|
+
'expected a Uint8Array or an ArrayBuffer');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The generation to open and its manifest bytes. With no `generation`,
|
|
37
|
+
* CURRENT names it (an activated collection); with one, that generation is
|
|
38
|
+
* opened directly, activated or not.
|
|
39
|
+
*/
|
|
40
|
+
export async function openGeneration(reader, generation) {
|
|
41
|
+
let name = generation;
|
|
42
|
+
if (name == null) {
|
|
43
|
+
let pointer;
|
|
44
|
+
try {
|
|
45
|
+
pointer = await readBytes(reader, 'CURRENT');
|
|
46
|
+
} catch (cause) {
|
|
47
|
+
const err = new Error(
|
|
48
|
+
`openStore: the collection root has no readable CURRENT pointer (${cause && cause.message})`);
|
|
49
|
+
err.cause = cause;
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
// CURRENT holds a UTF-8 generation name (docs/shardborough-storage-spec.md
|
|
53
|
+
// section 6.4); trailing ASCII whitespace is tolerated, as in
|
|
54
|
+
// store-host/index.mjs.
|
|
55
|
+
name = new TextDecoder('utf-8', { fatal: true }).decode(pointer).replace(/[\r\n\t ]+$/, '');
|
|
56
|
+
requireChildName(name, 'CURRENT');
|
|
57
|
+
} else {
|
|
58
|
+
requireChildName(String(name), 'generation');
|
|
59
|
+
name = String(name);
|
|
60
|
+
}
|
|
61
|
+
for (const manifestName of MANIFEST_NAMES) {
|
|
62
|
+
let manifest;
|
|
63
|
+
try {
|
|
64
|
+
manifest = await readBytes(reader, `${name}/${manifestName}`);
|
|
65
|
+
} catch (_err) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
return { generation: name, manifestName, manifest, activated: generation == null };
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`openStore: generation '${name}' has none of ${MANIFEST_NAMES.join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Read the named artifacts of one generation into one region, with a
|
|
74
|
+
* {key, offset, len} window per artifact. */
|
|
75
|
+
export async function readRegion(reader, generation, keys) {
|
|
76
|
+
const chunks = [];
|
|
77
|
+
for (const key of keys) chunks.push(await readBytes(reader, `${generation}/${key}`));
|
|
78
|
+
let total = 0;
|
|
79
|
+
for (const c of chunks) total += c.length;
|
|
80
|
+
const blob = new Uint8Array(total);
|
|
81
|
+
const artifacts = [];
|
|
82
|
+
let offset = 0;
|
|
83
|
+
for (let i = 0; i < chunks.length; i += 1) {
|
|
84
|
+
blob.set(chunks[i], offset);
|
|
85
|
+
artifacts.push({ key: keys[i], offset, len: chunks[i].length });
|
|
86
|
+
offset += chunks[i].length;
|
|
87
|
+
}
|
|
88
|
+
return { blob, artifacts, total };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The artifact keys a manifest declares, as storeManifestInspect lists
|
|
92
|
+
* them: every block, every index sidecar, every out-of-line literal. */
|
|
93
|
+
export function manifestKeys(inspectEnvelope) {
|
|
94
|
+
const keys = [];
|
|
95
|
+
for (const entry of inspectEnvelope.entries || []) {
|
|
96
|
+
keys.push(entry.key);
|
|
97
|
+
for (const key of Object.values(entry.sidecars || {})) keys.push(key);
|
|
98
|
+
}
|
|
99
|
+
for (const blob of inspectEnvelope.blobs || []) keys.push(blob.key);
|
|
100
|
+
return keys;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const HEX = (() => {
|
|
104
|
+
const t = new Array(256);
|
|
105
|
+
for (let b = 0; b < 256; b += 1) t[b] = b.toString(16).padStart(2, '0');
|
|
106
|
+
return t;
|
|
107
|
+
})();
|
|
108
|
+
|
|
109
|
+
/** Lowercase hexadecimal: the transport the manifest argument of the store
|
|
110
|
+
* ops takes. Artifact bytes never take this path. */
|
|
111
|
+
export function hexOfBytes(bytes) {
|
|
112
|
+
const parts = new Array(bytes.length);
|
|
113
|
+
for (let i = 0; i < bytes.length; i += 1) parts[i] = HEX[bytes[i]];
|
|
114
|
+
return parts.join('');
|
|
115
|
+
}
|
package/lib/version.mjs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The package version, as a module the browser can import (a JSON import
|
|
2
|
+
// needs import attributes, which not every runtime this package targets
|
|
3
|
+
// reads). test/dts-drift.test.js checks that it equals package.json's
|
|
4
|
+
// `version`.
|
|
5
|
+
export const version = '0.10.0';
|
package/lws/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# `@factoidal/core/lws` — a Node host for Linked Web Storage 1.0
|
|
2
|
+
|
|
3
|
+
The W3C Linked Web Storage Protocol 1.0 core
|
|
4
|
+
(<https://w3c.github.io/lws-protocol/lws10-core/>, Linked Web Storage
|
|
5
|
+
Working Group editor's draft) served over Node's `http` module.
|
|
6
|
+
|
|
7
|
+
Tracking issue: <https://github.com/danbri/factoidal/issues/659>.
|
|
8
|
+
Design record:
|
|
9
|
+
[`docs/designissues/2026-09-06-lws-and-solid-protocols.md`](../../../docs/designissues/2026-09-06-lws-and-solid-protocols.md).
|
|
10
|
+
Conformance ledger:
|
|
11
|
+
[`docs/lws-solid-conformance.md`](../../../docs/lws-solid-conformance.md).
|
|
12
|
+
|
|
13
|
+
## What is here and what is not
|
|
14
|
+
|
|
15
|
+
`server.mjs` is a socket, a body reader and a header writer. Every
|
|
16
|
+
protocol decision — status codes, `Link` relations, `Last-Modified`,
|
|
17
|
+
containment, the blank-node refusal in PATCH — is made by
|
|
18
|
+
`formal/lean4/L4Factoidal/LWS/` and reaches this file only as a
|
|
19
|
+
`{status, headers, body}` record. Iron rule 7 of `CLAUDE.md`: no
|
|
20
|
+
protocol logic in a host.
|
|
21
|
+
|
|
22
|
+
The one status code this file writes itself is `500`, and only when the
|
|
23
|
+
host or the engine failed rather than answered.
|
|
24
|
+
|
|
25
|
+
## The wasm contract
|
|
26
|
+
|
|
27
|
+
| op | arguments | answer |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `lwsOpen` | `[configJson]` | `{ ok, handle, root }` |
|
|
30
|
+
| `lwsStep` | `[handle, requestJson]` | `{ ok, response }` |
|
|
31
|
+
| `lwsClose` | `[handle]` | `{ ok }` |
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
request { method, target, headers: [[name, value]], body }
|
|
35
|
+
response { status, headers: [[name, value]], body }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`headers` is a list of pairs, not an object: HTTP allows a field name to
|
|
39
|
+
repeat and the order of repeated fields carries meaning. `target` is a
|
|
40
|
+
path within the storage, so Node's `request.url` is passed through
|
|
41
|
+
unchanged and this host composes no IRI.
|
|
42
|
+
|
|
43
|
+
`configJson` members, all optional: `baseIri` (the absolute IRI the
|
|
44
|
+
storage root maps to), `now` (the engine clock in seconds, which advances
|
|
45
|
+
by one second per mutation so a write sequence has increasing
|
|
46
|
+
`Last-Modified` values with no real clock), `owner`, `agent`. A request
|
|
47
|
+
may carry `now` and `agent` of its own to override the handle's for one
|
|
48
|
+
step; `listen({realClock: true})` is how a server with a real clock feeds
|
|
49
|
+
one in, and a test leaves it off to keep the engine's deterministic
|
|
50
|
+
clock. The full contract is `docs/lws-solid-conformance.md`, section
|
|
51
|
+
"wasm ABI".
|
|
52
|
+
|
|
53
|
+
A WebAssembly module built before these ops landed answers `unknown op`.
|
|
54
|
+
`lwsOpsAvailable(engine)` reports that as `{available: false, reason}`,
|
|
55
|
+
and every test in `tests/lws/` skips with the reason printed rather than
|
|
56
|
+
passing on an engine that cannot answer.
|
|
57
|
+
|
|
58
|
+
## Use
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import { listen } from '@factoidal/core/lws'
|
|
62
|
+
|
|
63
|
+
const running = await listen({ port: 3000, baseIri: 'https://storage.example/' })
|
|
64
|
+
console.log(running.origin)
|
|
65
|
+
await running.close()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
From the command:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
factoidal lws-serve /var/lib/lws --port 3000
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`--port 0` takes an ephemeral port and the command prints the one it got;
|
|
75
|
+
`--base IRI` sets `baseIri`, which defaults to the bound origin. `DIR`
|
|
76
|
+
names the storage directory, but the first slice keeps the resource tree
|
|
77
|
+
in the engine handle, so nothing is read from it or written to it yet
|
|
78
|
+
(design record, section 2).
|
|
79
|
+
|
|
80
|
+
## Exports
|
|
81
|
+
|
|
82
|
+
| name | what it is |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `listen(options)` | start a server, resolve when it is listening |
|
|
85
|
+
| `createLwsServer(options)` | build the server without listening |
|
|
86
|
+
| `openLwsStorage(options)` | open a handle and get `step` / `close` |
|
|
87
|
+
| `createLwsServer(...).setBaseIri(iri)` | set `baseIri` before the first request |
|
|
88
|
+
| `lwsOpsAvailable(engine)` | probe the loaded module for the ops |
|
|
89
|
+
| `LWS_OPS` | the three op names |
|
|
90
|
+
| `LwsHostError` | a host failure, with `unknownOp` set when the op is absent |
|
|
91
|
+
|
|
92
|
+
## Tests
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
node tests/lws/server.mjs
|
|
96
|
+
deno run --allow-read --allow-net --allow-write --allow-env tests/lws/server.mjs
|
|
97
|
+
```
|