Implement W3C DID Resolution HTTPS Binding (v1)#1
Conversation
Add Express server with GET/POST /1.0/identifiers/:did resolution and DID URL dereferencing endpoints. Uses @digitalbazaar/did-io CachedResolver with did:key and did:web drivers. Includes HTTP status code mapping per W3C spec and a Mocha test suite. All 5 tests passing.
- Fix ESLint config to use flat config format (ESLint 9) - Register Ed25519VerificationKey2020 suite on both did:key and did:web drivers so z6Mk keys resolve correctly - Plumb POST body options through resolve handler - Fix all sort-imports, line length, and arrow-parens lint errors - Expand test suite: 13 tests covering GET resolution, POST resolution, full resolution result format, Content-Type headers, error status codes (404/501), and DID URL dereferencing with fragment and application/did-url-dereferencing Accept header
Uses nock to intercept HTTPS calls to @digitalbazaar/http-client, enabling offline/CI-safe did:web resolution tests without a real server. Also tests live resolution of did:web:identity.foundation (skippable via SKIP_LIVE_TESTS env var). 19 tests passing total.
The did-io drivers do not handle ?service= DID URL params (marked FIXME in did-method-web source). Implement it in dereference.js: - Resolve the base DID document, find the service by ID - Accept: text/uri-list -> HTTP 303 + Location header - Accept: application/did-url-dereferencing -> full result object - Default -> JSON with serviceEndpoint URL - ?relativeRef= appended to the endpoint URL when present - 404 when service ID not found, with full dereferencing body 6 new tests cover all branches. 25 tests passing total.
- Correct did-io package name to @digitalbazaar/did-io - Fix curl examples to use http:// (server runs plain HTTP locally) - Use the actual test DID from the test suite in curl examples - Add service endpoint redirect curl example - Name Express as the HTTP framework - Fix driver registration pattern to match actual code (named imports, .use() for key suites)
Replace manual if-chain and void-suppressed unused variable with a find() over the mode-specific supported types array. The set now actually drives the lookup instead of existing as dead code. Also change Sets to Arrays so find() works without iteration.
…fix serviceEndpoint handling.
Centralize classifyError() into src/http/errors.js with a mode param
('resolution'|'dereferencing') so invalidDid vs invalidDidUrl is handled
in one place. Remove duplicate local implementations from both route files.
Wrap all decodeURIComponent() calls in try/catch so malformed percent-
encoding (e.g. %E0%A4%A) returns a 400 JSON error instead of throwing
an unhandled URIError through Express.
Fix serviceEndpoint handling in _dereferenceService: extract a string URL
from string, array, or object-map forms before use; 400 if none found.
Replace hand-rolled relativeRef concatenation with URL API construction
to correctly handle query strings and fragments in the relativeRef value.
Flip live did:web tests to opt-in (LIVE_TESTS=1) instead of opt-out, so
CI and local runs are deterministic by default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLAUDE.md, SMELLS*.md, SPEC.md, and ARCHITECTURE.md are local working documents and should not be committed to the repo. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
02fc576 to
804740c
Compare
Runs lint on Node 24, then tests on Node 22 and 24 in parallel. Live network tests are skipped by default (LIVE_TESTS not set). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
davidlehn
left a comment
There was a problem hiding this comment.
Adding some basic updates, didn't get to the meat of it.
- All the copyright dates should be updated.
- In general we've used ./lib/ rather than ./src/. Mostly for historical reasons. (Honestly, I'd use ./src/ these days otherwise.) Not sure if it matters other than convention.
- Rename .github/workflows/main.yml to main.yaml (.yaml preferred extension)
- Add permissions: {} to CI workflow for least-privilege security
- Upgrade actions/checkout and actions/setup-node from v4 to v6
- Remove needs: [lint] to run lint and test jobs in parallel
- Add Node.js 26.x to test matrix
- Rename src/ to lib/ per DB convention
- Set package version to 0.0.1-0 (scripts bump to 1.0.0 on release)
- Add LICENSE file (BSD-3-Clause)
- Switch eslint config to node-recommended preset
- Update copyright year to 2025 across all source files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mocha 10 bundles yargs 16 which fails with a require/ESM error under Node.js 26's stricter module handling. Mocha 11 resolves this. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add makeErrorObject() — errors are now RFC 9457-style objects with a W3C DID namespace URI type (e.g. https://www.w3.org/ns/did#NOT_FOUND) instead of plain strings, matching what the test suite asserts - Add _isValidDid() — validate DID syntax (did:<method>:<id>) before hitting the driver; returns INVALID_DID + 400 for malformed input like 'not-a-did' or 'did:example' - Fix 303 redirect body — spec requires empty body; was sending URL string - Update tests to match new error object shape Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 406 response now returns a conformant resolution result object (with didResolutionMetadata.error) when client sent Accept: application/did-resolution, instead of a plain JSON error - Deactivated DID documents (deactivated: true on the document) now return HTTP 410, null didDocument, and deactivated: true in didDocumentMetadata per spec requirement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Document RFC 9457 error object shape with W3C DID namespace URIs - Explain deactivated DID behaviour and which methods support it - Add spec conformance table against did-resolution-mocha-test-suite - Add test suite link to Spec References Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add missing @returns tags to all exported and private functions, add descriptions to @returns where only the type was present, and end all JSDoc summary sentences with a period to satisfy the jsdoc/require-returns and jsdoc/require-description-complete-sentence lint rules. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
End the JSDoc description sentence in dereferenceHandler with a period and wrap the @returns line in _hasPathBeyondDid to stay within the 80-character limit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Set didResolutionMetadata.contentType to application/did-resolution and dereferencingMetadata.contentType to application/did-url-dereferencing so the values match the Content-Type response header, as required by the w3c-ccg/did-resolution-mocha-test-suite. All 35 suite tests now pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Change "targets" to "passes" in the conformance section and add the NOT_FOUND + 404 row that was missing from the table. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
msporny
left a comment
There was a problem hiding this comment.
Partial, high-level review (have not looked at the code yet):
- DID URL dereferencing: fragment (#key), service endpoint (?service=), relativeRef
relativeRef is likely to be removed in v1.0. We should probably drop support for it eventually (next month or two).
Content negotiation: application/did-resolution, application/did-url-dereferencing, application/did+ld+json, text/uri-list
Do not support did-url-dereferencing, it's an attack vector that can DDoS a server (fetch these 50GB files for me).
application/did+ld+json is the wrong media type... should be application/did
text/uri-list is being removed
303 redirect with empty body for text/uri-list (spec-required)
This should be removed as a feature.
- CI: simplify lint job (fixed Node 22.x, no matrix); jobs already run in parallel - errors.js: merge ERROR_STATUS_MAP and ERROR_TYPE_URI into single ERROR_MAP; rewrite classifyError to use e.status and instanceof TypeError before message parsing; add comment flagging did-io improvement opportunity and INTERNAL_ERROR vocabulary issue #337 - headers.js: change DID_DOCUMENT media type to application/did per msporny; remove DEREFERENCING and URI_LIST types - dereference.js: remove text/uri-list and application/did-url- dereferencing support; remove relativeRef support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
All feedback addressed in commit e81371c:
|
- errors.js: fix TypeError network-vs-bad-input disambiguation using e.cause; fix JSDoc sentence periods; remove unused mode param from getResponseContentType - headers.js: remove mode param (only one type list now) - tests: update content-type assertions to application/did; rewrite removed-feature tests (text/uri-list, did-url-dereferencing, relativeRef) to assert new behaviour Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
application/did substring-matched inside application/did-resolution and application/did-url-dereferencing, causing 200 instead of 406. Split on commas and strip quality params before comparing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Capitalize words after → arrows so eslint jsdoc/no-bad-blocks sentence-case rule passes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rename the ERROR_MAP key and classifyError return value from methodNotSupported to NotSupportedError, matching the typed error name landing in did-io PR #70. Update the tracking comment to point at the PR and the final error name (changed from MethodNotSupportedError in review to match the standard DOMException name). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump @digitalbazaar/did-io to ^2.2.0, which sets err.name to 'NotSupportedError' when no driver is registered for a DID method. Replace the brittle message-string check in classifyError with the typed e.name check and update the JSDoc classification priority. Addresses #70. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap the DID URL examples in a code fence so eslint's jsdoc/require-description-complete-sentence rule no longer parses the lowercase did: examples as description prose, and reword the lead-in to end with a period. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the ?service= dereferencing path (_dereferenceService) and the DID URL query parser. Resolving caller-supplied service endpoints turns the server into an outbound HTTP request engine, an SSRF and DDoS amplification vector. This also drops relativeRef handling, which is expected to be removed from DID Resolution v1.0. Fragment and path dereferencing remain: they operate on the resolved DID document and make no caller-controlled outbound requests. Remove the corresponding tests/dereference-service.spec.js suite.
|
From @msporny
Fixed both in the latest commit, now explicitly mentioned in the PR - see PR review changes addressed |
Replace application/did+ld+json with application/did per PR review. Remove stale service-endpoint dereferencing and text/uri-list 303 redirect references left over from the SSRF-surface removal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address code review findings on the HTTPS binding implementation: - Remove manual decodeURIComponent calls; Express already decodes wildcard params, so decoding again corrupted identifiers with literal percent characters. A JSON error middleware now converts malformed percent-encoding and malformed JSON bodies into JSON 400 responses instead of Express HTML error pages. - Reject service endpoint dereferencing (?service=) explicitly with 501, in both unencoded and percent-encoded forms, instead of silently resolving the bare DID. - Classify any remote 4xx during did:web resolution as notFound rather than invalidDid; the DID may be valid when the host rejects the request (401/403/429). - Make didResolutionMetadata.contentType match the Content-Type header on dereferencing responses. - Restore the application/did-url-dereferencing media type, which was removed alongside service dereferencing although it is a response format only and adds no outbound request surface. Dereferencing now returns the spec dereferencing result envelope (dereferencingMetadata, contentStream, contentMetadata) for that Accept type. The W3C did-resolution-mocha-test-suite passes 35/35. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename tests/*.spec.js to test/mocha/NN-kebab-description.js, matching the Digital Bazaar test layout used across repos. Update the mocha glob in package.json and the eslint test-globals file pattern accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What
GETandPOST /1.0/identifiers/{did})#key) and path, against the resolved DID documentdid:keyanddid:webmethods via@digitalbazaar/did-iodidResolutionMetadataapplication/did(DID document),application/did-resolution(full result), andapplication/did-url-dereferencing(dereferencing result, dereferencing only), with wildcard/empty Accept defaulting toapplication/diddeactivated: truein metadataGET /healthSpec conformance
All 35 tests in w3c-ccg/did-resolution-mocha-test-suite pass:
GET /1.0/identifiers/{did}bindingPOST /1.0/identifiers/{did}bindingContent-Typeheader matchesdidResolutionMetadata.contentTypeINVALID_DID+ 400NOT_FOUND+ 404METHOD_NOT_SUPPORTED+ 501REPRESENTATION_NOT_SUPPORTED+ 406 (conformant result shape)application/did-url-dereferencing→ dereferencing result envelope + 200Content-Typeheader matchesdereferencingMetadata.contentType?service=)Error handling: did-io typed errors
Driver-not-found classification now relies on a typed error from
@digitalbazaar/did-iorather than parsing error message strings:@digitalbazaar/did-ioto^2.2.0, which setserr.name = 'NotSupportedError'when no driver is registered for a DIDmethod (did-io#70).
classifyErrornow checkse.name === 'NotSupportedError'instead of thebrittle
e.message.includes('Driver') && ...('not found')substring match,so it no longer breaks if did-io reworded the message.
METHOD_NOT_SUPPORTED+ 501 for unsupportedDID methods.
PR review changes addressed
?service=) andrelativeRefto close the SSRF / DDoS-amplification surface (per @msporny).yaml; upgraded toactions/checkout@v6,actions/setup-node@v6src/→lib/per DB convention0.0.1-0LICENSEfile (BSD-3-Clause)node-recommendedpreset@returnsdeclarations and description sentencesSecond review round
decodeURIComponentcalls corrupted identifiers containing literal percent characters?service=is now rejected explicitly with 501 instead of being silently ignored (previously the unencoded form resolved the bare DID as if no query were present)NOT_FOUNDinstead ofINVALID_DID— the DID may be valid even when the host rejects the requestdidResolutionMetadata.contentTypenow matches theContent-Typeheader on dereferencing responsesapplication/did-url-dereferencingmedia type, which had been removed alongside service dereferencing in 4630197 although it is a response format only (a local lookup in the already-resolved document — no outbound request surface). Dereferencing returns the spec envelope (dereferencingMetadata,contentStream,contentMetadata) for that Accept type; this brings the W3C suite back from 33/35 to 35/35Testing
npm testpasses on Node 22, 24, 26 (CI green)npm run lintpasses with 0 errors, 0 warningsVerifying conformance locally
The suite needs a
localConfig.cjsin its root to point at this server.Create it with:
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com