@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,317 @@
1
+ // A Node HTTP host for the Solid Protocol v0.11.0 server 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
+ // Accept a socket, turn one HTTP request into the JSON request record,
8
+ // call the engine's `solidStep` on a handle, and write the JSON response
9
+ // record back. Nothing else.
10
+ //
11
+ // It decides no status code, builds no `Link` header, writes no
12
+ // containment triple, reads no ACL and answers no OPTIONS. Storage
13
+ // discovery, containment, slash semantics, the auxiliary lifecycle, N3
14
+ // Patch, Web Access Control, CORS and the LDN inbox are all in
15
+ // `formal/lean4/L4Factoidal/Solid/Server/` (iron rule 7 of CLAUDE.md).
16
+ // The CORS PREFLIGHT is not an exception: an OPTIONS request is passed
17
+ // to `solidStep` like every other request and the engine's headers are
18
+ // written back unread. The only status this file writes is 500, for a
19
+ // failure of the host itself.
20
+ //
21
+ // THE WASM CONTRACT
22
+ // Fixed in docs/lws-solid-conformance.md, section "wasm ABI".
23
+ // solidOpen [configJson] -> { ok, handle, root, storage }
24
+ // solidStep [handle, requestJson] -> { ok, response }
25
+ // solidClose [handle] -> { ok }
26
+ //
27
+ // request { method, target, headers: [[name, value]], body }
28
+ // response { status, headers: [[name, value]], body }
29
+ //
30
+ // The config members are `baseIri` (the absolute IRI the storage root
31
+ // maps to), `now` (the engine clock in seconds, which advances by one
32
+ // second per mutation so a write sequence has increasing Last-Modified
33
+ // values with no real clock), `owner` and `agent`. All are optional.
34
+ //
35
+ // `target` is a path within the storage, so Node's `request.url` is
36
+ // passed through unchanged and this host composes no IRI. With an
37
+ // ephemeral port the origin is not known until the socket is bound, so
38
+ // the storage handle is opened after binding rather than before.
39
+ //
40
+ // `agent` is the WebID of the requesting agent. Verifying a token is a
41
+ // host job (WebCrypto in Node) and Web Access Control decisions are the
42
+ // engine's; until token verification lands, a handle with no `agent`
43
+ // is an unauthenticated agent and that is what these tests exercise.
44
+ //
45
+ // This file shares no code with `../client/index.mjs` beyond the engine
46
+ // loader, by the design record's rule that the two conformance classes
47
+ // stay distinct.
48
+
49
+ import { loadEngine } from '../../bin/engine.mjs'
50
+
51
+ /** The ops this host needs from the engine. */
52
+ export const SOLID_SERVER_OPS = ['solidOpen', 'solidStep', 'solidClose']
53
+
54
+ /** An error raised by the Solid server host. `unknownOp` is set when the
55
+ * loaded WebAssembly module does not carry the operation at all, which
56
+ * is what a test skips on. */
57
+ export class SolidServerHostError extends Error {
58
+ constructor (message, detail = {}) {
59
+ super(message)
60
+ this.name = 'SolidServerHostError'
61
+ this.unknownOp = detail.unknownOp === true
62
+ }
63
+ }
64
+
65
+ function isUnknownOp (error) {
66
+ const text = error && error.message ? String(error.message) : String(error)
67
+ return text.indexOf('unknown op') >= 0
68
+ }
69
+
70
+ /**
71
+ * Whether the loaded engine carries the Solid server operations.
72
+ *
73
+ * @param {object} engine the handle from `loadEngine()`
74
+ * @returns {Promise<{available: boolean, reason: string}>}
75
+ */
76
+ export async function solidServerOpsAvailable (engine) {
77
+ try {
78
+ const opened = engine.call('solidOpen', [JSON.stringify({ probe: true })])
79
+ const handle = handleOf(opened)
80
+ if (handle !== null) {
81
+ try { engine.call('solidClose', [handle]) } catch (_error) { /* probe only */ }
82
+ }
83
+ return { available: true, reason: 'solidOpen answered' }
84
+ } catch (error) {
85
+ if (isUnknownOp(error)) {
86
+ return {
87
+ available: false,
88
+ reason: 'the loaded l4factoidal WebAssembly module does not carry ' +
89
+ 'the solid server ops (rebuild formal/lean4/Wasm)'
90
+ }
91
+ }
92
+ return { available: true, reason: 'solidOpen answered with an engine error' }
93
+ }
94
+ }
95
+
96
+ function handleOf (envelope) {
97
+ if (envelope === null || typeof envelope !== 'object') return null
98
+ for (const name of ['handle', 'id']) {
99
+ const value = envelope[name]
100
+ if (typeof value === 'string' && value.length > 0) return value
101
+ if (typeof value === 'number') return String(value)
102
+ }
103
+ return null
104
+ }
105
+
106
+ function responseOf (envelope) {
107
+ if (envelope === null || typeof envelope !== 'object') {
108
+ throw new SolidServerHostError('solidStep answered with no envelope')
109
+ }
110
+ const record = (envelope.response !== null && typeof envelope.response === 'object')
111
+ ? envelope.response
112
+ : envelope
113
+ if (typeof record.status !== 'number') {
114
+ throw new SolidServerHostError(
115
+ 'solidStep answered with no status in its response record')
116
+ }
117
+ return {
118
+ status: record.status,
119
+ headers: Array.isArray(record.headers) ? record.headers : [],
120
+ body: typeof record.body === 'string' ? record.body : ''
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Open one Solid storage on the engine.
126
+ *
127
+ * @param {object} options
128
+ * @param {string} [options.baseIri] the absolute IRI the storage root maps to
129
+ * @param {number} [options.now] the engine clock, seconds since the epoch
130
+ * @param {string} [options.owner] the WebID the storage advertises
131
+ * @param {string} [options.agent] the WebID of the requesting agent
132
+ * @param {boolean} [options.realClock] stamp each request with this host's clock
133
+ * @param {object} [options.engine] an engine handle to reuse
134
+ * @returns {Promise<{step: Function, close: Function, handle: string,
135
+ * engine: object}>}
136
+ */
137
+ export async function openSolidStorage (options = {}) {
138
+ const engine = options.engine !== undefined ? options.engine : await loadEngine()
139
+ const config = {}
140
+ if (typeof options.baseIri === 'string') config.baseIri = options.baseIri
141
+ if (typeof options.now === 'number') config.now = options.now
142
+ if (typeof options.owner === 'string') config.owner = options.owner
143
+ if (typeof options.agent === 'string') config.agent = options.agent
144
+ let opened
145
+ try {
146
+ opened = engine.call('solidOpen', [JSON.stringify(config)])
147
+ } catch (error) {
148
+ if (isUnknownOp(error)) {
149
+ throw new SolidServerHostError(
150
+ 'the loaded l4factoidal WebAssembly module does not carry solidOpen',
151
+ { unknownOp: true })
152
+ }
153
+ throw error
154
+ }
155
+ const handle = handleOf(opened)
156
+ if (handle === null) {
157
+ throw new SolidServerHostError('solidOpen answered with no handle')
158
+ }
159
+ return {
160
+ engine,
161
+ handle,
162
+ storageIri: typeof opened.storage === 'string' ? opened.storage : null,
163
+ /**
164
+ * One protocol step.
165
+ * @param {{method: string, target: string,
166
+ * headers: Array<[string, string]>, body: string}} request
167
+ * @returns {{status: number, headers: Array<[string, string]>, body: string}}
168
+ */
169
+ step (request) {
170
+ const record = options.realClock === true
171
+ ? { ...request, now: Math.floor(Date.now() / 1000) }
172
+ : request
173
+ return responseOf(engine.call('solidStep', [handle, JSON.stringify(record)]))
174
+ },
175
+ close () {
176
+ try { engine.call('solidClose', [handle]) } catch (_error) { /* already gone */ }
177
+ }
178
+ }
179
+ }
180
+
181
+ function readBody (request) {
182
+ return new Promise((resolve, reject) => {
183
+ const chunks = []
184
+ request.on('data', (chunk) => chunks.push(chunk))
185
+ request.on('error', reject)
186
+ request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
187
+ })
188
+ }
189
+
190
+ function headerPairs (raw) {
191
+ const pairs = []
192
+ for (let index = 0; index + 1 < raw.length; index += 2) {
193
+ pairs.push([raw[index], raw[index + 1]])
194
+ }
195
+ return pairs
196
+ }
197
+
198
+ /**
199
+ * A Node `http.Server` that answers every request through `solidStep`.
200
+ *
201
+ * Returned unlistened, so a test can take an ephemeral port. The storage
202
+ * handle is opened on demand, because `baseIri` is not known until the
203
+ * socket is bound.
204
+ *
205
+ * @param {object} options as `openSolidStorage`, plus `onError`
206
+ * @returns {Promise<{server: object, storage: Function, close: Function,
207
+ * setBaseIri: Function}>}
208
+ */
209
+ export async function createSolidServer (options = {}) {
210
+ const http = await import('node:http')
211
+ const onError = typeof options.onError === 'function'
212
+ ? options.onError
213
+ : (error) => { console.error(`solid-serve: ${error.message}`) }
214
+
215
+ let settings = { ...options }
216
+ let opening = null
217
+ let opened = null
218
+
219
+ function storage () {
220
+ if (opening === null) {
221
+ opening = openSolidStorage(settings).then((value) => { opened = value; return value })
222
+ }
223
+ return opening
224
+ }
225
+
226
+ const server = http.createServer(async (request, response) => {
227
+ let record
228
+ try {
229
+ record = {
230
+ method: request.method,
231
+ target: request.url,
232
+ headers: headerPairs(request.rawHeaders),
233
+ body: await readBody(request)
234
+ }
235
+ } catch (error) {
236
+ onError(error)
237
+ response.writeHead(500, { 'content-type': 'text/plain' })
238
+ response.end('solid-serve: could not read the request body\n')
239
+ return
240
+ }
241
+ let answer
242
+ try {
243
+ answer = (await storage()).step(record)
244
+ } catch (error) {
245
+ onError(error)
246
+ response.writeHead(500, { 'content-type': 'text/plain' })
247
+ response.end(`solid-serve: ${error.message}\n`)
248
+ return
249
+ }
250
+ writeHeaders(response, answer.headers)
251
+ response.statusCode = answer.status
252
+ response.end(answer.body)
253
+ })
254
+
255
+ return {
256
+ server,
257
+ storage,
258
+ setBaseIri (value) {
259
+ if (opening !== null) {
260
+ throw new SolidServerHostError('the storage is already open; set baseIri first')
261
+ }
262
+ settings = { ...settings, baseIri: value }
263
+ },
264
+ close () {
265
+ return new Promise((resolve) => {
266
+ server.close(() => {
267
+ if (opened !== null) opened.close()
268
+ resolve()
269
+ })
270
+ })
271
+ }
272
+ }
273
+ }
274
+
275
+ /** Write one response record's header pairs, keeping repeats. */
276
+ function writeHeaders (response, pairs) {
277
+ for (const pair of pairs) {
278
+ if (!Array.isArray(pair) || pair.length < 2) continue
279
+ const existing = response.getHeader(pair[0])
280
+ if (existing === undefined) {
281
+ response.setHeader(pair[0], String(pair[1]))
282
+ } else if (Array.isArray(existing)) {
283
+ response.setHeader(pair[0], existing.concat([String(pair[1])]))
284
+ } else {
285
+ response.setHeader(pair[0], [String(existing), String(pair[1])])
286
+ }
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Start a Solid server and resolve once it is listening and the storage
292
+ * is open, so that a module without the Solid ops is reported by the
293
+ * command that started the server rather than by a later request.
294
+ *
295
+ * @param {object} options as `createSolidServer`, plus `port` and `host`
296
+ * @returns {Promise<{server: object, storage: Function, port: number,
297
+ * origin: string, close: Function}>}
298
+ */
299
+ export async function listen (options = {}) {
300
+ const port = typeof options.port === 'number' ? options.port : 0
301
+ const host = typeof options.host === 'string' ? options.host : '127.0.0.1'
302
+ const created = await createSolidServer(options)
303
+ await new Promise((resolve, reject) => {
304
+ created.server.once('error', reject)
305
+ created.server.listen(port, host, resolve)
306
+ })
307
+ const bound = created.server.address()
308
+ const origin = `http://${host}:${bound.port}`
309
+ if (typeof options.baseIri !== 'string') created.setBaseIri(`${origin}/`)
310
+ try {
311
+ await created.storage()
312
+ } catch (error) {
313
+ await new Promise((resolve) => created.server.close(resolve))
314
+ throw error
315
+ }
316
+ return { ...created, port: bound.port, origin }
317
+ }
@@ -239,6 +239,14 @@ export function atomicReplace (path, bytes) {
239
239
  }
240
240
  }
241
241
 
242
+ export function listNames (directory) {
243
+ try {
244
+ return [...Deno.readDirSync(directory)].map((entry) => entry.name).sort()
245
+ } catch (cause) {
246
+ throw wrap('DIR_READ_FAILED', `cannot list ${directory}`, directory, cause)
247
+ }
248
+ }
249
+
242
250
  export function listGeneration (directory) {
243
251
  const out = []
244
252
  let entries
@@ -190,6 +190,18 @@ export function listGeneration (directory) {
190
190
  return impl.listGeneration(directory)
191
191
  }
192
192
 
193
+ /**
194
+ * The names (files and subdirectories) in one directory, sorted. A host
195
+ * primitive for the engine, which decides what the names mean (the current
196
+ * generation of a Jena TDB2 database: `tdb2ServeKeys`).
197
+ * @param {string} directory
198
+ * @returns {string[]}
199
+ */
200
+ export function listNames (directory) {
201
+ requirePath(directory, 'directory')
202
+ return impl.listNames(directory)
203
+ }
204
+
193
205
  // The manifest file names a generation directory can carry, in the order
194
206
  // `Harness.ShardMerklePread.readManifest` tries them. This module reads
195
207
  // whichever exists and returns its bytes untouched; it does not look
@@ -251,6 +251,14 @@ export function atomicReplace (path, bytes) {
251
251
  }
252
252
  }
253
253
 
254
+ export function listNames (directory) {
255
+ try {
256
+ return readdirSync(directory).sort()
257
+ } catch (cause) {
258
+ throw wrap('DIR_READ_FAILED', `cannot list ${directory}`, directory, cause)
259
+ }
260
+ }
261
+
254
262
  export function listGeneration (directory) {
255
263
  let names
256
264
  try {
package/xmpp/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # `@factoidal/core/xmpp`
2
+
3
+ A functional-programming-style JavaScript API over `L4Factoidal.XMPP`
4
+ (Lean 4, compiled to WebAssembly) — the fresh, GC3-targeting XMPP
5
+ implementation described in
6
+ [`formal/lean4/L4Factoidal/XMPP/README.md`](../../../formal/lean4/L4Factoidal/XMPP/README.md).
7
+
8
+ ```js
9
+ import { parseJid, parseStreamHeader, featuresFor, parseStanza } from '@factoidal/core/xmpp'
10
+
11
+ const jid = await parseJid('romeo@example.net/orchard')
12
+ // { ok: true, value: { localpart: 'romeo', domainpart: 'example.net',
13
+ // resourcepart: 'orchard', wellFormed: true } }
14
+
15
+ const bad = await parseJid('@example.com')
16
+ // { ok: false, error: 'not a structurally valid JID: @example.com', unknownOp: false }
17
+
18
+ const features = await featuresFor('postTls', ['SCRAM-SHA-256', 'PLAIN'])
19
+ // { ok: true, value: { startTlsRequired: null,
20
+ // saslMechanisms: ['SCRAM-SHA-256', 'PLAIN'],
21
+ // canBind: false, canSession: false,
22
+ // xml: '<stream:features>...</stream:features>' } }
23
+ ```
24
+
25
+ ## Design: functions in, results out, nothing thrown for an expected outcome
26
+
27
+ Every function here is pure: plain data in, plain data out, no classes,
28
+ nothing to construct or hold onto. A malformed JID, a stanza that isn't
29
+ `message`/`presence`/`iq`, an unknown negotiation stage — these are
30
+ **ordinary answers**, not exceptional control flow, so every function
31
+ returns
32
+
33
+ ```ts
34
+ { ok: true, value: T } | { ok: false, error: string, unknownOp: boolean }
35
+ ```
36
+
37
+ rather than throwing. The one thing that still throws is a genuine
38
+ host/environment failure — the wasm module not being resolvable or
39
+ loadable at all (`loadXmppEngine()`), which is not a protocol-level
40
+ outcome and has no sensible data shape to return instead.
41
+
42
+ ## API
43
+
44
+ | Function | Wraps op | Notes |
45
+ |---|---|---|
46
+ | `parseJid(jid, options?)` | `xmppJidParse` | RFC 7622 structural + length well-formedness; PRECIS not yet enforced (see below) |
47
+ | `renderJid(parts, options?)` | `xmppJidRender` | `{localpart?, domainpart, resourcepart?}` → string |
48
+ | `parseStreamHeader(xml, options?)` | `xmppStreamHeaderParse` | tolerates a leading `<?xml ...?>` declaration |
49
+ | `renderStreamHeader(header, options?)` | `xmppStreamHeaderRender` | inverse |
50
+ | `featuresFor(stage, mechanisms?, options?)` | `xmppFeaturesFor` | `stage` is one of `preTls`/`postTls`/`authenticated`/`bound` |
51
+ | `parseStanza(xml, options?)` | `xmppStanzaParse` | `message`/`presence`/`iq` only; payload children come back as one serialized `payloadXml` string, not a tree |
52
+
53
+ Every function accepts an optional `{ engine }` in its last argument to
54
+ reuse an already-loaded engine handle (`await loadXmppEngine()`) instead
55
+ of paying the load cost per call — see `loadXmppEngine()`'s doc comment.
56
+
57
+ `xmppOpsAvailable(options?)` reports whether the resolved wasm module
58
+ carries these ops at all, so a caller (or a test) can skip gracefully
59
+ against an older committed bundle instead of getting a confusing
60
+ "unknown op" error.
61
+
62
+ ## What this does not cover yet, on purpose
63
+
64
+ - **Stanza construction from JSON.** `parseStanza` has no inverse: building
65
+ a stanza's XML back from a JSON payload description needs a
66
+ sibling-node XML constructor the Lean side doesn't have yet.
67
+ - **GC3 itself.** Still an unfinished, early-stage upstream XSF spec —
68
+ see the design record.
69
+ - **SASL/SCRAM computation.** The message *format* exists on the Lean
70
+ side (`L4Factoidal.XMPP.Sasl`); the actual hash/HMAC computation
71
+ doesn't yet (no HACL* HMAC-SHA-256 binding — see the design record's
72
+ crypto section).
73
+ - **MUC/MIX interop modules.**
74
+
75
+ ## Upstream limit found via this module's tests (2026-09-07), repaired 2026-10-10
76
+
77
+ `parseStanza`/`parseStreamHeader` delegate general XML parsing to the
78
+ shared `L4Factoidal.XML` module (reused rather than duplicated — see
79
+ the design record). Until 2026-10-10 its scan of a text run or a start
80
+ tag for references (`XML.scanRefs`) used one stack frame per reference:
81
+ a text body with about 2,000 consecutive entity references (for example
82
+ `"&amp;".repeat(2000)`) overflowed the wasm call stack
83
+ (`Maximum call stack size exceeded`), and the adversarial test was
84
+ pinned at 1,000. Issue: https://github.com/danbri/factoidal/issues/673.
85
+
86
+ The scan is now a loop (`XML.scanRefsLoop`, proved equal to
87
+ `XML.scanRefs`). Measured 2026-10-10 with the rebuilt wasm module:
88
+ 100,000 sequential references in a text node or an attribute value parse
89
+ under `node --stack-size=512`; before the repair, 5,000 overflowed there.
90
+ `test/xmpp.test.mjs` now parses a body of 10,000 `&amp;` references and
91
+ checks the rendered payload. The depth of NESTED entity expansion (an
92
+ entity whose replacement text references another entity) still costs one
93
+ frame per level, bounded by the number of declared entities. RFC 6120
94
+ §11.1 excludes DTD subsets and every entity reference except the five
95
+ predefined ones from an XML stream, so conforming XMPP input has no
96
+ nested expansion.
97
+
98
+ ## Testing
99
+
100
+ ```bash
101
+ cd npm/factoidal
102
+ node --test test/xmpp.test.mjs
103
+ ```
104
+
105
+ Skips wholesale when the Lean wasm assets aren't resolvable, and skips
106
+ per-op when the resolved bundle predates a given op (see
107
+ `xmppOpsAvailable`) — the same pattern `test/l4-core.test.js` uses.