Skip to content

L2: LWS Storage Description + per-resource Linkset (discovery slice, --lws)#2

Merged
charlesvardeman merged 8 commits into
la3d/lwsfrom
la3d/lws-discovery
Jun 30, 2026
Merged

L2: LWS Storage Description + per-resource Linkset (discovery slice, --lws)#2
charlesvardeman merged 8 commits into
la3d/lwsfrom
la3d/lws-discovery

Conversation

@charlesvardeman

Copy link
Copy Markdown

What

Adds the L2 LWS storage-discovery layer over L1, all behind the existing --lws flag and strictly additive over LDP/Solid behavior. Delivers the two storage-side LWS discovery MUSTs:

  1. Storage DescriptionGET /.well-known/lws-storage serves {@context: ".../lws/v1", id, type: "Storage", service[]} as application/lws+json; advertised by Link: rel="https://www.w3.org/ns/lws#storageDescription" on all storage GET/HEAD responses.
  2. Per-resource linkset — RFC 9264 application/linkset+json (anchor/up/type/describedby) served via content negotiation from both container-GET and file-GET, advertised by Link: rel="linkset". Read-only (mutation deferred).

Plus HEAD content-type parity for lws+json/linkset+json (suppressing the stale on-disk Content-Length).

Commits

  • generateStorageDescription pure fn → serve at /.well-known/lws-storage
  • rel=storageDescription Link on all GET/HEAD (derived inside getAllHeaders from resource origin)
  • generateLinkset (RFC 9264) → serve via conneg + rel=linkset
  • HEAD parity + e2e conformance (16 new tests) + final scheme-parity fix

Scope decisions (deliberate, see plan)

  • /.well-known/lws-configuration is out — it is RFC 8414 authorization-server metadata; JSS is an OAuth2 resource server with a direct bearer (no RFC 8693 token-exchange). It belongs to the auth/Keycloak track.
  • Read-only linkset (mutation + If-Match/412/428 concurrency deferred), single-storage (multi-pod storage descriptions deferred), capability/TypeIndex advertising deferred (L3/Tranche 2).

Verification

  • Full suite 1031/1031 green. Default (non---lws) LDP path provably unchanged (negative controls: lws-off → 404 + no rels; lws-on + default Accept → still LDP).
  • Built subagent-driven (per-task spec+quality reviews + opus whole-branch review). The whole-branch review's one Important finding — a TLS-proxy scheme split (options.ssl vs request.protocol) — is fixed (8927ada) with an X-Forwarded-Proto regression test.

Plan + grounding: docs/superpowers/plans/2026-06-30-lws-L2-storage-discovery.md (in the lws-pod repo).

🤖 Generated with Claude Code

charlesvardeman and others added 8 commits June 30, 2026 09:16
- Register GET /.well-known/lws-storage gated on lwsEnabled
- Returns generateStorageDescription(root, services) as application/lws+json
- root = proto://hostname/ (subdomain-safe); services includes StorageDescription
  self-entry; conditionally adds NotificationService when notificationsEnabled
- 405 on PUT/POST/PATCH/DELETE (reuses existing methodNotAllowed helper)
- Route not registered at all when --lws is off (404 from wildcard)
- Add focused test (8 tests: 200+body+405s under --lws ON, 404 under --lws OFF)
- Full suite: 1003 tests, 0 failures

Co-Authored-By: Claude Opus 4.8 <[email protected]>
- Add RDF_TYPES.LINKSET = 'application/linkset+json' to conneg.js
- Add early-exit branch in selectContentType for LINKSET (independent of
  Turtle conneg flag, same pattern as LWS_JSON)
- Add container-GET linkset branch in resource.js (after LWS_JSON branch)
  serving generateLinkset with isContainer:true under --lws + explicit Accept
- Add file-GET linkset branch in resource.js (before conneg block)
  serving generateLinkset with isContainer:false under --lws + explicit Accept
- Advertise <resourceUrl>; rel="linkset" in getAllHeaders (headers.js)
  alongside rel=storageDescription, both gated by lwsEnabled
