(
owner: Owner,
slug: OAuthClientSlug,
)
| 1163 | // op never cascades into connections). |
| 1164 | // ----------------------------------------------------------------------- |
| 1165 | const removeClient = ( |
| 1166 | owner: Owner, |
| 1167 | slug: OAuthClientSlug, |
| 1168 | ): Effect.Effect<void, OrgWriteDeniedError | StorageFailure> => |
| 1169 | Effect.gen(function* () { |
| 1170 | // Config-declared apps have no row to remove; removing one is an env |
| 1171 | // change on the host, not a storage operation. Fail loudly rather than |
| 1172 | // returning a success that changed nothing. |
| 1173 | if (isFirstPartyOAuthClientSlug(String(slug))) { |
| 1174 | return yield* new StorageError({ |
| 1175 | message: `OAuth client "${String(slug)}" is a first-party app declared in host config; it cannot be removed through this surface.`, |
| 1176 | cause: undefined, |
| 1177 | }); |
| 1178 | } |
| 1179 | yield* deps.guardOrgWrite(owner); |
| 1180 | // "Is there an app at (owner, slug) right now?" — asked twice, for two |
| 1181 | // different reasons. Before the delete it says whether this call removes |
| 1182 | // anything at all; after the commit it says whether the secret key still |
| 1183 | // belongs to the app this call removed. |
| 1184 | const findClientRow = deps.fuma.use("oauth_client.findFirst", (db) => |
| 1185 | looseDb(db).findFirst("oauth_client", { |
| 1186 | where: (b: any) => b.and(b("owner", "=", owner), b("slug", "=", String(slug))), |
| 1187 | }), |
| 1188 | ); |
| 1189 | |
| 1190 | const removedRow = yield* deps.fuma.transaction( |
| 1191 | Effect.gen(function* () { |
| 1192 | const existing = yield* findClientRow; |
| 1193 | yield* deps.fuma |
| 1194 | .use("oauth_client.delete", (db) => |
| 1195 | looseDb(db).deleteMany("oauth_client", { |
| 1196 | where: (b: any) => b.and(b("owner", "=", owner), b("slug", "=", String(slug))), |
| 1197 | }), |
| 1198 | ) |
| 1199 | .pipe(Effect.asVoid); |
| 1200 | return existing; |
| 1201 | }), |
| 1202 | ); |
| 1203 | // Nothing matched, so this call removed nothing and owns no secret. The |
| 1204 | // idempotent no-op and the cross-subject miss both land here, and both |
| 1205 | // used to queue a delete of a key they never had a claim on. |
| 1206 | if (!removedRow) return; |
| 1207 | // Best-effort: drop the secret from the provider so it isn't orphaned. |
| 1208 | // |
| 1209 | // Deferred to the outermost commit. This function opens no transaction of |
| 1210 | // its own, but a caller can wrap it in one — and `provider.delete` reaches |
| 1211 | // a store that does not roll back with it. An abort would then restore the |
| 1212 | // client row while its secret stayed destroyed, leaving a client that |
| 1213 | // looks configured and can never authenticate again. Orphaning a secret is |
| 1214 | // recoverable; deleting one that is still referenced is not, so the |
| 1215 | // deletion waits until the row's removal is durable. With no transaction |
| 1216 | // active `afterCommit` runs it immediately, which is the behaviour this |
| 1217 | // path already had. |
| 1218 | const provider = deps.defaultWritableProvider(); |
| 1219 | const dropSecret = provider?.delete; |
| 1220 | if (provider && dropSecret) { |
| 1221 | yield* afterCommit( |
| 1222 | Effect.gen(function* () { |
nothing calls this directly
no test coverage detected