(
owner: Owner,
slug: OAuthClientSlug,
)
| 1127 | // op never cascades into connections). |
| 1128 | // ----------------------------------------------------------------------- |
| 1129 | const removeClient = ( |
| 1130 | owner: Owner, |
| 1131 | slug: OAuthClientSlug, |
| 1132 | ): Effect.Effect<void, OrgWriteDeniedError | StorageFailure> => |
| 1133 | Effect.gen(function* () { |
| 1134 | // Config-declared apps have no row to remove; removing one is an env |
| 1135 | // change on the host, not a storage operation. Fail loudly rather than |
| 1136 | // returning a success that changed nothing. |
| 1137 | if (isFirstPartyOAuthClientSlug(String(slug))) { |
| 1138 | return yield* new StorageError({ |
| 1139 | message: `OAuth client "${String(slug)}" is a first-party app declared in host config; it cannot be removed through this surface.`, |
| 1140 | cause: undefined, |
| 1141 | }); |
| 1142 | } |
| 1143 | yield* deps.guardOrgWrite(owner); |
| 1144 | // "Is there an app at (owner, slug) right now?" — asked twice, for two |
| 1145 | // different reasons. Before the delete it says whether this call removes |
| 1146 | // anything at all; after the commit it says whether the secret key still |
| 1147 | // belongs to the app this call removed. |
| 1148 | const findClientRow = deps.fuma.use("oauth_client.findFirst", (db) => |
| 1149 | looseDb(db).findFirst("oauth_client", { |
| 1150 | where: (b: any) => b.and(b("owner", "=", owner), b("slug", "=", String(slug))), |
| 1151 | }), |
| 1152 | ); |
| 1153 | |
| 1154 | const removedRow = yield* deps.fuma.transaction( |
| 1155 | Effect.gen(function* () { |
| 1156 | const existing = yield* findClientRow; |
| 1157 | yield* deps.fuma |
| 1158 | .use("oauth_client.delete", (db) => |
| 1159 | looseDb(db).deleteMany("oauth_client", { |
| 1160 | where: (b: any) => b.and(b("owner", "=", owner), b("slug", "=", String(slug))), |
| 1161 | }), |
| 1162 | ) |
| 1163 | .pipe(Effect.asVoid); |
| 1164 | return existing; |
| 1165 | }), |
| 1166 | ); |
| 1167 | // Nothing matched, so this call removed nothing and owns no secret. The |
| 1168 | // idempotent no-op and the cross-subject miss both land here, and both |
| 1169 | // used to queue a delete of a key they never had a claim on. |
| 1170 | if (!removedRow) return; |
| 1171 | // Best-effort: drop the secret from the provider so it isn't orphaned. |
| 1172 | // |
| 1173 | // Deferred to the outermost commit. This function opens no transaction of |
| 1174 | // its own, but a caller can wrap it in one — and `provider.delete` reaches |
| 1175 | // a store that does not roll back with it. An abort would then restore the |
| 1176 | // client row while its secret stayed destroyed, leaving a client that |
| 1177 | // looks configured and can never authenticate again. Orphaning a secret is |
| 1178 | // recoverable; deleting one that is still referenced is not, so the |
| 1179 | // deletion waits until the row's removal is durable. With no transaction |
| 1180 | // active `afterCommit` runs it immediately, which is the behaviour this |
| 1181 | // path already had. |
| 1182 | const provider = deps.defaultWritableProvider(); |
| 1183 | const dropSecret = provider?.delete; |
| 1184 | if (provider && dropSecret) { |
| 1185 | yield* afterCommit( |
| 1186 | Effect.gen(function* () { |
nothing calls this directly
no test coverage detected