@factoidal/core 0.10.0 → 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.
@@ -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. 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.
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 } 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: typeof record.body === 'string' ? record.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
  *
@@ -141,6 +180,19 @@ export async function openSolidStorage (options = {}) {
141
180
  if (typeof options.now === 'number') config.now = options.now
142
181
  if (typeof options.owner === 'string') config.owner = options.owner
143
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
+ }
144
196
  let opened
145
197
  try {
146
198
  opened = engine.call('solidOpen', [JSON.stringify(config)])
@@ -163,14 +215,40 @@ export async function openSolidStorage (options = {}) {
163
215
  /**
164
216
  * One protocol step.
165
217
  * @param {{method: string, target: string,
166
- * headers: Array<[string, string]>, body: string}} request
167
- * @returns {{status: number, headers: Array<[string, string]>, body: string}}
218
+ * headers: Array<[string, string]>, body: Uint8Array|string}} request
219
+ * @returns {{status: number, headers: Array<[string, string]>, body: Uint8Array}}
168
220
  */
169
- step (request) {
221
+ async step (request) {
222
+ const content = contentBytes(request)
223
+ const { body: _body, ...fields } = request
170
224
  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)]))
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
174
252
  },
175
253
  close () {
176
254
  try { engine.call('solidClose', [handle]) } catch (_error) { /* already gone */ }
@@ -183,7 +261,7 @@ function readBody (request) {
183
261
  const chunks = []
184
262
  request.on('data', (chunk) => chunks.push(chunk))
185
263
  request.on('error', reject)
186
- request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
264
+ request.on('end', () => resolve(new Uint8Array(Buffer.concat(chunks))))
187
265
  })
188
266
  }
189
267
 
@@ -240,7 +318,7 @@ export async function createSolidServer (options = {}) {
240
318
  }
241
319
  let answer
242
320
  try {
243
- answer = (await storage()).step(record)
321
+ answer = await (await storage()).step(record)
244
322
  } catch (error) {
245
323
  onError(error)
246
324
  response.writeHead(500, { 'content-type': 'text/plain' })