Skip to content

fix(ids)!: mint the id prefixes the spec documents - #122

Merged
gjtorikian merged 5 commits into
workos:mainfrom
danielloader:fix/spec-id-prefixes
Sep 22, 2026
Merged

gjtorikian merged 5 commits into
workos:mainfrom
danielloader:fix/spec-id-prefixes

Conversation

@danielloader

@danielloader danielloader commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Mint the id prefixes the spec documents

Nine entries in ID_PREFIXES used a prefix the upstream OpenAPI contract (@workos/openapi-spec v0.80.0) never shows in its own id examples — abbreviations the emulator invented. Response-shape conformance cannot catch this: a field set says nothing about what goes in the field, so id: "ra_01…" satisfies every existing spec check while breaking any consumer that string-matches an id.

Each prefix below is quoted from the spec example it was read from:

object spec example was now
event event_01EHZNVPK3SFK441A1RGBFSHRT (EventSchema, in its allOf[0]) evt event
invitation invitation_01E4ZCR3C56J083X43JQXF3JK5 (UserlandUserInvite.id) inv invitation
directory_group directory_group_01E1JJS84MFPPQ3G655FHTKX6Z (DirectoryGroup.id) directory_grp directory_group
cors_origin cors_origin_01HXYZ123456789ABCDEFGHIJ (CorsOriginResponse.id) cors cors_origin
authorization_resource authz_resource_01HXYZ123456789ABCDEFGH (AuthorizationResource.id) auth_res authz_resource
role_assignment role_assignment_01HXYZ123456789ABCDEFGH (UserRoleAssignment.id) ra role_assignment
audit_log_export audit_log_export_01GBZK5MP7TD1YCFQHFR22180V (AuditLogExportJson.id) audit_export audit_log_export
authorized_application authorized_connect_app_01HXYZ123456789ABCDEFGHIJ (AuthorizedConnectApplicationList.data.items.id) auth_app authorized_connect_app
radar_attempt radar_att_01HZBC6N1EB1ZY7KG32X (/radar/attempts/{id} parameter, repeated as RadarStandaloneResponse.attempt_id) radar_attempt radar_att

The radar attempt is the only one without an object-level id example; its two independent occurrences agree, and the emulator serves /radar/attempts/:id for the same resource, so the ids it mints are the ones a caller feeds back to the documented route. It is the one judgement call here and is easy to drop if you would rather not take it.

Checked and deliberately not changed:

  • connected_account: data_installation — the comment said it tracks production rather than the spec, but ConnectedAccount.id examples data_installation_01EHZNVPK3SFK441A1RGBFSHRT. The spec agrees; the comment is corrected so it is not re-filed as a divergence later.
  • connection_domain: conn_domain — the spec contradicts itself here. Connection.domains[] examples an org_domain_… id (the organization-domain prefix), while the connection.* event payloads example conn_domain_… for the same object: connection_domain. The emulator follows the events, which is what production emits. Tracked as a divergence rather than silently resolved, with both prefixes recorded.
  • 14 objects the emulator mints ids for that the spec gives no id example for (identity, refresh_token, role_permission, audit_log_action, …) are left alone and ledgered with a reason each.

BREAKING CHANGE

Ids minted for events, invitations, directory groups, CORS origins, authorization resources, role assignments, audit log exports, authorized connect applications and radar attempts now carry the prefix the WorkOS API documents.

Anything downstream that hardcodes or pattern-matches the old prefixes — evt_, inv_, directory_grp_, cors_, auth_res_, ra_, audit_export_, auth_app_, radar_attempt_ — needs updating. Snapshot fixtures, recorded HTTP interactions and id.startsWith(…) / /^evt_/-style assertions are what to look for. Nothing about the id format changed otherwise: still <prefix>_<26-char ULID>.

Drift guard

Follows the house pattern (generated catalog + spec file with an explicit ledger), reusing the gen-shapes pipeline rather than standing up a parallel one:

  • gen-shapes now also emits ID_PREFIX_REQUIREMENTS — 55 objects, each with the prefix, the verbatim example, the source schema, and any conflicting prefixes the spec uses elsewhere. Unlike the response-shape catalogs, nothing here is curated: every object with a prefixed id example is discovered structurally. That matters — authorized_connect_application has no top-level schema at all, its only example is an inline schema nested inside a list wrapper, which a curated schema-name map would never reach. object and id are resolved across the whole allOf/oneOf composition, since the spec routinely declares them in different members.
  • src/workos/id-prefixes.spec.ts asserts ID_PREFIXES matches it. Objects with no spec example, and prefixes that knowingly differ, live in two ledgers with a reason each. Closing a divergence fails until its ledger entry is deleted, and a new one fails outright — drift cannot accrue silently. There is also a guard-on-the-guard, so an extractor that silently returns nothing cannot make the suite pass vacuously.

Merge-order coupling with #121 — resolved

#121 corrected two further prefixes in the same table (connect_app_ → conn_app_, client_secret_ → secret_). This PR ledgered both as known divergences, because they still diverged on main at the time.

