(
input: RegisterDynamicClientInput,
issuer: string | null,
flowRedirectUri: string | null,
)
| 802 | ); |
| 803 | |
| 804 | const decideDcrClientReuse = ( |
| 805 | input: RegisterDynamicClientInput, |
| 806 | issuer: string | null, |
| 807 | flowRedirectUri: string | null, |
| 808 | ): Effect.Effect< |
| 809 | { |
| 810 | readonly existingSlug: OAuthClientSlug | null; |
| 811 | readonly registrationSlug: OAuthClientSlug; |
| 812 | }, |
| 813 | StorageFailure |
| 814 | > => |
| 815 | Effect.gen(function* () { |
| 816 | const candidates = yield* dcrCandidatesForIssuer(input.owner, issuer); |
| 817 | const resource = input.resource ?? null; |
| 818 | // A candidate is reusable only when the callback it registered with the |
| 819 | // AS still matches the current flow's callback — strict servers reject an |
| 820 | // authorize request whose redirect_uri differs from the registration |
| 821 | // (e.g. the callback origin changed after a sandbox was recreated while |
| 822 | // the persisted client survived). A null stored redirect is a legacy row |
| 823 | // predating the column: treated as matching so an upgrade doesn't |
| 824 | // re-register every client whose callback never changed. A null FLOW |
| 825 | // redirect has nothing to compare against, so it also reuses — the only |
| 826 | // alternative is a fresh registration, which the missing-redirectUri |
| 827 | // guard would fail. |
| 828 | const redirectMatches = (candidate: DcrReuseCandidate): boolean => |
| 829 | candidate.redirectUri === null || |
| 830 | flowRedirectUri === null || |
| 831 | candidate.redirectUri === flowRedirectUri; |
| 832 | // A fresh registration must never take a slug an existing candidate |
| 833 | // holds: `createClient` deletes any colliding (owner, slug) row first, |
| 834 | // which would clobber a client that live connections still refresh |
| 835 | // through (a redirect-mismatched client stays valid for refresh — the |
| 836 | // token grant doesn't involve the redirect URI). |
| 837 | const takenSlugs = new Set(candidates.map((client) => String(client.slug))); |
| 838 | if (resource !== null) { |
| 839 | const matchingResource = candidates.find((client) => client.resource === resource); |
| 840 | if (matchingResource && redirectMatches(matchingResource)) { |
| 841 | return { existingSlug: matchingResource.slug, registrationSlug: matchingResource.slug }; |
| 842 | } |
| 843 | const slug = uniqueDcrSlug( |
| 844 | dcrClientSlug(issuer, candidates.length > 0 ? resource : null, input.slug), |
| 845 | takenSlugs, |
| 846 | ); |
| 847 | return { |
| 848 | existingSlug: null, |
| 849 | registrationSlug: slug, |
| 850 | }; |
| 851 | } |
| 852 | |
| 853 | // Resource-less request: only reuse a resource-LESS candidate. A client |
| 854 | // minted for a specific RFC 8707 resource must NOT be reused for a |
| 855 | // resource-less flow (its tokens are bound to that resource), so when only |
| 856 | // resource-scoped candidates exist we register a fresh resource-less client |
| 857 | // rather than silently borrowing one (the old `?? candidates[0]` bug). |
| 858 | const reusable = candidates.find( |
| 859 | (client) => client.resource === null && redirectMatches(client), |
| 860 | ); |
| 861 | if (reusable) return { existingSlug: reusable.slug, registrationSlug: reusable.slug }; |
no test coverage detected