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
11 changes: 11 additions & 0 deletions .github/AGENTS.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,17 @@ Every mutating command is wrapped in a BTX lifecycle:
* Use `@Nullable` sparingly and only at boundaries that genuinely allow absence.
* Prefer `Optional` instead of `@Nullable` for return types.
* Never use `Optional` in fields, and avoid `Optional` parameters unless there is no cleaner API shape.
+
[NOTE]
====
*Exception — immutable DTO record components:* `Optional<T>` is acceptable as a record _component_ when all of the
following hold: (a) the record is an immutable value-type DTO that is never persisted or serialized to an external
format, (b) the optionality is an intrinsic part of the domain concept (e.g. `expirationTime` that may genuinely be
absent), and (c) there is no natural sentinel or null-object alternative.
`KeyIndexResult` and `UidIndexEntry` satisfy these criteria — they are transient application-layer DTOs consumed and
discarded within a single request, never mapped by JPA or written to JSON/XML.
JPA entity fields and any class with `@XmlRootElement`, `@JsonSerialize`, or similar must still not use `Optional`.
====
* Avoid mixing docs, refactors, and behavior changes unless the task needs it.
* After each Java or `pom.xml` change, run `./mvnw spotless:apply`.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2023-2024 The java-keyserver project team.
*
* SPDX-License-Identifier: EUPL-1.2 OR Apache-2.0
*/
package io.github.bmarwell.keyserver.application.api;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;

/// Full index entry for one key, used in `op=index` responses.
///
/// Contains the key metadata fields needed for the HKP machine-readable
/// format (`pub:` line) plus all verified UIDs (`uid:` lines).
/// Only verified UIDs appear in {@code verifiedUids} — unverified UIDs
/// are never exposed.
///
/// The {@code disabled} flag is a keyserver-administrative flag (HKP `d`).
/// It is independent of the OpenPGP {@code revoked} flag: an operator may
/// disable a key on this server without the key owner having revoked it.
public record KeyIndexResult(
String fingerprint,
int algorithm,
Optional<Integer> bitStrength,
OffsetDateTime creationTime,
Optional<OffsetDateTime> expirationTime,
boolean revoked,
boolean disabled,
List<UidIndexEntry> verifiedUids) {}
Comment thread
bmarwell marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.github.bmarwell.keyserver.common.ids.KeyId;
import io.github.bmarwell.keyserver.common.ids.PgpPublicKey;
import io.github.bmarwell.keyserver.common.ids.RepositoryName;
import java.util.List;
import java.util.Optional;

/**
Expand All @@ -30,4 +31,14 @@ public interface KeyRepositoryService {
/// @param search HKP search string
/// @param exactMatch if true, only exact fingerprint or email matches are returned
Optional<String> getArmoredKeyBySearch(String search, boolean exactMatch);

/// Searches for all matching keys and returns their index metadata for `op=index` responses.
///
/// Uses the same search routing as {@link #getArmoredKeyBySearch} but returns all matching
/// keys (not just the first) along with algorithm, timestamp, and verified-UID metadata
/// needed to render the HKP machine-readable index format.
///
/// @param search HKP search string
/// @param exactMatch if true, only exact fingerprint or email matches are returned
List<KeyIndexResult> searchForIndex(String search, boolean exactMatch);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright (C) 2023-2024 The java-keyserver project team.
*
* SPDX-License-Identifier: EUPL-1.2 OR Apache-2.0
*/
package io.github.bmarwell.keyserver.application.api;

import java.time.OffsetDateTime;
import java.util.Optional;

/// Metadata for a single verified UID, used in `op=index` responses.
///
/// All timestamps are `Optional` because UID packets do not always carry
/// creation/expiration data (older keys often omit them).
public record UidIndexEntry(
String uidRaw,
Optional<OffsetDateTime> creationTime,
Optional<OffsetDateTime> expirationTime,
boolean revoked) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright (C) 2023-2024 The java-keyserver project team.
*
* SPDX-License-Identifier: EUPL-1.2 OR Apache-2.0
*/

/// Primary application port: service interfaces and shared query result types.
///
/// All types in this package are non-null by default (jspecify `@NullMarked`).
/// Fields and parameters that may be `null` are explicitly annotated with
/// {@link org.jspecify.annotations.Nullable}.
@NullMarked
package io.github.bmarwell.keyserver.application.api;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
package io.github.bmarwell.keyserver.application.core;

import io.github.bmarwell.keyserver.application.api.KeyIndexResult;
import io.github.bmarwell.keyserver.application.api.KeyRepositoryService;
import io.github.bmarwell.keyserver.application.port.repository.KeyRepository;
import io.github.bmarwell.keyserver.common.ids.KeyId;
Expand All @@ -14,6 +15,7 @@
import jakarta.enterprise.inject.Default;
import jakarta.inject.Inject;
import java.io.Serializable;
import java.util.List;
import java.util.Optional;

@Default
Expand All @@ -38,6 +40,11 @@ public Optional<String> getArmoredKeyBySearch(String search, boolean exactMatch)
return keyRepository.findBySearch(search, exactMatch).map(KeyRepository.KeySearchResult::armoredKey);
}

@Override
public List<KeyIndexResult> searchForIndex(String search, boolean exactMatch) {
return this.keyRepository.findManyBySearch(search, exactMatch);
}

// CDI-friendly setter for unit testing
public void setKeyRepository(KeyRepository keyRepository) {
this.keyRepository = keyRepository;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ public void publishVerifiedUid(String fingerprint, String uidRaw, String uidEmai
public Optional<KeySearchResult> findBySearch(String search, boolean exactMatch) {
return Optional.empty();
}

@Override
public List<io.github.bmarwell.keyserver.application.api.KeyIndexResult> findManyBySearch(
String search, boolean exactMatch) {
return List.of();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,12 @@
<version>0.1.0-SNAPSHOT</version>
<name>Java Keyserver :: application :: port :: repository</name>

<dependencies>
<dependency>
<groupId>io.github.bmarwell.keyserver</groupId>
<artifactId>keyserver-application-api</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
*/
package io.github.bmarwell.keyserver.application.port.repository;

import io.github.bmarwell.keyserver.application.api.KeyIndexResult;
import java.util.List;
import java.util.Optional;

/// Secondary (outbound) port for the published key store.
Expand All @@ -17,7 +19,7 @@ public interface KeyRepository {

/// Result type returned by key search queries.
///
/// Contains the minimal data required by the HKP `op=get` and `op=index` responses.
/// Contains the minimal data required by the HKP `op=get` response.
/// The `armoredKey` field holds only the verified UIDs (privacy-preserving, per
/// {@link #publishVerifiedUid} contract).
record KeySearchResult(String fingerprint, String armoredKey) {}
Expand Down Expand Up @@ -52,4 +54,17 @@ record KeySearchResult(String fingerprint, String armoredKey) {}
/// @param search HKP search string
/// @param exactMatch when true, only exact fingerprint/email matches are returned
Optional<KeySearchResult> findBySearch(String search, boolean exactMatch);

/// Searches for keys by fingerprint, key ID, email address, or UID substring and returns
/// full index metadata for all matching keys.
///
/// Uses the same search routing as {@link #findBySearch} but returns all matching keys
/// (not limited to the first) and includes per-key algorithm, timestamp, and verified-UID
/// metadata needed to render HKP `op=index` responses.
///
/// Only verified UIDs are included in each {@link KeyIndexResult#verifiedUids()} list.
///
/// @param search HKP search string
/// @param exactMatch when true, only exact fingerprint/email matches are returned
List<KeyIndexResult> findManyBySearch(String search, boolean exactMatch);
}
Loading