Skip to content

feat(certifications): Wave D atestats — verifiable certificates + open-source decision - #309

Merged
Prekzursil merged 5 commits into
mainfrom
wave-d/certificates
Nov 14, 2026
Merged

Prekzursil merged 5 commits into
mainfrom
wave-d/certificates

Conversation

@Prekzursil

Copy link
Copy Markdown
Owner

What

Closes the last two open Wave D roadmap items (docs/ROADMAP-COMPETITIVE-2026-10-22.md §7 Wave D):

1. Certifications/attestations — the honest variant

Roadmap: "certifications/attestations (HackerRank/PAST pattern — only with brand recognition; a leaderboard-derived 'atestat' is the honest variant)"; also closes backlog H-120 (verifiable participation certificates; contest medals were the Wave C first increment).

  • Backend certificates app: Certificate model — public UUID verify id, WC-YYYY-NNNNNN serial, frozen bilingual achievement snapshot, revocation ledger with reason. Three derivations, all leaderboard-derived (never self-paced/unproctored):
    • CONTEST_PARTICIPATION — every official (non-virtual, non-DQ) finisher of a rated contest, final rank + post-contest rating.
    • RATING_MILESTONE — 1200/1400/1600/1800/2000 crossed in rated competition (peak semantics: an organic later drop never un-earns it).
    • SOLVE_MILESTONE — 10/50/100/250/500 distinct APPROVED problems solved (same derivation as the leaderboard, so they can never disagree).
  • Issuance is server-derived only (no create endpoint): hooks inside compute_contest_ratings and the post-AC update_user_entry / recompute_all_leaderboards; rollback_contest_ratings revokes the discarded contest's atestats and audits milestones the replayed ladder no longer supports; manage.py issue_certificates backfills idempotently (--notify opt-in — replaying history never spams).
  • API (/api/v1/certificates/): anonymous verify/<uuid>/ (the H-120 employer/teacher flow — answers VALID or REVOKED with reason, never vanishes), mine/ (paginated), public ?username= (LEG-2: minors + inactive holders never surface), admin <pk>/revoke/ (reason mandatory, first reason sticks).
  • Frontend: /certificates holder list + /certificates/[uuid] anonymous verification page with a printable atestat sheet (UXP-3 print CSS — Print / save-as-PDF); navbar account-menu link; certificate_issued notification rendering + deep link; 27 keys hand-written across all 28 locale catalogs (+ public copies re-exported; parity test green).
  • Tests: 30 backend (certificates modules at 100%; whole-suite 100% / 1784 passed / zero warnings), 12 vitest, 5 new e2e (valid/revoked/not-found/navbar/anonymous).

2. Open-source/self-host decision

docs/DECISION-OPEN-SOURCE.md — the roadmap's "DMOJ/infoarena precedent vs maintenance cost" call: closed platform pre-traction, rolling component-level OSS (K-149), no self-host support, content portability as the community promise, with an explicit reopen path and review triggers.

Also updates the trackers (BACKLOG.md, ROADMAP.md layers 0/4/5/6/8), API-MAP.md and RUNBOOK.md (Wave D operator notes: issue_certificates, TWO_FACTOR_ENFORCE).

Wave D verification (already-merged items re-verified this wave)

JDG-2 #239, OPS-2 #240, OPS-6 #241, EDU-10 #242 (+ LEG-2 gate #228), OPS-7 groundwork #243 (no paid flows), ANA-1 #244, COM-4 #246/#247, COM-5 #257 — each checked against its roadmap design text and covered by the green suites above.

Gates (run in this worktree)

  • SECRET_KEY=x DB_PASSWORD=x ...python -m pytest -q → 1784 passed, TOTAL 100% coverage, zero warnings
  • npm run test → 665 passed (82 files) · npx tsc -p tsconfig.json --noEmit → clean · npm run lint → 0 problems · npm run build → clean · npx playwright test → 136 passed / 3 skipped (pre-existing skips)

risk:low

…le certificates + open-source decision

Closes the two remaining Wave D roadmap items:

