@factoidal/core 0.7.1 → 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.
Files changed (193) hide show
  1. package/CHANGELOG.md +503 -1
  2. package/NOTICE +2 -21
  3. package/README.md +117 -993
  4. package/api.d.ts +53 -0
  5. package/api.js +5 -0
  6. package/api.mjs +56 -0
  7. package/bin/engine.mjs +38 -19
  8. package/bin/factoidal.mjs +512 -66
  9. package/bin/pack.mjs +37 -0
  10. package/bin/serve-worker.mjs +32 -0
  11. package/bin/serve.mjs +423 -0
  12. package/bin/store-worker-host.mjs +1 -1
  13. package/bin/store.mjs +1 -1
  14. package/browser.d.ts +110 -507
  15. package/browser.js +151 -2257
  16. package/docs/fuseki-replacement.md +108 -0
  17. package/fn.d.ts +24 -95
  18. package/fn.js +104 -150
  19. package/index.d.ts +598 -259
  20. package/index.js +4 -234
  21. package/index.mjs +176 -84
  22. package/l4-assets/l4factoidal.js +47 -7
  23. package/l4-assets/l4factoidal.mjs +1 -1
  24. package/l4-assets/l4factoidal.wasm +0 -0
  25. package/l4-assets/version.json +11 -7
  26. package/lib/api.mjs +2300 -0
  27. package/lib/browser-engine.mjs +69 -0
  28. package/lib/dataset.mjs +377 -0
  29. package/lib/delta-log-hosts.mjs +191 -0
  30. package/lib/lean-entry.mjs +204 -0
  31. package/lib/pure.mjs +98 -0
  32. package/lib/store.mjs +115 -0
  33. package/lib/version.mjs +5 -0
  34. package/lws/README.md +97 -0
  35. package/lws/server.mjs +331 -0
  36. package/package.json +59 -61
  37. package/rdfjs.js +5 -536
  38. package/rdfjs.mjs +594 -0
  39. package/sample-store.d.ts +8 -16
  40. package/sample-store.mjs +39 -29
  41. package/solid/client/README.md +100 -0
  42. package/solid/client/index.mjs +277 -0
  43. package/solid/server/README.md +96 -0
  44. package/solid/server/index.mjs +317 -0
  45. package/store-host/deno.mjs +8 -0
  46. package/store-host/index.mjs +12 -0
  47. package/store-host/node.mjs +8 -0
  48. package/xmpp/README.md +107 -0
  49. package/xmpp/index.mjs +229 -0
  50. package/browser-wasm.js +0 -872
  51. package/factoidal-npm-entry.js +0 -15477
  52. package/factoidal-npm-entry.wasm.assets/code-5a7fc68f2ab1323718b8.wasm +0 -0
  53. package/factoidal-npm-entry.wasm.js +0 -455
  54. package/factoidal.js +0 -13118
  55. package/factoidal.wasm.assets/code-bbe6099bfb5b10c4c3ab.wasm +0 -0
  56. package/factoidal.wasm.js +0 -457
  57. package/hacl-init.js +0 -92
  58. package/hacl-wasm/FStar.wasm +0 -0
  59. package/hacl-wasm/Hacl_Bignum.wasm +0 -0
  60. package/hacl-wasm/Hacl_Bignum25519_51.wasm +0 -0
  61. package/hacl-wasm/Hacl_Bignum_Base.wasm +0 -0
  62. package/hacl-wasm/Hacl_Curve25519_51.wasm +0 -0
  63. package/hacl-wasm/Hacl_Ed25519.wasm +0 -0
  64. package/hacl-wasm/Hacl_Ed25519_PrecompTable.wasm +0 -0
  65. package/hacl-wasm/Hacl_Hash_Base.wasm +0 -0
  66. package/hacl-wasm/Hacl_Hash_SHA2.wasm +0 -0
  67. package/hacl-wasm/Hacl_IntTypes_Intrinsics.wasm +0 -0
  68. package/hacl-wasm/LowStar_Endianness.wasm +0 -0
  69. package/hacl-wasm/WasmSupport.wasm +0 -0
  70. package/hacl-wasm/api.js +0 -775
  71. package/hacl-wasm/api.json +0 -3787
  72. package/hacl-wasm/layouts.json +0 -1
  73. package/hacl-wasm/loader.js +0 -568
  74. package/hacl-wasm/shell.js +0 -12
  75. package/l4-core.d.ts +0 -159
  76. package/l4-core.js +0 -188
  77. package/l4.d.ts +0 -50
  78. package/l4.js +0 -104
  79. package/lib/api.js +0 -2300
  80. package/lib/engine-js.js +0 -165
  81. package/lib/engine-wasm.js +0 -300
  82. package/sample-store/CURRENT +0 -1
  83. package/sample-store/gen-1/manifest.sbm2 +0 -0
  84. package/sample-store/gen-1/manifest.tsv +0 -14
  85. package/sample-store/gen-1/predicate-0.ibk3 +0 -0
  86. package/sample-store/gen-1/predicate-0.ibk3.merkle +0 -1
  87. package/sample-store/gen-1/predicate-0.ibk3.oli2 +0 -0
  88. package/sample-store/gen-1/predicate-0.ibk3.oli2.merkle +0 -1
  89. package/sample-store/gen-1/predicate-0.ibk3.sri2 +0 -0
  90. package/sample-store/gen-1/predicate-0.ibk3.sri2.merkle +0 -1
  91. package/sample-store/gen-1/predicate-0.ibk3.tli1 +0 -0
  92. package/sample-store/gen-1/predicate-0.ibk3.tli1.merkle +0 -0
  93. package/sample-store/gen-1/predicate-1.ibk3 +0 -0
  94. package/sample-store/gen-1/predicate-1.ibk3.merkle +0 -0
  95. package/sample-store/gen-1/predicate-1.ibk3.oli2 +0 -0
  96. package/sample-store/gen-1/predicate-1.ibk3.oli2.merkle +0 -0
  97. package/sample-store/gen-1/predicate-1.ibk3.sri2 +0 -0
  98. package/sample-store/gen-1/predicate-1.ibk3.sri2.merkle +0 -1
  99. package/sample-store/gen-1/predicate-1.ibk3.tli1 +0 -0
  100. package/sample-store/gen-1/predicate-1.ibk3.tli1.merkle +0 -1
  101. package/sample-store/gen-1/predicate-10.ibk3 +0 -0
  102. package/sample-store/gen-1/predicate-10.ibk3.merkle +0 -1
  103. package/sample-store/gen-1/predicate-10.ibk3.oli2 +0 -0
  104. package/sample-store/gen-1/predicate-10.ibk3.oli2.merkle +0 -0
  105. package/sample-store/gen-1/predicate-10.ibk3.sri2 +0 -0
  106. package/sample-store/gen-1/predicate-10.ibk3.sri2.merkle +0 -1
  107. package/sample-store/gen-1/predicate-10.ibk3.tli1 +0 -0
  108. package/sample-store/gen-1/predicate-10.ibk3.tli1.merkle +0 -1
  109. package/sample-store/gen-1/predicate-11.ibk3 +0 -0
  110. package/sample-store/gen-1/predicate-11.ibk3.merkle +0 -0
  111. package/sample-store/gen-1/predicate-11.ibk3.oli2 +0 -0
  112. package/sample-store/gen-1/predicate-11.ibk3.oli2.merkle +0 -1
  113. package/sample-store/gen-1/predicate-11.ibk3.sri2 +0 -0
  114. package/sample-store/gen-1/predicate-11.ibk3.sri2.merkle +0 -1
  115. package/sample-store/gen-1/predicate-11.ibk3.tli1 +0 -0
  116. package/sample-store/gen-1/predicate-11.ibk3.tli1.merkle +0 -1
  117. package/sample-store/gen-1/predicate-12.ibk3 +0 -0
  118. package/sample-store/gen-1/predicate-12.ibk3.merkle +0 -1
  119. package/sample-store/gen-1/predicate-12.ibk3.oli2 +0 -0
  120. package/sample-store/gen-1/predicate-12.ibk3.oli2.merkle +0 -1
  121. package/sample-store/gen-1/predicate-12.ibk3.sri2 +0 -0
  122. package/sample-store/gen-1/predicate-12.ibk3.sri2.merkle +0 -1
  123. package/sample-store/gen-1/predicate-12.ibk3.tli1 +0 -0
  124. package/sample-store/gen-1/predicate-12.ibk3.tli1.merkle +0 -1
  125. package/sample-store/gen-1/predicate-2.ibk3 +0 -0
  126. package/sample-store/gen-1/predicate-2.ibk3.merkle +0 -1
  127. package/sample-store/gen-1/predicate-2.ibk3.oli2 +0 -0
  128. package/sample-store/gen-1/predicate-2.ibk3.oli2.merkle +0 -1
  129. package/sample-store/gen-1/predicate-2.ibk3.sri2 +0 -0
  130. package/sample-store/gen-1/predicate-2.ibk3.sri2.merkle +0 -0
  131. package/sample-store/gen-1/predicate-2.ibk3.tli1 +0 -0
  132. package/sample-store/gen-1/predicate-2.ibk3.tli1.merkle +0 -0
  133. package/sample-store/gen-1/predicate-3.ibk3 +0 -0
  134. package/sample-store/gen-1/predicate-3.ibk3.merkle +0 -1
  135. package/sample-store/gen-1/predicate-3.ibk3.oli2 +0 -0
  136. package/sample-store/gen-1/predicate-3.ibk3.oli2.merkle +0 -1
  137. package/sample-store/gen-1/predicate-3.ibk3.sri2 +0 -0
  138. package/sample-store/gen-1/predicate-3.ibk3.sri2.merkle +0 -1
  139. package/sample-store/gen-1/predicate-3.ibk3.tli1 +0 -0
  140. package/sample-store/gen-1/predicate-3.ibk3.tli1.merkle +0 -1
  141. package/sample-store/gen-1/predicate-4.ibk3 +0 -0
  142. package/sample-store/gen-1/predicate-4.ibk3.merkle +0 -0
  143. package/sample-store/gen-1/predicate-4.ibk3.oli2 +0 -0
  144. package/sample-store/gen-1/predicate-4.ibk3.oli2.merkle +0 -1
  145. package/sample-store/gen-1/predicate-4.ibk3.sri2 +0 -0
  146. package/sample-store/gen-1/predicate-4.ibk3.sri2.merkle +0 -1
  147. package/sample-store/gen-1/predicate-4.ibk3.tli1 +0 -0
  148. package/sample-store/gen-1/predicate-4.ibk3.tli1.merkle +0 -2
  149. package/sample-store/gen-1/predicate-5.ibk3 +0 -0
  150. package/sample-store/gen-1/predicate-5.ibk3.merkle +0 -1
  151. package/sample-store/gen-1/predicate-5.ibk3.oli2 +0 -0
  152. package/sample-store/gen-1/predicate-5.ibk3.oli2.merkle +0 -1
  153. package/sample-store/gen-1/predicate-5.ibk3.sri2 +0 -0
  154. package/sample-store/gen-1/predicate-5.ibk3.sri2.merkle +0 -1
  155. package/sample-store/gen-1/predicate-5.ibk3.tli1 +0 -0
  156. package/sample-store/gen-1/predicate-5.ibk3.tli1.merkle +0 -1
  157. package/sample-store/gen-1/predicate-6.ibk3 +0 -0
  158. package/sample-store/gen-1/predicate-6.ibk3.merkle +0 -1
  159. package/sample-store/gen-1/predicate-6.ibk3.oli2 +0 -0
  160. package/sample-store/gen-1/predicate-6.ibk3.oli2.merkle +0 -1
  161. package/sample-store/gen-1/predicate-6.ibk3.sri2 +0 -0
  162. package/sample-store/gen-1/predicate-6.ibk3.sri2.merkle +0 -1
  163. package/sample-store/gen-1/predicate-6.ibk3.tli1 +0 -0
  164. package/sample-store/gen-1/predicate-6.ibk3.tli1.merkle +0 -0
  165. package/sample-store/gen-1/predicate-7.ibk3 +0 -0
  166. package/sample-store/gen-1/predicate-7.ibk3.merkle +0 -0
  167. package/sample-store/gen-1/predicate-7.ibk3.oli2 +0 -0
  168. package/sample-store/gen-1/predicate-7.ibk3.oli2.merkle +0 -2
  169. package/sample-store/gen-1/predicate-7.ibk3.sri2 +0 -0
  170. package/sample-store/gen-1/predicate-7.ibk3.sri2.merkle +0 -1
  171. package/sample-store/gen-1/predicate-7.ibk3.tli1 +0 -0
  172. package/sample-store/gen-1/predicate-7.ibk3.tli1.merkle +0 -1
  173. package/sample-store/gen-1/predicate-8.ibk3 +0 -0
  174. package/sample-store/gen-1/predicate-8.ibk3.merkle +0 -1
  175. package/sample-store/gen-1/predicate-8.ibk3.oli2 +0 -0
  176. package/sample-store/gen-1/predicate-8.ibk3.oli2.merkle +0 -1
  177. package/sample-store/gen-1/predicate-8.ibk3.sri2 +0 -0
  178. package/sample-store/gen-1/predicate-8.ibk3.sri2.merkle +0 -1
  179. package/sample-store/gen-1/predicate-8.ibk3.tli1 +0 -0
  180. package/sample-store/gen-1/predicate-8.ibk3.tli1.merkle +0 -1
  181. package/sample-store/gen-1/predicate-9.ibk3 +0 -0
  182. package/sample-store/gen-1/predicate-9.ibk3.merkle +0 -1
  183. package/sample-store/gen-1/predicate-9.ibk3.oli2 +0 -0
  184. package/sample-store/gen-1/predicate-9.ibk3.oli2.merkle +0 -1
  185. package/sample-store/gen-1/predicate-9.ibk3.sri2 +0 -0
  186. package/sample-store/gen-1/predicate-9.ibk3.sri2.merkle +0 -3
  187. package/sample-store/gen-1/predicate-9.ibk3.tli1 +0 -0
  188. package/sample-store/gen-1/predicate-9.ibk3.tli1.merkle +0 -2
  189. package/select.d.ts +0 -116
  190. package/select.js +0 -492
  191. package/version.json +0 -102
  192. package/wasm.d.ts +0 -130
  193. 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
+ }
@@ -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
+ ```