MCPcopy Create free account
hub / github.com/UsefulSoftwareCo/executor / removeClient

Function removeClient

packages/core/sdk/src/oauth-service.ts:942–1007  ·  view source on GitHub ↗
(owner: Owner, slug: OAuthClientSlug)

Source from the content-addressed store, hash-verified

940 // op never cascades into connections).
941 // -----------------------------------------------------------------------
942 const removeClient = (owner: Owner, slug: OAuthClientSlug): Effect.Effect<void, StorageFailure> =>
943 Effect.gen(function* () {
944 // Config-declared apps have no row to remove; removing one is an env
945 // change on the host, not a storage operation. Fail loudly rather than
946 // returning a success that changed nothing.
947 if (isFirstPartyOAuthClientSlug(String(slug))) {
948 return yield* new StorageError({
949 message: `OAuth client "${String(slug)}" is a first-party app declared in host config; it cannot be removed through this surface.`,
950 cause: undefined,
951 });
952 }
953 // "Is there an app at (owner, slug) right now?" — asked twice, for two
954 // different reasons. Before the delete it says whether this call removes
955 // anything at all; after the commit it says whether the secret key still
956 // belongs to the app this call removed.
957 const findClientRow = deps.fuma.use("oauth_client.findFirst", (db) =>
958 looseDb(db).findFirst("oauth_client", {
959 where: (b: any) => b.and(b("owner", "=", owner), b("slug", "=", String(slug))),
960 }),
961 );
962
963 const removedRow = yield* findClientRow;
964 yield* deps.fuma
965 .use("oauth_client.delete", (db) =>
966 looseDb(db).deleteMany("oauth_client", {
967 where: (b: any) => b.and(b("owner", "=", owner), b("slug", "=", String(slug))),
968 }),
969 )
970 .pipe(Effect.asVoid);
971 // Nothing matched, so this call removed nothing and owns no secret. The
972 // idempotent no-op and the cross-subject miss both land here, and both
973 // used to queue a delete of a key they never had a claim on.
974 if (!removedRow) return;
975
976 // Best-effort: drop the secret from the provider so it isn't orphaned.
977 //
978 // Deferred to the outermost commit. This function opens no transaction of
979 // its own, but a caller can wrap it in one — and `provider.delete` reaches
980 // a store that does not roll back with it. An abort would then restore the
981 // client row while its secret stayed destroyed, leaving a client that
982 // looks configured and can never authenticate again. Orphaning a secret is
983 // recoverable; deleting one that is still referenced is not, so the
984 // deletion waits until the row's removal is durable. With no transaction
985 // active `afterCommit` runs it immediately, which is the behaviour this
986 // path already had.
987 const provider = deps.defaultWritableProvider();
988 const dropSecret = provider?.delete;
989 if (provider && dropSecret) {
990 yield* afterCommit(
991 Effect.gen(function* () {
992 // Deferral alone is not enough: the secret is keyed by (owner, slug)
993 // ALONE, so the key outlives the row it belonged to. If the same
994 // slug is registered again before this hook runs, the key now holds
995 // the NEW app's secret, and deleting it recreates exactly the state
996 // the deferral exists to prevent — a client that looks configured
997 // and can never authenticate. Re-check that the app is still gone
998 // and stand down when it is not. A re-check that FAILS is caught
999 // below and also stands down, which is the deliberate direction:

Callers

nothing calls this directly

Calls 5

afterCommitFunction · 0.90
looseDbFunction · 0.85
clientSecretItemIdFunction · 0.85
callMethod · 0.80

Tested by

no test coverage detected