1. certifications/attestations (the honest variant — 'a leaderboard-derived
   atestat'; also H-120 verifiable participation certificates):
   - New backend app: Certificate model (uuid verify id, WC-YYYY-NNNNNN
     serial, frozen bilingual achievement snapshot, revocation ledger) with
     three derivations — CONTEST_PARTICIPATION (official rated finisher,
     rank + post-rating), RATING_MILESTONE (1200..2000, peak semantics),
     SOLVE_MILESTONE (10..500, same derivation as the leaderboard).
   - Issuance hooks: the ratings commit (inside compute_contest_ratings)
     and the post-AC leaderboard recompute; rollback_contest_ratings now
     revokes the discarded contest's atestats and audits milestones the
     replayed ladder no longer supports. Deliberately NO create endpoint.
   - API: anonymous GET verify/<uuid>/, GET mine/ (paginated), public
     GET ?username= (LEG-2: minors/inactive excluded), admin POST revoke
     (reason mandatory, first reason sticks).
   - manage.py issue_certificates: idempotent backfill (--notify opt-in).
   - Frontend: /certificates holder list + /certificates/[uuid] anonymous
     verification page with printable atestat sheet (UXP-3 print CSS);
     navbar account link; certificate_issued notification rendering +
     deep link; 26 keys hand-translated across all 28 locale catalogs
     (+nav_certificates), public copies re-exported.
   - Tests: 30 backend tests (100% module coverage; suite total 100%),
     12 vitest, 5 e2e (mock api) incl. honest REVOKED + not-found paths.

2. Open-source/self-host decision — docs/DECISION-OPEN-SOURCE.md: closed
   platform pre-traction (maintenance + security-audit cost), rolling
   component-level OSS (K-149), content portability as the community
   promise; explicit reopen path recorded.

Also updates the trackers (BACKLOG.md, ROADMAP.md layers 0/4/5/6/8),
API-MAP and RUNBOOK (Wave D operator notes).

risk:low
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-14T04:36:09.719557Z d36114e New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment on lines +207 to +217
for row in UserRating.objects.values("user_id", "rating")
}
stale = Certificate.objects.filter(
kind=Certificate.Kind.RATING_MILESTONE, revoked_at__isnull=True
)
for cert in stale:
rating = ratings.get(cert.user_id)
if rating is None or (cert.threshold or 0) > rating:
_revoke(cert, reason)
revoked += 1
return revoked

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: audit_rating_milestones compares thresholds against current UserRating.rating rather than peak rating, revoking certificates from users who experienced normal subsequent rating drops.
Severity: HIGH

Suggested Fix

Derive each user's peak historical rating from replayed RatingHistory records (e.g., using Max('rating')) instead of querying current UserRating.rating, and evaluate milestone revocation against that peak value.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: backend/certificates/services.py#L194-L217

Potential issue: `audit_rating_milestones` evaluates users against their current
`UserRating.rating` rather than their historical peak rating. When
`rollback_contest_ratings` is executed, ratings history is replayed and
`audit_rating_milestones` is called platform-wide. If a user previously achieved a
rating milestone but later dropped organically below that threshold in subsequent
contests, their valid milestone certificate is erroneously revoked. Because `_issue`
looks up existing records and skips re-issuance, revoked certificates cannot be
re-earned even if the rating increases again.

Did we get this right? 👍 / 👎 to inform future reviews.

Comment on lines +157 to +161
from certificates.services import recompute_solve_milestones

