@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,100 @@
|
|
|
1
|
+
# `@factoidal/core/solid/client` — a `fetch` host for the Solid Protocol
|
|
2
|
+
|
|
3
|
+
The Solid Protocol v0.11.0 client conformance class
|
|
4
|
+
(<https://solidproject.org/TR/protocol>) over the platform `fetch`.
|
|
5
|
+
|
|
6
|
+
Tracking issue: <https://github.com/danbri/factoidal/issues/659>.
|
|
7
|
+
Design record:
|
|
8
|
+
[`docs/designissues/2026-09-06-lws-and-solid-protocols.md`](../../../../docs/designissues/2026-09-06-lws-and-solid-protocols.md).
|
|
9
|
+
Conformance ledger:
|
|
10
|
+
[`docs/lws-solid-conformance.md`](../../../../docs/lws-solid-conformance.md).
|
|
11
|
+
|
|
12
|
+
## What is here and what is not
|
|
13
|
+
|
|
14
|
+
`index.mjs` calls `solidClientRequest` to obtain a request record, puts
|
|
15
|
+
that record on the wire, and gives the reply to `solidClientResponse` to
|
|
16
|
+
be interpreted. Every request the client sends was built by the engine
|
|
17
|
+
and every response it reports was read by the engine.
|
|
18
|
+
|
|
19
|
+
This file composes no URL, sets no `Content-Type`, adds no `Slug`, reads
|
|
20
|
+
no `Link` header and parses no body. Discovery, request construction,
|
|
21
|
+
response interpretation and the WebID profile read are in
|
|
22
|
+
`formal/lean4/L4Factoidal/Solid/Client/`.
|
|
23
|
+
|
|
24
|
+
It shares no code with the server host beyond the engine loader.
|
|
25
|
+
|
|
26
|
+
## The wasm contract
|
|
27
|
+
|
|
28
|
+
| op | arguments | answer |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `solidClientRequest` | `[kind, argsJson]` | `{ ok, request }` |
|
|
31
|
+
| `solidClientResponse` | `[kind, responseJson]` | `{ ok, interpretation }` |
|
|
32
|
+
|
|
33
|
+
The two operations take different kind vocabularies and this host keeps
|
|
34
|
+
them apart rather than deriving one from the other:
|
|
35
|
+
|
|
36
|
+
* request kinds — `read`, `create`, `replace`, `patch`, `delete`,
|
|
37
|
+
`discoverStorage`, `readProfile`;
|
|
38
|
+
* interpretation kinds — `storage`, `containment`, `auxiliaries`,
|
|
39
|
+
`profile`, `wacAllow`.
|
|
40
|
+
|
|
41
|
+
Which interpretation a reply wants is a protocol decision, so this file
|
|
42
|
+
never picks one. It uses the `interpret` member the request envelope
|
|
43
|
+
named, then the `interpret` the caller passed. With neither, the reply is
|
|
44
|
+
returned uninterpreted and `interpretation` is `null` — the raw record,
|
|
45
|
+
never a host reading of it.
|
|
46
|
+
|
|
47
|
+
A multi-step operation is a loop, not a special case. An interpretation
|
|
48
|
+
that answers `{ continue: true, state }` sends the state back into the
|
|
49
|
+
next `solidClientRequest`; walking from a resource up to its storage root
|
|
50
|
+
is the case that needs it. A single-request operation runs the loop once.
|
|
51
|
+
The loop is capped (`maxSteps`, default 16) so a state that never settles
|
|
52
|
+
is a reported failure rather than a hang.
|
|
53
|
+
|
|
54
|
+
The full contract is `docs/lws-solid-conformance.md`, section "wasm ABI".
|
|
55
|
+
|
|
56
|
+
## Use
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { createSolidClient } from '@factoidal/core/solid/client'
|
|
60
|
+
|
|
61
|
+
const client = await createSolidClient({ baseIri: 'https://pod.example/' })
|
|
62
|
+
const storage = await client.discoverStorage('/alice/notes/one')
|
|
63
|
+
await client.replace('/alice/notes/one', '<#a> <#b> <#c> .', 'text/turtle')
|
|
64
|
+
const read = await client.read('/alice/notes/one')
|
|
65
|
+
await client.delete('/alice/notes/one')
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
factoidal solid-client get https://pod.example/alice/notes/one
|
|
70
|
+
factoidal solid-client put https://pod.example/alice/notes/one \
|
|
71
|
+
--file note.ttl --content-type text/turtle
|
|
72
|
+
factoidal solid-client discover https://pod.example/alice/notes/one
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Exports
|
|
76
|
+
|
|
77
|
+
| name | what it is |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `createSolidClient(options)` | a client bound to one engine and one `fetch` |
|
|
80
|
+
| `solidClientOpsAvailable(engine)` | probe the loaded module for the ops |
|
|
81
|
+
| `SOLID_CLIENT_OPS` | the two op names |
|
|
82
|
+
| `SOLID_REQUEST_KINDS` | the seven request kinds the ABI names |
|
|
83
|
+
| `SOLID_INTERPRETATION_KINDS` | the five interpretation kinds |
|
|
84
|
+
| `SolidClientHostError` | a host failure, with `unknownOp` |
|
|
85
|
+
|
|
86
|
+
`options.fetch` replaces the platform `fetch`, which is how a test drives
|
|
87
|
+
the client at an in-process server without a proxy. `options.baseIri`
|
|
88
|
+
says where to send a request whose target the engine wrote as a path;
|
|
89
|
+
an absolute target is used verbatim.
|
|
90
|
+
|
|
91
|
+
The command's five verbs are aliases for the request kinds — `get`→`read`,
|
|
92
|
+
`put`→`replace`, `post`→`create`, `delete`→`delete`,
|
|
93
|
+
`discover`→`discoverStorage` — and a kind name is accepted directly.
|
|
94
|
+
|
|
95
|
+
## Tests and interop
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
node tests/solid/client/against-own-server.mjs # against our own server
|
|
99
|
+
bash tools/solid-client-interop.sh # against Community Solid Server
|
|
100
|
+
```
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// A `fetch` host for the Solid Protocol v0.11.0 client 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
|
+
// Call `solidClientRequest` to get a request record, put that record on
|
|
8
|
+
// the wire with `fetch`, and give the reply to `solidClientResponse` to
|
|
9
|
+
// be interpreted. It is a socket and nothing else.
|
|
10
|
+
//
|
|
11
|
+
// It composes no URL, sets no `Content-Type`, adds no `Slug`, reads no
|
|
12
|
+
// `Link` header and parses no body. Discovery, request construction,
|
|
13
|
+
// response interpretation and the WebID profile read are in
|
|
14
|
+
// `formal/lean4/L4Factoidal/Solid/Client/` (iron rule 7 of CLAUDE.md).
|
|
15
|
+
// A reviewer who finds a header name, a media type or a link relation
|
|
16
|
+
// in this file has found a rule violation.
|
|
17
|
+
//
|
|
18
|
+
// THE WASM CONTRACT
|
|
19
|
+
// Fixed in docs/lws-solid-conformance.md, section "wasm ABI".
|
|
20
|
+
// solidClientRequest [kind, argsJson] -> { ok, request }
|
|
21
|
+
// solidClientResponse [kind, responseJson] -> { ok, interpretation }
|
|
22
|
+
//
|
|
23
|
+
// request { method, target, headers: [[name, value]], body }
|
|
24
|
+
// response { status, headers: [[name, value]], body, target }
|
|
25
|
+
//
|
|
26
|
+
// The `target` of a response record is the request target the reply
|
|
27
|
+
// answers, echoed back from the request the engine built. The `storage`
|
|
28
|
+
// interpretation needs it to move up the path hierarchy.
|
|
29
|
+
//
|
|
30
|
+
// The two operations take DIFFERENT kind vocabularies, and this host
|
|
31
|
+
// keeps them apart rather than deriving one from the other.
|
|
32
|
+
//
|
|
33
|
+
// request kinds: read, create, replace, patch, delete,
|
|
34
|
+
// discoverStorage, readProfile
|
|
35
|
+
// interpretation kinds: storage, containment, auxiliaries, profile,
|
|
36
|
+
// wacAllow
|
|
37
|
+
//
|
|
38
|
+
// Which interpretation a reply wants is a protocol decision, so this
|
|
39
|
+
// file never picks one. It uses, in order: the `interpret` member the
|
|
40
|
+
// request envelope named, then the `interpret` the caller passed. With
|
|
41
|
+
// neither, the reply is returned uninterpreted and `interpretation` is
|
|
42
|
+
// null -- the raw record, never a host reading of it.
|
|
43
|
+
//
|
|
44
|
+
// A multi-step operation is a loop, not a special case: an
|
|
45
|
+
// interpretation that answers `{ continue: true, state }` sends the
|
|
46
|
+
// state back into the next `solidClientRequest`. Walking from a resource
|
|
47
|
+
// up to its storage root is the case that needs it. A single-request
|
|
48
|
+
// operation runs the loop once, and the loop is capped so a state that
|
|
49
|
+
// never settles is a reported failure rather than a hang.
|
|
50
|
+
//
|
|
51
|
+
// This file shares no code with `../server/index.mjs` beyond the engine
|
|
52
|
+
// loader.
|
|
53
|
+
|
|
54
|
+
import { loadEngine } from '../../bin/engine.mjs'
|
|
55
|
+
|
|
56
|
+
/** The ops this host needs from the engine. */
|
|
57
|
+
export const SOLID_CLIENT_OPS = ['solidClientRequest', 'solidClientResponse']
|
|
58
|
+
|
|
59
|
+
/** The request kinds the ABI names. Listed so a caller can be checked
|
|
60
|
+
* against the contract; this host attaches no meaning to any of them. */
|
|
61
|
+
export const SOLID_REQUEST_KINDS = [
|
|
62
|
+
'read', 'create', 'replace', 'patch', 'delete', 'discoverStorage', 'readProfile'
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
/** The interpretation kinds the ABI names. */
|
|
66
|
+
export const SOLID_INTERPRETATION_KINDS = [
|
|
67
|
+
'storage', 'containment', 'auxiliaries', 'profile', 'wacAllow', 'created'
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
/** An error raised by the Solid client host. `unknownOp` is set when the
|
|
71
|
+
* loaded WebAssembly module does not carry the operation at all. */
|
|
72
|
+
export class SolidClientHostError extends Error {
|
|
73
|
+
constructor (message, detail = {}) {
|
|
74
|
+
super(message)
|
|
75
|
+
this.name = 'SolidClientHostError'
|
|
76
|
+
this.unknownOp = detail.unknownOp === true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isUnknownOp (error) {
|
|
81
|
+
const text = error && error.message ? String(error.message) : String(error)
|
|
82
|
+
return text.indexOf('unknown op') >= 0
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether the loaded engine carries the Solid client operations.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} engine the handle from `loadEngine()`
|
|
89
|
+
* @returns {Promise<{available: boolean, reason: string}>}
|
|
90
|
+
*/
|
|
91
|
+
export async function solidClientOpsAvailable (engine) {
|
|
92
|
+
try {
|
|
93
|
+
engine.call('solidClientRequest', ['read', JSON.stringify({ target: '/' })])
|
|
94
|
+
return { available: true, reason: 'solidClientRequest answered' }
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (isUnknownOp(error)) {
|
|
97
|
+
return {
|
|
98
|
+
available: false,
|
|
99
|
+
reason: 'the loaded l4factoidal WebAssembly module does not carry ' +
|
|
100
|
+
'the solid client ops (rebuild formal/lean4/Wasm)'
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { available: true, reason: 'solidClientRequest answered with an engine error' }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function requestOf (envelope) {
|
|
108
|
+
if (envelope === null || typeof envelope !== 'object') {
|
|
109
|
+
throw new SolidClientHostError('solidClientRequest answered with no envelope')
|
|
110
|
+
}
|
|
111
|
+
const record = (envelope.request !== null && typeof envelope.request === 'object')
|
|
112
|
+
? envelope.request
|
|
113
|
+
: envelope
|
|
114
|
+
if (typeof record.method !== 'string' || typeof record.target !== 'string') {
|
|
115
|
+
throw new SolidClientHostError(
|
|
116
|
+
'solidClientRequest answered with no method and target')
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
method: record.method,
|
|
120
|
+
target: record.target,
|
|
121
|
+
headers: Array.isArray(record.headers) ? record.headers : [],
|
|
122
|
+
body: typeof record.body === 'string' ? record.body : null
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function interpretationOf (envelope) {
|
|
127
|
+
if (envelope === null || typeof envelope !== 'object') {
|
|
128
|
+
throw new SolidClientHostError('solidClientResponse answered with no envelope')
|
|
129
|
+
}
|
|
130
|
+
return (envelope.interpretation !== null && typeof envelope.interpretation === 'object')
|
|
131
|
+
? envelope.interpretation
|
|
132
|
+
: envelope
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The reply as the JSON response record the contract states. */
|
|
136
|
+
async function recordOfReply (reply) {
|
|
137
|
+
const headers = []
|
|
138
|
+
reply.headers.forEach((value, name) => { headers.push([name, value]) })
|
|
139
|
+
return { status: reply.status, headers, body: await reply.text() }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A Solid client bound to one engine and one `fetch`.
|
|
144
|
+
*
|
|
145
|
+
* @param {object} [options]
|
|
146
|
+
* @param {object} [options.engine] an engine handle to reuse
|
|
147
|
+
* @param {Function} [options.fetch] the fetch to use (default `globalThis.fetch`)
|
|
148
|
+
* @param {string} [options.baseIri] prefixed to a request target that is a path
|
|
149
|
+
* @param {number} [options.maxSteps] the loop cap for a multi-step operation
|
|
150
|
+
* @returns {Promise<object>} the client
|
|
151
|
+
*/
|
|
152
|
+
export async function createSolidClient (options = {}) {
|
|
153
|
+
const engine = options.engine !== undefined ? options.engine : await loadEngine()
|
|
154
|
+
const wire = typeof options.fetch === 'function' ? options.fetch : globalThis.fetch
|
|
155
|
+
if (typeof wire !== 'function') {
|
|
156
|
+
throw new SolidClientHostError('no fetch is available in this runtime')
|
|
157
|
+
}
|
|
158
|
+
const maxSteps = typeof options.maxSteps === 'number' ? options.maxSteps : 16
|
|
159
|
+
const baseIri = typeof options.baseIri === 'string' ? options.baseIri : null
|
|
160
|
+
|
|
161
|
+
/** Where to send a request whose target the engine wrote. The engine
|
|
162
|
+
* resolves the target against baseIri (RFC 3986 section 5.2, op
|
|
163
|
+
* resolveIri) and says whether it was already absolute
|
|
164
|
+
* (https://github.com/danbri/factoidal/issues/894 item 5). */
|
|
165
|
+
function endpoint (target) {
|
|
166
|
+
let r
|
|
167
|
+
try {
|
|
168
|
+
r = engine.call('resolveIri', [baseIri === null ? '' : baseIri, String(target)])
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (isUnknownOp(error)) {
|
|
171
|
+
throw new SolidClientHostError(
|
|
172
|
+
'the loaded l4factoidal WebAssembly module does not carry the ' +
|
|
173
|
+
'resolveIri operation; rebuild it with formal/lean4/Wasm/build-wasm.sh')
|
|
174
|
+
}
|
|
175
|
+
throw error
|
|
176
|
+
}
|
|
177
|
+
if (r.referenceAbsolute) return String(target)
|
|
178
|
+
if (baseIri === null) {
|
|
179
|
+
throw new SolidClientHostError(
|
|
180
|
+
`the engine answered the relative reference "${target}" and this client has no ` +
|
|
181
|
+
'baseIri to resolve it against; pass baseIri to createSolidClient')
|
|
182
|
+
}
|
|
183
|
+
return r.iri
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function callRequest (kind, args) {
|
|
187
|
+
try {
|
|
188
|
+
return engine.call('solidClientRequest', [kind, JSON.stringify(args)])
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (isUnknownOp(error)) {
|
|
191
|
+
throw new SolidClientHostError(
|
|
192
|
+
'the loaded l4factoidal WebAssembly module does not carry ' +
|
|
193
|
+
'solidClientRequest', { unknownOp: true })
|
|
194
|
+
}
|
|
195
|
+
throw error
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function callResponse (kind, record) {
|
|
200
|
+
try {
|
|
201
|
+
return engine.call('solidClientResponse', [kind, JSON.stringify(record)])
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (isUnknownOp(error)) {
|
|
204
|
+
throw new SolidClientHostError(
|
|
205
|
+
'the loaded l4factoidal WebAssembly module does not carry ' +
|
|
206
|
+
'solidClientResponse', { unknownOp: true })
|
|
207
|
+
}
|
|
208
|
+
throw error
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Run one client operation to its end.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} kind a request kind from `SOLID_REQUEST_KINDS`
|
|
216
|
+
* @param {object} args the operation's arguments, passed unread
|
|
217
|
+
* @param {object} [extra] `interpret`: an interpretation kind to use
|
|
218
|
+
* when the request envelope names none
|
|
219
|
+
* @returns {Promise<{kind: string, request: object, response: object,
|
|
220
|
+
* interpret: string|null, interpretation: object|null,
|
|
221
|
+
* steps: number}>}
|
|
222
|
+
*/
|
|
223
|
+
async function run (kind, args, extra = {}) {
|
|
224
|
+
let state = null
|
|
225
|
+
for (let step = 1; step <= maxSteps; step += 1) {
|
|
226
|
+
const built = callRequest(kind, state === null ? args : { ...args, state })
|
|
227
|
+
const request = requestOf(built)
|
|
228
|
+
const headers = {}
|
|
229
|
+
for (const pair of request.headers) {
|
|
230
|
+
if (!Array.isArray(pair) || pair.length < 2) continue
|
|
231
|
+
headers[pair[0]] = headers[pair[0]] === undefined
|
|
232
|
+
? String(pair[1])
|
|
233
|
+
: `${headers[pair[0]]}, ${String(pair[1])}`
|
|
234
|
+
}
|
|
235
|
+
const reply = await wire(endpoint(request.target), {
|
|
236
|
+
method: request.method,
|
|
237
|
+
headers,
|
|
238
|
+
// fetch refuses any body on GET and HEAD, even an empty string; the
|
|
239
|
+
// engine's record carries "" there, which is the same message.
|
|
240
|
+
body: (request.body === null || request.method === 'GET' || request.method === 'HEAD')
|
|
241
|
+
? undefined : request.body,
|
|
242
|
+
redirect: 'manual'
|
|
243
|
+
})
|
|
244
|
+
const record = await recordOfReply(reply)
|
|
245
|
+
const interpretKind = typeof built.interpret === 'string'
|
|
246
|
+
? built.interpret
|
|
247
|
+
: (typeof extra.interpret === 'string' ? extra.interpret : null)
|
|
248
|
+
const answer = {
|
|
249
|
+
kind,
|
|
250
|
+
request,
|
|
251
|
+
response: record,
|
|
252
|
+
interpret: interpretKind,
|
|
253
|
+
interpretation: null,
|
|
254
|
+
steps: step
|
|
255
|
+
}
|
|
256
|
+
if (interpretKind === null) return answer
|
|
257
|
+
// The record says what came back; `target` says what was asked. The
|
|
258
|
+
// storage walk needs both, and this is the engine's own target
|
|
259
|
+
// echoed back, not a URL this file composed. `requestUri` is the URL
|
|
260
|
+
// the request went to (the `endpoint` call above): the base IRI of
|
|
261
|
+
// the representation (RFC 3986 §5.1.3), which the engine resolves
|
|
262
|
+
// relative references against.
|
|
263
|
+
const interpretation = interpretationOf(
|
|
264
|
+
callResponse(interpretKind, { ...record, target: request.target,
|
|
265
|
+
requestUri: endpoint(request.target) }))
|
|
266
|
+
answer.interpretation = interpretation
|
|
267
|
+
if (interpretation.continue !== true) return answer
|
|
268
|
+
state = interpretation.state === undefined ? null : interpretation.state
|
|
269
|
+
}
|
|
270
|
+
throw new SolidClientHostError(
|
|
271
|
+
`the "${kind}" operation did not finish within ${maxSteps} requests`)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
engine,
|
|
276
|
+
run,
|
|
277
|
+
/** Named wrappers over `run`. Each one passes its arguments through;
|
|
278
|
+
* none of them adds a header, a media type or a URL. */
|
|
279
|
+
read: (target, extra = {}) => run('read', { target, ...extra }),
|
|
280
|
+
create: (target, body, contentType, extra = {}) =>
|
|
281
|
+
run('create', { target, body, contentType, ...extra }),
|
|
282
|
+
replace: (target, body, contentType, extra = {}) =>
|
|
283
|
+
run('replace', { target, body, contentType, ...extra }),
|
|
284
|
+
patch: (target, body, contentType, extra = {}) =>
|
|
285
|
+
run('patch', { target, body, contentType, ...extra }),
|
|
286
|
+
delete: (target, extra = {}) => run('delete', { target, ...extra }),
|
|
287
|
+
discoverStorage: (target, extra = {}) =>
|
|
288
|
+
run('discoverStorage', { target, interpret: 'storage', ...extra },
|
|
289
|
+
{ interpret: 'storage' }),
|
|
290
|
+
readProfile: (target, extra = {}) =>
|
|
291
|
+
run('readProfile', { target, ...extra }, { interpret: 'profile' })
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# `@factoidal/core/solid/server` — a Node host for the Solid Protocol
|
|
2
|
+
|
|
3
|
+
The Solid Protocol v0.11.0 server conformance class
|
|
4
|
+
(<https://solidproject.org/TR/protocol>) served over Node's `http`
|
|
5
|
+
module.
|
|
6
|
+
|
|
7
|
+
Tracking issue: <https://github.com/danbri/factoidal/issues/659>.
|
|
8
|
+
Design record:
|
|
9
|
+
[`docs/designissues/2026-09-06-lws-and-solid-protocols.md`](../../../../docs/designissues/2026-09-06-lws-and-solid-protocols.md).
|
|
10
|
+
Conformance ledger:
|
|
11
|
+
[`docs/lws-solid-conformance.md`](../../../../docs/lws-solid-conformance.md).
|
|
12
|
+
|
|
13
|
+
## What is here and what is not
|
|
14
|
+
|
|
15
|
+
`index.mjs` is a socket, a body reader and a header writer. Storage
|
|
16
|
+
discovery (`Link: rel="type"` for `pim:Storage`), containment triples,
|
|
17
|
+
slash semantics, `Allow` and the `Accept-*` family, the PUT/POST/PATCH/
|
|
18
|
+
DELETE rules with their status codes, the `acl` and `describedby`
|
|
19
|
+
auxiliary lifecycle, N3 Patch, Web Access Control, CORS and the LDN
|
|
20
|
+
inbox are all in `formal/lean4/L4Factoidal/Solid/Server/` and reach this
|
|
21
|
+
file only as a `{status, headers, body}` record.
|
|
22
|
+
|
|
23
|
+
The CORS preflight is not an exception. An `OPTIONS` request goes to
|
|
24
|
+
`solidStep` like any other and the engine's headers are written back
|
|
25
|
+
unread. The only status this file writes itself is `500`, for a failure
|
|
26
|
+
of the host.
|
|
27
|
+
|
|
28
|
+
Solid defers to LWS in the engine, not here: `Solid/Server/*`
|
|
29
|
+
instantiates `LWS.Store` and refines `LWS.Operations.step`. This host
|
|
30
|
+
knows nothing of that layering.
|
|
31
|
+
|
|
32
|
+
## The wasm contract
|
|
33
|
+
|
|
34
|
+
| op | arguments | answer |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `solidOpen` | `[configJson]` | `{ ok, handle, root, storage }` |
|
|
37
|
+
| `solidStep` | `[handle, requestJson]` | `{ ok, response }` |
|
|
38
|
+
| `solidClose` | `[handle]` | `{ ok }` |
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
request { method, target, headers: [[name, value]], body }
|
|
42
|
+
response { status, headers: [[name, value]], body }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`configJson` members, all optional: `baseIri` (the absolute IRI the
|
|
46
|
+
storage root maps to), `now` (the engine clock in seconds, advancing one
|
|
47
|
+
second per mutation), `owner` (the WebID advertised through
|
|
48
|
+
`Link: rel="…solid/terms#owner"`) and `agent` (the WebID of the
|
|
49
|
+
requesting agent). With an ephemeral port the origin is not known until
|
|
50
|
+
the socket is bound, so `listen` binds first and opens the storage
|
|
51
|
+
after. `target` is a path within the storage; this host composes no IRI.
|
|
52
|
+
|
|
53
|
+
`agent` is where authentication meets the engine. Verifying a Solid-OIDC
|
|
54
|
+
token is a host job and is not built yet, so a handle with no `agent` is
|
|
55
|
+
an unauthenticated agent, which is what the first slice serves. Web
|
|
56
|
+
Access Control decisions are the engine's, over the agent the host
|
|
57
|
+
verified.
|
|
58
|
+
|
|
59
|
+
The full contract is `docs/lws-solid-conformance.md`, section "wasm ABI".
|
|
60
|
+
|
|
61
|
+
A module built before these ops landed answers `unknown op`.
|
|
62
|
+
`solidServerOpsAvailable(engine)` reports that, and `tests/solid/server/`
|
|
63
|
+
skips with the reason printed.
|
|
64
|
+
|
|
65
|
+
## Use
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { listen } from '@factoidal/core/solid/server'
|
|
69
|
+
|
|
70
|
+
const running = await listen({ port: 3000, baseIri: 'https://pod.example/' })
|
|
71
|
+
console.log(running.origin)
|
|
72
|
+
await running.close()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
factoidal solid-serve /var/lib/solid --port 3000 --base https://pod.example/
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Exports
|
|
80
|
+
|
|
81
|
+
| name | what it is |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `listen(options)` | start a server, resolve when it is listening |
|
|
84
|
+
| `createSolidServer(options)` | build the server without listening |
|
|
85
|
+
| `openSolidStorage(options)` | open a handle and get `step` / `close` |
|
|
86
|
+
| `solidServerOpsAvailable(engine)` | probe the loaded module for the ops |
|
|
87
|
+
| `SOLID_SERVER_OPS` | the three op names |
|
|
88
|
+
| `SolidServerHostError` | a host failure, with `unknownOp` |
|
|
89
|
+
|
|
90
|
+
## Tests and interop
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
node tests/solid/server/protocol.mjs
|
|
94
|
+
bash tools/solid-server-interop.sh # solid-crud-tests + WAC tests
|
|
95
|
+
bash tools/solid-conformance-harness.sh # the Gherkin harness, needs Docker
|
|
96
|
+
```
|