@factoidal/core 0.7.1 → 0.11.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 (195) hide show
  1. package/CHANGELOG.md +616 -1
  2. package/NOTICE +2 -21
  3. package/README.md +123 -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/ext.mjs +16 -20
  9. package/bin/factoidal.mjs +667 -118
  10. package/bin/pack.mjs +37 -0
  11. package/bin/serve-worker.mjs +32 -0
  12. package/bin/serve.mjs +423 -0
  13. package/bin/store-worker-host.mjs +1 -1
  14. package/bin/store.mjs +1 -1
  15. package/browser.d.ts +114 -507
  16. package/browser.js +155 -2257
  17. package/docs/fuseki-replacement.md +108 -0
  18. package/fn.d.ts +25 -96
  19. package/fn.js +131 -240
  20. package/index.d.ts +620 -260
  21. package/index.js +4 -234
  22. package/index.mjs +180 -84
  23. package/l4-assets/l4factoidal.js +47 -7
  24. package/l4-assets/l4factoidal.mjs +1 -1
  25. package/l4-assets/l4factoidal.wasm +0 -0
  26. package/l4-assets/version.json +11 -7
  27. package/lib/api.mjs +2337 -0
  28. package/lib/browser-engine.mjs +69 -0
  29. package/lib/dataset.mjs +471 -0
  30. package/lib/delta-log-hosts.mjs +191 -0
  31. package/lib/lean-entry.mjs +213 -0
  32. package/lib/pure.mjs +98 -0
  33. package/lib/store.mjs +115 -0
  34. package/lib/version.mjs +5 -0
  35. package/lws/README.md +137 -0
  36. package/lws/persist.mjs +78 -0
  37. package/lws/server.mjs +416 -0
  38. package/package.json +59 -61
  39. package/rdfjs.js +5 -536
  40. package/rdfjs.mjs +517 -0
  41. package/sample-store.d.ts +8 -16
  42. package/sample-store.mjs +39 -29
  43. package/solid/client/README.md +100 -0
  44. package/solid/client/index.mjs +293 -0
  45. package/solid/server/README.md +96 -0
  46. package/solid/server/index.mjs +395 -0
  47. package/store-host/deno.mjs +8 -0
  48. package/store-host/index.mjs +12 -0
  49. package/store-host/node.mjs +8 -0
  50. package/xmpp/README.md +107 -0
  51. package/xmpp/index.mjs +229 -0
  52. package/browser-wasm.js +0 -872
  53. package/factoidal-npm-entry.js +0 -15477
  54. package/factoidal-npm-entry.wasm.assets/code-5a7fc68f2ab1323718b8.wasm +0 -0
  55. package/factoidal-npm-entry.wasm.js +0 -455
  56. package/factoidal.js +0 -13118
  57. package/factoidal.wasm.assets/code-bbe6099bfb5b10c4c3ab.wasm +0 -0
  58. package/factoidal.wasm.js +0 -457
  59. package/hacl-init.js +0 -92
  60. package/hacl-wasm/FStar.wasm +0 -0
  61. package/hacl-wasm/Hacl_Bignum.wasm +0 -0
  62. package/hacl-wasm/Hacl_Bignum25519_51.wasm +0 -0
  63. package/hacl-wasm/Hacl_Bignum_Base.wasm +0 -0
  64. package/hacl-wasm/Hacl_Curve25519_51.wasm +0 -0
  65. package/hacl-wasm/Hacl_Ed25519.wasm +0 -0
  66. package/hacl-wasm/Hacl_Ed25519_PrecompTable.wasm +0 -0
  67. package/hacl-wasm/Hacl_Hash_Base.wasm +0 -0
  68. package/hacl-wasm/Hacl_Hash_SHA2.wasm +0 -0
  69. package/hacl-wasm/Hacl_IntTypes_Intrinsics.wasm +0 -0
  70. package/hacl-wasm/LowStar_Endianness.wasm +0 -0
  71. package/hacl-wasm/WasmSupport.wasm +0 -0
  72. package/hacl-wasm/api.js +0 -775
  73. package/hacl-wasm/api.json +0 -3787
  74. package/hacl-wasm/layouts.json +0 -1
  75. package/hacl-wasm/loader.js +0 -568
  76. package/hacl-wasm/shell.js +0 -12
  77. package/l4-core.d.ts +0 -159
  78. package/l4-core.js +0 -188
  79. package/l4.d.ts +0 -50
  80. package/l4.js +0 -104
  81. package/lib/api.js +0 -2300
  82. package/lib/engine-js.js +0 -165
  83. package/lib/engine-wasm.js +0 -300
  84. package/sample-store/CURRENT +0 -1
  85. package/sample-store/gen-1/manifest.sbm2 +0 -0
  86. package/sample-store/gen-1/manifest.tsv +0 -14
  87. package/sample-store/gen-1/predicate-0.ibk3 +0 -0
  88. package/sample-store/gen-1/predicate-0.ibk3.merkle +0 -1
  89. package/sample-store/gen-1/predicate-0.ibk3.oli2 +0 -0
  90. package/sample-store/gen-1/predicate-0.ibk3.oli2.merkle +0 -1
  91. package/sample-store/gen-1/predicate-0.ibk3.sri2 +0 -0
  92. package/sample-store/gen-1/predicate-0.ibk3.sri2.merkle +0 -1
  93. package/sample-store/gen-1/predicate-0.ibk3.tli1 +0 -0
  94. package/sample-store/gen-1/predicate-0.ibk3.tli1.merkle +0 -0
  95. package/sample-store/gen-1/predicate-1.ibk3 +0 -0
  96. package/sample-store/gen-1/predicate-1.ibk3.merkle +0 -0
  97. package/sample-store/gen-1/predicate-1.ibk3.oli2 +0 -0
  98. package/sample-store/gen-1/predicate-1.ibk3.oli2.merkle +0 -0
  99. package/sample-store/gen-1/predicate-1.ibk3.sri2 +0 -0
  100. package/sample-store/gen-1/predicate-1.ibk3.sri2.merkle +0 -1
  101. package/sample-store/gen-1/predicate-1.ibk3.tli1 +0 -0
  102. package/sample-store/gen-1/predicate-1.ibk3.tli1.merkle +0 -1
  103. package/sample-store/gen-1/predicate-10.ibk3 +0 -0
  104. package/sample-store/gen-1/predicate-10.ibk3.merkle +0 -1
  105. package/sample-store/gen-1/predicate-10.ibk3.oli2 +0 -0
  106. package/sample-store/gen-1/predicate-10.ibk3.oli2.merkle +0 -0
  107. package/sample-store/gen-1/predicate-10.ibk3.sri2 +0 -0
  108. package/sample-store/gen-1/predicate-10.ibk3.sri2.merkle +0 -1
  109. package/sample-store/gen-1/predicate-10.ibk3.tli1 +0 -0
  110. package/sample-store/gen-1/predicate-10.ibk3.tli1.merkle +0 -1
  111. package/sample-store/gen-1/predicate-11.ibk3 +0 -0
  112. package/sample-store/gen-1/predicate-11.ibk3.merkle +0 -0
  113. package/sample-store/gen-1/predicate-11.ibk3.oli2 +0 -0
  114. package/sample-store/gen-1/predicate-11.ibk3.oli2.merkle +0 -1
  115. package/sample-store/gen-1/predicate-11.ibk3.sri2 +0 -0
  116. package/sample-store/gen-1/predicate-11.ibk3.sri2.merkle +0 -1
  117. package/sample-store/gen-1/predicate-11.ibk3.tli1 +0 -0
  118. package/sample-store/gen-1/predicate-11.ibk3.tli1.merkle +0 -1
  119. package/sample-store/gen-1/predicate-12.ibk3 +0 -0
  120. package/sample-store/gen-1/predicate-12.ibk3.merkle +0 -1
  121. package/sample-store/gen-1/predicate-12.ibk3.oli2 +0 -0
  122. package/sample-store/gen-1/predicate-12.ibk3.oli2.merkle +0 -1
  123. package/sample-store/gen-1/predicate-12.ibk3.sri2 +0 -0
  124. package/sample-store/gen-1/predicate-12.ibk3.sri2.merkle +0 -1
  125. package/sample-store/gen-1/predicate-12.ibk3.tli1 +0 -0
  126. package/sample-store/gen-1/predicate-12.ibk3.tli1.merkle +0 -1
  127. package/sample-store/gen-1/predicate-2.ibk3 +0 -0
  128. package/sample-store/gen-1/predicate-2.ibk3.merkle +0 -1
  129. package/sample-store/gen-1/predicate-2.ibk3.oli2 +0 -0
  130. package/sample-store/gen-1/predicate-2.ibk3.oli2.merkle +0 -1
  131. package/sample-store/gen-1/predicate-2.ibk3.sri2 +0 -0
  132. package/sample-store/gen-1/predicate-2.ibk3.sri2.merkle +0 -0
  133. package/sample-store/gen-1/predicate-2.ibk3.tli1 +0 -0
  134. package/sample-store/gen-1/predicate-2.ibk3.tli1.merkle +0 -0
  135. package/sample-store/gen-1/predicate-3.ibk3 +0 -0
  136. package/sample-store/gen-1/predicate-3.ibk3.merkle +0 -1
  137. package/sample-store/gen-1/predicate-3.ibk3.oli2 +0 -0
  138. package/sample-store/gen-1/predicate-3.ibk3.oli2.merkle +0 -1
  139. package/sample-store/gen-1/predicate-3.ibk3.sri2 +0 -0
  140. package/sample-store/gen-1/predicate-3.ibk3.sri2.merkle +0 -1
  141. package/sample-store/gen-1/predicate-3.ibk3.tli1 +0 -0
  142. package/sample-store/gen-1/predicate-3.ibk3.tli1.merkle +0 -1
  143. package/sample-store/gen-1/predicate-4.ibk3 +0 -0
  144. package/sample-store/gen-1/predicate-4.ibk3.merkle +0 -0
  145. package/sample-store/gen-1/predicate-4.ibk3.oli2 +0 -0
  146. package/sample-store/gen-1/predicate-4.ibk3.oli2.merkle +0 -1
  147. package/sample-store/gen-1/predicate-4.ibk3.sri2 +0 -0
  148. package/sample-store/gen-1/predicate-4.ibk3.sri2.merkle +0 -1
  149. package/sample-store/gen-1/predicate-4.ibk3.tli1 +0 -0
  150. package/sample-store/gen-1/predicate-4.ibk3.tli1.merkle +0 -2
  151. package/sample-store/gen-1/predicate-5.ibk3 +0 -0
  152. package/sample-store/gen-1/predicate-5.ibk3.merkle +0 -1
  153. package/sample-store/gen-1/predicate-5.ibk3.oli2 +0 -0
  154. package/sample-store/gen-1/predicate-5.ibk3.oli2.merkle +0 -1
  155. package/sample-store/gen-1/predicate-5.ibk3.sri2 +0 -0
  156. package/sample-store/gen-1/predicate-5.ibk3.sri2.merkle +0 -1
  157. package/sample-store/gen-1/predicate-5.ibk3.tli1 +0 -0
  158. package/sample-store/gen-1/predicate-5.ibk3.tli1.merkle +0 -1
  159. package/sample-store/gen-1/predicate-6.ibk3 +0 -0
  160. package/sample-store/gen-1/predicate-6.ibk3.merkle +0 -1
  161. package/sample-store/gen-1/predicate-6.ibk3.oli2 +0 -0
  162. package/sample-store/gen-1/predicate-6.ibk3.oli2.merkle +0 -1
  163. package/sample-store/gen-1/predicate-6.ibk3.sri2 +0 -0
  164. package/sample-store/gen-1/predicate-6.ibk3.sri2.merkle +0 -1
  165. package/sample-store/gen-1/predicate-6.ibk3.tli1 +0 -0
  166. package/sample-store/gen-1/predicate-6.ibk3.tli1.merkle +0 -0
  167. package/sample-store/gen-1/predicate-7.ibk3 +0 -0
  168. package/sample-store/gen-1/predicate-7.ibk3.merkle +0 -0
  169. package/sample-store/gen-1/predicate-7.ibk3.oli2 +0 -0
  170. package/sample-store/gen-1/predicate-7.ibk3.oli2.merkle +0 -2
  171. package/sample-store/gen-1/predicate-7.ibk3.sri2 +0 -0
  172. package/sample-store/gen-1/predicate-7.ibk3.sri2.merkle +0 -1
  173. package/sample-store/gen-1/predicate-7.ibk3.tli1 +0 -0
  174. package/sample-store/gen-1/predicate-7.ibk3.tli1.merkle +0 -1
  175. package/sample-store/gen-1/predicate-8.ibk3 +0 -0
  176. package/sample-store/gen-1/predicate-8.ibk3.merkle +0 -1
  177. package/sample-store/gen-1/predicate-8.ibk3.oli2 +0 -0
  178. package/sample-store/gen-1/predicate-8.ibk3.oli2.merkle +0 -1
  179. package/sample-store/gen-1/predicate-8.ibk3.sri2 +0 -0
  180. package/sample-store/gen-1/predicate-8.ibk3.sri2.merkle +0 -1
  181. package/sample-store/gen-1/predicate-8.ibk3.tli1 +0 -0
  182. package/sample-store/gen-1/predicate-8.ibk3.tli1.merkle +0 -1
  183. package/sample-store/gen-1/predicate-9.ibk3 +0 -0
  184. package/sample-store/gen-1/predicate-9.ibk3.merkle +0 -1
  185. package/sample-store/gen-1/predicate-9.ibk3.oli2 +0 -0
  186. package/sample-store/gen-1/predicate-9.ibk3.oli2.merkle +0 -1
  187. package/sample-store/gen-1/predicate-9.ibk3.sri2 +0 -0
  188. package/sample-store/gen-1/predicate-9.ibk3.sri2.merkle +0 -3
  189. package/sample-store/gen-1/predicate-9.ibk3.tli1 +0 -0
  190. package/sample-store/gen-1/predicate-9.ibk3.tli1.merkle +0 -2
  191. package/select.d.ts +0 -116
  192. package/select.js +0 -492
  193. package/version.json +0 -102
  194. package/wasm.d.ts +0 -130
  195. package/wasm.js +0 -158
