Skip to content

Nothing records that a key was minted or revoked #498

Open

Description

@Miracle656

Background

POST /admin/keys creates a row. DELETE /admin/keys/:id sets revokedAt.
Neither writes any record of the action itself — no actor, no time, no reason,
no source address. The ApiKey row carries createdAt and revokedAt, and
that is the entire history available.

So: a key exists that nobody remembers creating, and there is no way to find
out when it was created against which request, by whom, or why a key was
revoked. For a surface that mints credentials to a paid API, that is the first
thing an incident review asks for and the one thing that will not exist.

What to build

  • An append-only audit record for every admin action: what was done, to which
    key id, when, by which identity (L233 — design for it being absent until
    then), and from which address.
  • Record attempts, including rejected ones. A string of failed admin
    authentications is the signal worth having.
  • Never record the admin token, the plaintext key, or the key hash.
  • A way to read recent entries, admin-guarded like the rest.
  • Decide retention deliberately and say what it is.

Acceptance criteria

  • Key creation and revocation are both recorded
  • Failed admin authentications are recorded
  • Records carry time, action, target and source
  • No secret or plaintext key appears in a record
  • Entries are readable through an admin-guarded route

Drips Wave · Complexity: Intermediate · 150 points


Repo: Divinity-Labs/Lens. Auth is src/api/auth.ts, key minting
src/api/admin.ts, usage reporting src/api/usage.ts, metering
src/x402/metering.ts, the ApiKey model in prisma/schema.prisma.

Run the tests: npm test. The tests are vitest, not jest. A single file
is npx vitest run src/__tests__/auth.test.ts.

Load test (prices) is already failing on main. Not your PR.

Do not weaken what is already right. Three things are correct today and
should survive every change here:

  • generateApiKey() is lens_ plus randomBytes(24) — 192 bits. Do not
    shorten it, and do not make it guessable or sequential.
  • hashApiKey() is an unsalted SHA-256. That is correct for a
    high-entropy random key: it must stay fast and deterministic so a lookup is
    one indexed query. Do not "improve" it into bcrypt or argon2 — those are for
    low-entropy human passwords, and here they would only make every request slow.
  • Only the hash is ever stored. The plaintext is returned once, at creation,
    and never again.

Admin routes carry config: { public: true }. That does not mean public —
it means "skip the API-key hook", because they enforce ADMIN_TOKEN instead.
Removing that flag locks the admin surface behind the keys it exists to mint.

Never elide an address, a key or a token in code, comments, tests or
documentation. Write it in full or not at all — and never write a real key
anywhere.



Required: Before submitting, join the contributor Telegram so your work can be tracked and counted toward the Stellar Wave: https://t.me/+fxHXq8f1SwlkZDBk

Activity

  1. Miracle656 commented on Oct 18, 2026

    @Miracle656
    CollaboratorAuthor

    ⏸️ Not funded in the current Wave — please do not start yet

    This issue is real and the work is wanted, but it is not currently part of the Drips Wave programme, so a PR against it right now would earn no points. I would rather say that plainly up front than have anyone find out after writing the code.

    The repo's Wave budget for this round is exhausted — 25,000 points across 271 labelled issues — and this batch was published just after it ran out. That is my error in pacing, not a reflection on the issue.

    What happens next: it will be added to the Wave when the next round opens, at the points value in its label. The description, the acceptance criteria and the code references all stay as they are, so nothing here is wasted.

    If you want something to work on today, pick an issue that is already in the Wave — the Stellar Wave label plus an unassigned state is the signal, and Drips will let you apply for it. If Drips will not let you apply for an issue, that is the budget, not you.

    If you have already started on this one, say so here and I will make sure you are first in line for it when the next round opens.

  2. solaawojobi00-bit commented on Oct 18, 2026

    @solaawojobi00-bit
    Contributor

    @solaawojobi00-bit has applied to work on this issue as part of the Stellar Wave Program's 10th wave.

    I’ll jump on this — src/api/websocket.ts lacks authentication entirely, allowing unauthenticated clients to freely consume high-value live price feeds without metering, keys, quotas, or usage attribution.
    To resolve this:

    • Authenticate incoming WebSocket connections during the handshake (via subprotocol or connection parameters) using API keys.
    • Enforce quota tracking and usage metering for all streamed data connections.
      Kindly assign it to me.

    ℹ️ Repo Maintainers: To accept this application, review their application or assign @solaawojobi00-bit to this issue.

  3. mysterio123865 commented on Oct 18, 2026

    @mysterio123865

    @mysterio123865 has applied to work on this issue as part of the Stellar Wave Program's 10th wave.

    Application: admin audit log

    Decision: an append-only AdminAuditLog table, 90-day retention, configurable. Ninety days covers a normal incident-review window without the table growing forever. It's set with AUDIT_RETENTION_DAYS, and a scheduled prune is the only thing that ever deletes rows.

    Approach

    • Add an AdminAuditLog model with id, at, action, targetKeyId (nullable), actor (nullable), sourceIp, outcome (success, rejected, error) and reason (nullable). The model has no hash, token, or key columns, so a secret can't be recorded by accident.
    • actor is nullable so the log works now, and No admin action has an author #497's named identity (L233) fills it in when it lands. Until then, legacy-token requests record no actor.
    • Add a small recordAudit() helper and call it from POST /admin/keys and DELETE /admin/keys/:id. It records the attempt before the action runs, so rejected requests are captured too.
    • Failed admin authentications are recorded as admin.auth_failed with the source IP and no target. The presented token is never read into the record.
    • reason on revoke is optional free text from the request body, length-capped.
    • Add GET /admin/audit?limit=&before= for recent entries, newest first with cursor pagination. It stays admin-guarded and keeps config: { public: true }.
    • The source IP is request.ip. I'll document the trustProxy setting needed behind a proxy.
    • generateApiKey(), hashApiKey(), and hash-only storage are untouched.

    Tests (vitest):

    • Create and revoke each write one record with time, action, target and source.
    • A failed admin auth writes a rejected record.
    • A rejected request writes no key or token content.
    • A scan of the record JSON finds no plaintext key, hash, or admin token.
    • The audit route rejects unauthenticated requests.
    • Prune removes only rows older than the retention window.

    I've joined the contributor Telegram and am ready to start once assigned.

    ℹ️ Repo Maintainers: To accept this application, review their application or assign @mysterio123865 to this issue.

  4. drips-wave commented on Oct 18, 2026

    @drips-wave

    Congratulations, @mysterio123865! 🎉 Your application was accepted by the repo's maintainers, and the issue is due on October 22, 2026.

    🧑‍💻 @mysterio123865: Please resolve the issue such that the repo's maintainers have enough time to review your contribution before the due date. You'll earn Points for completing the issue on-time, which will make you eligible for a share of the Stellar Wave Program's reward pool.

    Warning

    When opening a PR, please link it to this issue to ensure it gets tracked accurately. Points are awarded when this issue is marked as completed by the maintainer.

    🤠 Repo maintainers: Please keep an eye on the contributor's progress and review their work before the due date. You can manage this issue, including adjusting its complexity and points, here.

    🌊 Happy Wave 🌊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions