(
owner: Owner,
issuer: string | null,
)
| 733 | }; |
| 734 | |
| 735 | const dcrCandidatesForIssuer = ( |
| 736 | owner: Owner, |
| 737 | issuer: string | null, |
| 738 | ): Effect.Effect<readonly DcrReuseCandidate[], StorageFailure> => |
| 739 | deps.fuma |
| 740 | .use("oauth_client.findMany", (db) => |
| 741 | looseDb(db).findMany("oauth_client", { |
| 742 | where: (b: any) => b("owner", "=", owner), |
| 743 | }), |
| 744 | ) |
| 745 | .pipe( |
| 746 | Effect.map((rows) => { |
| 747 | const matches = rows.flatMap( |
| 748 | (row): readonly (DcrReuseCandidate & { readonly createdAt: number })[] => { |
| 749 | if (parseOAuthClientOrigin(row).kind !== "dynamic_client_registration") return []; |
| 750 | // A candidate matches only via a non-null, canonicalized stored |
| 751 | // issuer. The GC migration backfills origin_issuer on every |
| 752 | // surviving DCR row, so post-migration a null-issuer row is a |
| 753 | // transient (unmigrated) row; skipping it just mints one duplicate |
| 754 | // the migration then GCs, rather than reusing on a fuzzy token-host |
| 755 | // guess. |
| 756 | const rowIssuer = |
| 757 | row.origin_issuer == null ? null : canonicalIssuerUrl(String(row.origin_issuer)); |
| 758 | const issuerMatches = rowIssuer !== null && dcrIssuerMatches(rowIssuer, issuer); |
| 759 | if (!issuerMatches) return []; |
| 760 | return [ |
| 761 | { |
| 762 | slug: OAuthClientSlug.make(String(row.slug)), |
| 763 | resource: row.resource == null ? null : String(row.resource), |
| 764 | redirectUri: |
| 765 | row.origin_redirect_uri == null ? null : String(row.origin_redirect_uri), |
| 766 | createdAt: candidateCreatedAt(row.created_at), |
| 767 | }, |
| 768 | ]; |
| 769 | }, |
| 770 | ); |
| 771 | // Deterministic reuse order: oldest first, slug as a stable tiebreak |
| 772 | // when timestamps collide or are missing. Without this, which of |
| 773 | // several live duplicates sharing an (owner, issuer) gets reused is |
| 774 | // whatever order the storage backend returned rows in — the reuse |
| 775 | // pick must be stable across boots and backends. |
| 776 | return [...matches] |
| 777 | .sort( |
| 778 | (a, b) => |
| 779 | a.createdAt - b.createdAt || (a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0), |
| 780 | ) |
| 781 | .map( |
| 782 | ({ slug, resource, redirectUri }): DcrReuseCandidate => ({ |
| 783 | slug, |
| 784 | resource, |
| 785 | redirectUri, |
| 786 | }), |
| 787 | ); |
| 788 | }), |
| 789 | ); |
| 790 | |
| 791 | const decideDcrClientReuse = ( |
| 792 | input: RegisterDynamicClientInput, |
no test coverage detected