Results of the Touchstone client sessions for the W3C Linked Web Storage (LWS) protocol, run against LWS client implementations: lws-client and Factoidal. A client session plays everything a client talks to (the storage, its authorization server, an OpenID Provider, the hosts of identity documents and the notification service that calls the client's inbox) and judges every request the client sends against the client requirements of the LWS specifications (the 19 October 2026 core Working Draft, the authentication suites' latest Working Drafts, and the editor's drafts of the webhook notification and index specifications). For this page Touchstone master ea131ee took each client through every client rule, the checklist's tasks, and the faults a server may legally answer with (a refused linkset PUT, a lost create, an expired page of search results, an expired token, and four forged notifications). A client project may implement LWS in several languages; each language is tested as a client of its own, and grouped under its project. The server results are on LWS Conformance.
lws-client
Version main ff51462: eleven languages, each tested as a client of its own. Touchstone drove each client itself (touchstone drive), through lws-client's own adapters, one per language (driver/adapters/ in lws-client ff51462), which speak Touchstone's driver protocol touchstone-driver/1, in a client session on the machine that ran it.
- Java [ lws-client (Java) 0.1.0 ] — tested 2026-10-29 17:59:03 EDT (Touchstone client session AWPY4g6rFYx1F7t4): 53 rules, 52 passed, 0 failed, 1 untested, 0 inapplicable. No MUST rule failed: 40 of the 41 MUST rules that apply were exercised.
- JavaScript [ lws-client (JavaScript) 0.1.0 ] — tested 2026-10-29 17:59:19 EDT (Touchstone client session KF1NdsOkhW5vzRLT): 53 rules, 52 passed, 0 failed, 1 untested, 0 inapplicable. No MUST rule failed: 40 of the 41 MUST rules that apply were exercised.
- C++ [ lws-client (C++) 0.1.0 ] — tested 2026-10-29 18:00:02 EDT (Touchstone client session EEv1JQ4UeeWeB_dv): 53 rules, 52 passed, 0 failed, 1 untested, 0 inapplicable. No MUST rule failed: 40 of the 41 MUST rules that apply were exercised.
- Rust [ lws-client (Rust) 0.1.0 ] — tested 2026-10-29 17:59:52 EDT (Touchstone client session efObgBEGe7yN4iPw): 53 rules, 52 passed, 0 failed, 1 untested, 0 inapplicable. No MUST rule failed: 40 of the 41 MUST rules that apply were exercised.
- Go [ lws-client (Go) 0.1.0 ] — tested 2026-10-29 17:59:41 EDT (Touchstone client session gNbOKxOgVg3y0O3V): 53 rules, 52 passed, 0 failed, 1 untested, 0 inapplicable. No MUST rule failed: 40 of the 41 MUST rules that apply were exercised.
- Python [ lws-client (Python) 0.1.0 ] — tested 2026-10-29 17:59:30 EDT (Touchstone client session 8-DemTGOsOJXKKRs): 53 rules, 52 passed, 0 failed, 1 untested, 0 inapplicable. No MUST rule failed: 40 of the 41 MUST rules that apply were exercised.
- C# [ lws-client (C#) 0.1.0 ] — tested 2026-10-29 18:00:12 EDT (Touchstone client session EK_pztUTOu05u98_): 53 rules, 52 passed, 0 failed, 1 untested, 0 inapplicable. No MUST rule failed: 40 of the 41 MUST rules that apply were exercised.
- Swift [ lws-client (Swift) 0.1.0 ] — tested 2026-10-29 18:00:24 EDT (Touchstone client session K7nb3vOGcdy0UXsF): 53 rules, 52 passed, 0 failed, 1 untested, 0 inapplicable. No MUST rule failed: 40 of the 41 MUST rules that apply were exercised.
- PHP [ lws-client (PHP) 0.1.0 ] — tested 2026-10-29 18:00:35 EDT (Touchstone client session KrXwGEv0nOrcpR8r): 53 rules, 52 passed, 0 failed, 1 untested, 0 inapplicable. No MUST rule failed: 40 of the 41 MUST rules that apply were exercised.
- Kotlin [ lws-client (Kotlin) 0.1.0 ] — tested 2026-10-29 18:00:49 EDT (Touchstone client session hgz3h2Df-2k35e_W): 53 rules, 52 passed, 0 failed, 1 untested, 0 inapplicable. No MUST rule failed: 40 of the 41 MUST rules that apply were exercised.
- WASM [ lws-client (Rust as a WebAssembly component) 0.1.0 ] — tested 2026-10-29 18:01:01 EDT (Touchstone client session jd2wM7ioJ8QOJN1r): 53 rules, 52 passed, 0 failed, 1 untested, 0 inapplicable. No MUST rule failed: 40 of the 41 MUST rules that apply were exercised.
Factoidal
Version 0.26.0 (npm, 7a58d95): one language. Touchstone drove the client itself (touchstone drive), through the adapter Touchstone keeps for Factoidal's client (driver/adapters/factoidal, D-0109 and D-0110, over the library @factoidal/core/lws/client), which speaks Touchstone's driver protocol touchstone-driver/1, in a client session on the machine that ran it.
The client is a Lean 4 state machine compiled to WebAssembly and run in Node.js. Since 0.26.0 the adapter gives it everything the drives ask for: a write's whole If-None-Match, every policy of an access document, and the notification service's endpoint for a subscription.
- Lean 4 (wasm in Node.js) [ Factoidal LWS client 0.26.0 ] — tested 2026-10-29 18:01:11 EDT (Touchstone client session XryBJ_dlFeHhsHF4): 53 rules, 52 passed, 0 failed, 1 untested, 0 inapplicable. No MUST rule failed: 40 of the 41 MUST rules that apply were exercised.
PASS / FAIL: at least one request was a trial of the rule, and none / at least one failed it (hover a cell for the number of trials). UNTESTED: nothing the client did was a trial of the rule, such as a rule about recovering from a refusal the client never provokes. INAPPLICABLE: the rule's area was left out of the session. Only MUST rules decide a verdict; a SHOULD or MAY failure is advisory. A rule name is coloured by its worst outcome across all the clients. Times are when each session's results began, in US Eastern time.
| Requirement | Level | Rule | lws-client | Factoidal | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Java | JavaScript | C++ | Rust | Go | Python | C# | Swift | PHP | Kotlin | WASM | Lean 4 (wasm in Node.js) | |||
| Core | ||||||||||||||
| The client presents its access token to the storage in an Authorization Bearer header and nowhere else | MUST | client-token-in-authorization-header | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client replaces a linkset with PUT only after the linkset advertised PUT | SHOULD | client-linkset-put-only-when-advertised | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client patches a linkset only in a format the linkset advertised | SHOULD | client-linkset-patch-format-advertised | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client makes a PUT that replaces a data resource conditional | SHOULD | client-put-conditional | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client makes PUT and PATCH on a linkset conditional | SHOULD | client-linkset-write-conditional | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client requests only the page URLs the server gave it | SHOULD | client-page-urls-issued | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client reaches resources through URLs the server gave it, not by building paths | SHOULD | client-member-urls-issued | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's access requests and grants have an @context array that includes the LWS context | MUST | client-access-document-context | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's access requests have a type that includes AccessRequest | MUST | client-access-request-type | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's access grants have a type that includes AccessGrant | MUST | client-access-grant-type | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's access requests and grants name their storage with a URI | MUST | client-access-document-storage | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's access requests and grants carry an access array of one or more objects | MUST | client-access-document-access | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| Every policy in the client's access requests and grants has a type that includes AccessPolicy | MUST | client-access-policy-type | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| Every policy in the client's access requests and grants names one or more actions the server recognises | MUST | client-access-policy-action | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| Every policy in the client's access requests and grants names its assignee with a URI | MUST | client-access-policy-assignee | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| A policy target in the client's access requests and grants is an object with a type and one or more values | MUST | client-access-policy-target | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| A policy's constraints in the client's access requests and grants are objects with leftOperand, operator and rightOperand | MUST | client-access-policy-constraint | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| An inbox in the client's access requests and grants is a URI | MUST | client-access-document-inbox | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client makes a DELETE conditional on the ETag it last read | MAY | client-delete-conditional | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The Link headers of the client's creates carry no server-managed relation, such as linkset | MUST | client-create-no-server-managed-links | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| When the client creates a container, its POST carries Link rel="type" to lws#Container | MUST | client-create-container-type-link | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| When the client deletes a container with everything in it, it sends Depth infinity, or deletes the members first | MUST | client-delete-container-depth | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| When the client updates a resource's content and metadata in one request, it asks for that with Prefer set-linkset | MUST | client-combined-update-prefer-set-linkset | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| After a linkset refuses a request with 405 or 415, the client does not send the same request again | MUST | client-no-repeat-after-405-415 | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| After a create fails with a server error, the client checks before it POSTs the same thing again | SHOULD | client-no-blind-retry-of-create | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| Authentication | ||||||||||||||
| The client's token exchange requests name the storage they want a token for in resource | MUST | client-token-exchange-resource | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's token exchange requests present an authentication credential in subject_token | MUST | client-token-exchange-subject-token | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| After a 401, the client asks for a token only when the discovery checks pass | MUST | client-token-after-discovery-checks | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client presents a self-issued credential with the jwt token type | MUST | client-cid-token-type-jwt | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's self-issued credential is signed, not alg none | MUST | client-cid-credential-signed | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's self-issued credential names its subject, a URI, in sub | MUST | client-cid-subject-claim | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's self-issued credential names its issuer, a URI, in iss | MUST | client-cid-issuer-claim | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's self-issued credential names the client in client_id | MUST | client-cid-client-id-claim | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's self-issued credential has the same URI in sub, iss and client_id | MUST | client-cid-identifiers-agree | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The audience of the client's self-issued credential includes the authorization server | MUST | client-cid-audience-includes-as | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's self-issued credential restricts its audience with aud | SHOULD | client-cid-audience-restricted | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's self-issued credential says when it expires, in exp | MUST | client-cid-expiry-claim | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's self-issued credential says when it was issued, in iat | MUST | client-cid-issued-at-claim | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client presents an ID Token with the id_token token type | MUST | client-oidc-token-type-id-token | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client presents a SAML 2.0 assertion with the saml2 token type | MUST | client-saml-token-type-saml2 | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| Notifications | ||||||||||||||
| The client sends a subscription request as an application/lws+json object | MUST | client-subscription-media-type | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's subscription requests have a type the NotificationService advertises | MUST | client-subscription-type | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's subscription requests have a topic array of URIs | MUST | client-subscription-topic | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's webhook subscription requests name an inbox URI | MUST | client-subscription-inbox | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| Each of the client's webhook subscriptions has an inbox URL of its own | MAY | client-subscription-own-inbox | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's inbox acknowledges a genuine notification with a 2xx | SHOULD | client-inbox-acknowledges-genuine-delivery | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's inbox refuses a notification signed with a key the storage description does not publish | MUST | client-inbox-refuses-unpublished-key | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's inbox refuses a notification whose body no longer matches its signed Content-Digest | MUST | client-inbox-refuses-altered-body | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's inbox refuses a notification whose keyid is not a URL with a fragment | MUST | client-inbox-refuses-keyid-without-fragment | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| The client's inbox refuses a notification whose keyid names a document that is not the storage description it claims to be | MUST | client-inbox-refuses-foreign-key-document | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| Index | ||||||||||||||
| The client's search requests carry a Content-Type naming their query format | MUST | client-query-content-type | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |
| After a search format is refused with 415, the client's next search uses the baseline format | MUST | client-query-baseline-after-415 | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED |
| After a page of search results is refused, the client restarts the search | SHOULD | client-restart-after-refused-page | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS | PASS |