@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
package/sample-store.mjs CHANGED
@@ -1,37 +1,47 @@
1
- // The sample Shardborough store this package carries, so a fresh install
2
- // answers a SPARQL query with no other download and no native binary.
1
+ // @factoidal/core/sample-store - a small store made on demand (Node only).
3
2
  //
4
- // The store was packed by `l4block-shard-pack … ibk3` and activated by
5
- // `l4block-shard-activate`. Nothing here decodes it; this module only
6
- // resolves its path inside the installed package.
7
- //
8
- // CONTENT AND LICENCE. Five IPTC NewsCodes vocabularies, published by the
9
- // IPTC under CC BY 4.0 and taken from danbri/skosdex third_party/skos:
10
- // spamfstat, videoqualifier, subjectqualifier, videocodec and spct.
11
- // 4,434 triples in 13 predicate blocks. See NOTICE.
3
+ // Owner, 2026-10-20, verbatim: "All the CLIs are still bundled i just didn't
4
+ // want any actual demo triples cluttering the file." So the package ships no
5
+ // store. sampleStorePath() packs the few triples below (written for this
6
+ // package, no third-party content) into a Shardborough store with the
7
+ // package's own `factoidal pack` and `factoidal activate`, under DIR or the
8
+ // OS temporary directory, and returns the collection root. A DIR that
9
+ // already holds CURRENT is returned as it is.
12
10
 
13
- import { fileUrlToPath, joinPath } from './store-host/paths.mjs'
11
+ import { execFileSync } from 'node:child_process'
12
+ import { existsSync } from 'node:fs'
13
+ import { join } from 'node:path'
14
+ import { fileURLToPath } from 'node:url'
14
15
 
15
- /** The collection root of the bundled sample store: the directory holding
16
- * CURRENT. Pass it to `factoidal inspect` / `factoidal query`, or to
17
- * `openStore` from `@factoidal/core/store-host`. */
18
- export function sampleStorePath () {
19
- const packageRoot = fileUrlToPath(new URL('.', import.meta.url).href)
20
- return joinPath(packageRoot, 'sample-store')
21
- }
16
+ /** The triples of the sample store, as Turtle. */
17
+ export const SAMPLE_TURTLE = `@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
18
+ @prefix ex: <http://example.org/codec/> .
19
+ ex:scheme a skos:ConceptScheme ; skos:prefLabel "Video codecs"@en .
20
+ ex:c1 a skos:Concept ; skos:inScheme ex:scheme ; skos:prefLabel "PAL"@en ; skos:notation "c1" .
21
+ ex:c2 a skos:Concept ; skos:inScheme ex:scheme ; skos:prefLabel "NTSC"@en ; skos:notation "c2" .
22
+ ex:c3 a skos:Concept ; skos:inScheme ex:scheme ; skos:prefLabel "SECAM"@en ; skos:notation "c3" ;
23
+ skos:broader ex:c1 .
24
+ `
22
25
 
23
- /** What the sample store holds, as recorded when it was packed. The engine
24
- * is the authority on every one of these numbers at run time; they are here
25
- * so a caller can print something before it opens the store. */
26
+ /** What the sample store holds. */
26
27
  export const sampleStoreFacts = Object.freeze({
27
- triples: 4434,
28
- blocks: 13,
29
- layout: 'predicate-ibk3-ptd1-sri2-tli1-oli2-merkle-v0',
30
- wireVersion: 6,
31
- source: 'IPTC NewsCodes (spamfstat, videoqualifier, subjectqualifier, videocodec, spct)',
32
- licence: 'CC BY 4.0',
33
- licenceUrl: 'https://creativecommons.org/licenses/by/4.0/',
34
- publisher: 'https://iptc.org/'
28
+ triples: 15,
29
+ source: 'SAMPLE_TURTLE in sample-store.mjs (written for this package)',
30
+ layout: 'ibk3'
35
31
  })
36
32
 
33
+ /**
34
+ * Pack the sample store (if DIR holds no CURRENT) and return its root.
35
+ * @param {string} [dir] the collection root to use; default a new
36
+ * directory under the OS temporary directory
37
+ * @returns {string}
38
+ */
39
+ export function sampleStorePath (dir) {
40
+ if (typeof dir === 'string' && existsSync(join(dir, 'CURRENT'))) return dir
41
+ const cli = fileURLToPath(new URL('./bin/factoidal.mjs', import.meta.url))
42
+ const args = [cli, 'sample-store']
43
+ if (typeof dir === 'string') args.push(dir)
44
+ return execFileSync(process.execPath, args, { encoding: 'utf8' }).trim()
45
+ }
46
+
37
47
  export default sampleStorePath