#121 has now merged, main is merged in here, and those two TRACKED_DIVERGENCES entries are deleted — the guard reported them as stale the moment the prefixes stopped diverging, which is the ledger behaving as designed rather than a defect. Nothing outstanding.

Verification

npm run typecheck, npm run lint, npm run fmt:check clean. bun test: 1288 pass, 1 fail — redirect-hosts.spec.ts's punycode case, which fails identically on clean main (it is a Bun 1.4 domainToASCII behaviour change; CI pins 1.3.14, where it passes). gen:shapes, gen:events and gen:supported all re-run byte-identical, so there is no codegen drift.

Related: #120 (the emulator is pinned to an older spec version) — this PR does not address or close it; it corrects prefixes against the spec version currently pinned.

🤖 Generated with Claude Code

Nine entries in ID_PREFIXES used a prefix the OpenAPI spec (v0.80.0) never
shows in its own `id` examples — abbreviations the emulator invented, such as
`evt_` where the spec documents `event_` and `ra_` where it documents
`role_assignment_`. Response shape conformance cannot see this: a field set
says nothing about what goes in the field, so a wrong prefix passes every
existing spec check while breaking any consumer that string-matches an id.

Prefixes changed, each against the spec example it was read from:

  event                  evt            -> event
  invitation             inv            -> invitation
  directory_group        directory_grp  -> directory_group
  cors_origin            cors           -> cors_origin
  authorization_resource auth_res       -> authz_resource
  role_assignment        ra             -> role_assignment
  audit_log_export       audit_export   -> audit_log_export
  authorized_application auth_app       -> authorized_connect_app
  radar_attempt          radar_attempt  -> radar_att

The radar attempt is the one with no object-level example: its prefix comes
from the `/radar/attempts/{id}` parameter example and
`RadarStandaloneResponse.attempt_id`, which agree on `radar_att_`.

So this cannot drift back, gen-shapes now extracts the spec's per-object id
prefix into ID_PREFIX_REQUIREMENTS, discovered structurally rather than from a
curated map, and src/workos/id-prefixes.spec.ts asserts ID_PREFIXES matches it.
Objects the spec gives no example for, and prefixes that knowingly differ, live
in ledgers there with a reason each — closing one fails until its entry is
deleted.

BREAKING CHANGE: ids minted for events, invitations, directory groups, CORS
origins, authorization resources, role assignments, audit log exports,
authorized connect applications and radar attempts now carry the prefix the
WorkOS API documents. Tests and fixtures that hardcode or pattern-match the
old prefixes (`evt_`, `inv_`, `directory_grp_`, `cors_`, `auth_res_`, `ra_`,
`audit_export_`, `auth_app_`, `radar_attempt_`) need updating.

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 outstanding previous finding or actionable new defect was identified.

Summary

This PR aligns generated resource identifiers with the prefixes documented by the pinned WorkOS OpenAPI specification and adds a generated conformance catalog to detect future prefix drift. Changes incorporated since the previous review also complete the Connect Applications API surface and align its application and client-secret contracts.

  • Updates shared ID prefixes and affected route fixtures.
  • Structurally extracts documented ID prefixes, including inline and composed schemas.
  • Adds explicit ledgers and conformance tests for missing or contradictory specification examples.
  • Escapes all specification-derived generated keys and values safely.
  • Adds Connect application update, deletion, filtering, and client-secret lifecycle behavior.
  • Adds the authenticated /client/token endpoint and response-envelope coverage.
  • The previous unsafe generated-property finding was fixed and its thread is resolved.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Spec[OpenAPI schemas and paths] --> Extractor[gen-shapes prefix extractor]
  Extractor --> Catalog[Generated ID prefix requirements]
  Runtime[Shared ID_PREFIXES] --> Conformance[ID prefix conformance tests]
  Catalog --> Conformance
  Conformance --> Ledger{Matched, missing example, or tracked divergence}
  Runtime --> Routes[WorkOS route responses]
  Connect[Connect application and secret lifecycle] --> Routes
  ClientToken[Client token endpoint] --> Routes
Loading

Reviews (4) · Last reviewed commit: "test(ids): close the Connect divergences..."

Comment thread scripts/gen-shapes-lib.ts Outdated
danielloader and others added 4 commits September 22, 2026 09:53
The id-prefix catalog is discovered structurally from the spec rather than
from a curated map, so its keys and values are whatever upstream ships. They
were written straight into the generated TypeScript: an object discriminator
containing a hyphen would have produced an unparseable property name, and a
schema name containing a quote would have ended the string literal early —
either way leaving a generator that cannot regenerate.

Keys and spec-derived strings now go through JSON.stringify. oxfmt still
normalizes the redundant quoting afterwards, so the generated file is
byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Radar has no object-level example in the spec, so the prefix suite
excludes it; reverting `radar_attempt` to its old prefix left every
test green.
`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

Copy link
Copy Markdown
Collaborator

thanks !

@gjtorikian
gjtorikian merged commit ad7aa1c into workos:main Sep 22, 2026
10 checks passed
@danielloader
danielloader deleted the fix/spec-id-prefixes 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