Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
155 commits
Select commit Hold shift + click to select a range
4102454
wip(lws): LWS module skeleton: resources, tokens, grants
claude Oct 5, 2026
4b26407
wip(lws): AS metadata/JWKS, key files, Touchstone runner
claude Oct 5, 2026
0bd8251
feat(lws): type index and type search services (lws10-index)
claude Oct 5, 2026
8fd9a3f
feat(lws): webhook notifications and subscriptions
claude Oct 5, 2026
bc6f9b1
wip(lws): CORS, probes, key reload, lws-net runner, conformance floor
claude Oct 5, 2026
84574e5
Merge branch 'feat/lws-notify' into feat/lws
claude Oct 5, 2026
77a2a41
Merge branch 'feat/lws-index' into feat/lws
claude Oct 5, 2026
13df308
feat(lws): RFC 8693 token exchange for did:key, CID and OpenID Connec…
claude Oct 5, 2026
803168d
docs(lws): document LWS 1.0 mode in the server skill and README
claude Oct 5, 2026
5d8d281
Merge commit '13df308' into feat/lws
claude Oct 5, 2026
71cacaa
chore(lws): rustfmt the LWS module, raise the Touchstone floor
claude Oct 5, 2026
4ec912b
feat(lws): SAML 2.0 subject tokens with enveloped XML-DSig
claude Oct 5, 2026
5af14f6
Merge branch 'feat/lws-as' into feat/lws
claude Oct 5, 2026
6755b0e
feat(lws): honour SOLID_SERVER_OPEN_MODE, raise the auth floor to 31
claude Oct 5, 2026
3a8580f
fix(lws): allow QUERY cross-origin, report LWS.net counts in CI
claude Oct 5, 2026
4e9ad51
Merge remote-tracking branch 'origin/main' into feat/lws
claude Oct 5, 2026
877db84
fix(lws): close resource-handling spec gaps against lws-protocol ef02548
claude Oct 5, 2026
18bd5ee
Merge branch 'feat/lws-gaps-res' into feat/lws
claude Oct 5, 2026
95cb45b
fix(lws): close access-control, token and service-container spec gaps
claude Oct 5, 2026
ac95cea
Merge branch 'feat/lws-gaps-acc' into feat/lws
claude Oct 5, 2026
f8ccb44
fix(lws): close five security-review findings in LWS mode
claude Oct 5, 2026
5d06c3d
fix(lws): ask LWS.net for its TRX report the Microsoft.Testing.Platfo…
claude Oct 5, 2026
7dd770f
fix(lws): third review pass — panic-free datetimes, authz on absent t…
claude Oct 5, 2026
000ad57
fix(lws): fourth review pass — authorize under the lock, listing vali…
claude Oct 5, 2026
b80d223
fix(lws): fifth review pass — bounded multikey decode, locked type se…
claude Oct 5, 2026
13a96fa
fix(lws): sixth review pass — fail-closed content+metadata writes, bo…
claude Oct 5, 2026
4aa7c91
fix(lws): seventh review pass — DPoP for Solid-OIDC exchange, stale-m…
claude Oct 5, 2026
f7274e1
perf(lws): run a POST create's writes inline, not in a spawned task
claude Oct 5, 2026
b7184a3
fix(lws): eighth review pass: metadata that reads back, cancelled-sub…
claude Oct 5, 2026
890480c
fix(lws): ninth review pass: in-place bounded merge patch, cancel-saf…
claude Oct 6, 2026
dba50eb
ci(lws): rust-cache saves on main only
claude Oct 6, 2026
c05e28e
fix(lws): tenth review pass: cancel-safe mutations, bounded issuer an…
claude Oct 6, 2026
5501efa
lws: announce cancelled POSTs; bound JSON Patch work
claude Oct 6, 2026
cf0b08e
Merge remote-tracking branch 'origin/main' into feat/lws
claude Oct 8, 2026
adbef98
lws: hold admission slots in detached writes; keep uncertain writes p…
claude Oct 8, 2026
dcf4fa0
lws: share SAML namespace scopes; keep created records registered; ho…
claude Oct 8, 2026
12decd3
Merge remote-tracking branch 'origin/main' into feat/lws
claude Oct 8, 2026
d08a4c1
lws: budget SAML namespace bytes; keep a POST's metadata after an unc…
claude Oct 8, 2026
64f17c1
lws: charge SAML namespace URI copies; evaluate preconditions on crea…
claude Oct 8, 2026
66b609d
fix(lws): bound c14n work, PATCH to max_body, hold listings condition…
claude Oct 8, 2026
a3bb843
Merge remote-tracking branch 'origin/main' into feat/lws
claude Oct 8, 2026
f926886
fix(lws): linear attribute and type dedupe, nested touch lock, contai…
claude Oct 8, 2026
a6a9ab0
fix(lws): bound remaining per-request work and settle bodiless responses
claude Oct 8, 2026
c0778b7
Merge remote-tracking branch 'origin/main' into feat/lws
claude Oct 8, 2026
a27f6a7
fix(lws): expansion budget for Turtle, bounded notification prep, Cod…
claude Oct 8, 2026
6fa3185
fix(lws): count notifications past the prepare bound as dropped; clippy
claude Oct 8, 2026
79120c5
lws: take the SAML subject-token suite out of the core PR
claude Oct 8, 2026
dc3ce2c
lws: take OpenID Connect ID tokens and DPoP out of the core PR
claude Oct 8, 2026
a97f1d6
Merge origin/main into feat/lws
claude Oct 8, 2026
6b157d9
fix(lws): bounded record bodies, reserved quotas, atomic deletes; Cod…
claude Oct 9, 2026
7d3eda0
Merge origin/main into feat/lws
claude Oct 9, 2026
f057c46
fix(lws): route limits while reading, one removal path, owner share; …
claude Oct 9, 2026
86e9702
docs(lws): route body limits and the owner's subscription share
claude Oct 9, 2026
a5e64dd
fix(lws): bound what headers expand to; failure counts live in subscr…
claude Oct 9, 2026
d810f5b
Merge origin/main into feat/lws
claude Oct 9, 2026
a37dd3d
fix(lws): bound resource metadata, weigh Link targets as kept, settle…
claude Oct 9, 2026
c4b25b6
feat(lws): resources, containers and preconditions
claude Oct 9, 2026
5f6bb07
feat(lws): PATCH with JSON Merge Patch and JSON Patch, and linkset PATCH
claude Oct 9, 2026
92b2fa7
feat(lws): access grants and access requests
claude Oct 9, 2026
b3c218d
feat(lws): notifications and subscriptions
claude Oct 9, 2026
64f8a39
Merge origin/main into the LWS resources slice
claude Oct 9, 2026
972e4ae
Merge lws-1 into the LWS PATCH slice
claude Oct 9, 2026
8dc200b
Merge lws-2 into the LWS access slice
claude Oct 9, 2026
7b9f212
Merge lws-3 into the LWS notifications slice
claude Oct 9, 2026
a6ec4d6
Merge origin/main into feat/lws
claude Oct 9, 2026
bf023cb
Stack feat/lws on the notifications slice
claude Oct 9, 2026
84035b2
docs(lws): keep the crate README within the template's line cap
claude Oct 9, 2026
dbea171
Merge lws-2 into the LWS access slice
claude Oct 9, 2026
e95878f
Merge lws-1 into the LWS PATCH slice
claude Oct 9, 2026
b75354a
fix(lws): hold grants to their storage, compare exact instants, refus…
claude Oct 9, 2026
4b148bb
fix(lws): make writes and deletes whole or not at all
claude Oct 9, 2026
f1abbd1
fix(lws): read every precondition line, refuse coded bodies
claude Oct 9, 2026
9bd6a4a
Merge lws-1 into the LWS PATCH slice
claude Oct 9, 2026
f858f42
fix(lws): parse JSON Pointers once, measure linksets as served
claude Oct 9, 2026
5531b12
Merge lws-2 into the LWS access slice
claude Oct 9, 2026
69dd57e
Merge lws-2 into the LWS access slice
claude Oct 9, 2026
f0e41a9
fix(lws): put back only store steps that may have landed
claude Oct 9, 2026
ac734c2
Merge lws-1 into the LWS PATCH slice
claude Oct 9, 2026
8a4ad1b
test(lws): fail each store step of access request changes
claude Oct 9, 2026
a36f33a
Merge lws-3 into the LWS notifications slice
claude Oct 9, 2026
45d5d94
fix(lws): announce access records as they are registered, count every…
claude Oct 9, 2026
3bc5c5b
Merge lws-4 into the LWS type index slice
claude Oct 9, 2026
cefac33
fix(lws): check every client IRI with one RFC 3987 parser
claude Oct 9, 2026
899f21d
Merge lws-1 into the LWS PATCH slice
claude Oct 9, 2026
3989add
Merge lws-2 into the LWS access slice
claude Oct 9, 2026
55fd939
Merge lws-4 into the LWS type index slice
claude Oct 9, 2026
de9d184
Merge lws-3 into the LWS notifications slice
claude Oct 9, 2026
e94293d
fix(lws): type search reads QUERY bodies, fails whole and names nothing
claude Oct 9, 2026
00e92d9
Merge lws-2 into the LWS access slice
claude Oct 9, 2026
03d4e9d
fix(lws): ignore invalid date preconditions; coding check covers QUERY
claude Oct 9, 2026
e2a63ea
Merge lws-1 into the LWS PATCH slice
claude Oct 9, 2026
79ac054
Merge lws-4 into the LWS type index slice
claude Oct 9, 2026
d6baf78
Merge lws-3 into the LWS notifications slice
claude Oct 9, 2026
9bd9075
fix(lws): type index and search evaluate preconditions, refuse coded …
claude Oct 9, 2026
204005d
Merge lws-2 into the LWS access slice
claude Oct 9, 2026
97530c5
fix(lws): refuse codings at the router, report exactly what a failed …
claude Oct 9, 2026
a9fd8fc
Merge lws-1 into the LWS PATCH slice
claude Oct 9, 2026
cb8017b
fix(lws): uncertain creates come into force only once seen stored; se…
claude Oct 9, 2026
b590b98
Merge lws-3 into the LWS notifications slice
claude Oct 9, 2026
d7ab1ba
fix(lws): undo keeps validators and recovery data; parse entity-tag l…
claude Oct 9, 2026
34e8b1f
Merge lws-1 into the LWS PATCH slice
claude Oct 9, 2026
899f9b4
Merge lws-2 into the LWS access slice
claude Oct 9, 2026
9fb077d
fix(lws): resolve linkset references against the linkset URI
claude Oct 9, 2026
a3a86cb
Merge lws-3 into the LWS notifications slice
claude Oct 9, 2026
7c310d6
test(lws): a write that cannot be put back stays pending, then comes …
claude Oct 9, 2026
ed87528
Merge lws-4 into the LWS type index slice
claude Oct 9, 2026
72b4d6e
fix(lws): a failed delete announces nothing; purge expired subscripti…
claude Oct 9, 2026
4464818
test(lws): walk every route for codings and preconditions
claude Oct 9, 2026
9fea34d
fix(lws): the type index walk keeps only what it returns
claude Oct 9, 2026
dd62086
fix(lws): fence background recovery by record; an empty tag list matc…
claude Oct 9, 2026
64ee014
Merge lws-1 into the LWS PATCH slice
claude Oct 9, 2026
d4effec
Merge lws-2 into the LWS access slice
claude Oct 9, 2026
5cf50e5
Merge lws-3 into the LWS notifications slice
claude Oct 9, 2026
c3802bf
Merge lws-4 into the LWS type index slice
claude Oct 9, 2026
d882773
fix(lws): roll back under the held locks until undone; refuse malform…
claude Oct 9, 2026
c077447
Merge lws-1 into the LWS PATCH slice
claude Oct 9, 2026
2450c17
Merge lws-2 into the LWS access slice
claude Oct 9, 2026
c2395bc
Merge lws-3 into the LWS notifications slice
claude Oct 9, 2026
6c86319
Merge lws-4 into the LWS type index slice
claude Oct 9, 2026
d9a724c
fix(lws): settle uncertain record creates under the lock; compare for…
claude Oct 9, 2026
2e0e8fc
Merge lws-3 into the LWS notifications slice
claude Oct 9, 2026
e30a1ef
Merge lws-4 into the LWS type index slice
claude Oct 9, 2026
53a2e2c
fix(lws): grant notices reach only requests their assignees made; che…
claude Oct 9, 2026
1134f45
fix(lws): charge the index walk's own state; an empty filter body is …
claude Oct 9, 2026
fc4ebf3
fix(lws): hold linkset target attributes to their RFC 9264 shapes
claude Oct 9, 2026
fd2965e
fix(lws): read an index walk's listings only while they fit the budget
claude Oct 9, 2026
8645f17
fix(lws): notify subscriptions to a resource's linkset
claude Oct 9, 2026
611b2ba
fix(lws): bound rollback retries and set aside what cannot be put back
claude Oct 9, 2026
d4e3142
Merge lws-1 into the LWS PATCH slice
claude Oct 9, 2026
8119a4d
Merge lws-2 into the LWS access slice
claude Oct 9, 2026
ba72437
fix(lws): set aside the container whose listing a stuck change alters
claude Oct 9, 2026
8cf17b9
Merge lws-1 into the LWS PATCH slice
claude Oct 9, 2026
edc7b7e
Merge lws-2 into the LWS access slice
claude Oct 9, 2026
51c6bbf
Merge lws-4 into the LWS type index slice
claude Oct 9, 2026
1084c45
Merge lws-3 into the LWS notifications slice
claude Oct 9, 2026
b1ee401
fix(lws): count a listing before reading it, refuse rather than cut i…
claude Oct 9, 2026
faac9d2
fix(lws): refuse empty internationalized target attribute arrays
claude Oct 9, 2026
cbcdf23
fix(lws): one visibility check for set-aside resources, counted and b…
claude Oct 9, 2026
36174f1
Merge lws-1 into the LWS PATCH slice
claude Oct 9, 2026
4c4885a
Merge lws-2 into the LWS access slice
claude Oct 9, 2026
839c29c
fix(lws): keep grants whose create or revocation did not settle out o…
claude Oct 9, 2026
a859fac
Merge lws-3 into the LWS notifications slice
claude Oct 9, 2026
7119113
Merge lws-4 into the LWS type index slice
claude Oct 9, 2026
e1e27b0
fix(lws): walks and deliveries skip set-aside resources rather than wait
claude Oct 9, 2026
e8daebf
fix(lws): a stuck recursive delete sets aside everything it holds
claude Oct 9, 2026
627f32f
Merge lws-1 into lws-2
claude Oct 9, 2026
de0eb27
Merge lws-2 into lws-3
claude Oct 9, 2026
e20a3c9
Merge lws-3 into lws-4
claude Oct 9, 2026
e8adee9
Merge lws-4 into lws-5
claude Oct 9, 2026
278daf1
fix(lws): locks only while visible; listings and dates never show a c…
claude Oct 9, 2026
b757742
Merge lws-1 into lws-2
claude Oct 9, 2026
0c208c4
Merge lws-2 into lws-3
claude Oct 9, 2026
d3b1c6e
fix(lws): grants settle in their own record, under one lock per grant
claude Oct 9, 2026
2cf0408
Merge lws-3 into lws-4
claude Oct 9, 2026
fbedd39
Merge lws-4 into lws-5
claude Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions .github/workflows/lws-conformance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Linked Web Storage conformance for sparq-lws-core in LWS mode (SOLID_SERVER_PROTOCOL=lws).
#
# - touchstone: the Touchstone suite (ebremer/touchstone) against a secured sparq: the harness holds
# the authorization server key, serves alice's and bob's identity documents and webhook inboxes,
# and alice owns the storage. A ratchet: the run fails when any module passes fewer tests than
# crates/sparq-lws-core/conformance/lws/floor.json records.
# - lws-net: the LWS.net harness (langsamu/LWS.net) with the elf-pavlik/lws-test-suite YAML-LD
# definition, as lws-contrib/dagger-workspace runs it, against sparq in LWS open mode.
#
# Both harnesses are pinned to a commit; bump the pins deliberately and re-baseline floor.json.
name: lws-conformance

