@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.
- package/CHANGELOG.md +616 -1
- package/NOTICE +2 -21
- package/README.md +123 -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/ext.mjs +16 -20
- package/bin/factoidal.mjs +667 -118
- 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 +114 -507
- package/browser.js +155 -2257
- package/docs/fuseki-replacement.md +108 -0
- package/fn.d.ts +25 -96
- package/fn.js +131 -240
- package/index.d.ts +620 -260
- package/index.js +4 -234
- package/index.mjs +180 -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 +2337 -0
- package/lib/browser-engine.mjs +69 -0
- package/lib/dataset.mjs +471 -0
- package/lib/delta-log-hosts.mjs +191 -0
- package/lib/lean-entry.mjs +213 -0
- package/lib/pure.mjs +98 -0
- package/lib/store.mjs +115 -0
- package/lib/version.mjs +5 -0
- package/lws/README.md +137 -0
- package/lws/persist.mjs +78 -0
- package/lws/server.mjs +416 -0
- package/package.json +59 -61
- package/rdfjs.js +5 -536
- package/rdfjs.mjs +517 -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 +293 -0
- package/solid/server/README.md +96 -0
- package/solid/server/index.mjs +395 -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,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
|
+
}
|
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.
|