@@ -0,0 +1,100 @@
1
+ # `@factoidal/core/solid/client` — a `fetch` host for the Solid Protocol
2
+
3
+ The Solid Protocol v0.11.0 client conformance class
4
+ (<https://solidproject.org/TR/protocol>) over the platform `fetch`.
5
+
6
+ Tracking issue: <https://github.com/danbri/factoidal/issues/659>.
7
+ Design record:
8
+ [`docs/designissues/2026-09-06-lws-and-solid-protocols.md`](../../../../docs/designissues/2026-09-06-lws-and-solid-protocols.md).
9
+ Conformance ledger:
10
+ [`docs/lws-solid-conformance.md`](../../../../docs/lws-solid-conformance.md).
11
+
12
+ ## What is here and what is not
13
+
14
+ `index.mjs` calls `solidClientRequest` to obtain a request record, puts
15
+ that record on the wire, and gives the reply to `solidClientResponse` to
16
+ be interpreted. Every request the client sends was built by the engine
17
+ and every response it reports was read by the engine.
18
+
19
+ This file composes no URL, sets no `Content-Type`, adds no `Slug`, reads
20
+ no `Link` header and parses no body. Discovery, request construction,
21
+ response interpretation and the WebID profile read are in
22
+ `formal/lean4/L4Factoidal/Solid/Client/`.
23
+
24
+ It shares no code with the server host beyond the engine loader.
25
+
26
+ ## The wasm contract
27
+
28
+ | op | arguments | answer |
29
+ | --- | --- | --- |
30
+ | `solidClientRequest` | `[kind, argsJson]` | `{ ok, request }` |
31
+ | `solidClientResponse` | `[kind, responseJson]` | `{ ok, interpretation }` |
32
+
33
+ The two operations take different kind vocabularies and this host keeps
34
+ them apart rather than deriving one from the other:
35
+
36
+ * request kinds — `read`, `create`, `replace`, `patch`, `delete`,
37
+ `discoverStorage`, `readProfile`;
38
+ * interpretation kinds — `storage`, `containment`, `auxiliaries`,
39
+ `profile`, `wacAllow`.
40
+
41
+ Which interpretation a reply wants is a protocol decision, so this file
42
+ never picks one. It uses the `interpret` member the request envelope
43
+ named, then the `interpret` the caller passed. With neither, the reply is
44
+ returned uninterpreted and `interpretation` is `null` — the raw record,
45
+ never a host reading of it.
46
+
47
+ A multi-step operation is a loop, not a special case. An interpretation
48
+ that answers `{ continue: true, state }` sends the state back into the
49
+ next `solidClientRequest`; walking from a resource up to its storage root
50
+ is the case that needs it. A single-request operation runs the loop once.
51
+ The loop is capped (`maxSteps`, default 16) so a state that never settles
52
+ is a reported failure rather than a hang.
53
+
54
+ The full contract is `docs/lws-solid-conformance.md`, section "wasm ABI".
55
+
56
+ ## Use
57
+
58
+ ```js
59
+ import { createSolidClient } from '@factoidal/core/solid/client'
60
+
61
+ const client = await createSolidClient({ baseIri: 'https://pod.example/' })
62
+ const storage = await client.discoverStorage('/alice/notes/one')
63
+ await client.replace('/alice/notes/one', '<#a> <#b> <#c> .', 'text/turtle')
64
+ const read = await client.read('/alice/notes/one')
65
+ await client.delete('/alice/notes/one')
66
+ ```
67
+
68
+ ```
69
+ factoidal solid-client get https://pod.example/alice/notes/one
70
+ factoidal solid-client put https://pod.example/alice/notes/one \
71
+ --file note.ttl --content-type text/turtle
72
+ factoidal solid-client discover https://pod.example/alice/notes/one
73
+ ```
74
+
75
+ ## Exports
76
+
77
+ | name | what it is |
78
+ | --- | --- |
79
+ | `createSolidClient(options)` | a client bound to one engine and one `fetch` |
80
+ | `solidClientOpsAvailable(engine)` | probe the loaded module for the ops |
81
+ | `SOLID_CLIENT_OPS` | the two op names |
82
+ | `SOLID_REQUEST_KINDS` | the seven request kinds the ABI names |
83
+ | `SOLID_INTERPRETATION_KINDS` | the five interpretation kinds |
84
+ | `SolidClientHostError` | a host failure, with `unknownOp` |
85
+
86
+ `options.fetch` replaces the platform `fetch`, which is how a test drives
87
+ the client at an in-process server without a proxy. `options.baseIri`
88
+ says where to send a request whose target the engine wrote as a path;
89
+ an absolute target is used verbatim.
90
+
91
+ The command's five verbs are aliases for the request kinds — `get`→`read`,
92
+ `put`→`replace`, `post`→`create`, `delete`→`delete`,
93
+ `discover`→`discoverStorage` — and a kind name is accepted directly.
94
+
95
+ ## Tests and interop
96
+
97
+ ```
98
+ node tests/solid/client/against-own-server.mjs # against our own server
99
+ bash tools/solid-client-interop.sh # against Community Solid Server
100
+ ```
@@ -0,0 +1,277 @@
1
+ // A `fetch` host for the Solid Protocol v0.11.0 client conformance class.
2
+ // https://github.com/danbri/factoidal/issues/659
3
+ // Design record: docs/designissues/2026-09-06-lws-and-solid-protocols.md
4
+ // Specification: https://solidproject.org/TR/protocol
5
+ //
6
+ // WHAT THIS FILE IS ALLOWED TO DO
7
+ // Call `solidClientRequest` to get a request record, put that record on
8
+ // the wire with `fetch`, and give the reply to `solidClientResponse` to
9
+ // be interpreted. It is a socket and nothing else.
10
+ //
11
+ // It composes no URL, sets no `Content-Type`, adds no `Slug`, reads no
12
+ // `Link` header and parses no body. Discovery, request construction,
13
+ // response interpretation and the WebID profile read are in
14
+ // `formal/lean4/L4Factoidal/Solid/Client/` (iron rule 7 of CLAUDE.md).
15
+ // A reviewer who finds a header name, a media type or a link relation
16
+ // in this file has found a rule violation.
17
+ //
18
+ // THE WASM CONTRACT
19
+ // Fixed in docs/lws-solid-conformance.md, section "wasm ABI".
20
+ // solidClientRequest [kind, argsJson] -> { ok, request }
21
+ // solidClientResponse [kind, responseJson] -> { ok, interpretation }
22
+ //
23
+ // request { method, target, headers: [[name, value]], body }
24
+ // response { status, headers: [[name, value]], body, target }
25
+ //
26
+ // The `target` of a response record is the request target the reply
27
+ // answers, echoed back from the request the engine built. The `storage`
28
+ // interpretation needs it to move up the path hierarchy.
29
+ //
30
+ // The two operations take DIFFERENT kind vocabularies, and this host
31
+ // keeps them apart rather than deriving one from the other.
32
+ //
33
+ // request kinds: read, create, replace, patch, delete,
34
+ // discoverStorage, readProfile
35
+ // interpretation kinds: storage, containment, auxiliaries, profile,
36
+ // wacAllow
37
+ //
38
+ // Which interpretation a reply wants is a protocol decision, so this
39
+ // file never picks one. It uses, in order: the `interpret` member the
40
+ // request envelope named, then the `interpret` the caller passed. With
41
+ // neither, the reply is returned uninterpreted and `interpretation` is
42
+ // null -- the raw record, never a host reading of it.
43
+ //
44
+ // A multi-step operation is a loop, not a special case: an
45
+ // interpretation that answers `{ continue: true, state }` sends the
46
+ // state back into the next `solidClientRequest`. Walking from a resource
47
+ // up to its storage root is the case that needs it. A single-request
48
+ // operation runs the loop once, and the loop is capped so a state that
49
+ // never settles is a reported failure rather than a hang.
50
+ //
51
+ // This file shares no code with `../server/index.mjs` beyond the engine
52
+ // loader.
53
+
54
+ import { loadEngine } from '../../bin/engine.mjs'
55
+
56
+ /** The ops this host needs from the engine. */
57
+ export const SOLID_CLIENT_OPS = ['solidClientRequest', 'solidClientResponse']
58
+
59
+ /** The request kinds the ABI names. Listed so a caller can be checked
60
+ * against the contract; this host attaches no meaning to any of them. */
61
+ export const SOLID_REQUEST_KINDS = [
62
+ 'read', 'create', 'replace', 'patch', 'delete', 'discoverStorage', 'readProfile'
63
+ ]
64
+
65
+ /** The interpretation kinds the ABI names. */
66
+ export const SOLID_INTERPRETATION_KINDS = [
67
+ 'storage', 'containment', 'auxiliaries', 'profile', 'wacAllow', 'created'
68
+ ]
69
+
70
+ /** An error raised by the Solid client host. `unknownOp` is set when the
71
+ * loaded WebAssembly module does not carry the operation at all. */
72
+ export class SolidClientHostError extends Error {
73
+ constructor (message, detail = {}) {
74
+ super(message)
75
+ this.name = 'SolidClientHostError'
76
+ this.unknownOp = detail.unknownOp === true
77
+ }
78
+ }
79
+
80
+ function isUnknownOp (error) {
81
+ const text = error && error.message ? String(error.message) : String(error)
82
+ return text.indexOf('unknown op') >= 0
83
+ }
84
+
85
+ /**
86
+ * Whether the loaded engine carries the Solid client operations.
87
+ *
88
+ * @param {object} engine the handle from `loadEngine()`
89
+ * @returns {Promise<{available: boolean, reason: string}>}
90
+ */
91
+ export async function solidClientOpsAvailable (engine) {
92
+ try {
93
+ engine.call('solidClientRequest', ['read', JSON.stringify({ target: '/' })])
94
+ return { available: true, reason: 'solidClientRequest answered' }
95
+ } catch (error) {
96
+ if (isUnknownOp(error)) {
97
+ return {
98
+ available: false,
99
+ reason: 'the loaded l4factoidal WebAssembly module does not carry ' +
100
+ 'the solid client ops (rebuild formal/lean4/Wasm)'
101
+ }
102
+ }
103
+ return { available: true, reason: 'solidClientRequest answered with an engine error' }
104
+ }
105
+ }
106
+
107
+ function requestOf (envelope) {
108
+ if (envelope === null || typeof envelope !== 'object') {
109
+ throw new SolidClientHostError('solidClientRequest answered with no envelope')
110
+ }
111
+ const record = (envelope.request !== null && typeof envelope.request === 'object')
112
+ ? envelope.request
113
+ : envelope
114
+ if (typeof record.method !== 'string' || typeof record.target !== 'string') {
115
+ throw new SolidClientHostError(
116
+ 'solidClientRequest answered with no method and target')
117
+ }
118
+ return {
119
+ method: record.method,
120
+ target: record.target,
121
+ headers: Array.isArray(record.headers) ? record.headers : [],
122
+ body: typeof record.body === 'string' ? record.body : null
123
+ }
124
+ }
125
+
126
+ function interpretationOf (envelope) {
127
+ if (envelope === null || typeof envelope !== 'object') {
128
+ throw new SolidClientHostError('solidClientResponse answered with no envelope')
129
+ }
130
+ return (envelope.interpretation !== null && typeof envelope.interpretation === 'object')
131
+ ? envelope.interpretation
132
+ : envelope
133
+ }
134
+
135
+ /** The reply as the JSON response record the contract states. */
136
+ async function recordOfReply (reply) {
137
+ const headers = []
138
+ reply.headers.forEach((value, name) => { headers.push([name, value]) })
139
+ return { status: reply.status, headers, body: await reply.text() }
140
+ }
141
+
142
+ /**
143
+ * A Solid client bound to one engine and one `fetch`.
144
+ *
145
+ * @param {object} [options]
146
+ * @param {object} [options.engine] an engine handle to reuse
147
+ * @param {Function} [options.fetch] the fetch to use (default `globalThis.fetch`)
148
+ * @param {string} [options.baseIri] prefixed to a request target that is a path
149
+ * @param {number} [options.maxSteps] the loop cap for a multi-step operation
150
+ * @returns {Promise<object>} the client
151
+ */
152
+ export async function createSolidClient (options = {}) {
153
+ const engine = options.engine !== undefined ? options.engine : await loadEngine()
154
+ const wire = typeof options.fetch === 'function' ? options.fetch : globalThis.fetch
155
+ if (typeof wire !== 'function') {
156
+ throw new SolidClientHostError('no fetch is available in this runtime')
157
+ }
158
+ const maxSteps = typeof options.maxSteps === 'number' ? options.maxSteps : 16
159
+ const baseIri = typeof options.baseIri === 'string' ? options.baseIri : null
160
+
161
+ /** Where to send a request whose target the engine wrote as a path.
162
+ * This is address resolution, not IRI construction: the engine's
163
+ * target is used verbatim when it is already absolute. */
164
+ function endpoint (target) {
165
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(target)) return target
166
+ if (baseIri === null) {
167
+ throw new SolidClientHostError(
168
+ `the engine answered the path "${target}" and this client has no ` +
169
+ 'baseIri to send it to; pass baseIri to createSolidClient')
170
+ }
171
+ return baseIri.replace(/\/$/, '') + target
172
+ }
173
+
174
+ function callRequest (kind, args) {
175
+ try {
176
+ return engine.call('solidClientRequest', [kind, JSON.stringify(args)])
177
+ } catch (error) {
178
+ if (isUnknownOp(error)) {
179
+ throw new SolidClientHostError(
180
+ 'the loaded l4factoidal WebAssembly module does not carry ' +
181
+ 'solidClientRequest', { unknownOp: true })
182
+ }
183
+ throw error
184
+ }
185
+ }
186
+
187
+ function callResponse (kind, record) {
188
+ try {
189
+ return engine.call('solidClientResponse', [kind, JSON.stringify(record)])
190
+ } catch (error) {
191
+ if (isUnknownOp(error)) {
192
+ throw new SolidClientHostError(
193
+ 'the loaded l4factoidal WebAssembly module does not carry ' +
194
+ 'solidClientResponse', { unknownOp: true })
195
+ }
196
+ throw error
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Run one client operation to its end.
202
+ *
203
+ * @param {string} kind a request kind from `SOLID_REQUEST_KINDS`
204
+ * @param {object} args the operation's arguments, passed unread
205
+ * @param {object} [extra] `interpret`: an interpretation kind to use
206
+ * when the request envelope names none
207
+ * @returns {Promise<{kind: string, request: object, response: object,
208
+ * interpret: string|null, interpretation: object|null,
209
+ * steps: number}>}
210
+ */
211
+ async function run (kind, args, extra = {}) {
212
+ let state = null
213
+ for (let step = 1; step <= maxSteps; step += 1) {
214
+ const built = callRequest(kind, state === null ? args : { ...args, state })
215
+ const request = requestOf(built)
216
+ const headers = {}
217
+ for (const pair of request.headers) {
218
+ if (!Array.isArray(pair) || pair.length < 2) continue
219
+ headers[pair[0]] = headers[pair[0]] === undefined
220
+ ? String(pair[1])
221
+ : `${headers[pair[0]]}, ${String(pair[1])}`
222
+ }
223
+ const reply = await wire(endpoint(request.target), {
224
+ method: request.method,
225
+ headers,
226
+ // fetch refuses any body on GET and HEAD, even an empty string; the
227
+ // engine's record carries "" there, which is the same message.
228
+ body: (request.body === null || request.method === 'GET' || request.method === 'HEAD')
229
+ ? undefined : request.body,
230
+ redirect: 'manual'
231
+ })
232
+ const record = await recordOfReply(reply)
233
+ const interpretKind = typeof built.interpret === 'string'
234
+ ? built.interpret
235
+ : (typeof extra.interpret === 'string' ? extra.interpret : null)
236
+ const answer = {
237
+ kind,
238
+ request,
239
+ response: record,
240
+ interpret: interpretKind,
241
+ interpretation: null,
242
+ steps: step
243
+ }
244
+ if (interpretKind === null) return answer
245
+ // The record says what came back; `target` says what was asked. The
246
+ // storage walk needs both, and this is the engine's own target
247
+ // echoed back, not a URL this file composed.
248
+ const interpretation = interpretationOf(
249
+ callResponse(interpretKind, { ...record, target: request.target }))
250
+ answer.interpretation = interpretation
251
+ if (interpretation.continue !== true) return answer
252
+ state = interpretation.state === undefined ? null : interpretation.state
253
+ }
254
+ throw new SolidClientHostError(
255
+ `the "${kind}" operation did not finish within ${maxSteps} requests`)
256
+ }
257
+
258
+ return {
259
+ engine,
260
+ run,
261
+ /** Named wrappers over `run`. Each one passes its arguments through;
262
+ * none of them adds a header, a media type or a URL. */
263
+ read: (target, extra = {}) => run('read', { target, ...extra }),
264
+ create: (target, body, contentType, extra = {}) =>
265
+ run('create', { target, body, contentType, ...extra }),
266
+ replace: (target, body, contentType, extra = {}) =>
267
+ run('replace', { target, body, contentType, ...extra }),
268
+ patch: (target, body, contentType, extra = {}) =>
269
+ run('patch', { target, body, contentType, ...extra }),
270
+ delete: (target, extra = {}) => run('delete', { target, ...extra }),
271
+ discoverStorage: (target, extra = {}) =>
272
+ run('discoverStorage', { target, interpret: 'storage', ...extra },
273
+ { interpret: 'storage' }),
274
+ readProfile: (target, extra = {}) =>
275
+ run('readProfile', { target, ...extra }, { interpret: 'profile' })
276
+ }
277
+ }
@@ -0,0 +1,96 @@
1
+ # `@factoidal/core/solid/server` — a Node host for the Solid Protocol
2
+
3
+ The Solid Protocol v0.11.0 server conformance class
4
+ (<https://solidproject.org/TR/protocol>) served over Node's `http`
5
+ 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
+ `index.mjs` is a socket, a body reader and a header writer. Storage
16
+ discovery (`Link: rel="type"` for `pim:Storage`), containment triples,
17
+ slash semantics, `Allow` and the `Accept-*` family, the PUT/POST/PATCH/
18
+ DELETE rules with their status codes, the `acl` and `describedby`
19
+ auxiliary lifecycle, N3 Patch, Web Access Control, CORS and the LDN
20
+ inbox are all in `formal/lean4/L4Factoidal/Solid/Server/` and reach this
21
+ file only as a `{status, headers, body}` record.
22
+
23
+ The CORS preflight is not an exception. An `OPTIONS` request goes to
24
+ `solidStep` like any other and the engine's headers are written back
25
+ unread. The only status this file writes itself is `500`, for a failure
26
+ of the host.
27
+
28
+ Solid defers to LWS in the engine, not here: `Solid/Server/*`
29
+ instantiates `LWS.Store` and refines `LWS.Operations.step`. This host
30
+ knows nothing of that layering.
31
+
32
+ ## The wasm contract
33
+
34
+ | op | arguments | answer |
35
+ | --- | --- | --- |
36
+ | `solidOpen` | `[configJson]` | `{ ok, handle, root, storage }` |
37
+ | `solidStep` | `[handle, requestJson]` | `{ ok, response }` |
38
+ | `solidClose` | `[handle]` | `{ ok }` |
39
+
40
+ ```
41
+ request { method, target, headers: [[name, value]], body }
42
+ response { status, headers: [[name, value]], body }
43
+ ```
44
+
45
+ `configJson` members, all optional: `baseIri` (the absolute IRI the
46
+ storage root maps to), `now` (the engine clock in seconds, advancing one
47
+ second per mutation), `owner` (the WebID advertised through
48
+ `Link: rel="…solid/terms#owner"`) and `agent` (the WebID of the
49
+ requesting agent). With an ephemeral port the origin is not known until
50
+ the socket is bound, so `listen` binds first and opens the storage
51
+ after. `target` is a path within the storage; this host composes no IRI.
52
+
53
+ `agent` is where authentication meets the engine. Verifying a Solid-OIDC
54
+ token is a host job and is not built yet, so a handle with no `agent` is
55
+ an unauthenticated agent, which is what the first slice serves. Web
56
+ Access Control decisions are the engine's, over the agent the host
57
+ verified.
58
+
59
+ The full contract is `docs/lws-solid-conformance.md`, section "wasm ABI".
60
+
61
+ A module built before these ops landed answers `unknown op`.
62
+ `solidServerOpsAvailable(engine)` reports that, and `tests/solid/server/`
63
+ skips with the reason printed.
64
+
65
+ ## Use
66
+
67
+ ```js
68
+ import { listen } from '@factoidal/core/solid/server'
69
+
70
+ const running = await listen({ port: 3000, baseIri: 'https://pod.example/' })
71
+ console.log(running.origin)
72
+ await running.close()
73
+ ```
74
+
75
+ ```
76
+ factoidal solid-serve /var/lib/solid --port 3000 --base https://pod.example/
77
+ ```
78
+
79
+ ## Exports
80
+
81
+ | name | what it is |
82
+ | --- | --- |
83
+ | `listen(options)` | start a server, resolve when it is listening |
84
+ | `createSolidServer(options)` | build the server without listening |
85
+ | `openSolidStorage(options)` | open a handle and get `step` / `close` |
86
+ | `solidServerOpsAvailable(engine)` | probe the loaded module for the ops |
87
+ | `SOLID_SERVER_OPS` | the three op names |
88
+ | `SolidServerHostError` | a host failure, with `unknownOp` |
89
+
90
+ ## Tests and interop
91
+
92
+ ```
93
+ node tests/solid/server/protocol.mjs
94
+ bash tools/solid-server-interop.sh # solid-crud-tests + WAC tests
95
+ bash tools/solid-conformance-harness.sh # the Gherkin harness, needs Docker
96
+ ```