feat(oidc): a kept identity taken in the host's transaction, and told kept without taking it

`takeKept` takes a `transaction`, so a host's step that fails (a name
taken meanwhile) gives the identity back instead of losing it; `isKept`
tells whether an identity waits under an attempt for its caller, so a
host can check before asking more of the person.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-23 21:21:55 +02:00
commit c6831b0b7b
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -262,13 +262,21 @@ final class OidcIdp implements IdentityProvider {
}
/// The identity [keep] kept under [attempt], taken once, by who began it
/// ([caller]); refusals as [verify]'s.
/// ([caller]), in the host's [transaction] if given: one that fails gives
/// it back, as when the name chosen is taken. Refusals as [verify]'s.
Future<VerifiedOidcIdentity> takeKept(
Session session, {
required UuidValue attempt,
UuidValue? caller,
Transaction? transaction,
}) async {
final taken = await _take(session, attempt, caller: caller, verified: true);
final taken = await _take(
session,
attempt,
caller: caller,
verified: true,
transaction: transaction,
);
return VerifiedOidcIdentity(
provider: taken.provider,
subject: taken.subject!,
@ -277,13 +285,39 @@ final class OidcIdp implements IdentityProvider {
);
}
/// The attempt [id], taken out: verified ([OidcAttempt.subject] set) or
/// not as asked, its time not run out, begun by [caller].
/// Whether [keep] keeps an identity under [attempt] for [caller], its time
/// not run out, without taking it: for the host to check before asking
/// more of the person, as whether the name they chose is free.
Future<bool> isKept(
Session session, {
required UuidValue attempt,
UuidValue? caller,
}) async {
// SELECT count(*) FROM "halkyon_oidc_attempt" WHERE "id" = $1
// AND "subject" IS NOT NULL AND "callerId" = $2 (IS NULL for none)
// AND "expiresAt" > $3
final kept = await OidcAttempt.db.count(
session,
where: (t) =>
t.id.equals(attempt) &
t.subject.notEquals(null) &
(caller == null
? t.callerId.equals(null)
: t.callerId.equals(caller)) &
(t.expiresAt > clock.now().toUtc()),
);
return kept > 0;
}
/// The attempt [id], taken out (in [transaction] if given): verified
/// ([OidcAttempt.subject] set) or not as asked, its time not run out,
/// begun by [caller].
Future<OidcAttempt> _take(
Session session,
UuidValue id, {
required UuidValue? caller,
required bool verified,
Transaction? transaction,
}) async {
// DELETE FROM "halkyon_oidc_attempt" WHERE "id" = $1
// AND "subject" IS [NOT] NULL RETURNING *
@ -292,6 +326,7 @@ final class OidcIdp implements IdentityProvider {
where: (t) =>
t.id.equals(id) &
(verified ? t.subject.notEquals(null) : t.subject.equals(null)),
transaction: transaction,
);
final attempt = taken.singleOrNull;
if (attempt == null || !clock.now().isBefore(attempt.expiresAt)) {