@factoidal/core 0.10.0 → 0.12.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 +496 -1
- package/README.md +70 -4
- package/api.d.ts +2 -3
- package/api.mjs +2 -2
- package/bin/ext.mjs +16 -20
- package/bin/factoidal.mjs +308 -69
- package/browser.d.ts +7 -2
- package/browser.js +5 -2
- package/fn.d.ts +1 -1
- package/fn.js +33 -100
- package/index.d.ts +105 -29
- package/index.mjs +5 -2
- package/l4-assets/l4factoidal.js +1 -1
- package/l4-assets/l4factoidal.mjs +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +6 -6
- package/lib/api.mjs +214 -287
- package/lib/dataset.mjs +164 -75
- package/lib/delta-log-hosts.mjs +9 -8
- package/lib/errors.mjs +97 -0
- package/lib/lean-entry.mjs +36 -22
- package/lib/store.mjs +4 -5
- package/lib/version.mjs +1 -1
- package/lws/README.md +114 -7
- package/lws/persist.mjs +309 -0
- package/lws/server.mjs +364 -17
- package/package.json +2 -1
- package/rdfjs.mjs +151 -226
- package/solid/client/index.mjs +25 -9
- package/solid/server/index.mjs +99 -15
package/solid/server/index.mjs
CHANGED
|
@@ -23,6 +23,16 @@
|
|
|
23
23
|
// solidOpen [configJson] -> { ok, handle, root, storage }
|
|
24
24
|
// solidStep [handle, requestJson] -> { ok, response }
|
|
25
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.
|
|
26
36
|
//
|
|
27
37
|
// request { method, target, headers: [[name, value]], body }
|
|
28
38
|
// response { status, headers: [[name, value]], body }
|
|
@@ -37,19 +47,39 @@
|
|
|
37
47
|
// ephemeral port the origin is not known until the socket is bound, so
|
|
38
48
|
// the storage handle is opened after binding rather than before.
|
|
39
49
|
//
|
|
40
|
-
// `agent` is the WebID of the requesting agent.
|
|
41
|
-
//
|
|
42
|
-
// engine
|
|
43
|
-
// is an unauthenticated agent and that is what these tests exercise.
|
|
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.
|
|
44
53
|
//
|
|
45
54
|
// This file shares no code with `../client/index.mjs` beyond the engine
|
|
46
55
|
// loader, by the design record's rule that the two conformance classes
|
|
47
56
|
// stay distinct.
|
|
48
57
|
|
|
49
58
|
import { loadEngine } from '../../bin/engine.mjs'
|
|
59
|
+
import { readSnapshot, persistIfChanged, storageKeyText } from '../../lws/persist.mjs'
|
|
50
60
|
|
|
51
61
|
/** The ops this host needs from the engine. */
|
|
52
|
-
export const SOLID_SERVER_OPS = ['solidOpen', 'solidStep', 'solidClose'
|
|
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
|
+
}
|
|
53
83
|
|
|
54
84
|
/** An error raised by the Solid server host. `unknownOp` is set when the
|
|
55
85
|
* loaded WebAssembly module does not carry the operation at all, which
|
|
@@ -103,7 +133,7 @@ function handleOf (envelope) {
|
|
|
103
133
|
return null
|
|
104
134
|
}
|
|
105
135
|
|
|
106
|
-
function responseOf (envelope) {
|
|
136
|
+
function responseOf (envelope, bytes) {
|
|
107
137
|
if (envelope === null || typeof envelope !== 'object') {
|
|
108
138
|
throw new SolidServerHostError('solidStep answered with no envelope')
|
|
109
139
|
}
|
|
@@ -117,10 +147,19 @@ function responseOf (envelope) {
|
|
|
117
147
|
return {
|
|
118
148
|
status: record.status,
|
|
119
149
|
headers: Array.isArray(record.headers) ? record.headers : [],
|
|
120
|
-
body:
|
|
150
|
+
body: bytes instanceof Uint8Array ? bytes : new Uint8Array(0)
|
|
121
151
|
}
|
|
122
152
|
}
|
|
123
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
|
+
|
|
124
163
|
/**
|
|
125
164
|
* Open one Solid storage on the engine.
|
|
126
165
|
*
|
|
@@ -130,6 +169,8 @@ function responseOf (envelope) {
|
|
|
130
169
|
* @param {string} [options.owner] the WebID the storage advertises
|
|
131
170
|
* @param {string} [options.agent] the WebID of the requesting agent
|
|
132
171
|
* @param {boolean} [options.realClock] stamp each request with this host's clock
|
|
172
|
+
* @param {string|null} [options.storageKey] the storage key text (64 hex digits or base64url);
|
|
173
|
+
* default the environment variable FACTOIDAL_LWS_KEY; `null` reads no key
|
|
133
174
|
* @param {object} [options.engine] an engine handle to reuse
|
|
134
175
|
* @returns {Promise<{step: Function, close: Function, handle: string,
|
|
135
176
|
* engine: object}>}
|
|
@@ -141,6 +182,23 @@ export async function openSolidStorage (options = {}) {
|
|
|
141
182
|
if (typeof options.now === 'number') config.now = options.now
|
|
142
183
|
if (typeof options.owner === 'string') config.owner = options.owner
|
|
143
184
|
if (typeof options.agent === 'string') config.agent = options.agent
|
|
185
|
+
// Solid-OIDC: `auth` is passed through unread; `insecureNoAuth` is the
|
|
186
|
+
// explicit development mode.
|
|
187
|
+
if (options.auth !== undefined && options.auth !== null) config.auth = options.auth
|
|
188
|
+
if (options.insecureNoAuth === true) config.insecureNoAuth = true
|
|
189
|
+
if (typeof options.enforceWac === 'boolean') config.enforceWac = options.enforceWac
|
|
190
|
+
if (typeof options.rootAcl === 'string') config.rootAcl = options.rootAcl
|
|
191
|
+
// Encryption at rest (issue 921), as the LWS host: the key text from
|
|
192
|
+
// `storageKey` or FACTOIDAL_LWS_KEY, passed unread; the engine decides.
|
|
193
|
+
const storageKey = storageKeyText(options.storageKey)
|
|
194
|
+
if (storageKey !== undefined) config.storageKey = storageKey
|
|
195
|
+
// `dir` names the storage directory. The snapshot found there, if any,
|
|
196
|
+
// is the storage to continue from; the engine checks it.
|
|
197
|
+
const dir = typeof options.dir === 'string' ? options.dir : null
|
|
198
|
+
if (dir !== null) {
|
|
199
|
+
const snapshot = readSnapshot(dir)
|
|
200
|
+
if (snapshot !== null) config.snapshot = snapshot
|
|
201
|
+
}
|
|
144
202
|
let opened
|
|
145
203
|
try {
|
|
146
204
|
opened = engine.call('solidOpen', [JSON.stringify(config)])
|
|
@@ -163,14 +221,40 @@ export async function openSolidStorage (options = {}) {
|
|
|
163
221
|
/**
|
|
164
222
|
* One protocol step.
|
|
165
223
|
* @param {{method: string, target: string,
|
|
166
|
-
* headers: Array<[string, string]>, body: string}} request
|
|
167
|
-
* @returns {{status: number, headers: Array<[string, string]>, body:
|
|
224
|
+
* headers: Array<[string, string]>, body: Uint8Array|string}} request
|
|
225
|
+
* @returns {{status: number, headers: Array<[string, string]>, body: Uint8Array}}
|
|
168
226
|
*/
|
|
169
|
-
step (request) {
|
|
227
|
+
async step (request) {
|
|
228
|
+
const content = contentBytes(request)
|
|
229
|
+
const { body: _body, ...fields } = request
|
|
170
230
|
const record = options.realClock === true
|
|
171
|
-
? { ...
|
|
172
|
-
:
|
|
173
|
-
|
|
231
|
+
? { ...fields, now: Math.floor(Date.now() / 1000) }
|
|
232
|
+
: fields
|
|
233
|
+
// The byte-region entry: request content in, response content out.
|
|
234
|
+
const callStep = () =>
|
|
235
|
+
engine.callBlobIO('solidStep', [handle, JSON.stringify(record)], content)
|
|
236
|
+
let { envelope, bytes } = callStep()
|
|
237
|
+
let fetches = 0
|
|
238
|
+
while (envelope !== null && typeof envelope === 'object' &&
|
|
239
|
+
envelope.fetch !== undefined && envelope.fetch !== null) {
|
|
240
|
+
let wanted = envelope.fetch
|
|
241
|
+
while (wanted !== undefined && wanted !== null) {
|
|
242
|
+
fetches += 1
|
|
243
|
+
if (fetches > MAX_FETCHES) {
|
|
244
|
+
throw new SolidServerHostError('the engine asked for more documents than one request may fetch')
|
|
245
|
+
}
|
|
246
|
+
const got = await fetchDocument(wanted.url)
|
|
247
|
+
const told = engine.call('solidFetched', [handle, JSON.stringify({
|
|
248
|
+
kind: wanted.kind, key: wanted.key, url: wanted.url, status: got.status, body: got.body,
|
|
249
|
+
...(typeof record.now === 'number' ? { now: record.now } : {})
|
|
250
|
+
})])
|
|
251
|
+
wanted = told !== null && typeof told === 'object' ? told.fetch : null
|
|
252
|
+
}
|
|
253
|
+
({ envelope, bytes } = callStep())
|
|
254
|
+
}
|
|
255
|
+
const answer = responseOf(envelope, bytes)
|
|
256
|
+
if (dir !== null) persistIfChanged(engine, 'solidSnapshot', handle, envelope, dir)
|
|
257
|
+
return answer
|
|
174
258
|
},
|
|
175
259
|
close () {
|
|
176
260
|
try { engine.call('solidClose', [handle]) } catch (_error) { /* already gone */ }
|
|
@@ -183,7 +267,7 @@ function readBody (request) {
|
|
|
183
267
|
const chunks = []
|
|
184
268
|
request.on('data', (chunk) => chunks.push(chunk))
|
|
185
269
|
request.on('error', reject)
|
|
186
|
-
request.on('end', () => resolve(Buffer.concat(chunks)
|
|
270
|
+
request.on('end', () => resolve(new Uint8Array(Buffer.concat(chunks))))
|
|
187
271
|
})
|
|
188
272
|
}
|
|
189
273
|
|
|
@@ -240,7 +324,7 @@ export async function createSolidServer (options = {}) {
|
|
|
240
324
|
}
|
|
241
325
|
let answer
|
|
242
326
|
try {
|
|
243
|
-
answer = (await storage()).step(record)
|
|
327
|
+
answer = await (await storage()).step(record)
|
|
244
328
|
} catch (error) {
|
|
245
329
|
onError(error)
|
|
246
330
|
response.writeHead(500, { 'content-type': 'text/plain' })
|