Skip to content

feat(pipes): vend credentials for user- and organization-owned accounts - #178

Open
dkarten wants to merge 9 commits into
workos:mainfrom
dkarten:pipes-credentials
Open

dkarten wants to merge 9 commits into
workos:mainfrom
dkarten:pipes-credentials

Conversation

@dkarten

@dkarten dkarten commented Oct 29, 2026

Copy link
Copy Markdown

Stacks on #177.

Summary

  • Adds POST /data-integrations/{slug}/credentials. It returns the account's token as a credential. Without a usable account it returns { active: false, error } with not_installed or needs_reauthorization.
  • connection_owner: organization vends the organization's account and requires organization_id. user, the default, vends the user's own.
  • A user who is not an active member of the organization gets a 403, for either owner.
  • A malformed body gets the spec's 400 { message, error: 'Bad Request' }.
  • /credentials and /token share the token logic (vendAccessToken).

Behaviour

Request Outcome
/credentials with a malformed body 400, "error": "Bad Request"
/credentials naming an organization the user is not an active member of 403
/credentials resolving a personal account scoped to an organization the user is not an active member of 403
/credentials with no account for the owner 200, { "active": false, "error": "not_installed" }

Verification

Full suite: 1918 pass, 0 fail. bun run typecheck, bun run lint, bun run fmt:check and bun run gen:supported are clean. New tests in data-integrations.spec.ts cover both owners, the reauthorization and disconnect states, both 403s, each malformed body, and the user-owned path through the Node SDK's createDataIntegrationCredential.

Resolves part of #176

dkarten and others added 8 commits October 29, 2026 09:51
Serve GET, POST, PUT and DELETE on /organizations/{organization_id}/connected_accounts/{slug}.
An organization holds one account per provider with user_id null, apart from any its members
hold. Import and update name the acting member in user_id, who must be an active member.

Accounts now report established_by_user_id, cleared when that user is deleted. Deleting an
organization deletes its accounts and its members' accounts scoped to it. The connectedAccounts
seed takes owner: organization.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Describe the organization-owned rules in prose, as the other behavior sections do.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
An overlapping import otherwise misses the other's write and both answer 201. Cover the members'
scoped accounts in the organization-delete cascade test, and say plainly that ending a membership
does not clear credentials, unlike production.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
As CLAUDE.md asks, and cite production's invalidation when the establishing member leaves.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Serve POST /data-integrations/{slug}/credentials. It returns the account's token as a
credential, or active: false with not_installed or needs_reauthorization. With
connection_owner: organization it vends the organization's account. Naming an organization
the user is not an active member of returns 403 for either owner, and a malformed body
returns the spec's 400.

/credentials and /token share the token logic. /token is otherwise unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…olves

A request that leaves organization_id out still resolves a user's only account for the provider,
and that account may be scoped to an organization the user has left.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@dkarten dkarten changed the title Pipes credentials feat(pipes): vend credentials for user- and organization-owned accounts Oct 29, 2026
@dkarten
dkarten marked this pull request as ready for review October 29, 2026 18:36
@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Critical impact] The PR appears safe to merge, with one remaining non-blocking error-response mismatch.

Findings

  1. P2 Invalid JSON gets different fields ▶
Fix with agent prompt
### Issue 1
src/workos/routes/data-integrations.ts:undefined-95
Invalid JSON sent to `/credentials` bypasses `badRequest()`. For example, a body of `{` returns `{ message: 'Problems parsing JSON', code: 'invalid_request_body' }`, rather than the documented `{ message, error: 'Bad Request' }`. Convert parsing failures to this route's error shape and add an invalid-JSON test.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds credential retrieval for user- and organization-owned connected accounts.

  • Organizations can manage their own connected accounts.
  • Credential requests return a connected account's token for either owner.
  • Seed files can create organization-owned connected accounts.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[POST credentials] --> B[Check user and named organization]
  B --> C{Account owner}
  C -->|User| D[Find user's account]
  C -->|Organization| E[Find organization's account]
  D --> F{Account found?}
  E --> F
  F -->|No| G[Return not_installed]
  F -->|Yes| H[Check membership when organization-scoped]
  H --> I[vendAccessToken]
  I --> J[Return credential or needs_reauthorization]
Loading

Reviews (2) · Last reviewed commit: "fix(pipes): assert the vended value in t..." · Reviewed by Greptile

// The spec's credential vend, for user- and organization-owned accounts alike.
app.post('/data-integrations/:slug/credentials', async (c) => {
const slug = c.req.param('slug');
const owner = resolveOwner(await parseJsonBody(c));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Invalid JSON gets different fields

Invalid JSON sent to /credentials bypasses badRequest(). For example, a body of { returns { message: 'Problems parsing JSON', code: 'invalid_request_body' }, rather than the documented { message, error: 'Bad Request' }. Convert parsing failures to this route's error shape and add an invalid-JSON test.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/workos/routes/data-integrations.ts
Line: 95

Comment:
**Invalid JSON gets different fields**

Invalid JSON sent to `/credentials` bypasses `badRequest()`. For example, a body of `{` returns `{ message: 'Problems parsing JSON', code: 'invalid_request_body' }`, rather than the documented `{ message, error: 'Bad Request' }`. Convert parsing failures to this route's error shape and add an invalid-JSON test.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're following the convention of other existing endpoints here. Leaving for now.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. Keeping parseJsonBody behavior consistent with the existing endpoints is reasonable; I’ll treat this as intentional and won’t press the finding.

Comment thread src/workos/routes/data-integrations.spec.ts Outdated
…0 row

The 400 with "error": "Bad Request" answers a missing or invalid field; a body that is not JSON
gets the emulator-wide invalid_request_body, as on every route.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant