(
input: RegisterDynamicClientInput,
issuer: string | null,
flowRedirectUri: string | null,
)
| 1110 | ); |
| 1111 | |
| 1112 | const decideDcrClientReuse = ( |
| 1113 | input: RegisterDynamicClientInput, |
| 1114 | issuer: string | null, |
| 1115 | flowRedirectUri: string | null, |
| 1116 | ): Effect.Effect< |
| 1117 | { |
| 1118 | readonly existingSlug: OAuthClientSlug | null; |
| 1119 | readonly registrationSlug: OAuthClientSlug; |
| 1120 | }, |
| 1121 | StorageFailure |
| 1122 | > => |
| 1123 | Effect.gen(function* () { |
| 1124 | const candidates = yield* dcrCandidatesForIssuer(input.owner, issuer); |
| 1125 | const resource = input.resource ?? null; |
| 1126 | // A candidate is reusable only when the callback it registered with the |
| 1127 | // AS still matches the current flow's callback — strict servers reject an |
| 1128 | // authorize request whose redirect_uri differs from the registration |
| 1129 | // (e.g. the callback origin changed after a sandbox was recreated while |
| 1130 | // the persisted client survived). A null stored redirect is a legacy row |
| 1131 | // predating the column: treated as matching so an upgrade doesn't |
| 1132 | // re-register every client whose callback never changed. A null FLOW |
| 1133 | // redirect has nothing to compare against, so it also reuses — the only |
| 1134 | // alternative is a fresh registration, which the missing-redirectUri |
| 1135 | // guard would fail. |
| 1136 | const redirectMatches = (candidate: DcrReuseCandidate): boolean => |
| 1137 | candidate.redirectUri === null || |
| 1138 | flowRedirectUri === null || |
| 1139 | candidate.redirectUri === flowRedirectUri; |
| 1140 | // A fresh registration must never take a slug an existing candidate |
| 1141 | // holds: `createClient` deletes any colliding (owner, slug) row first, |
| 1142 | // which would clobber a client that live connections still refresh |
| 1143 | // through (a redirect-mismatched client stays valid for refresh — the |
| 1144 | // token grant doesn't involve the redirect URI). |
| 1145 | const takenSlugs = new Set(candidates.map((client) => String(client.slug))); |
| 1146 | if (resource !== null) { |
| 1147 | // Prefer a candidate matching resource AND the current redirect across |
| 1148 | // ALL candidates (mirroring the resource-less branch below). Candidates |
| 1149 | // are oldest-first, so after an origin drift the oldest matching- |
| 1150 | // resource row is the STRANDED one — but the first drift recovery |
| 1151 | // already minted a client bound to the CURRENT callback, and later |
| 1152 | // reconnects must reuse that instead of registering another duplicate |
| 1153 | // each time. Known limitation: the legacy null-redirect rule in |
| 1154 | // `redirectMatches` (a legacy row with no stored redirect matches any |
| 1155 | // flow redirect) still lets such a row win over a later, exactly- |
| 1156 | // matching one; kept deliberately so upgrades don't re-register every |
| 1157 | // client whose callback never changed. |
| 1158 | const reusable = candidates.find( |
| 1159 | (client) => client.resource === resource && redirectMatches(client), |
| 1160 | ); |
| 1161 | if (reusable) { |
| 1162 | return { existingSlug: reusable.slug, registrationSlug: reusable.slug }; |
| 1163 | } |
| 1164 | const slug = uniqueDcrSlug( |
| 1165 | dcrClientSlug(issuer, candidates.length > 0 ? resource : null, input.slug), |
| 1166 | takenSlugs, |
| 1167 | ); |
| 1168 | return { |
| 1169 | existingSlug: null, |
no test coverage detected