@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.
- package/CHANGELOG.md +503 -1
- package/NOTICE +2 -21
- package/README.md +117 -993
- package/api.d.ts +53 -0
- package/api.js +5 -0
- package/api.mjs +56 -0
- package/bin/engine.mjs +38 -19
- package/bin/factoidal.mjs +512 -66
- package/bin/pack.mjs +37 -0
- package/bin/serve-worker.mjs +32 -0
- package/bin/serve.mjs +423 -0
- package/bin/store-worker-host.mjs +1 -1
- package/bin/store.mjs +1 -1
- package/browser.d.ts +110 -507
- package/browser.js +151 -2257
- package/docs/fuseki-replacement.md +108 -0
- package/fn.d.ts +24 -95
- package/fn.js +104 -150
- package/index.d.ts +598 -259
- package/index.js +4 -234
- package/index.mjs +176 -84
- package/l4-assets/l4factoidal.js +47 -7
- package/l4-assets/l4factoidal.mjs +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +11 -7
- package/lib/api.mjs +2300 -0
- package/lib/browser-engine.mjs +69 -0
- package/lib/dataset.mjs +377 -0
- package/lib/delta-log-hosts.mjs +191 -0
- package/lib/lean-entry.mjs +204 -0
- package/lib/pure.mjs +98 -0
- package/lib/store.mjs +115 -0
- package/lib/version.mjs +5 -0
- package/lws/README.md +97 -0
- package/lws/server.mjs +331 -0
- package/package.json +59 -61
- package/rdfjs.js +5 -536
- package/rdfjs.mjs +594 -0
- package/sample-store.d.ts +8 -16
- package/sample-store.mjs +39 -29
- package/solid/client/README.md +100 -0
- package/solid/client/index.mjs +277 -0
- package/solid/server/README.md +96 -0
- package/solid/server/index.mjs +317 -0
- package/store-host/deno.mjs +8 -0
- package/store-host/index.mjs +12 -0
- package/store-host/node.mjs +8 -0
- package/xmpp/README.md +107 -0
- package/xmpp/index.mjs +229 -0
- package/browser-wasm.js +0 -872
- package/factoidal-npm-entry.js +0 -15477
- package/factoidal-npm-entry.wasm.assets/code-5a7fc68f2ab1323718b8.wasm +0 -0
- package/factoidal-npm-entry.wasm.js +0 -455
- package/factoidal.js +0 -13118
- package/factoidal.wasm.assets/code-bbe6099bfb5b10c4c3ab.wasm +0 -0
- package/factoidal.wasm.js +0 -457
- package/hacl-init.js +0 -92
- package/hacl-wasm/FStar.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum25519_51.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum_Base.wasm +0 -0
- package/hacl-wasm/Hacl_Curve25519_51.wasm +0 -0
- package/hacl-wasm/Hacl_Ed25519.wasm +0 -0
- package/hacl-wasm/Hacl_Ed25519_PrecompTable.wasm +0 -0
- package/hacl-wasm/Hacl_Hash_Base.wasm +0 -0
- package/hacl-wasm/Hacl_Hash_SHA2.wasm +0 -0
- package/hacl-wasm/Hacl_IntTypes_Intrinsics.wasm +0 -0
- package/hacl-wasm/LowStar_Endianness.wasm +0 -0
- package/hacl-wasm/WasmSupport.wasm +0 -0
- package/hacl-wasm/api.js +0 -775
- package/hacl-wasm/api.json +0 -3787
- package/hacl-wasm/layouts.json +0 -1
- package/hacl-wasm/loader.js +0 -568
- package/hacl-wasm/shell.js +0 -12
- package/l4-core.d.ts +0 -159
- package/l4-core.js +0 -188
- package/l4.d.ts +0 -50
- package/l4.js +0 -104
- package/lib/api.js +0 -2300
- package/lib/engine-js.js +0 -165
- package/lib/engine-wasm.js +0 -300
- package/sample-store/CURRENT +0 -1
- package/sample-store/gen-1/manifest.sbm2 +0 -0
- package/sample-store/gen-1/manifest.tsv +0 -14
- package/sample-store/gen-1/predicate-0.ibk3 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-0.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-0.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-0.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.oli2.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-1.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-10.ibk3 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-10.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.oli2.merkle +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-10.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-11.ibk3 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-11.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-11.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-2.ibk3 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-2.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-2.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.sri2.merkle +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-3.ibk3 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-3.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-3.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-3.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-4.ibk3 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-4.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-4.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.tli1.merkle +0 -2
- package/sample-store/gen-1/predicate-5.ibk3 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-5.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-5.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-5.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-7.ibk3 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.oli2.merkle +0 -2
- package/sample-store/gen-1/predicate-7.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-7.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-9.ibk3 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-9.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-9.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.sri2.merkle +0 -3
- package/sample-store/gen-1/predicate-9.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.tli1.merkle +0 -2
- package/select.d.ts +0 -116
- package/select.js +0 -492
- package/version.json +0 -102
- package/wasm.d.ts +0 -130
- package/wasm.js +0 -158
|
@@ -0,0 +1,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
|
+
}
|
package/store-host/deno.mjs
CHANGED
|
@@ -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
|
package/store-host/index.mjs
CHANGED
|
@@ -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
|
package/store-host/node.mjs
CHANGED
|
@@ -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
|
+
`"&".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 `&` 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.
|