@factoidal/core 0.19.0 → 0.21.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 +65 -0
- package/README.md +59 -15
- package/authz/server.mjs +23 -2
- package/bin/engine.mjs +22 -1
- package/bin/factoidal.mjs +710 -38
- package/bin/pack-host.mjs +5 -0
- package/bin/serve.mjs +19 -3
- package/browser.d.ts +23 -0
- package/browser.js +26 -0
- package/index.d.ts +463 -24
- package/index.mjs +29 -0
- package/l4-assets/l4factoidal.js +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +6 -6
- package/lib/api.mjs +480 -28
- package/lib/browser-engine.mjs +28 -1
- package/lib/cid-node.mjs +77 -0
- package/lib/cid.mjs +146 -0
- package/lib/cipher-host.mjs +126 -0
- package/lib/dataset.mjs +96 -12
- package/lib/engine-integrity.mjs +56 -0
- package/lib/lean-entry.mjs +30 -12
- package/lib/nodes.mjs +318 -17
- package/lib/sbm11-lws.mjs +4 -2
- package/lib/sbm11.mjs +29 -6
- package/lib/version.mjs +1 -1
- package/lws/README.md +9 -0
- package/lws/client.mjs +19 -2
- package/lws/persist.mjs +169 -0
- package/lws/server.mjs +82 -29
- package/package.json +2 -1
- package/solid/server/index.mjs +79 -13
- package/webdav/server.mjs +154 -25
- package/xmpp/client.mjs +332 -0
- package/xmpp/proxy.mjs +114 -0
package/lws/persist.mjs
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// This file decides only where the one file lives and how it is written:
|
|
10
10
|
//
|
|
11
11
|
// DIR/lws-state.json the snapshot, replaced atomically
|
|
12
|
+
// DIR/lws.lock the lock of the one writing process (named by
|
|
13
|
+
// the engine, issue 1135; see the end of this file)
|
|
12
14
|
//
|
|
13
15
|
// No request target ever becomes a file name here, so a request cannot
|
|
14
16
|
// address a file outside DIR.
|
|
@@ -26,6 +28,7 @@
|
|
|
26
28
|
|
|
27
29
|
import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync, existsSync } from 'node:fs'
|
|
28
30
|
import { dirname, join, resolve, sep } from 'node:path'
|
|
31
|
+
import { hostname } from 'node:os'
|
|
29
32
|
|
|
30
33
|
export const STATE_FILE = 'lws-state.json'
|
|
31
34
|
|
|
@@ -323,3 +326,169 @@ export function persistFilesIfChanged (engine, handle, envelope, dir) {
|
|
|
323
326
|
const answered = engine.callBlobIO('lwsPersist', [handle, JSON.stringify({ seed: seedBase64() })], new Uint8Array(0))
|
|
324
327
|
performPlan(dir, answered.envelope.plan, answered.bytes)
|
|
325
328
|
}
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// ONE WRITING PROCESS PER STORAGE DIRECTORY
|
|
332
|
+
// (https://github.com/danbri/factoidal/issues/1135)
|
|
333
|
+
//
|
|
334
|
+
// The engine decides the lock file name, its contents and what a start does
|
|
335
|
+
// (`lwsDirLock`, formal/lean4/L4Factoidal/LWS/DirLock.lean). This host does
|
|
336
|
+
// three things only: the exclusive create of the file it names (flag `wx`,
|
|
337
|
+
// O_EXCL), the liveness probe of a process id it names (`kill(pid, 0)`), and
|
|
338
|
+
// the removal of the file at a clean exit. Each result goes back to the
|
|
339
|
+
// engine as data.
|
|
340
|
+
|
|
341
|
+
/** Random text of this process, drawn once: it tells a lock this process
|
|
342
|
+
* wrote from the lock of an earlier process with the same process id. */
|
|
343
|
+
let processToken = null
|
|
344
|
+
function processTokenText () {
|
|
345
|
+
if (processToken === null) {
|
|
346
|
+
const bytes = new Uint8Array(18)
|
|
347
|
+
globalThis.crypto.getRandomValues(bytes)
|
|
348
|
+
let text = ''
|
|
349
|
+
for (const b of bytes) text += String.fromCharCode(b)
|
|
350
|
+
processToken = btoa(text)
|
|
351
|
+
}
|
|
352
|
+
return processToken
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** The lock files this process holds: absolute path -> contents. */
|
|
356
|
+
const heldLocks = new Map()
|
|
357
|
+
let exitHookSet = false
|
|
358
|
+
|
|
359
|
+
/** Remove a lock file this process holds, when it still holds this
|
|
360
|
+
* process's contents. Pure I/O. */
|
|
361
|
+
function removeHeldLock (file) {
|
|
362
|
+
if (!heldLocks.has(file)) return
|
|
363
|
+
const contents = heldLocks.get(file)
|
|
364
|
+
heldLocks.delete(file)
|
|
365
|
+
try {
|
|
366
|
+
if (readFileSync(file, 'utf8') === contents) unlinkSync(file)
|
|
367
|
+
} catch (_error) {
|
|
368
|
+
// Already gone, or not readable: nothing to remove.
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** At process exit (normal end, `process.exit`, and after a SIGINT or
|
|
373
|
+
* SIGTERM handler ends the process), remove every lock still held. */
|
|
374
|
+
function ensureExitHook () {
|
|
375
|
+
if (exitHookSet || globalThis.process === undefined || typeof globalThis.process.on !== 'function') return
|
|
376
|
+
exitHookSet = true
|
|
377
|
+
globalThis.process.on('exit', () => {
|
|
378
|
+
for (const file of [...heldLocks.keys()]) removeHeldLock(file)
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Is process `pid` alive on this host? Pure I/O: `kill(pid, 0)`; EPERM
|
|
383
|
+
* means it exists under another user. */
|
|
384
|
+
function processAlive (pid) {
|
|
385
|
+
try {
|
|
386
|
+
globalThis.process.kill(pid, 0)
|
|
387
|
+
return true
|
|
388
|
+
} catch (error) {
|
|
389
|
+
return error !== null && typeof error === 'object' && error.code === 'EPERM'
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Create `file` with O_EXCL and write `contents`; false when it exists. */
|
|
394
|
+
function createExclusive (file, contents) {
|
|
395
|
+
let fd
|
|
396
|
+
try {
|
|
397
|
+
fd = openSync(file, 'wx', 0o600)
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (error.code === 'EEXIST') return false
|
|
400
|
+
throw error
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
writeSync(fd, contents)
|
|
404
|
+
fsyncSync(fd)
|
|
405
|
+
} finally {
|
|
406
|
+
closeSync(fd)
|
|
407
|
+
}
|
|
408
|
+
return true
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** An error that stops a start because another process holds the storage
|
|
412
|
+
* directory. `envelope` carries the engine's message, as for every other
|
|
413
|
+
* refusal of a start. */
|
|
414
|
+
export class StorageLockedError extends Error {
|
|
415
|
+
constructor (message) {
|
|
416
|
+
super(message)
|
|
417
|
+
this.name = 'StorageLockedError'
|
|
418
|
+
this.envelope = { ok: false, error: message }
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Take the lock of storage directory `dir` for `command` (`lws-serve`,
|
|
424
|
+
* `solid-serve`, `webdav-serve`). Returns `{ file, warning, release }`:
|
|
425
|
+
* `warning` is the engine's text when it took over a stale lock, else null;
|
|
426
|
+
* `release()` removes the lock. Throws `StorageLockedError` with the
|
|
427
|
+
* engine's message when the engine refuses.
|
|
428
|
+
*/
|
|
429
|
+
export function acquireDirLock (engine, dir, command) {
|
|
430
|
+
const root = resolve(dir)
|
|
431
|
+
mkdirSync(root, { recursive: true })
|
|
432
|
+
const base = {
|
|
433
|
+
dir: root,
|
|
434
|
+
pid: globalThis.process.pid,
|
|
435
|
+
command,
|
|
436
|
+
started: new Date().toISOString(),
|
|
437
|
+
host: hostname(),
|
|
438
|
+
token: processTokenText()
|
|
439
|
+
}
|
|
440
|
+
let request = { ...base }
|
|
441
|
+
// Each round is one engine answer; a create that loses to another start
|
|
442
|
+
// reads the file again and asks again.
|
|
443
|
+
for (let round = 0; round < 8; round += 1) {
|
|
444
|
+
const told = engine.call('lwsDirLock', [JSON.stringify(request)])
|
|
445
|
+
if (told === null || typeof told !== 'object' || told.ok === false) {
|
|
446
|
+
throw Object.assign(new Error(`lwsDirLock: ${told && told.error ? told.error : 'no answer'}`), { envelope: told })
|
|
447
|
+
}
|
|
448
|
+
switch (told.action) {
|
|
449
|
+
case 'create':
|
|
450
|
+
case 'replace': {
|
|
451
|
+
const file = insideDir(root, told.file)
|
|
452
|
+
if (told.action === 'replace') {
|
|
453
|
+
try { unlinkSync(file) } catch (error) { if (error.code !== 'ENOENT') throw error }
|
|
454
|
+
}
|
|
455
|
+
if (createExclusive(file, told.contents)) {
|
|
456
|
+
heldLocks.set(file, told.contents)
|
|
457
|
+
ensureExitHook()
|
|
458
|
+
return {
|
|
459
|
+
file,
|
|
460
|
+
warning: told.action === 'replace' ? told.warning : null,
|
|
461
|
+
release: () => removeHeldLock(file)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
let present = null
|
|
465
|
+
try { present = readFileSync(file, 'utf8') } catch (error) { if (error.code !== 'ENOENT') throw error }
|
|
466
|
+
request = present === null ? { ...base } : { ...base, present }
|
|
467
|
+
break
|
|
468
|
+
}
|
|
469
|
+
case 'probe':
|
|
470
|
+
request = { ...request, alive: processAlive(Number(told.pid)) }
|
|
471
|
+
break
|
|
472
|
+
case 'refuse':
|
|
473
|
+
throw new StorageLockedError(told.message)
|
|
474
|
+
default:
|
|
475
|
+
throw new Error(`lwsDirLock answered an unknown action ${String(told.action)}`)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
throw new Error('lwsDirLock: the lock file changed on every attempt')
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* The start notice about encryption at rest of the storage in `dir`
|
|
483
|
+
* (`lwsAtRestNotice`): `{ sealed, keyId?, notice }`. The key text is passed
|
|
484
|
+
* unread; the engine checks it.
|
|
485
|
+
*/
|
|
486
|
+
export function atRestNotice (engine, dir, storageKey) {
|
|
487
|
+
const request = { dir: resolve(dir) }
|
|
488
|
+
if (typeof storageKey === 'string') request.storageKey = storageKey
|
|
489
|
+
const told = engine.call('lwsAtRestNotice', [JSON.stringify(request)])
|
|
490
|
+
if (told === null || typeof told !== 'object' || told.ok === false) {
|
|
491
|
+
throw Object.assign(new Error(`lwsAtRestNotice: ${told && told.error ? told.error : 'no answer'}`), { envelope: told })
|
|
492
|
+
}
|
|
493
|
+
return told
|
|
494
|
+
}
|
package/lws/server.mjs
CHANGED
|
@@ -84,11 +84,11 @@
|
|
|
84
84
|
// produced and this code keeps working unchanged.
|
|
85
85
|
|
|
86
86
|
import { loadEngine } from '../bin/engine.mjs'
|
|
87
|
-
import { readSnapshot, persistIfChanged, storageKeyText, storageModeText, loadFiles, persistFilesIfChanged, readStepBodies } from './persist.mjs'
|
|
87
|
+
import { readSnapshot, persistIfChanged, storageKeyText, storageModeText, loadFiles, persistFilesIfChanged, readStepBodies, acquireDirLock, atRestNotice } from './persist.mjs'
|
|
88
88
|
|
|
89
89
|
/** The ops this host needs from the engine. */
|
|
90
90
|
export const LWS_OPS = ['lwsOpen', 'lwsStep', 'lwsClose', 'lwsSnapshot', 'lwsFetched', 'lwsDelivered',
|
|
91
|
-
'lwsLoad', 'lwsPersist', 'lwsRead']
|
|
91
|
+
'lwsLoad', 'lwsPersist', 'lwsRead', 'lwsDirLock', 'lwsAtRestNotice']
|
|
92
92
|
|
|
93
93
|
/** How long this host waits for the answer to one delivery request before
|
|
94
94
|
* it reports "no answer" (status 0) to the engine. */
|
|
@@ -254,8 +254,11 @@ function contentBytes (request) {
|
|
|
254
254
|
* file) or "files" (one file per body and a journal, issue 918); passed unread
|
|
255
255
|
* @param {boolean|string[]} [options.cors] CORS (issue 923): false for none, a list of
|
|
256
256
|
* origins to answer; default: echo any valid request Origin. Passed unread
|
|
257
|
+
* @param {string} [options.lockCommand] the command named in the lock file of DIR
|
|
258
|
+
* (default "lws-serve"; https://github.com/danbri/factoidal/issues/1135)
|
|
257
259
|
* @param {object} [options.engine] an engine handle to reuse
|
|
258
|
-
* @returns {Promise<{step: Function, close: Function, handle: string, engine: object
|
|
260
|
+
* @returns {Promise<{step: Function, close: Function, handle: string, engine: object,
|
|
261
|
+
* lockFile: string|null, notices: string[]}>}
|
|
259
262
|
*/
|
|
260
263
|
export async function openLwsStorage (options = {}) {
|
|
261
264
|
const engine = options.engine !== undefined ? options.engine : await loadEngine()
|
|
@@ -315,8 +318,14 @@ export async function openLwsStorage (options = {}) {
|
|
|
315
318
|
if (typeof options.controllerInbox === 'string') config.controllerInbox = options.controllerInbox
|
|
316
319
|
// CORS (https://github.com/danbri/factoidal/issues/923): `cors` is false or
|
|
317
320
|
// a list of origins, passed unread; absent, the engine echoes the request
|
|
318
|
-
// Origin
|
|
321
|
+
// Origin under authorization and sends no CORS field with insecureNoAuth
|
|
322
|
+
// (issue 1124). L4Factoidal/LWS/Cors.lean decides every CORS field.
|
|
319
323
|
if (options.cors !== undefined) config.cors = options.cors
|
|
324
|
+
// The Host and Origin guard of a storage with insecureNoAuth
|
|
325
|
+
// (https://github.com/danbri/factoidal/issues/1124): extra host names and
|
|
326
|
+
// origins, passed unread; L4Factoidal/HTTP/LocalGuard.lean decides.
|
|
327
|
+
if (Array.isArray(options.allowHosts)) config.allowHosts = options.allowHosts
|
|
328
|
+
if (Array.isArray(options.allowOrigins)) config.allowOrigins = options.allowOrigins
|
|
320
329
|
// What this host bound: data for the engine's inbox check (issue 915).
|
|
321
330
|
if (options.listen !== undefined && options.listen !== null) config.listen = options.listen
|
|
322
331
|
config.entropy = entropyBase64()
|
|
@@ -335,36 +344,69 @@ export async function openLwsStorage (options = {}) {
|
|
|
335
344
|
const dir = typeof options.dir === 'string' ? options.dir : null
|
|
336
345
|
const storage = dir !== null ? storageModeText(options.storage) : options.storage
|
|
337
346
|
if (typeof storage === 'string') config.storage = storage
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
347
|
+
// One writing process per storage directory
|
|
348
|
+
// (https://github.com/danbri/factoidal/issues/1135): the lock is taken
|
|
349
|
+
// before anything in DIR is read, and released by `close` and at process
|
|
350
|
+
// exit. The engine decides the file, its contents and every refusal
|
|
351
|
+
// (L4Factoidal/LWS/DirLock.lean); a refusal is thrown with its message.
|
|
352
|
+
let lock = null
|
|
353
|
+
if (dir !== null) {
|
|
354
|
+
try {
|
|
355
|
+
lock = acquireDirLock(engine, dir, typeof options.lockCommand === 'string' ? options.lockCommand : 'lws-serve')
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (isUnknownOp(error)) {
|
|
358
|
+
throw new LwsHostError(
|
|
359
|
+
'the loaded l4factoidal WebAssembly module does not carry lwsDirLock',
|
|
360
|
+
{ unknownOp: true })
|
|
361
|
+
}
|
|
362
|
+
throw error
|
|
363
|
+
}
|
|
343
364
|
}
|
|
344
365
|
let opened
|
|
366
|
+
let handle
|
|
367
|
+
let filesMode
|
|
368
|
+
let notices = []
|
|
345
369
|
try {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
{ unknownOp: true })
|
|
370
|
+
// `dir` names the storage directory. The snapshot found there, if any,
|
|
371
|
+
// is the storage to continue from; the engine checks it.
|
|
372
|
+
if (dir !== null && storage !== 'files') {
|
|
373
|
+
const snapshot = readSnapshot(dir)
|
|
374
|
+
if (snapshot !== null) config.snapshot = snapshot
|
|
352
375
|
}
|
|
376
|
+
try {
|
|
377
|
+
opened = engine.call('lwsOpen', [JSON.stringify(config)])
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (isUnknownOp(error)) {
|
|
380
|
+
throw new LwsHostError(
|
|
381
|
+
'the loaded l4factoidal WebAssembly module does not carry lwsOpen',
|
|
382
|
+
{ unknownOp: true })
|
|
383
|
+
}
|
|
384
|
+
throw error
|
|
385
|
+
}
|
|
386
|
+
handle = handleOf(opened)
|
|
387
|
+
if (handle === null) {
|
|
388
|
+
throw new LwsHostError('lwsOpen answered with no handle')
|
|
389
|
+
}
|
|
390
|
+
// A storage kept in files: read the files the engine names until it is
|
|
391
|
+
// ready, and perform the plan of the start. Without a DIR, there are no
|
|
392
|
+
// files: the engine is told none is present.
|
|
393
|
+
filesMode = opened !== null && typeof opened === 'object' &&
|
|
394
|
+
opened.load !== undefined && opened.load !== null
|
|
395
|
+
if (filesMode) {
|
|
396
|
+
if (dir === null) throw new LwsHostError('a storage kept in files needs a storage directory (dir)')
|
|
397
|
+
loadFiles(engine, handle, opened, dir)
|
|
398
|
+
}
|
|
399
|
+
// What a server prints at start, after the experimental code notice:
|
|
400
|
+
// a take-over of a stale lock, and whether the storage on disk is
|
|
401
|
+
// sealed (encryption at rest; the engine words both).
|
|
402
|
+
if (dir !== null) {
|
|
403
|
+
if (lock.warning !== null) notices.push(`WARNING: ${lock.warning}`)
|
|
404
|
+
notices.push(atRestNotice(engine, dir, storageKey).notice)
|
|
405
|
+
}
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if (lock !== null) lock.release()
|
|
353
408
|
throw error
|
|
354
409
|
}
|
|
355
|
-
const handle = handleOf(opened)
|
|
356
|
-
if (handle === null) {
|
|
357
|
-
throw new LwsHostError('lwsOpen answered with no handle')
|
|
358
|
-
}
|
|
359
|
-
// A storage kept in files: read the files the engine names until it is
|
|
360
|
-
// ready, and perform the plan of the start. Without a DIR, there are no
|
|
361
|
-
// files: the engine is told none is present.
|
|
362
|
-
const filesMode = opened !== null && typeof opened === 'object' &&
|
|
363
|
-
opened.load !== undefined && opened.load !== null
|
|
364
|
-
if (filesMode) {
|
|
365
|
-
if (dir === null) throw new LwsHostError('a storage kept in files needs a storage directory (dir)')
|
|
366
|
-
loadFiles(engine, handle, opened, dir)
|
|
367
|
-
}
|
|
368
410
|
/** After a call whose envelope may say `changed`: write what the engine
|
|
369
411
|
* gives (the snapshot, or the plan of a storage kept in files). */
|
|
370
412
|
const persist = (envelope) => {
|
|
@@ -448,6 +490,10 @@ export async function openLwsStorage (options = {}) {
|
|
|
448
490
|
return {
|
|
449
491
|
engine,
|
|
450
492
|
handle,
|
|
493
|
+
/** The lock file of the storage directory, or null with no DIR. */
|
|
494
|
+
lockFile: lock !== null ? lock.file : null,
|
|
495
|
+
/** The start notices, for the server to print on standard error. */
|
|
496
|
+
notices,
|
|
451
497
|
/**
|
|
452
498
|
* One protocol step.
|
|
453
499
|
* @param {{method: string, target: string,
|
|
@@ -538,6 +584,7 @@ export async function openLwsStorage (options = {}) {
|
|
|
538
584
|
closed = true
|
|
539
585
|
if (flushTimer !== null) clearTimeout(flushTimer)
|
|
540
586
|
try { engine.call('lwsClose', [handle]) } catch (_error) { /* already gone */ }
|
|
587
|
+
if (lock !== null) lock.release()
|
|
541
588
|
}
|
|
542
589
|
}
|
|
543
590
|
}
|
|
@@ -593,6 +640,12 @@ export async function createLwsServer (options = {}) {
|
|
|
593
640
|
* (L4Factoidal/Authz/Mount.lean). */
|
|
594
641
|
function storage () {
|
|
595
642
|
if (opening === null) {
|
|
643
|
+
// What the socket bound, when the caller listened itself and did not
|
|
644
|
+
// say (data for the engine's Host check, issue 1124).
|
|
645
|
+
const bound = server.address()
|
|
646
|
+
if (settings.listen === undefined && bound !== null && typeof bound === 'object') {
|
|
647
|
+
settings = { ...settings, listen: { address: bound.address, port: bound.port, addresses: [] } }
|
|
648
|
+
}
|
|
596
649
|
opening = (async () => {
|
|
597
650
|
let storageSettings = settings
|
|
598
651
|
if (settings.authz !== undefined && settings.authz !== null) {
|
|
@@ -720,7 +773,7 @@ export async function listen (options = {}) {
|
|
|
720
773
|
const bound = created.server.address()
|
|
721
774
|
const origin = `http://${host}:${bound.port}`
|
|
722
775
|
if (typeof options.baseIri !== 'string') created.setBaseIri(`${origin}/`)
|
|
723
|
-
created.setListen({ address: bound.address, port: bound.port, addresses: await interfaceAddresses() })
|
|
776
|
+
created.setListen({ host, address: bound.address, port: bound.port, addresses: await interfaceAddresses() })
|
|
724
777
|
try {
|
|
725
778
|
await created.storage()
|
|
726
779
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@factoidal/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "EXPERIMENTAL UNREVIEWED CODE: do not run public services on it without careful independent expert review. RDF parsing and writing (Turtle, TriG, N-Triples, N-Quads, RDF/XML, JSON-LD), SPARQL 1.1 and 1.2, RDFS and OWL reasoning, SHACL and ShEx validation, CSVW and RML mapping, RDFC-1.0, and SPARQL, LWS and Solid servers, for Node, Deno and browsers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sparql",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"./store-host/paths": "./store-host/paths.mjs",
|
|
71
71
|
"./xmpp": "./xmpp/index.mjs",
|
|
72
72
|
"./xmpp/server": "./xmpp/server.mjs",
|
|
73
|
+
"./cipher-host": "./lib/cipher-host.mjs",
|
|
73
74
|
"./store": {
|
|
74
75
|
"default": "./bin/store.mjs"
|
|
75
76
|
},
|
package/solid/server/index.mjs
CHANGED
|
@@ -55,8 +55,9 @@
|
|
|
55
55
|
// loader, by the design record's rule that the two conformance classes
|
|
56
56
|
// stay distinct.
|
|
57
57
|
|
|
58
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
58
59
|
import { loadEngine } from '../../bin/engine.mjs'
|
|
59
|
-
import { readSnapshot, persistIfChanged, storageKeyText } from '../../lws/persist.mjs'
|
|
60
|
+
import { readSnapshot, persistIfChanged, storageKeyText, acquireDirLock, atRestNotice } from '../../lws/persist.mjs'
|
|
60
61
|
|
|
61
62
|
/** The ops this host needs from the engine. */
|
|
62
63
|
export const SOLID_SERVER_OPS = ['solidOpen', 'solidStep', 'solidClose', 'solidSnapshot',
|
|
@@ -187,6 +188,11 @@ export async function openSolidStorage (options = {}) {
|
|
|
187
188
|
if (options.auth !== undefined && options.auth !== null) config.auth = options.auth
|
|
188
189
|
if (options.insecureNoAuth === true) config.insecureNoAuth = true
|
|
189
190
|
if (typeof options.enforceWac === 'boolean') config.enforceWac = options.enforceWac
|
|
191
|
+
// The Host and Origin guard with insecureNoAuth (issue 1124): what the
|
|
192
|
+
// socket bound and the operator's names and origins, passed unread.
|
|
193
|
+
if (options.listen !== undefined && options.listen !== null) config.listen = options.listen
|
|
194
|
+
if (Array.isArray(options.allowHosts)) config.allowHosts = options.allowHosts
|
|
195
|
+
if (Array.isArray(options.allowOrigins)) config.allowOrigins = options.allowOrigins
|
|
190
196
|
if (typeof options.rootAcl === 'string') config.rootAcl = options.rootAcl
|
|
191
197
|
// Encryption at rest (issue 921), as the LWS host: the key text from
|
|
192
198
|
// `storageKey` or FACTOIDAL_LWS_KEY, passed unread; the engine decides.
|
|
@@ -195,28 +201,65 @@ export async function openSolidStorage (options = {}) {
|
|
|
195
201
|
// `dir` names the storage directory. The snapshot found there, if any,
|
|
196
202
|
// is the storage to continue from; the engine checks it.
|
|
197
203
|
const dir = typeof options.dir === 'string' ? options.dir : null
|
|
204
|
+
// One writing process per storage directory
|
|
205
|
+
// (https://github.com/danbri/factoidal/issues/1135), as the LWS host: the
|
|
206
|
+
// engine decides; the lock is taken before DIR is read.
|
|
207
|
+
let lock = null
|
|
198
208
|
if (dir !== null) {
|
|
199
|
-
|
|
200
|
-
|
|
209
|
+
try {
|
|
210
|
+
lock = acquireDirLock(engine, dir, 'solid-serve')
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (isUnknownOp(error)) {
|
|
213
|
+
throw new SolidServerHostError(
|
|
214
|
+
'the loaded l4factoidal WebAssembly module does not carry lwsDirLock',
|
|
215
|
+
{ unknownOp: true })
|
|
216
|
+
}
|
|
217
|
+
throw error
|
|
218
|
+
}
|
|
201
219
|
}
|
|
202
220
|
let opened
|
|
221
|
+
let handle
|
|
222
|
+
const notices = []
|
|
203
223
|
try {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
224
|
+
if (dir !== null) {
|
|
225
|
+
const snapshot = readSnapshot(dir)
|
|
226
|
+
if (snapshot !== null) config.snapshot = snapshot
|
|
227
|
+
// The names in DIR, unread: the engine refuses a storage that LWS
|
|
228
|
+
// keeps there (`solidRefusesLws`, Wasm/Ops/Solid.lean); only lws-serve
|
|
229
|
+
// serves an LWS storage.
|
|
230
|
+
config.dirFiles = existsSync(dir) ? readdirSync(dir) : []
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
opened = engine.call('solidOpen', [JSON.stringify(config)])
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (isUnknownOp(error)) {
|
|
236
|
+
throw new SolidServerHostError(
|
|
237
|
+
'the loaded l4factoidal WebAssembly module does not carry solidOpen',
|
|
238
|
+
{ unknownOp: true })
|
|
239
|
+
}
|
|
240
|
+
throw error
|
|
241
|
+
}
|
|
242
|
+
handle = handleOf(opened)
|
|
243
|
+
if (handle === null) {
|
|
244
|
+
throw new SolidServerHostError('solidOpen answered with no handle')
|
|
210
245
|
}
|
|
246
|
+
// The start notices: a take-over of a stale lock, and whether the
|
|
247
|
+
// storage on disk is sealed (encryption at rest).
|
|
248
|
+
if (dir !== null) {
|
|
249
|
+
if (lock.warning !== null) notices.push(`WARNING: ${lock.warning}`)
|
|
250
|
+
notices.push(atRestNotice(engine, dir, storageKey).notice)
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (lock !== null) lock.release()
|
|
211
254
|
throw error
|
|
212
255
|
}
|
|
213
|
-
const handle = handleOf(opened)
|
|
214
|
-
if (handle === null) {
|
|
215
|
-
throw new SolidServerHostError('solidOpen answered with no handle')
|
|
216
|
-
}
|
|
217
256
|
return {
|
|
218
257
|
engine,
|
|
219
258
|
handle,
|
|
259
|
+
/** The lock file of the storage directory, or null with no DIR. */
|
|
260
|
+
lockFile: lock !== null ? lock.file : null,
|
|
261
|
+
/** The start notices, for the server to print on standard error. */
|
|
262
|
+
notices,
|
|
220
263
|
storageIri: typeof opened.storage === 'string' ? opened.storage : null,
|
|
221
264
|
/**
|
|
222
265
|
* One protocol step.
|
|
@@ -258,6 +301,7 @@ export async function openSolidStorage (options = {}) {
|
|
|
258
301
|
},
|
|
259
302
|
close () {
|
|
260
303
|
try { engine.call('solidClose', [handle]) } catch (_error) { /* already gone */ }
|
|
304
|
+
if (lock !== null) lock.release()
|
|
261
305
|
}
|
|
262
306
|
}
|
|
263
307
|
}
|
|
@@ -302,6 +346,10 @@ export async function createSolidServer (options = {}) {
|
|
|
302
346
|
|
|
303
347
|
function storage () {
|
|
304
348
|
if (opening === null) {
|
|
349
|
+
const bound = server.address()
|
|
350
|
+
if (settings.listen === undefined && bound !== null && typeof bound === 'object') {
|
|
351
|
+
settings = { ...settings, listen: { address: bound.address, port: bound.port, addresses: [] } }
|
|
352
|
+
}
|
|
305
353
|
opening = openSolidStorage(settings).then((value) => { opened = value; return value })
|
|
306
354
|
}
|
|
307
355
|
return opening
|
|
@@ -345,6 +393,13 @@ export async function createSolidServer (options = {}) {
|
|
|
345
393
|
}
|
|
346
394
|
settings = { ...settings, baseIri: value }
|
|
347
395
|
},
|
|
396
|
+
/** Pass what the socket bound, before the first request (issue 1124). */
|
|
397
|
+
setListen (value) {
|
|
398
|
+
if (opening !== null) {
|
|
399
|
+
throw new SolidServerHostError('the storage is already open; set listen first')
|
|
400
|
+
}
|
|
401
|
+
settings = { ...settings, listen: value }
|
|
402
|
+
},
|
|
348
403
|
close () {
|
|
349
404
|
return new Promise((resolve) => {
|
|
350
405
|
server.close(() => {
|
|
@@ -380,6 +435,16 @@ function writeHeaders (response, pairs) {
|
|
|
380
435
|
* @returns {Promise<{server: object, storage: Function, port: number,
|
|
381
436
|
* origin: string, close: Function}>}
|
|
382
437
|
*/
|
|
438
|
+
/** The addresses of this machine's interfaces. Pure I/O. */
|
|
439
|
+
async function interfaceAddresses () {
|
|
440
|
+
try {
|
|
441
|
+
const os = await import('node:os')
|
|
442
|
+
return Object.values(os.networkInterfaces()).flat().filter(Boolean).map((i) => i.address)
|
|
443
|
+
} catch (_error) {
|
|
444
|
+
return []
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
383
448
|
export async function listen (options = {}) {
|
|
384
449
|
const port = typeof options.port === 'number' ? options.port : 0
|
|
385
450
|
const host = typeof options.host === 'string' ? options.host : '127.0.0.1'
|
|
@@ -391,6 +456,7 @@ export async function listen (options = {}) {
|
|
|
391
456
|
const bound = created.server.address()
|
|
392
457
|
const origin = `http://${host}:${bound.port}`
|
|
393
458
|
if (typeof options.baseIri !== 'string') created.setBaseIri(`${origin}/`)
|
|
459
|
+
created.setListen({ host, address: bound.address, port: bound.port, addresses: await interfaceAddresses() })
|
|
394
460
|
try {
|
|
395
461
|
await created.storage()
|
|
396
462
|
} catch (error) {
|