@@ -0,0 +1,395 @@
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
+ // solidFetched [handle, documentJson] -> { ok, fetch? | installed? | refused? }
27
+ //
28
+ // Solid-OIDC (issue 899): with `auth` in the configuration the engine
29
+ // verifies the DPoP-bound access token itself
30
+ // (formal/lean4/L4Factoidal/Solid/Server/Oidc.lean). When it needs a
31
+ // document it does not hold -- an issuer's OpenID configuration, its JWK
32
+ // Set, or a WebID profile -- `solidStep` answers `{ fetch: {kind, key,
33
+ // url} }`. This host fetches exactly that URL, hands status and body to
34
+ // `solidFetched` unread, follows any `fetch` that answers, and steps the
35
+ // request again. It chooses no URL and reads no document.
36
+ //
37
+ // request { method, target, headers: [[name, value]], body }
38
+ // response { status, headers: [[name, value]], body }
39
+ //
40
+ // The config members are `baseIri` (the absolute IRI the storage root
41
+ // maps to), `now` (the engine clock in seconds, which advances by one
42
+ // second per mutation so a write sequence has increasing Last-Modified
43
+ // values with no real clock), `owner` and `agent`. All are optional.
44
+ //
45
+ // `target` is a path within the storage, so Node's `request.url` is
46
+ // passed through unchanged and this host composes no IRI. With an
47
+ // ephemeral port the origin is not known until the socket is bound, so
48
+ // the storage handle is opened after binding rather than before.
49
+ //
50
+ // `agent` is the WebID of the requesting agent. It stands only when the
51
+ // configuration has no `auth` (or has `insecureNoAuth`); with `auth` the
52
+ // engine decides the WebID from the credential.
53
+ //
54
+ // This file shares no code with `../client/index.mjs` beyond the engine
55
+ // loader, by the design record's rule that the two conformance classes
56
+ // stay distinct.
57
+
58
+ import { loadEngine } from '../../bin/engine.mjs'
59
+ import { readSnapshot, persistIfChanged } from '../../lws/persist.mjs'
60
+
61
+ /** The ops this host needs from the engine. */
62
+ export const SOLID_SERVER_OPS = ['solidOpen', 'solidStep', 'solidClose', 'solidSnapshot',
63
+ 'solidFetched']
64
+
65
+ /** The most documents one request may make this host fetch. The engine
66
+ * names at most three per request (issuer metadata, key set, profile);
67
+ * this bound only stops a defective module from looping the host. */
68
+ const MAX_FETCHES = 8
69
+
70
+ /** Fetch one document the engine named. Pure I/O; a network failure is
71
+ * status 0, which the engine refuses like any failed fetch. */
72
+ async function fetchDocument (url) {
73
+ try {
74
+ const answer = await fetch(url, {
75
+ redirect: 'error',
76
+ headers: { accept: 'text/turtle, application/json;q=0.9, */*;q=0.1' }
77
+ })
78
+ return { status: answer.status, body: await answer.text() }
79
+ } catch (error) {
80
+ return { status: 0, body: String(error && error.message ? error.message : error) }
81
+ }
82
+ }
83
+
84
+ /** An error raised by the Solid server host. `unknownOp` is set when the
85
+ * loaded WebAssembly module does not carry the operation at all, which
86
+ * is what a test skips on. */
87
+ export class SolidServerHostError extends Error {
88
+ constructor (message, detail = {}) {
89
+ super(message)
90
+ this.name = 'SolidServerHostError'
91
+ this.unknownOp = detail.unknownOp === true
92
+ }
93
+ }
94
+
95
+ function isUnknownOp (error) {
96
+ const text = error && error.message ? String(error.message) : String(error)
97
+ return text.indexOf('unknown op') >= 0
98
+ }
99
+
100
+ /**
101
+ * Whether the loaded engine carries the Solid server operations.
102
+ *
103
+ * @param {object} engine the handle from `loadEngine()`
104
+ * @returns {Promise<{available: boolean, reason: string}>}
105
+ */
106
+ export async function solidServerOpsAvailable (engine) {
107
+ try {
108
+ const opened = engine.call('solidOpen', [JSON.stringify({ probe: true })])
109
+ const handle = handleOf(opened)
110
+ if (handle !== null) {
111
+ try { engine.call('solidClose', [handle]) } catch (_error) { /* probe only */ }
112
+ }
113
+ return { available: true, reason: 'solidOpen answered' }
114
+ } catch (error) {
115
+ if (isUnknownOp(error)) {
116
+ return {
117
+ available: false,
118
+ reason: 'the loaded l4factoidal WebAssembly module does not carry ' +
119
+ 'the solid server ops (rebuild formal/lean4/Wasm)'
120
+ }
121
+ }
122
+ return { available: true, reason: 'solidOpen answered with an engine error' }
123
+ }
124
+ }
125
+
126
+ function handleOf (envelope) {
127
+ if (envelope === null || typeof envelope !== 'object') return null
128
+ for (const name of ['handle', 'id']) {
129
+ const value = envelope[name]
130
+ if (typeof value === 'string' && value.length > 0) return value
131
+ if (typeof value === 'number') return String(value)
132
+ }
133
+ return null
134
+ }
135
+
136
+ function responseOf (envelope, bytes) {
137
+ if (envelope === null || typeof envelope !== 'object') {
138
+ throw new SolidServerHostError('solidStep answered with no envelope')
139
+ }
140
+ const record = (envelope.response !== null && typeof envelope.response === 'object')
141
+ ? envelope.response
142
+ : envelope
143
+ if (typeof record.status !== 'number') {
144
+ throw new SolidServerHostError(
145
+ 'solidStep answered with no status in its response record')
146
+ }
147
+ return {
148
+ status: record.status,
149
+ headers: Array.isArray(record.headers) ? record.headers : [],
150
+ body: bytes instanceof Uint8Array ? bytes : new Uint8Array(0)
151
+ }
152
+ }
153
+
154
+ /** The request content as bytes: a byte array is passed as it is, and a
155
+ * string (a JavaScript caller's convenience) as its UTF-8 bytes. Bodies
156
+ * are bytes end to end (https://github.com/danbri/factoidal/issues/889). */
157
+ function contentBytes (request) {
158
+ if (request.body instanceof Uint8Array) return request.body
159
+ if (typeof request.body === 'string') return new TextEncoder().encode(request.body)
160
+ return new Uint8Array(0)
161
+ }
162
+
163
+ /**
164
+ * Open one Solid storage on the engine.
165
+ *
166
+ * @param {object} options
167
+ * @param {string} [options.baseIri] the absolute IRI the storage root maps to
168
+ * @param {number} [options.now] the engine clock, seconds since the epoch
169
+ * @param {string} [options.owner] the WebID the storage advertises
170
+ * @param {string} [options.agent] the WebID of the requesting agent
171
+ * @param {boolean} [options.realClock] stamp each request with this host's clock
172
+ * @param {object} [options.engine] an engine handle to reuse
173
+ * @returns {Promise<{step: Function, close: Function, handle: string,
174
+ * engine: object}>}
175
+ */
176
+ export async function openSolidStorage (options = {}) {
177
+ const engine = options.engine !== undefined ? options.engine : await loadEngine()
178
+ const config = {}
179
+ if (typeof options.baseIri === 'string') config.baseIri = options.baseIri
180
+ if (typeof options.now === 'number') config.now = options.now
181
+ if (typeof options.owner === 'string') config.owner = options.owner
182
+ if (typeof options.agent === 'string') config.agent = options.agent
183
+ // Solid-OIDC: `auth` is passed through unread; `insecureNoAuth` is the
184
+ // explicit development mode.
185
+ if (options.auth !== undefined && options.auth !== null) config.auth = options.auth
186
+ if (options.insecureNoAuth === true) config.insecureNoAuth = true
187
+ if (typeof options.enforceWac === 'boolean') config.enforceWac = options.enforceWac
188
+ if (typeof options.rootAcl === 'string') config.rootAcl = options.rootAcl
189
+ // `dir` names the storage directory. The snapshot found there, if any,
190
+ // is the storage to continue from; the engine checks it.
191
+ const dir = typeof options.dir === 'string' ? options.dir : null
192
+ if (dir !== null) {
193
+ const snapshot = readSnapshot(dir)
194
+ if (snapshot !== null) config.snapshot = snapshot
195
+ }
196
+ let opened
197
+ try {
198
+ opened = engine.call('solidOpen', [JSON.stringify(config)])
199
+ } catch (error) {
200
+ if (isUnknownOp(error)) {
201
+ throw new SolidServerHostError(
202
+ 'the loaded l4factoidal WebAssembly module does not carry solidOpen',
203
+ { unknownOp: true })
204
+ }
205
+ throw error
206
+ }
207
+ const handle = handleOf(opened)
208
+ if (handle === null) {
209
+ throw new SolidServerHostError('solidOpen answered with no handle')
210
+ }
211
+ return {
212
+ engine,
213
+ handle,
214
+ storageIri: typeof opened.storage === 'string' ? opened.storage : null,
215
+ /**
216
+ * One protocol step.
217
+ * @param {{method: string, target: string,
218
+ * headers: Array<[string, string]>, body: Uint8Array|string}} request
219
+ * @returns {{status: number, headers: Array<[string, string]>, body: Uint8Array}}
220
+ */
221
+ async step (request) {
222
+ const content = contentBytes(request)
223
+ const { body: _body, ...fields } = request
224
+ const record = options.realClock === true
225
+ ? { ...fields, now: Math.floor(Date.now() / 1000) }
226
+ : fields
227
+ // The byte-region entry: request content in, response content out.
228
+ const callStep = () =>
229
+ engine.callBlobIO('solidStep', [handle, JSON.stringify(record)], content)
230
+ let { envelope, bytes } = callStep()
231
+ let fetches = 0
232
+ while (envelope !== null && typeof envelope === 'object' &&
233
+ envelope.fetch !== undefined && envelope.fetch !== null) {
234
+ let wanted = envelope.fetch
235
+ while (wanted !== undefined && wanted !== null) {
236
+ fetches += 1
237
+ if (fetches > MAX_FETCHES) {
238
+ throw new SolidServerHostError('the engine asked for more documents than one request may fetch')
239
+ }
240
+ const got = await fetchDocument(wanted.url)
241
+ const told = engine.call('solidFetched', [handle, JSON.stringify({
242
+ kind: wanted.kind, key: wanted.key, url: wanted.url, status: got.status, body: got.body,
243
+ ...(typeof record.now === 'number' ? { now: record.now } : {})
244
+ })])
245
+ wanted = told !== null && typeof told === 'object' ? told.fetch : null
246
+ }
247
+ ({ envelope, bytes } = callStep())
248
+ }
249
+ const answer = responseOf(envelope, bytes)
250
+ if (dir !== null) persistIfChanged(engine, 'solidSnapshot', handle, envelope, dir)
251
+ return answer
252
+ },
253
+ close () {
254
+ try { engine.call('solidClose', [handle]) } catch (_error) { /* already gone */ }
255
+ }
256
+ }
257
+ }
258
+
259
+ function readBody (request) {
260
+ return new Promise((resolve, reject) => {
261
+ const chunks = []
262
+ request.on('data', (chunk) => chunks.push(chunk))
263
+ request.on('error', reject)
264
+ request.on('end', () => resolve(new Uint8Array(Buffer.concat(chunks))))
265
+ })
266
+ }
267
+
268
+ function headerPairs (raw) {
269
+ const pairs = []
270
+ for (let index = 0; index + 1 < raw.length; index += 2) {
271
+ pairs.push([raw[index], raw[index + 1]])
272
+ }
273
+ return pairs
274
+ }
275
+
276
+ /**
277
+ * A Node `http.Server` that answers every request through `solidStep`.
278
+ *
279
+ * Returned unlistened, so a test can take an ephemeral port. The storage
280
+ * handle is opened on demand, because `baseIri` is not known until the
281
+ * socket is bound.
282
+ *
283
+ * @param {object} options as `openSolidStorage`, plus `onError`
284
+ * @returns {Promise<{server: object, storage: Function, close: Function,
285
+ * setBaseIri: Function}>}
286
+ */
287
+ export async function createSolidServer (options = {}) {
288
+ const http = await import('node:http')
289
+ const onError = typeof options.onError === 'function'
290
+ ? options.onError
291
+ : (error) => { console.error(`solid-serve: ${error.message}`) }
292
+
293
+ let settings = { ...options }
294
+ let opening = null
295
+ let opened = null
296
+
297
+ function storage () {
298
+ if (opening === null) {
299
+ opening = openSolidStorage(settings).then((value) => { opened = value; return value })
300
+ }
301
+ return opening
302
+ }
303
+
304
+ const server = http.createServer(async (request, response) => {
305
+ let record
306
+ try {
307
+ record = {
308
+ method: request.method,
309
+ target: request.url,
310
+ headers: headerPairs(request.rawHeaders),
311
+ body: await readBody(request)
312
+ }
313
+ } catch (error) {
314
+ onError(error)
315
+ response.writeHead(500, { 'content-type': 'text/plain' })
316
+ response.end('solid-serve: could not read the request body\n')
317
+ return
318
+ }
319
+ let answer
320
+ try {
321
+ answer = await (await storage()).step(record)
322
+ } catch (error) {
323
+ onError(error)
324
+ response.writeHead(500, { 'content-type': 'text/plain' })
325
+ response.end(`solid-serve: ${error.message}\n`)
326
+ return
327
+ }
328
+ writeHeaders(response, answer.headers)
329
+ response.statusCode = answer.status
330
+ response.end(answer.body)
331
+ })
332
+
333
+ return {
334
+ server,
335
+ storage,
336
+ setBaseIri (value) {
337
+ if (opening !== null) {
338
+ throw new SolidServerHostError('the storage is already open; set baseIri first')
339
+ }
340
+ settings = { ...settings, baseIri: value }
341
+ },
342
+ close () {
343
+ return new Promise((resolve) => {
344
+ server.close(() => {
345
+ if (opened !== null) opened.close()
346
+ resolve()
347
+ })
348
+ })
349
+ }
350
+ }
351
+ }
352
+
353
+ /** Write one response record's header pairs, keeping repeats. */
354
+ function writeHeaders (response, pairs) {
355
+ for (const pair of pairs) {
356
+ if (!Array.isArray(pair) || pair.length < 2) continue
357
+ const existing = response.getHeader(pair[0])
358
+ if (existing === undefined) {
359
+ response.setHeader(pair[0], String(pair[1]))
360
+ } else if (Array.isArray(existing)) {
361
+ response.setHeader(pair[0], existing.concat([String(pair[1])]))
362
+ } else {
363
+ response.setHeader(pair[0], [String(existing), String(pair[1])])
364
+ }
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Start a Solid server and resolve once it is listening and the storage
370
+ * is open, so that a module without the Solid ops is reported by the
371
+ * command that started the server rather than by a later request.
372
+ *
373
+ * @param {object} options as `createSolidServer`, plus `port` and `host`
374
+ * @returns {Promise<{server: object, storage: Function, port: number,
375
+ * origin: string, close: Function}>}
376
+ */
377
+ export async function listen (options = {}) {
378
+ const port = typeof options.port === 'number' ? options.port : 0
379
+ const host = typeof options.host === 'string' ? options.host : '127.0.0.1'
380
+ const created = await createSolidServer(options)
381
+ await new Promise((resolve, reject) => {
382
+ created.server.once('error', reject)
383
+ created.server.listen(port, host, resolve)
384
+ })
385
+ const bound = created.server.address()
386
+ const origin = `http://${host}:${bound.port}`
387
+ if (typeof options.baseIri !== 'string') created.setBaseIri(`${origin}/`)
388
+ try {
389
+ await created.storage()
390
+ } catch (error) {
391
+ await new Promise((resolve) => created.server.close(resolve))
392
+ throw error
393
+ }
394
+ return { ...created, port: bound.port, origin }
395
+ }
@@ -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.