auth.provider API
    Preparing search index...

    Interface RefreshTokenFamilyStore

    Storage primitive for refresh-token families.

    Theme A: this interface exposes only single-key atomic primitives. The 4-outcome rotation ceremony (rotated | replayed | revoked | unknown_family) is composed in the wrapper layer (RefreshTokenFamilyRotation), NOT classified by the adapter.

    Per A3 §5.1.

    interface RefreshTokenFamilyStore {
        kind: string;
        findFamily(familyId: string): Promise<RefreshTokenFamily | null>;
        registerFamily(family: RefreshTokenFamily): Promise<void>;
        updateFamily(
            familyId: string,
            updater: (current: RefreshTokenFamily) => RefreshTokenFamilyUpdateDecision,
        ): Promise<RefreshTokenFamilyUpdateResult>;
    }
    Index
    kind: string
    • Atomically register a new refresh-token family.

      MUST throw RefreshTokenStorageError({ reason: "duplicate-family" }) if a family with the same familyId already exists (regardless of revoke / TTL state — duplicate familyId indicates RNG collision or programming bug).

      MUST throw RefreshTokenStorageError({ reason: "expired-at-issue" }) if family.expiresAtMs <= now() at call time.

      Concurrency contract: N concurrent calls with the same familyId MUST result in exactly one success and N-1 throws of "duplicate-family".

      Per A3 §5.1.

      Parameters

      Returns Promise<void>

    • Atomically read-modify-write the family aggregate.

      Adapter performs:

      1. Read current family state (or null if non-existent).
      2. Invoke updater(current).
      3. If the decision is { action: "commit", family }, attempt atomic CAS commit using an adapter-internal version token (NOT exposed in RefreshTokenFamily — adapters may use a separate version field, ETag, Redis WATCH, or any backend-native CAS primitive).
      4. On CAS conflict, retry by re-reading state and re-invoking updater up to a bounded retry limit; on exhaustion, throws RefreshTokenStorageError({ reason: "conflict-exhausted" }).
      5. If the decision is { action: "abort" }, abort without state change (no retry).

      Updater contract (NORMATIVE):

      • Updater is invoked with the current RefreshTokenFamily value (NEVER null — when the family does not exist, the adapter returns { outcome: "not-found" } directly without invoking the updater).
      • Updater MUST be a pure function (no observable side effects, no async I/O). Adapter MAY invoke updater multiple times due to CAS retry; consumers MUST NOT rely on exactly-once invocation.
      • Updater MUST NOT mutate the input RefreshTokenFamily (it is readonly at the type level; runtime adapters MAY freeze it additionally as defence-in-depth).
      • Updater MUST return a RefreshTokenFamilyUpdateDecision: { action: "commit", family, reason? } or { action: "abort", reason? }. Returning a bare RefreshTokenFamily, or null, is the pre-#274 shape and is no longer accepted.
      • A decision's reason is opaque to the adapter. The adapter MUST echo the settling decision's reason on the returned result and MUST NOT interpret, validate, or persist it. This is how a caller classifies an outcome inside the same atomic operation — the replacement for the pre-#274 closure-captured-variable pattern, which could not describe a commit that is nevertheless a rejection (see createRefreshTokenFamilyRotation, #274).
      • When the settling decision carried NO reason, the adapter MUST omit the key from the result rather than set it to undefined. The field is optional, so "absent" and "present but undefined" must not both occur: they are one value to the type and two to "reason" in result, Object.keys, toStrictEqual, and anything serialising the result. Use the exported withReason helper — { outcome: "aborted", ...withReason(decision.reason) } — which is what both in-tree adapters do, so a third store stays substitutable for them.
      • Updater MUST NOT commit a RefreshTokenFamily whose expiresAtMs is <= now(). Both adapters fail-closed by throwing RefreshTokenStorageError({ reason: "expired-at-issue" }) — symmetric with registerFamily and prevents committing a dead-on-arrival entry. Callers shrinking TTL during rotation should compute the new expiresAtMs from a forward window.

      Return value:

      • { outcome: "committed", family, reason? } — CAS succeeded; family is the newly-persisted state.
      • { outcome: "not-found" } — family did not exist; updater not invoked.
      • { outcome: "aborted", reason? } — updater aborted; no state change.

      Per A3 §5.1 + #274.

      Parameters

      Returns Promise<RefreshTokenFamilyUpdateResult>