Skip to content

feat!: complete the Connect Applications contract - #121

Merged
gjtorikian merged 3 commits into
workos:mainfrom
danielloader:feat/connect-applications-contract
Sep 22, 2026
Merged

gjtorikian merged 3 commits into
workos:mainfrom
danielloader:feat/connect-applications-contract

Conversation

@danielloader

Copy link
Copy Markdown
Contributor

Applications was the emulator's most incomplete surface in SUPPORTED.md: 4/5 read and 5/8 write. This implements the four endpoints the spec defines and the emulator did not, and closes the gaps the existing routes had against the same contract.

Spec source throughout: @workos/openapi-spec@0.80.0, the version this repo pins (see #120 on why that pin is stale — this PR deliberately stays inside 0.80.0, and none of the Applications operations differ in 0.107.0).

New endpoints

Method Path Notes
GET /connect/applications/{id}/client_secrets The spec returns a bare array here, not the list envelope every other collection route uses
PUT /connect/applications/{id} UpdateOAuthApplicationDto: absent leaves a field alone, explicit null clears
DELETE /connect/applications/{id} 204, and drains the application's children
POST /client/token 201 { token }, signed with the emulator key so it verifies against /sso/jwks

Applications is now ✅ 5/5 read, ✅ 8/8 write. Repo coverage 180 → 184 of 250 (73.6%).

Contract gaps fixed in the existing routes

  • A third-party OAuth application was unrepresentable. ConnectApplication's oauth branch is a three-way oneOf on how the application came to exist, but the formatter hardcoded is_first_party: true and dropped organization_id. Creating one with {is_first_party: false, organization_id: …} returned it as first-party with no owner. The entity now carries is_first_party, was_dynamically_registered and uses_pkce, and the formatter emits the matching arm.
  • uses_pkce was a documented create field that was discarded and then contradicted as false in every response.
  • GET /connect/applications ignored its documented filters. organization_id and registration_types are implemented, including the spec's "defaults to authenticated only when not specified".
  • Create accepted redirect_uris on m2m applications, which CreateM2MApplicationDto does not define and no route reads — and the m2m formatter never serialized them, so the value was invisible and unclearable.
  • A blank redirect URI locked out every callback. /oauth2/authorize treats a non-empty list as an allow-list, so a single "" entry turned every authorize into invalid_request. Blank entries are now rejected.
  • PUT with no body returned 400. It is a no-op now, matching api-keys.ts and widgets.ts.

Cascade and credential lifetime

DELETE drains the application's client secrets and any in-flight Standalone Connect session or authorization code bound to its client_id. Without that, a browser mid-login still completed at /oauth2/authorize/complete, created a user, and landed on the callback holding a code that /oauth2/token could never redeem.

last_used_at is stamped when an exchange actually produces a token — not when the secret is merely presented. A request rejected on grant type, code or scope no longer records the credential as used.

Test and conformance work

  • The client secret resource joins the generated shape-conformance harness (OBJECT_SCHEMA_MAP), which brings it under the secret-leak guard. Verified by mutation: removing value from the exclude set fails two assertions.
  • POST /client/token and POST …/client_secrets join ENVELOPE_SCHEMA_MAP, so their envelopes are checked against the spec rather than by hand.
  • connect_application is deliberately not in the resource catalog — ConnectApplication is an allOf over a four-way oneOf, and that catalog models one flat shape per object. Flattening it would drop the discriminated fields, which is exactly what resolveSchema refuses to do. Each arm is pinned by a route test instead, and the reason is recorded where the entry would have gone.
  • Four existing/new assertions that could not fail were rewritten: an aud compared against the constant that produced it, a list-order assertion that insertion order already satisfied (now back-dates a record so the two disagree), and two hints compared against their own response rather than the stored value.

BREAKING CHANGE

The client secret resource now matches the spec's NewConnectApplicationSecret:

Before After
object: "client_secret" object: "connect_application_secret"
last_four secret_hint
value (plaintext, on create) secret
— last_used_at
application_id serialized not serialized — it is the emulator's foreign key, not a spec field

ID prefixes now follow the spec's own examples: conn_app_… replaces connect_app_…, and secret_… replaces client_secret_….

WorkOSClientSecret is part of the published type surface via @workos/emulate/workos, so a consumer reading secret.last_four, switching on object === "client_secret", or inserting a client-secret literal will need updating. The connectApplications seed key — including client_secret — is unchanged, so seed files keep working.

The remaining ID-prefix divergences from the spec (evt_, inv_, auth_res_ and others) are deliberately not in this PR; they are unrelated to Applications and follow separately.

Verification

npm run typecheck, npm run lint, npm run fmt:check clean. bun test: 1313 pass, 1 fail — redirect-hosts.spec.ts's punycode case, which fails identically on clean main and is untouched here. gen:shapes and gen:supported re-run byte-identical, so there is no codegen drift.

🤖 Generated with Claude Code

Applications was the emulator's most incomplete surface at 4/5 read and 5/8
write endpoints. This implements the four the spec defines and the emulator
did not, and fixes the places the existing routes diverged from it.

Added:

  GET    /connect/applications/:id/client_secrets
  PUT    /connect/applications/:id
  DELETE /connect/applications/:id
  POST   /client/token

`GET /connect/applications` now honours the documented `organization_id` and
`registration_types` filters, and the application resource carries the
`is_first_party`, `was_dynamically_registered` and `uses_pkce` fields the
spec's oneOf discriminates on — a third-party OAuth application was
previously unrepresentable and always reported as first-party with no owner.

DELETE drains the application's client secrets and any in-flight Standalone
Connect session, so a login cannot outlive the application it was started
for. A secret's `last_used_at` is stamped when an exchange actually produces
a token, not merely when the secret is presented.

The client secret resource is brought onto the shape-conformance harness, so
its plaintext can no longer escape a serializer without a test failing.

BREAKING CHANGE: the client secret resource now matches the spec's
`NewConnectApplicationSecret`. `object` is `connect_application_secret`
rather than `client_secret`, `last_four` is `secret_hint`, the plaintext is
returned as `secret` rather than `value`, `last_used_at` is new, and
`application_id` is no longer serialized. Connect Application and client
secret ids now use the prefixes the spec's own examples use — `conn_app_`
and `secret_`, replacing `connect_app_` and `client_secret_`. The
`connectApplications` seed key is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no actionable new issue remains, and the previously reported ownerless third-party seed problem is resolved.

Summary

Completes the Connect Applications contract and strengthens its conformance coverage.

  • Adds application update and deletion, client-secret listing, and Client API token creation.
  • Correctly represents first- and third-party OAuth applications, PKCE configuration, filters, redirect URIs, and the revised client-secret resource contract.
  • Cascades application deletion to credentials and in-flight standalone OAuth state.
  • Records successful client-secret use while avoiding stamps for failed exchanges.
  • Adds generated shape and envelope coverage plus focused route and lifecycle tests.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  App[Connect application] --> Secret[Client secrets]
  App --> Session[External auth sessions]
  App --> Code[Authorization codes]
  Secret --> Exchange[OAuth token exchange]
  Exchange -->|Success| Stamp[Stamp last_used_at]
  Exchange --> Token[Signed access token]
  Delete[Delete application] --> App
  Delete -->|Cascade| Secret
  Delete -->|Cascade| Session
  Delete -->|Cascade| Code
Loading

Reviews (3) · Last reviewed commit: "test(connect): cover the login cascade o..."

Comment thread src/workos/index.ts Outdated
@danielloader

Copy link
Copy Markdown
Contributor Author

Heads-up on merge order: #122 corrects the remaining ID_PREFIXES divergences against the same spec, and ledgers this PR's two (connect_application, client_secret) as known-divergent because they still are on main today.

The branches merge cleanly — no textual conflict, verified with git merge-tree — but once both land, #122's drift guard correctly reports those two entries as stale and fails until they are deleted:

(fail) ID prefix conformance > has no stale divergence — a tracked divergence
       that now matches the spec must be deleted
  - []
  + [ "connect_application", "client_secret" ]

So whichever merges second needs a two-line follow-up deleting those entries from TRACKED_DIVERGENCES in src/workos/id-prefixes.spec.ts. I verified this by merging the two branches locally and running the suite, and I'm happy to push that follow-up once you've decided the order.

danielloader and others added 2 commits September 22, 2026 09:50
A seeded oauth application with `is_first_party: false` and no resolvable
organization was inserted with a null owner, so the list and get routes
returned a third-party application missing the `organization_id` its arm of
the spec's oneOf requires — the exact shape `POST /connect/applications`
rejects. Seed config and the API now agree.

`is_first_party` and `uses_pkce` are validated as booleans, and the
organization requirement is reported by config validation rather than at
insert time, so the failure names the offending key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deleting an application drops its external-auth sessions and codes,
but the tests only asserted the secrets went with it: either cascade
could be removed and the suite would still pass.
@gjtorikian
gjtorikian merged commit b59f904 into workos:main Sep 22, 2026
9 checks passed
@gjtorikian

Copy link
Copy Markdown
Collaborator

thanks!

gjtorikian added a commit to danielloader/emulate that referenced this pull request Sep 22, 2026
`connect_application` and `client_secret` now mint `conn_app_` and
`secret_` on main, so the ledger entries tracking them as known
divergences are stale and the guard that exists for exactly this
case fails.
gjtorikian added a commit that referenced this pull request Sep 22, 2026
PR #121 changed the support matrix while the dependency fixes were in
flight. Regenerate it against the merged routes and spec 0.98.0 so the
freshness checks reflect both changes.
@danielloader
danielloader deleted the feat/connect-applications-contract branch September 24, 2026 15:29
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.

2 participants