- Extend test/lws-conneg.test.js: LINKSET type + negotiability
- Extend test/lws-storage-link.test.js: rel=linkset present/absent assertions

Co-Authored-By: Claude Opus 4.8 <[email protected]>
- Container branch: after existing conneg, apply LWS override when
  lwsEnabled so Accept: application/lws+json or application/linkset+json
  sets the matching Content-Type (mirrors GET's early-return for these
  types, resolves the TODO(lws-head-parity) marker).
- File branch: after negotiateHeadFileContentType, override contentType
  to LINKSET when lwsEnabled + explicitly negotiated; mark
  negotiationConverted=true to suppress the on-disk Content-Length
  (generated linkset differs in size from the stored file).
- HEAD already emitted storageDescription + linkset Link rels via the
  existing lwsEnabled path in getAllHeaders (Task 3); no change needed
  there.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…y + negative controls

Three-suite e2e covering the L2 discovery slice:
- --lws ON: storage description body shape (@context/type/service[]),
  storageDescription + linkset Link rels on GET, per-resource linkset
  via conneg (anchor/type/up), HEAD Link-rel parity, HEAD content-type
  parity for application/linkset+json (no body), LDP additivity control.
- --lws OFF: /.well-known/lws-storage → 404; storageDescription and
  linkset rels absent on resource GET (negative controls).
- --lws + --notifications: service[] contains NotificationService entry
  with serviceEndpoint ending /notification/api (Task 2 gap).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
…me parity) + guard/test tightening

- src/server.js: replace `options.ssl ? 'https' : 'http'` with
  `request.protocol` in the GET /.well-known/lws-storage handler so the
  body's id and serviceEndpoints honor X-Forwarded-Proto (trustProxy:true),
  matching the scheme used by storageDescriptionUrl() and Updates-Via
- src/lws/storage-description.js: add guard that throws a clear Error on
  relative/non-URL args before new URL() can throw a raw TypeError
- test/lws-storage-description-route.test.js: add proxy-scheme-parity test
  that sends X-Forwarded-Proto: https and asserts id + StorageDescription
  serviceEndpoint both start with https://
- test/lws-storage-link.test.js: add test asserting the guard throws with
  /requires an absolute URL/ on '/alice/note.ttl'
- test/lws-conneg.test.js: add assertion that selectContentType with
  connegEnabled=true also returns 'application/linkset+json' (fires before
  the connegEnabled guard)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@charlesvardeman
charlesvardeman merged commit 281de43 into la3d/lws Jun 30, 2026
@charlesvardeman
charlesvardeman deleted the la3d/lws-discovery branch June 30, 2026 14:55
charlesvardeman added a commit that referenced this pull request Jul 3, 2026
…rors, sanitize

Review fixes #2,JavaScriptSolidServer#3,JavaScriptSolidServer#5,JavaScriptSolidServer#6,JavaScriptSolidServer#8,JavaScriptSolidServer#12:
- JavaScriptSolidServer#3 readAcl: detect container-ness from storage and resolve <dir>/.acl (not
  <dir>.acl) for a slashless container path, and run the Control check against
  the normalized target — was showing an empty ACL for a governed container.
- JavaScriptSolidServer#5/JavaScriptSolidServer#6/JavaScriptSolidServer#12 new src/mcp/read.js#readBounded: one byte-range bounded read shared
  by lws://resource and describe_resource. Bounds memory (no full read of a
  huge object), and surfaces an explicit truncation marker so a clipped body
  isn't taken as whole. Body content-type label now uses getContentType.
- JavaScriptSolidServer#8 readSkillResource: only a genuinely-missing skill is not-found; a read/
  parse failure surfaces as INTERNAL_ERROR instead of being masked.
- #2 sanitizeTypes: client-controlled declared types + describedby shape IRIs
  are stripped of hidden/bidi chars before entering linkset/describe responses;
  captureDeclaredTypes now validates the MCP `types` path to absolute URIs (the
  HTTP Link path already did) so no free-text value is persisted.
