Skip to content

pipes.deleteUserConnectedAccount never sends organization_id, so org-scoped connected accounts cannot be deleted #1667

Description

@smorimoto

Summary

workos.pipes.deleteUserConnectedAccount() silently drops its organizationId option, so the organization_id query parameter never reaches the API. Deleting an organization-scoped connected account therefore always fails with a 404 while the account itself stays connected:

NotFoundException: Data Installation not found: 'slug=linear userland_user_id=user_XXX organization_id=null'.

Calling the same REST endpoint directly with ?organization_id=… deletes the account fine, so the API is not at fault — the parameter is lost inside the SDK.

This is easy to miss in application code: treating 404 as "already deleted" is a natural pattern for a delete call, which turns this bug into deletions that appear to succeed while the connected account (and its stored credentials) survive.

Root cause

The generated Pipes method wraps its query in an options object:

// as published in 10.8.0
async deleteUserConnectedAccount(options) {
  const { userId, slug } = options;
  await this.workos.delete(
    `/user_management/users/${encodeURIComponent(userId)}/connected_accounts/${encodeURIComponent(slug)}`,
    { query: { ...(options.organizationId !== undefined && { organization_id: options.organizationId }) } },
  );
}

But unlike WorkOS#get/#post/#put — which take an options object with a query field — WorkOS#delete expects the query object itself as its second argument:

async delete(path, query) {
  this.requireApiKey(path);
  await this.client.delete(path, { params: query });
}

So the HTTP client receives params: { query: { organization_id: … } } and the query string never contains organization_id.

Other generated call sites pass the query directly and are fine, e.g.:

await this.workos.delete(`/authorization/resources/${resourceId}`, query);

deleteUserConnectedAccount appears to be the only affected method at the moment. It looks like it came in with the OpenAPI-generated Pipes module (#1625), where the generator emitted the { query: … } shape that get expects. The asymmetric WorkOS#delete(path, query) signature makes this an easy trap for the generator, so it may be worth normalising the signature (or adding a lint/codegen check) in addition to fixing this method.

Reproduction

  1. Authorize a data integration with an organizationId so the resulting connected account is org-scoped.
  2. Run:
    await workos.pipes.deleteUserConnectedAccount({ userId, slug: "linear", organizationId });
  3. NotFoundException with organization_id=null in the message, and the connected account remains connected (verify via GET /user_management/users/{userId}/data_providers?organization_id=…).
  4. Run the REST call directly:
    DELETE /user_management/users/{userId}/connected_accounts/linear?organization_id={organizationId}
    
    → the connected account is deleted.

Expected behaviour

organizationId is serialised as the organization_id query parameter and the org-scoped connected account is deleted.

Workaround

Bypass the generated method and call the endpoint through the base client:

await workos.delete(
  `/user_management/users/${encodeURIComponent(userId)}/connected_accounts/${encodeURIComponent(slug)}`,
  { organization_id: organizationId },
);

Environment

  • @workos-inc/node 10.8.0 (latest release at time of writing)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions