feat!: complete the Connect Applications contract - #121
Conversation
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>
|
|
Heads-up on merge order: #122 corrects the remaining The branches merge cleanly — no textual conflict, verified with So whichever merges second needs a two-line follow-up deleting those entries from |
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.
|
thanks! |
`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.
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.
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
GET/connect/applications/{id}/client_secretslistenvelope every other collection route usesPUT/connect/applications/{id}UpdateOAuthApplicationDto: absent leaves a field alone, explicitnullclearsDELETE/connect/applications/{id}POST/client/token{ token }, signed with the emulator key so it verifies against/sso/jwksApplications is now ✅ 5/5 read, ✅ 8/8 write. Repo coverage 180 → 184 of 250 (73.6%).
Contract gaps fixed in the existing routes
ConnectApplication's oauth branch is a three-wayoneOfon how the application came to exist, but the formatter hardcodedis_first_party: trueand droppedorganization_id. Creating one with{is_first_party: false, organization_id: …}returned it as first-party with no owner. The entity now carriesis_first_party,was_dynamically_registeredanduses_pkce, and the formatter emits the matching arm.uses_pkcewas a documented create field that was discarded and then contradicted asfalsein every response.GET /connect/applicationsignored its documented filters.organization_idandregistration_typesare implemented, including the spec's "defaults toauthenticatedonly when not specified".redirect_urison m2m applications, whichCreateM2MApplicationDtodoes not define and no route reads — and the m2m formatter never serialized them, so the value was invisible and unclearable./oauth2/authorizetreats a non-empty list as an allow-list, so a single""entry turned every authorize intoinvalid_request. Blank entries are now rejected.PUTwith no body returned 400. It is a no-op now, matchingapi-keys.tsandwidgets.ts.Cascade and credential lifetime
DELETEdrains the application's client secrets and any in-flight Standalone Connect session or authorization code bound to itsclient_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/tokencould never redeem.last_used_atis 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
OBJECT_SCHEMA_MAP), which brings it under the secret-leak guard. Verified by mutation: removingvaluefrom the exclude set fails two assertions.POST /client/tokenandPOST …/client_secretsjoinENVELOPE_SCHEMA_MAP, so their envelopes are checked against the spec rather than by hand.connect_applicationis deliberately not in the resource catalog —ConnectApplicationis anallOfover a four-wayoneOf, and that catalog models one flat shape per object. Flattening it would drop the discriminated fields, which is exactly whatresolveSchemarefuses to do. Each arm is pinned by a route test instead, and the reason is recorded where the entry would have gone.audcompared 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:object: "client_secret"object: "connect_application_secret"last_foursecret_hintvalue(plaintext, on create)secretlast_used_atapplication_idserializedID prefixes now follow the spec's own examples:
conn_app_…replacesconnect_app_…, andsecret_…replacesclient_secret_….WorkOSClientSecretis part of the published type surface via@workos/emulate/workos, so a consumer readingsecret.last_four, switching onobject === "client_secret", or inserting a client-secret literal will need updating. TheconnectApplicationsseed key — includingclient_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:checkclean.bun test: 1313 pass, 1 fail —redirect-hosts.spec.ts's punycode case, which fails identically on cleanmainand is untouched here.gen:shapesandgen:supportedre-run byte-identical, so there is no codegen drift.🤖 Generated with Claude Code