- JavaScriptSolidServer#12 wac.parentPath delegates to utils/url.getParentContainer.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
charlesvardeman added a commit that referenced this pull request Jul 3, 2026
Correctness/security: transactional put_typed_resource .meta (#1),
type/describedby + federated-content sanitize (#2/JavaScriptSolidServer#7), container ACL-path
resolution (JavaScriptSolidServer#3), malformed-percent invalid-params (JavaScriptSolidServer#4), bounded read +
truncation signal (JavaScriptSolidServer#5/JavaScriptSolidServer#6), skill read-error not masked (JavaScriptSolidServer#8), resource-error
teaching content (JavaScriptSolidServer#9). Maintainability: single surface registry (JavaScriptSolidServer#11),
helper reuse (JavaScriptSolidServer#12), restored live-gate coverage (JavaScriptSolidServer#10, lws-pod repo).

All fork changes TDD'd in test/mcp-v2-review-fixes.test.js; mcp/lws/wac/acl/
storage/container/handler suites green per-file.
charlesvardeman added a commit that referenced this pull request Jul 13, 2026
…he write choke point (review #2, JavaScriptSolidServer#10)

- writeTypeConsistency runs inside applyLwsWrite: HTTP PUT/POST and all
  three MCP write tools share ONE gate; the MCP bypass is closed
- application/json gates as JSON-LD (pipeline parity)
- non-RDF body at an RDF-extension name is refused (the SHACL-skip lie)
- callers handle the new problem return; HTTP local gate calls deleted

Co-Authored-By: Claude Opus 4.8 <[email protected]>
charlesvardeman added a commit that referenced this pull request Jul 13, 2026
…message (task-6 review #2)

- the --lws-off 415 base string returns to the pre-round wording
  ("...text/n3 (N3 Patch) or application/sparql-update (SPARQL Update)");
  the merge-patch clause is appended only under request.lwsEnabled
- new test pins the exact --lws-off 415 body with deepEqual against a
  { conneg: true } (no lws) server

Co-Authored-By: Claude Opus 4.8 <[email protected]>
charlesvardeman added a commit that referenced this pull request Jul 13, 2026
…ag/304 conformance, PATCH MUSTs, MCP sanitize + SSRF consolidation (review 2026-07-12 B1-B12 + P1-P3)

Closes the 12 (B) findings + 3 (C) pre-existing conformance violations from the
2026-07-12 post-drain code review, TDD per finding, each task spec+quality reviewed:

- #2/JavaScriptSolidServer#10 write-consistency gate moves into applyLwsWrite (the shared choke point);
  application/json gates as JSON-LD; non-RDF body at an RDF name refused
- JavaScriptSolidServer#9 slug-less POST of RDF derives a server-side extension
- JavaScriptSolidServer#6 verbatim NT/NQ parse through n3; JSON-LD is graph-capable
- JavaScriptSolidServer#5 JSON-LD conversion arm gets a variant ETag (RFC 9110 §8.8.3)
- JavaScriptSolidServer#4 pending RDF conversion defers the early 304; 406 carries no ETag (§13.2.2)
- JavaScriptSolidServer#7/P1/P2 PATCH applies at the dataset level (no JSON-LD document detour);
  JSON Merge Patch (RFC 7386); bodied write without Content-Type → 400
- JavaScriptSolidServer#3/JavaScriptSolidServer#12/JavaScriptSolidServer#13 MCP sanitizes altr reps + type-search items; read_resource mimeType
- JavaScriptSolidServer#8/JavaScriptSolidServer#14 per-hop redirect revalidation restores the 303 VoID rail; one SSRF table
- P3 application/json is a first-class container + storage-description label

Fork suite 1545/0/1 (the 1 skip + the isolated mcp-lws-read open-handle file
both pre-existing). la3d/lws-review = 4824fe2..7165855, 18 commits.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
charlesvardeman added a commit that referenced this pull request Jul 14, 2026
…ntResolution capability

- buildStorageDescription gains uriSpacePrefixes; the capability carries
  uriSpace: [<absolute prefix>...] (the void:uriSpace values) so a cold agent
  recognizes minted IRIs on its first storage-description read (probe #2)
- prefixes only (container/suffix mapping stays internal; 303 is the resolver);
  threaded identically to HTTP + MCP; flag-off document byte-identical

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant