@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.
@@ -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, 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: 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
  *
@@ -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: string}}
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
- ? { ...request, now: Math.floor(Date.now() / 1000) }
172
- : request
173
- return responseOf(engine.call('solidStep', [handle, JSON.stringify(record)]))
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).toString('utf8')))
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' })