Results of the Touchstone client sessions for the W3C Linked Web Storage (LWS) protocol, run against LWS client implementations: lws-client. 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 5 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 6a3206e 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 99b8b10: eleven languages, each tested as a client of its own. Touchstone drove each client itself, through the driver at hellion.bmi.stonybrook.edu (an MCP server that runs one adapter per language over that language's library).
- Java [ lws-client-java/0.1.0 ] — tested 2026-10-09 00:06:25 EDT (Touchstone client session XRJeM3WKsCR7jgiS): 53 rules, 50 passed, 0 failed, 3 untested, 0 inapplicable. No MUST rule failed: 38 of the 41 MUST rules that apply were exercised.
- JavaScript [ lws-client-js/0.1.0 ] — tested 2026-10-09 00:06:38 EDT (Touchstone client session V-7l62Ilrilzwdug): 53 rules, 50 passed, 0 failed, 3 untested, 0 inapplicable. No MUST rule failed: 38 of the 41 MUST rules that apply were exercised.
- C++ [ lws-client-cpp/0.1.0 ] — tested 2026-10-09 00:06:01 EDT (Touchstone client session c6lNx2k_aZ45CcuM): 53 rules, 50 passed, 0 failed, 3 untested, 0 inapplicable. No MUST rule failed: 38 of the 41 MUST rules that apply were exercised.
- Rust [ lws-client-rust/0.1.0 ] — tested 2026-10-09 00:07:16 EDT (Touchstone client session ZLdFdOwrjGfstXRH): 53 rules, 50 passed, 0 failed, 3 untested, 0 inapplicable. No MUST rule failed: 38 of the 41 MUST rules that apply were exercised.
- Go [ lws-client-go/0.1.0 ] — tested 2026-10-09 00:06:18 EDT (Touchstone client session -QZW5nk3dRyH_0O1): 53 rules, 50 passed, 0 failed, 3 untested, 0 inapplicable. No MUST rule failed: 38 of the 41 MUST rules that apply were exercised.
- Python [ lws-client-python/0.1.0 ] — tested 2026-10-09 00:07:08 EDT (Touchstone client session W8cDA4wasWddZvGe): 53 rules, 50 passed, 0 failed, 3 untested, 0 inapplicable. No MUST rule failed: 38 of the 41 MUST rules that apply were exercised.
- C# [ lws-client-csharp/0.1.0 ] — tested 2026-10-09 00:06:09 EDT (Touchstone client session OPHZCBub3BZOm154): 53 rules, 50 passed, 0 failed, 3 untested, 0 inapplicable. No MUST rule failed: 38 of the 41 MUST rules that apply were exercised.
- Swift [ lws-client-swift/0.1.0 ] — tested 2026-10-09 00:07:24 EDT (Touchstone client session Zorw4HM10-uJvfNW): 53 rules, 50 passed, 0 failed, 3 untested, 0 inapplicable. No MUST rule failed: 38 of the 41 MUST rules that apply were exercised.
- PHP [ lws-client-php/0.1.0 ] — tested 2026-10-09 00:06:58 EDT (Touchstone client session KaVUpW8r1U3wHBD9): 53 rules, 50 passed, 0 failed, 3 untested, 0 inapplicable. No MUST rule failed: 38 of the 41 MUST rules that apply were exercised.
- Kotlin [ lws-client-kotlin/0.1.0 ] — tested 2026-10-09 00:06:46 EDT (Touchstone client session rSlwk3Pyk__AvxIh): 53 rules, 50 passed, 0 failed, 3 untested, 0 inapplicable. No MUST rule failed: 38 of the 41 MUST rules that apply were exercised.
- WASM [ lws-client-rust/0.1.0 (WebAssembly component) ] — tested 2026-10-09 00:07:32 EDT (Touchstone client session A7pkcU2mptOUDtHh): 53 rules, 50 passed, 0 failed, 3 untested, 0 inapplicable. No MUST rule failed: 38 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 | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Java | JavaScript | C++ | Rust | Go | Python | C# | Swift | PHP | Kotlin | WASM | |||
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED |
| 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 |
| 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 |
| 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 |
| 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 |
| After a 401, the client asks for a token only for a realm that contains the URL it was refused | MUST | client-token-for-containing-realm | 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| The client presents a SAML 2.0 assertion with the saml2 token type | MUST | client-saml-token-type-saml2 | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED | UNTESTED |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |