class

KemalIdentity::ApiTokens::Repository

Inherits Reference < Object

Where personal access tokens live.

auth_version is read but not compared here

Sessions store the auth_version they were minted under and fail when it moves. Tokens do not: a password change should not silently break a deploy key, because the token was created deliberately and separately and its holder may be a machine with no way to notice.

The account's current version is still returned, so an application that does want tokens to die with a password change can compare it and act. That is a policy decision, and the repository's job is to make it possible rather than to make it.

Concurrency

Implementations must be safe for concurrent use from multiple fibers on multiple threads.

Instance methods

create(token : Token) : Nil

Stores a newly issued token.

Raises KemalIdentity::InfrastructureError on a duplicate digest — the unique index makes a collision a loud error rather than two accounts sharing a credential.

Source
delete_expired(before : Time) : Int32

Deletes rows past their expiry, returning the count. Disk reclamation only — correctness never depends on it, because expiry is evaluated on read.

Source
expire(id : String, at : Time) : Bool

Brings a token's expiry forward to at, returning false if it does not exist, is already revoked, or already expires at or before at.

This is what a rotation with an overlap window needs: the replacement is issued, the old credential is given a deadline instead of being killed outright, and the fleet has until then to pick the new one up. Because the deadline lands on the row, expiry is enforced by the authentication path on every request — no sweeper, no scheduled revoke, nothing that has to have run for the window to close (blueprints/0025, TOK-08).

It must never lengthen a token's life. "Expire" is not "renew": a rotation that could extend the credential it replaces is not a rotation, and a management screen that could push a deadline out is a way to keep a compromised credential alive. The comparison belongs in the statement rather than in a read followed by a write, so that two callers cannot interleave into a later deadline than either asked for:

UPDATE auth_api_tokens SET expires_at = $2
 WHERE id = $1 AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > $2)

A time in the past is allowed and closes the window immediately. The token then fails as Expired rather than Revoked, which is the honest reason: nobody revoked it.

Source
find_by_digest(digest : Bytes) : Lookup | Nil

Resolves a token by the digest of its secret, with account status.

Returns nil — never raises — when nothing matches, and when the token's account does not exist: the reference SQL is an inner join, so a token pointing at a deleted account resolves to nothing and the failure mode is closed.

Expiry and revocation are not evaluated here. This reports facts; the service decides what they mean, which is what lets a management screen list revoked tokens through the same repository.

Source
list_for_account(account_id : String) : Array(Token)

Every token for an account, newest first, revoked ones included.

This is the management screen. It returns revoked tokens too, because "when did I revoke that?" is exactly the question such a screen exists to answer.

Source
revoke(id : String, at : Time) : Bool

Marks one token revoked, returning false if it does not exist or was already revoked.

Source
revoke_all_for_account(account_id : String, at : Time) : Int32

Revokes every live token for an account, returning how many it revoked. What "revoke all my API tokens" calls, and the right response to a compromised account.

Source
revoke_family(ids : Array(String), account_id : String, at : Time) : Array(String)

Revokes, in one atomic step, every live token in ids that belongs to account_id, returning the ids it revoked. Ids that are unknown, already revoked or somebody else's are skipped and not reported, for the reason the owner-scoped Service#revoke gives: a token id is not secret, so reporting it would confirm it exists.

This is what a rotation needs when it is abandoned, or a set of related tokens is compromised together: two #revoke calls are two statements, and anything that reads between them — or a failure after the first — leaves the family half alive (blueprints/0025, TOK-08). In SQL it is one statement:

UPDATE auth_api_tokens SET revoked_at = $1
 WHERE account_id = $2 AND id = ANY($3) AND revoked_at IS NULL
RETURNING id

Why this is not abstract

An abstract method added here would stop every third-party adapter compiling, for a capability most applications never call. And a default that called #revoke in a loop would be the very thing this method exists to replace, while looking like it. So the default raises, an adapter overrides it when it can do it atomically, and it_behaves_like_an_api_token_repository_with_family_revocation is the contract for one that does. The three shipped adapters all do.

Source
touch(id : String, last_used_at : Time) : Bool

Moves last_used_at forward. Called only when the service's throttle allows it, never on every request.

Source