on:
pull_request:
paths:
- "crates/sparq-lws-core/**"
- ".github/workflows/lws-conformance.yml"
push:
branches: [main]
paths:
- "crates/sparq-lws-core/**"
- ".github/workflows/lws-conformance.yml"
workflow_dispatch:

concurrency:
group: lws-conformance-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
TOUCHSTONE_REF: 971dc8def56200c3eb0f08348295ca545cf92a00
LWS_NET_REF: eca99a4a5beb949561a9989a183dd3216712bb79
LWS_TESTS_REF: 72cfc0a80c718dddbcf43ea6bea98a13a3c14e99

jobs:
touchstone:
name: lws touchstone
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ebremer/touchstone
ref: ${{ env.TOUCHSTONE_REF }}
path: touchstone
persist-credentials: false
- uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
distribution: temurin
java-version: "21"
cache: maven
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: lws-conformance
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: build touchstone
working-directory: touchstone
run: ./mvnw -q -B -ntp -pl harness-cli -am -Dmaven.test.skip=true package
- name: build sparq-lws-core
run: cargo build -p sparq-lws-core
- name: run touchstone
env:
TOUCHSTONE: ${{ github.workspace }}/touchstone
run: crates/sparq-lws-core/conformance/lws/touchstone.sh all || true
- name: check the floor
run: python3 crates/sparq-lws-core/conformance/lws/check-floor.py target/lws-touchstone/runs
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: lws-touchstone-report
path: |
target/lws-touchstone/runs
target/lws-touchstone/server.log
retention-days: 14

lws-net:
name: lws lws-net
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: langsamu/LWS.net
ref: ${{ env.LWS_NET_REF }}
path: LWS.net
persist-credentials: false
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: elf-pavlik/lws-test-suite
ref: ${{ env.LWS_TESTS_REF }}
path: lws-test-suite
persist-credentials: false
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: "10.0.x"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: lws-conformance
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: build sparq-lws-core
run: cargo build -p sparq-lws-core
- name: run lws-net
env:
LWS_NET: ${{ github.workspace }}/LWS.net
LWS_TESTS: ${{ github.workspace }}/lws-test-suite/lws10/tests.yaml
run: crates/sparq-lws-core/conformance/lws/lws-net.sh
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions crates/sparq-lws-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ itoa = "1"
# Declared for native targets below.

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# Linked Web Storage (`src/lws/`): ES256 access tokens, did:key / CID / OpenID Connect subject
# tokens and RFC 9421 notification signatures. p256 and reqwest are already in the resolved graph
# (p256 as a dev-dependency, reqwest via solid-oidc-verifier), so these entries add no new crate.
p256 = { version = "0.13", features = ["ecdsa", "jwk", "std"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
bs58 = "0.5"
httpdate = "1"
# Preserve the native dependency graph and feature set exactly. The target split keeps listener,
# runtime, verifier, filesystem, TLS, PoP, notification, and non-memory backend code out of wasm.
axum = { version = "0.8", features = ["ws"] }
Expand Down Expand Up @@ -265,6 +272,9 @@ http-body-util = "0.1"
sha2 = "0.10"
base64 = "0.22"
url = "2"
# RFC 3987 IRI validation: every IRI a client sends (filters, grants, inboxes) is checked by
# one parser. Already in the tree through oxttl.
oxiri = "0.2"
aws-lc-rs = "1"
subtle = "2.6"
redis = { version = "1", default-features = false, features = ["r2d2"], optional = true }
Expand Down
4 changes: 2 additions & 2 deletions crates/sparq-lws-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ docker run --rm --name sparq-lws-core -p 127.0.0.1:3000:3000 \

**Required production configuration:**

- **Base URL:** `SOLID_SERVER_BASE_URL` = the public `https://` origin (set
`SOLID_SERVER_AUDIENCE` only if the token audience differs).
- **Base URL:** `SOLID_SERVER_BASE_URL` = the public `https://` origin (set `SOLID_SERVER_AUDIENCE` only if the token audience differs).
- **TLS:** terminate at a trusted proxy (keep the container port private), or mount PEMs
and set both `SOLID_SERVER_TLS_CERT` + `SOLID_SERVER_TLS_KEY` (setting one fails boot;
uid 65532 must read them).
Expand Down Expand Up @@ -86,6 +85,7 @@ docker run --rm --name sparq-lws-core -p 127.0.0.1:3000:3000 \
- **Transport hardening** — HTTP/2 rapid-reset and HTTP/1 slowloris guards (explicit
header-count, aggregate-byte, and slow-header timeout bounds); request timeouts, body
limits, per-connection max-requests, rate limiting, and overload shedding.
- **LWS 1.0 mode** — `SOLID_SERVER_PROTOCOL=lws` serves W3C Linked Web Storage (`src/lws/`, skill § *Run the LWS 1.0 protocol*, [`conformance/lws/`](./conformance/lws)).
- Cargo features:
- `embedded-sparq` (**default-on**, sq-gg0qq.3) — the first-class in-process
SPARQ engine backend (in-workspace path deps on `sparq-core`/`sparq-engine`);
Expand Down
50 changes: 50 additions & 0 deletions crates/sparq-lws-core/conformance/lws/check-floor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""Fail when the latest Touchstone run passes fewer tests in any module than floor.json records.

check-floor.py <runs-dir> [floor.json]

Modules are the first path segment of a test id (core, auth, index), except notifications/webhook.
Raise the floor in the same change that makes more tests pass; never lower it to go green.
"""
import glob
import json
import os
import sys
from collections import Counter


def module(test_id: str) -> str:
path = test_id.split("#")[0]
return "notifications/webhook" if path.startswith("notifications/") else path.split("/")[0]


def main() -> int:
runs = sys.argv[1]
floor_path = sys.argv[2] if len(sys.argv) > 2 else os.path.join(os.path.dirname(__file__), "floor.json")
reports = sorted(glob.glob(os.path.join(runs, "*", "report.json")))
if not reports:
print(f"no Touchstone report under {runs}", file=sys.stderr)
return 2
report = json.load(open(reports[-1]))
passed, total = Counter(), Counter()
for t in report["tests"]:
m = module(t["id"])
total[m] += 1
passed[m] += t["outcome"] == "passed"
floor = json.load(open(floor_path))["passed"]
ok = True
for m in sorted(set(total) | set(floor)):
want = floor.get(m, 0)
line = f"{m:24} {passed[m]:4} passed of {total[m]:4} (floor {want})"
if passed[m] < want:
ok = False
line += " BELOW FLOOR"
print(line)
for t in report["tests"]:
if t["outcome"] in ("failed", "cantTell"):
print(f" {t['outcome']:9} {t['level']:6} {t['id']}")
return 0 if ok else 1


if __name__ == "__main__":
sys.exit(main())
9 changes: 9 additions & 0 deletions crates/sparq-lws-core/conformance/lws/floor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"_doc": "Touchstone passes per module that CI must keep (check-floor.py). Raise with the change that earns it.",
"passed": {
"core": 121,
"auth": 22,
"index": 39,
"notifications/webhook": 11
}
}
57 changes: 57 additions & 0 deletions crates/sparq-lws-core/conformance/lws/lws-net.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Run the LWS.net conformance harness (https://github.com/langsamu/LWS.net) with the YAML-LD
# suite definition from elf-pavlik/lws-test-suite (branch yaml), as lws-contrib/dagger-workspace
# does, against sparq-lws-core in LWS open mode (the harness sends no credentials).
#
# LWS_NET=../LWS.net LWS_TESTS=../lws-test-suite/lws10/tests.yaml \
# crates/sparq-lws-core/conformance/lws/lws-net.sh
#
# Needs the .NET 10 SDK and Node.js (to convert the YAML-LD definition to N-Triples).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
repo=$(cd "$here/../../../.." && pwd)
LWS_NET=${LWS_NET:-$repo/../LWS.net}
LWS_TESTS=${LWS_TESTS:-$repo/../lws-test-suite/lws10/tests.yaml}
PORT=${PORT:-3919}
OUT=${OUT:-$repo/target/lws-net}
BIN=${BIN:-$repo/target/debug/sparq-lws-core}
mkdir -p "$OUT"
[ -x "$BIN" ] || cargo build -q -p sparq-lws-core --manifest-path "$repo/Cargo.toml"

# YAML-LD -> N-Triples, mounted as the harness's embedded suite definition (Resources/new.ttl).
conv="$OUT/convert"
mkdir -p "$conv"
if [ ! -d "$conv/node_modules/jsonld" ]; then
(cd "$conv" && npm init -y >/dev/null && npm install --silent jsonld@8 yaml@2 >/dev/null)
fi
cat > "$conv/convert.mjs" <<'JS'
import { readFile, writeFile } from "node:fs/promises";
import jsonld from "jsonld";
import { parse } from "yaml";
const [, , src, dst] = process.argv;
const doc = parse(await readFile(src, "utf8"));
await writeFile(dst, await jsonld.toRDF(doc, { format: "application/n-quads" }));
JS
(cd "$conv" && node convert.mjs "$LWS_TESTS" "$LWS_NET/Suite/Model/Resources/new.ttl")

SOLID_SERVER_PROTOCOL=lws \
SOLID_SERVER_LWS_OPEN=1 \
SOLID_SERVER_BIND=127.0.0.1:$PORT \
SOLID_SERVER_BASE_URL=http://localhost:$PORT \
"$BIN" > "$OUT/server.log" 2>&1 &
pid=$!
trap 'kill $pid 2>/dev/null || true' EXIT
for _ in $(seq 1 100); do
curl -s -o /dev/null "http://localhost:$PORT/" && break
sleep 0.2
done
cd "$LWS_NET"
status=0
# LWS.net's global.json selects Microsoft.Testing.Platform, which takes --report-trx rather than
# VSTest's --logger (it exits 5, invalid arguments, on --logger).
Suite__BaseUri="http://localhost:$PORT/" dotnet test Suite/Test \
--report-trx --report-trx-filename lws-net.trx --results-directory "$OUT" || status=$?
# One line with the counts, as a CI annotation when running in GitHub Actions.
counts=$(grep -o '<Counters [^>]*>' "$OUT/lws-net.trx" 2>/dev/null | head -1 || true)
echo "${GITHUB_ACTIONS:+::notice title=lws-net::}lws-net ${counts:-no results}"
exit $status
91 changes: 91 additions & 0 deletions crates/sparq-lws-core/conformance/lws/touchstone.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# Run the Touchstone LWS conformance suite (https://github.com/ebremer/touchstone) against
# sparq-lws-core in LWS mode, as a secured target: the harness holds the authorization server's
# signing key (HarnessIssuedTokens), serves alice's and bob's identity documents and webhook
# inboxes from its fixture host (ReachableFixtures), its SAML identity provider is trusted
# (SamlTrust, when openssl is available), and alice owns the storage.
#
# TOUCHSTONE=../touchstone crates/sparq-lws-core/conformance/lws/touchstone.sh [module]
#
# module: all (default), core, auth, notifications/webhook, index, or one manifest or test.
# Needs JDK 21 and a built touchstone.jar (cd $TOUCHSTONE && ./mvnw -q -pl harness-cli -am
# -Dmaven.test.skip=true package). Reports land in $OUT (default target/lws-touchstone).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
repo=$(cd "$here/../../../.." && pwd)
TOUCHSTONE=${TOUCHSTONE:-$repo/../touchstone}
MODULE=${1:-all}
PORT=${PORT:-3917}
FIXTURES_PORT=${FIXTURES_PORT:-3918}
OUT=${OUT:-$repo/target/lws-touchstone}
BIN=${BIN:-$repo/target/debug/sparq-lws-core}
mkdir -p "$OUT"
key="$OUT/as-key.json"
rm -f "$key"
[ -x "$BIN" ] || cargo build -q -p sparq-lws-core --manifest-path "$repo/Cargo.toml"

fixtures="http://localhost:$FIXTURES_PORT/"

# The harness SAML identity provider's key (SamlTrust): an RSA key pair whose private JWK the
# harness signs assertions with, and whose public key the authorization server trusts for the
# entity ${fixtures}idp. Needs openssl; without it the SAML tests are inapplicable.
saml_idps="$OUT/saml-idps.json"
saml_jwk=""
capabilities="Authentication, HarnessIssuedTokens, ReachableFixtures"
rm -f "$saml_idps"
if command -v openssl > /dev/null && command -v python3 > /dev/null; then
openssl genrsa -out "$OUT/saml-idp.pem" 2048 2> /dev/null
openssl rsa -in "$OUT/saml-idp.pem" -pubout -out "$OUT/saml-idp.pub.pem" 2> /dev/null
saml_jwk=$(openssl rsa -in "$OUT/saml-idp.pem" -text -noout 2> /dev/null | python3 -c '
import base64, json, re, sys
text = sys.stdin.read()
def field(name):
m = re.search(r"^" + name + r":\s*((?:\s+[0-9a-f:]+\n?)+)", text, re.M)
return int(m.group(1).replace(":", "").replace("\n", "").replace(" ", ""), 16)
def b64(n):
return base64.urlsafe_b64encode(n.to_bytes((n.bit_length() + 7) // 8, "big")).rstrip(b"=").decode()
e = int(re.search(r"publicExponent: (\d+)", text).group(1))
names = {"n": "modulus", "d": "privateExponent", "p": "prime1", "q": "prime2", "dp": "exponent1", "dq": "exponent2", "qi": "coefficient"}
jwk = {"kty": "RSA", "kid": "saml-idp", "e": b64(e)}
jwk.update({k: b64(field(v)) for k, v in names.items()})
print(json.dumps(jwk))
')
python3 -c 'import json, sys; print(json.dumps({sys.argv[1]: open(sys.argv[2]).read()}))' \
"${fixtures}idp" "$OUT/saml-idp.pub.pem" > "$saml_idps"
capabilities="$capabilities, SamlTrust"
fi
SOLID_SERVER_PROTOCOL=lws \
SOLID_SERVER_BIND=127.0.0.1:$PORT \
SOLID_SERVER_BASE_URL=http://localhost:$PORT \
SOLID_SERVER_LWS_OWNER="${fixtures}agents/alice" \
SOLID_SERVER_LWS_AS_KEY_FILE="$key" \
SOLID_SERVER_LWS_PAGE_SIZE=${PAGE_SIZE:-4} \
SOLID_SERVER_LWS_ALLOW_INSECURE_FETCH=1 \
SOLID_SERVER_LWS_SAML_IDPS_FILE="$([ -s "$saml_idps" ] && echo "$saml_idps")" \
"$BIN" > "$OUT/server.log" 2>&1 &
pid=$!
trap 'kill $pid 2>/dev/null || true' EXIT
for _ in $(seq 1 100); do
[ -s "$key" ] && curl -s -o /dev/null "http://localhost:$PORT/" && break
sleep 0.2
done
signing_key=$(cat "$key")
cat > "$OUT/targets.yaml" <<YAML
targets:
sparq:
baseUrl: http://localhost:$PORT/
adapter: env
capabilities: [$capabilities]
properties:
saml.idpKey: '$saml_jwk'
as.signingKey: '$signing_key'
fixtures.baseUrl: '$fixtures'
webid.alice: '${fixtures}agents/alice'
webid.bob: '${fixtures}agents/bob'
YAML
cd "$TOUCHSTONE"
set +e
java -jar harness-cli/target/touchstone.jar run --target sparq --targets "$OUT/targets.yaml" \
--module "$MODULE" --report-dir "$OUT/runs" 2>&1 | grep -v '^Picked up JAVA_TOOL_OPTIONS'
status=${PIPESTATUS[0]}
exit "$status"
Loading
Loading