for user_id, acc in per_user.items():
if acc["solved_count"] >= 10:
recompute_solve_milestones(user_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: recompute_all_leaderboards invokes recompute_solve_milestones using the default notify_user=True, causing scheduled background recomputations to dispatch user notifications.
Severity: MEDIUM

Suggested Fix

Pass notify_user=False when calling recompute_solve_milestones within the loop in recompute_all_leaderboards.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: backend/leaderboard/aggregation.py#L157-L161

Potential issue: `recompute_all_leaderboards` in `backend/leaderboard/aggregation.py`
invokes `recompute_solve_milestones(user_id)` without specifying `notify_user=False`. As
a result, the default `notify_user=True` triggers `notify()` via `_issue()` for newly
generated certificates during background processing. When the scheduled nightly Celery
beat task runs, it sends non-event-driven notification messages to users rather than
keeping replay and backfill tasks silent.

Did we get this right? 👍 / 👎 to inform future reviews.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6c2e45c75

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +205 to +208
ratings = {
row["user_id"]: row["rating"]
for row in UserRating.objects.values("user_id", "rating")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Audit rating milestones against the surviving historical peak

When a user legitimately reached 1800 and later fell to 1450, rolling back an unrelated contest revokes their 1600 and 1800 certificates even if the surviving, replayed history still contains the qualifying result. This audit reads the current UserRating rather than the maximum surviving RatingHistory.rating, contradicting the peak-achievement semantics. Because _issue never restores revoked certificates, this incorrectly invalidates those achievements permanently. Compare each threshold against the surviving historical maximum instead.

Useful? React with 👍 / 👎.

Comment thread backend/certificates/services.py Outdated
Comment on lines +267 to +269
for row in UserRating.objects.values("user_id", "rating"):
milestones += len(
issue_rating_milestones(row["user_id"], row["rating"], notify_user=notify_users)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Backfill milestones from historical ratings rather than current state

For existing users whose ratings have fallen since their peak, the initial issue_certificates backfill omits milestones they previously earned. For example, a user with an 1800 history entry and a current rating of 1450 receives only the 1200 and 1400 certificates, whereas a user following the same trajectory after deployment retains all four milestones. Derive the backfilled rating milestones from each user's maximum RatingHistory.rating so rollout and recovery reproduce the event-driven issuance semantics.

Useful? React with 👍 / 👎.

Comment on lines +32 to +35
certificate = get_object_or_404(
Certificate.objects.select_related("user"), uuid=certificate_uuid
)
return Response(CertificateSerializer(certificate).data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Revoke participation certificates when their holder is disqualified

If an organizer disqualifies a participant after certificates have been issued, this verification endpoint continues returning VALID for that participant's certificate. The existing ContestViewSet.disqualify action in backend/contests/views.py only updates the participation flags; the new certificate code has no disqualification hook, and revoke_contest_certificates is called only by the separate whole-contest ratings rollback. Consequently, someone removed from the official standings can still present a valid final-rank certificate. Wire the disqualification action to revoke the affected participant's certificate with the recorded reason.

Useful? React with 👍 / 👎.

Comment thread src/app/certificates/[uuid]/page.tsx Outdated
Comment on lines +183 to +187
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center', marginTop: '1.25rem' }} data-noprint>
<PrintCertificateButton label={t('certificates_print_button', 'Print / save as PDF')} />
<p style={{ fontSize: '0.8rem', color: 'var(--muted)' }}>
{t('certificates_verify_note', 'Anyone can verify this certificate at')}{' '}
<code style={{ fontFamily: 'monospace' }}>{verifyUrl}</code>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the verification URL visible on printed certificates

When the holder prints or saves the certificate as a PDF, src/app/globals.css hides this entire data-noprint container, including the only displayed verification URL and UUID. The remaining certificate contains a serial number, but backend/certificates/urls.py provides verification only by UUID, not by serial. A recipient of the exported certificate therefore has no verification identifier unless browser-generated print headers happen to include the page URL. Hide only the print button and retain an explicit verification URL on the printable sheet.

Useful? React with 👍 / 👎.

Comment on lines +61 to +71
cert, created = Certificate.objects.get_or_create(
user_id=user_id,
kind=kind,
contest=contest,
threshold=threshold,
defaults={"details": details or {}},
)
if not created:
return None
cert.serial = f"WC-{timezone.now().year}-{cert.pk:06d}"
cert.save(update_fields=["serial"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Milestone certificates revoked during rating rollbacks cannot be re-issued or un-revoked when a user re-earns the milestone threshold.
Severity: LOW

Suggested Fix

Distinguish between administrative revocations and audit-driven revocations (e.g., via a revocation reason or source field). Allow _issue or a dedicated re-validation path to reactivate or update audit-revoked milestone certificates when a user legitimately re-qualifies for the threshold.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: backend/certificates/services.py#L59-L71

Potential issue: In `_issue`, `Certificate.objects.get_or_create` returns
`created=False` if a milestone certificate record already exists, returning `None`
without un-revoking or updating it. When rating history is rolled back and replayed via
`rollback_contest_ratings`, `audit_rating_milestones` revokes milestones for users who
temporarily drop below the threshold. If a user subsequently climbs back and re-earns
the milestone rating legitimately in a future contest, `_issue` skips issuance because
the revoked row already exists. Due to the `uniq_certificate_threshold` database
constraint, a new certificate cannot be created, leaving the milestone permanently
revoked.

Comment on lines +137 to +147
# milestones for every finisher (idempotent; notifications follow the
# caller's replay-suppression choice).
from certificates.services import (
issue_contest_certificates,
issue_rating_milestones,
)

ratings_after = {result.user_id: result.rating for result in results}
issue_contest_certificates(contest, ranks, ratings_after, notify_users=notify_users)
for user_id, rating in ratings_after.items():
issue_rating_milestones(user_id, rating, notify_user=notify_users)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Certificate issuance runs outside the rating transaction, causing permanent certificate loss if post-commit issuance encounters an error.
Severity: MEDIUM

Suggested Fix

Wrap certificate issuance within a transaction or error handling mechanism that allows retrying certificate generation independently if ratings are already committed, rather than returning early without issuing certificates.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: backend/ratings/services.py#L136-L147

Potential issue: In `compute_contest_ratings`, rating computations and history rows are
committed inside an atomic transaction, while certificate issuance
(`issue_contest_certificates` and `issue_rating_milestones`) runs afterward outside the
transaction block. If an unhandled exception or connection error occurs during
certificate generation or notification dispatch, the task fails with ratings already
committed. On subsequent retries or manual re-finalization attempts, the
`RatingHistory.objects.filter(contest=contest).exists()` guard returns early, preventing
certificate issuance from running again automatically.

Comment on lines +144 to +147
ratings_after = {result.user_id: result.rating for result in results}
issue_contest_certificates(contest, ranks, ratings_after, notify_users=notify_users)
for user_id, rating in ratings_after.items():
issue_rating_milestones(user_id, rating, notify_user=notify_users)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Milestone certificates are issued using internal Glicko ratings instead of display ratings, causing new users to receive certificates for thresholds higher than their public rating.
Severity: MEDIUM

Suggested Fix

Convert result.rating to display_rating before passing values to issue_rating_milestones, taking rating suppression into account. Update audit_rating_milestones and backfill functions to inspect display rating instead of UserRating.rating.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: backend/ratings/services.py#L144-L147

Potential issue: Milestone certificates are evaluated and awarded using internal Glicko
ratings (`result.rating`) instead of user-facing display ratings (`display_rating`).
Because newcomers have a high suppression value applied to calculate display ratings
(e.g., starting at internal 1500 with a -400 suppression), a new user can earn milestone
certificates for thresholds like 1200 or 1400 after their first contest despite their
public profile and rating band displaying around 1100. Similarly, audit and backfill
routines compare thresholds against internal ratings rather than display ratings.

Also affects:

  • backend/certificates/services.py:205~216

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41530a0f5b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +186 to +189
if had:
revoke_contest_certificates(
contest, reason=f"contest {contest.id} ratings discarded by organizer"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile replayed contests' certificate snapshots after rollback

When an earlier contest is discarded, replay recalculates the post-contest ratings of surviving later contests, but this hook revokes participation certificates only for the discarded contest. During replay, issue_contest_certificates reaches _issue, which returns immediately for existing rows without reconciling their snapshots. Consequently, later contests' certificates remain VALID while displaying a details.rating_after that disagrees with the rebuilt RatingHistory. Reconcile affected surviving certificates as well, either by correcting their snapshots with an audit trail or revoking and replacing them.

Useful? React with 👍 / 👎.

Comment on lines +183 to 193
revoke_contest_certificates,
)

if had:
revoke_contest_certificates(
contest, reason=f"contest {contest.id} ratings discarded by organizer"
)
audit_rating_milestones()
if not had:
return f"contest {contest.id} was never rated; nothing to roll back"
return f"contest {contest.id} discarded (was rated); {replayed} contests replayed"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: rollback_contest_ratings does not update rating_after in existing contest participation certificates during contest replay, leaving certificates with stale rating snapshots.
Severity: MEDIUM

Suggested Fix

In _issue or rollback_contest_ratings, ensure existing participation certificates for replayed contests have their details['rating_after'] snapshot updated when ratings are recomputed, or revoke and reissue them alongside discarded contest certificates.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: backend/ratings/services.py#L175-L193

Potential issue: During `rollback_contest_ratings`, all rating histories and user
ratings are cleared and subsequently replayed chronologically across all remaining
contests via `compute_contest_ratings`. This recalculates ratings and invokes
`issue_contest_certificates`. However, `_issue` uses `get_or_create` with certificate
snapshots passed in `defaults`. Because participation certificates already exist for
subsequent replayed contests, the defaults are ignored and the certificates remain
marked VALID with outdated `rating_after` snapshot details, causing certificates to
contradict recalculated rating history.

Also affects:

  • backend/certificates/services.py:59~71

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cdb082f616

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +159 to +161
for user_id, acc in per_user.items():
if acc["solved_count"] >= 10:
recompute_solve_milestones(user_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Audit solve certificates after corrected verdicts reduce solve counts

If an admin rejudges a previously accepted submission and it becomes non-AC, running recompute_all_leaderboards() corrects the user's solve count but leaves any now-unsupported solve certificates VALID. This loop only issues certificates, skips users below 10 solves, and never visits users whose leaderboard rows were deleted. For example, a user whose tenth solve was invalidated still presents a valid ten-problem certificate after the full rebuild. Audit existing solve certificates against the corrected results and revoke unsupported milestones with a reason; invalidated judging results should not be treated as genuine lifetime achievements.

Useful? React with 👍 / 👎.

)

ratings_after = {result.user_id: result.rating for result in results}
issue_contest_certificates(contest, ranks, ratings_after, notify_users=notify_users)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive participation certificates from the official standings

When a rated contest includes registered users who never submitted, this call issues them final-rank certificates despite their absence from the official standings. _final_rank_map() includes every non-DQ, non-virtual registration, even without any ContestScore rows, whereas ContestViewSet.standings() uses contests/awards.py::build_ranking_input(), which includes only users with score rows. These extra registrations can also shift zero-score contestants' certificate ranks relative to the displayed board. Derive certificate recipients and final ranks from the official standings rather than assuming the rating rank map represents the same field.

Useful? React with 👍 / 👎.

@Prekzursil

Copy link
Copy Markdown
Owner Author

On the failing quality check (pre-existing, not from this PR): the basedpyright gate has been red repo-wide on every run since it started actually executing (workflow fixed in #304 on 2026-10-27) — including on main itself and on merged PRs #307/#308. Root cause: the gate runs basedpyright against a runner without Django installed, so every tuple-form TextChoices member used as str (and similar inferences) errors — ~230 pre-existing locations across submissions/contests/classroom/etc. (enumerated in run 43557342180).

This PR adds zero errors to that gate — all certificates-app errors were fixed in-repo (single-string member form + Optional narrowing, commits cdb082f/d36114e4). All other checks are green, including the required CodeQL. Fixing the 230 pre-existing locations belongs to a dedicated type-hygiene PR (tracked in the wave report).

@Prekzursil
Prekzursil merged commit 5c70093 into main Nov 14, 2026
10 of 11 checks passed
@Prekzursil
Prekzursil deleted the wave-d/certificates branch November 14, 2026 04:43
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.

2 participants