Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
59 changes: 56 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,63 @@ jobs:

./mvnw clean deploy -P release -B --no-transfer-progress -DskipTests

- name: Extract release notes from CHANGELOG
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
# Pull the section between "## [VERSION]" and the next "## [" heading.
awk -v ver="$version" '
$0 ~ "^## \\["ver"\\]" { flag=1; print; next }
flag && /^## \[/ { exit }
flag { print }
' CHANGELOG.md > .release-notes.md

if [ ! -s .release-notes.md ]; then
echo "::error::No CHANGELOG section found for [$version]. Add a '## [$version] - YYYY-MM-DD' entry before tagging."
exit 1
fi

{
echo
echo "---"
echo
echo "### Artifacts"
echo
echo "**Maven Central** (recommended for library users):"
echo
echo '```xml'
echo "<dependency>"
echo " <groupId>io.github.decentralized-identity</groupId>"
echo " <artifactId>didwebvh-java</artifactId>"
echo " <version>${version}</version>"
echo "</dependency>"
echo '```'
echo
echo "The aggregate \`didwebvh-java\` artifact transitively pulls in \`didwebvh-core\` and \`didwebvh-signing-local\`. Depend on \`didwebvh-core\` directly if you plan to supply your own \`Signer\` implementation."
echo
echo '```xml'
echo "<!-- Core only (BYO Signer): -->"
echo "<dependency>"
echo " <groupId>io.github.decentralized-identity</groupId>"
echo " <artifactId>didwebvh-core</artifactId>"
echo " <version>${version}</version>"
echo "</dependency>"
echo '```'
echo
echo "Browse all modules: <https://central.sonatype.com/namespace/io.github.decentralized-identity>"
echo
echo "**CLI wizard uber-jar** is attached to this release below (not published to Maven Central)."
echo
echo "Full changelog: <https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_REF_NAME}/CHANGELOG.md>"
} >> .release-notes.md

echo "----- release body -----"
cat .release-notes.md
echo "------------------------"

- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
generate_release_notes: true
body_path: .release-notes.md
files: |
didwebvh-core/target/*.jar
didwebvh-wizard/target/*-shaded.jar
didwebvh-wizard/target/didwebvh-wizard.jar
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] - 2026-05-28
## [0.3.0] - 2026-05-29

This release closes the failures reported by the
[did:webvh interop test suite](https://github.com/swcurran/didwebvh-test-suite)
Expand Down Expand Up @@ -86,6 +86,21 @@ against the v0.2.0 line (see issue #2).
Node.js 24, and Node.js 24 is forced in both the CI and release
workflows to silence the deprecation warning emitted by older
actions on the GHA runners.
- **Release notes**: the GitHub Release body is now generated
directly from the matching `## [VERSION]` section of this
CHANGELOG (with an appended Maven Central coordinate), and the
release attaches only the self-contained `didwebvh-wizard.jar`
uber-jar — library modules are consumed from Maven Central.
- **Test coverage**: raised `didwebvh-core` from ~77% to ~82% on
the Codecov metric (line coverage 93%) by covering real branches
in `LogChainValidator`, `LogProcessor`, `DidResolver`,
`MigrateDidOperation`, `CreateDidOperation`, `DidWebVhUrl`, and
the file fetcher — malformed versionId/versionTime, future
versionTime rejection, witness threshold bounds, method-version
downgrade, query-param parsing, PROACTIVE / WHEN_REQUIRED witness
fetch branches, migrate guards (null/empty inputs, deactivated,
newPath, alsoKnownAs dedup), controller-list array vs string
forms, and URL port / IPv6 / empty-domain rejection.

## [0.2.0] - 2026-05-06

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,66 @@ void nextKeyHashEmptyEntryThrows() {
.hasMessageContaining("non-empty");
}

@Test
void controllersEmptyListOmitsControllerProperty() {
CreateDidResult result = DidWebVh.create("example.com", testSigner)
.controllers(Collections.emptyList())
.execute();
JsonObject doc = result.getLogEntry().getState();
assertThat(doc.has("controller")).isFalse();
}

@Test
void controllersSingleElementUsesStringForm() {
CreateDidResult result = DidWebVh.create("example.com", testSigner)
.controllers(Collections.singletonList("did:web:other.example.com"))
.execute();
JsonObject doc = result.getLogEntry().getState();
assertThat(doc.get("controller").isJsonPrimitive()).isTrue();
assertThat(doc.get("controller").getAsString())
.isEqualTo("did:web:other.example.com");
}

@Test
void controllersMultipleElementsUsesArrayForm() {
CreateDidResult result = DidWebVh.create("example.com", testSigner)
.controllers(Arrays.asList(
"did:web:a.example.com", "did:web:b.example.com"))
.execute();
JsonObject doc = result.getLogEntry().getState();
assertThat(doc.get("controller").isJsonArray()).isTrue();
assertThat(doc.getAsJsonArray("controller")).hasSize(2);
}

@Test
void extractMultikeyAcceptsBareDidKeyVerificationMethod() {
// Signer whose verificationMethod is the did:key URI without a fragment.
Signer bareSigner = new Signer() {
@Override public String keyType() { return testSigner.keyType(); }
@Override public String verificationMethod() {
return "did:key:" + testMultikey;
}
@Override public byte[] sign(byte[] data) { return testSigner.sign(data); }
};

CreateDidResult result = DidWebVh.create("example.com", bareSigner).execute();
assertThat(result.getLogEntry().getParameters().getUpdateKeys())
.containsExactly(testMultikey);
}

@Test
void extractMultikeyRejectsForeignVerificationMethod() {
Signer foreignSigner = new Signer() {
@Override public String keyType() { return testSigner.keyType(); }
@Override public String verificationMethod() { return "https://example.com/keys/1"; }
@Override public byte[] sign(byte[] data) { return testSigner.sign(data); }
};

assertThatThrownBy(() -> DidWebVh.create("example.com", foreignSigner).execute())
.isInstanceOf(ValidationException.class)
.hasMessageContaining("Cannot extract multikey");
}

@Test
void didDocumentHasVerificationMethod() {
CreateDidResult result = DidWebVh.create("example.com", testSigner)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package io.github.decentralizedidentity.didwebvh.core.crypto;

import com.google.gson.JsonObject;
import io.github.decentralizedidentity.didwebvh.core.ValidationException;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class JcsTest {

Expand Down Expand Up @@ -52,4 +54,11 @@ void canonicalizeUnicode() {
String result = new String(Jcs.canonicalize(input), StandardCharsets.UTF_8);
assertThat(result).contains("\u20ac");
}

@Test
void canonicalizeInvalidJsonThrowsValidationException() {
assertThatThrownBy(() -> Jcs.canonicalize("{not-json"))
.isInstanceOf(ValidationException.class)
.hasMessageContaining("JCS canonicalization failed");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,145 @@ void resolverCanBeConfiguredWithHttpTimeoutAndResponseLimit() {
assertThat(resolver).isNotNull();
}

@Test
void invalidVersionNumberQueryThrows() {
Signer signer = makeTestSigner();
CreateDidResult create = DidWebVh.create("example.com", signer).execute();
DidResolver resolver = new DidResolver(new StubRemoteDidFetcher(create.getLogLine()),
new FileDidFetcher(), new LogProcessor());

assertThatThrownBy(() -> resolver.resolve(create.getDid() + "?versionNumber=not-a-number"))
.isInstanceOf(ResolutionException.class)
.hasMessageContaining("Invalid versionNumber")
.extracting("error").isEqualTo("invalidDid");
}

@Test
void versionTimeQueryParamIsHonoured() {
Signer signer = makeTestSigner();
CreateDidResult create = DidWebVh.create("example.com", signer).execute();
LogEntry first = create.getLogEntry();
JsonObject secondState = first.getState().deepCopy();
secondState.addProperty("updated", "true");
LogEntry second = buildUpdateEntry(first, signer, null, secondState);
String log = first.toJsonLine() + "\n" + second.toJsonLine();

DidResolver resolver = new DidResolver(new StubRemoteDidFetcher(log),
new FileDidFetcher(), new LogProcessor());

// versionTime equal to the first entry's time selects the first entry.
ResolveResult result = resolver.resolve(
create.getDid() + "?versionTime=" + first.getVersionTime());

assertThat(result.getMetadata().getVersionId()).isEqualTo(first.getVersionId());
}

@Test
void proactiveWitnessFetchPullsWitnessFile() {
Signer author = makeTestSigner();
Signer witness = makeTestSigner();
String witnessDid = "did:key:" + extractMultikey(witness.verificationMethod());
WitnessConfig witnessConfig = new WitnessConfig(1,
Collections.singletonList(new WitnessEntry(witnessDid)));
CreateDidResult create = DidWebVh.create("example.com", author)
.witness(witnessConfig)
.execute();
WitnessProofCollection proofs = witnessProofs(
create.getLogEntry().getVersionId(), witness);
StubRemoteDidFetcher fetcher = new StubRemoteDidFetcher(create.getLogLine(),
JsonSupport.compact().toJson(proofs.getEntries()));

DidResolver resolver = new DidResolver(fetcher, new FileDidFetcher(),
new LogProcessor());

ResolveResult result = resolver.resolve(create.getDid(), ResolveOptions.builder()
.witnessFetchMode(ResolveOptions.WitnessFetchMode.PROACTIVE)
.build());

assertThat(result.getMetadata().getWitness()).isEqualTo(witnessConfig);
assertThat(fetcher.witnessFetchCount).isEqualTo(1);
}

@Test
void proactiveWitnessSwallowsNotFoundForUnwitnessedLog() {
Signer signer = makeTestSigner();
CreateDidResult create = DidWebVh.create("example.com", signer).execute();
StubRemoteDidFetcher fetcher = new StubRemoteDidFetcher(create.getLogLine());

DidResolver resolver = new DidResolver(fetcher, new FileDidFetcher(),
new LogProcessor());

ResolveResult result = resolver.resolve(create.getDid(), ResolveOptions.builder()
.witnessFetchMode(ResolveOptions.WitnessFetchMode.PROACTIVE)
.build());

// PROACTIVE attempted a witness fetch, but a 404 (notFound) must be
// swallowed and the resolve must succeed when the log has no witnesses.
assertThat(result.getDidDocument().getId()).isEqualTo(create.getDid());
assertThat(fetcher.witnessFetchCount).isEqualTo(1);
}

@Test
void proactiveWitnessRethrowsNonNotFoundErrors() {
Signer signer = makeTestSigner();
CreateDidResult create = DidWebVh.create("example.com", signer).execute();
RemoteDidFetcher fetcher = new RemoteDidFetcher() {
@Override public String fetchDidLog(String httpsUrl) {
return create.getLogLine();
}
@Override public String fetchWitnessProofs(String witnessUrl) {
throw new ResolutionException("upstream down", "httpError");
}
};
DidResolver resolver = new DidResolver(fetcher, new FileDidFetcher(),
new LogProcessor());

assertThatThrownBy(() -> resolver.resolve(create.getDid(),
ResolveOptions.builder()
.witnessFetchMode(ResolveOptions.WitnessFetchMode.PROACTIVE)
.build()))
.isInstanceOf(ResolutionException.class)
.extracting("error").isEqualTo("httpError");
}

@Test
void whenRequiredButWitnessMissingMapsToInvalidDid() {
Signer author = makeTestSigner();
Signer witness = makeTestSigner();
String witnessDid = "did:key:" + extractMultikey(witness.verificationMethod());
WitnessConfig witnessConfig = new WitnessConfig(1,
Collections.singletonList(new WitnessEntry(witnessDid)));
CreateDidResult create = DidWebVh.create("example.com", author)
.witness(witnessConfig)
.execute();
// Fetcher signals notFound for witness proofs (the chain *needs* them).
StubRemoteDidFetcher fetcher = new StubRemoteDidFetcher(create.getLogLine());

DidResolver resolver = new DidResolver(fetcher, new FileDidFetcher(),
new LogProcessor());

assertThatThrownBy(() -> resolver.resolve(create.getDid(),
ResolveOptions.builder()
.witnessFetchMode(ResolveOptions.WitnessFetchMode.WHEN_REQUIRED)
.build()))
.isInstanceOf(ResolutionException.class)
.hasMessageContaining("Witness proofs are required")
.extracting("error").isEqualTo("invalidDid");
}

@Test
void resolveFromLogValidatesAgainstDid() {
Signer signer = makeTestSigner();
CreateDidResult create = DidWebVh.create("example.com", signer).execute();

ResolveResult result = new DidResolver().resolveFromLog(
create.getLogLine(), create.getDid());

assertThat(result.getDidDocument().getId()).isEqualTo(create.getDid());
assertThat(result.getMetadata().getVersionId())
.isEqualTo(create.getLogEntry().getVersionId());
}

private static final class StubRemoteDidFetcher implements RemoteDidFetcher {
private final String didLog;
private final String witnessContent;
Expand Down
Loading
Loading