Commit 6cfa55b
feat(api): add generic
* feat(api): add generic `workos api` gateway for raw API requests
Adds `workos api` command that provides direct, authenticated access to
any WorkOS API endpoint without needing a dedicated command per resource.
Three modes:
- `workos api ls [filter]` — list endpoints from the embedded OpenAPI spec
- `workos api <endpoint>` — make authenticated GET/POST/PUT/PATCH/DELETE
- Flags: -X method, -d data, --file, --include, --dry-run, --yes
The OpenAPI spec (170 endpoints, 40 tags) is embedded at build time from
the workos-openapi-spec repo. Auth resolves from the active environment
automatically (same as existing resource commands).
* feat(api): add interactive request builder and fix duplicate resolveApiKey
- `workos api` with no args launches an interactive builder (TTY only):
category picker → endpoint picker → path params → query params → body → confirm → execute
- Non-TTY fallback prints usage instructions
- Export colorMethod from index.ts to share with interactive.ts
- Remove duplicate resolveApiKey resolution in runApiRequest (request.ts handles the fallback)
* fix: Temporarily copy over the YAML spec
* refactor(api): use @workos/openapi-spec package instead of vendored YAML
Replace the 31K-line vendored OpenAPI spec with the published
@workos/openapi-spec package. The catalog now resolves the spec
via createRequire + require.resolve, keeping the same parse logic.
* chore(build): remove vendored YAML copy from postbuild
The spec is now resolved from @workos/openapi-spec in node_modules
at runtime via require.resolve, so the postbuild cp is unnecessary.
* refactor(api): address code review findings
- Extract colorMethod + printResponse to format.ts (breaks circular import)
- Collapse PathParam/QueryParam into single Param type
- Simplify endpointsByTag to accept EndpointInfo[] directly
- Add assertNotCancelled helper (eliminates 8x cancel+cast boilerplate)
- Add network error handling in apiRequest
- resolveBody returns undefined instead of null
- Remove unnecessary comments
* fix(api): handle invalid JSON body in dry-run JSON mode and add tests
- Catch JSON.parse errors in --dry-run --json path and exit with a
structured error instead of crashing with an unhandled SyntaxError
- Add spec coverage for the new src/commands/api/* files (catalog,
format, request, index) including JSON mode behavior
Addresses Devin Review findings on PR #142.
* fix(api): emit pure JSON in JSON mode and gracefully handle missing --file
- printResponse now produces a single structured JSON object on stdout
in JSON mode (including status and headers when --include is set),
instead of leaking human-readable status/header lines that would
corrupt machine-readable output.
- resolveBody now uses async readFile and surfaces missing files via
exitWithError(file_read_error) instead of throwing a raw ENOENT
stack trace.
- Update format.spec.ts to assert pure-JSON output in JSON mode and
add a regression test for the missing --file path.
Co-Authored-By: nick.nisi@workos.com <nick.nisi@workos.com>
* fix(api): emit structured error from runApiInteractive in JSON mode
When stdout/stdin is non-interactive but --json is requested, instead
of printing a human-readable usage block to stdout (which corrupts
machine-readable consumers) we now exit with a tty_required structured
error on stderr.
Co-Authored-By: nick.nisi@workos.com <nick.nisi@workos.com>
* fix(api): respect empty bodies and require --yes for mutating JSON requests
- resolveBody/runApiRequest now treat an explicit empty string body
the same as any other body. Previously --data '' (or piping an
empty file) would silently fall through to GET method inference and
drop the body from dry-run/preview/confirm paths.
- In JSON mode a mutating request without --yes now exits with a
confirmation_required structured error instead of falling through
to a human prompt that would corrupt machine-readable output.
Co-Authored-By: nick.nisi@workos.com <nick.nisi@workos.com>
* fix(api): close remaining bugs flagged by Devin Review and CodeRabbit
- runApiRequest: refuse mutating requests without --yes in
non-interactive human mode. Previously the control flow had a gap
where a non-TTY environment with WORKOS_FORCE_TTY=1 (or any
non-JSON, non-interactive setup) silently proceeded with the
request. We now print a stderr error and exit 1 in that case so
destructive operations are never executed implicitly.
- bin.ts: register the shared insecure-storage option on the api
command so applyInsecureStorage actually receives the flag instead
of failing yargs strict-mode.
- request.ts: send Content-Type: application/json whenever a body is
defined (including the empty string), matching the new empty-body
semantics in resolveBody.
- interactive.spec.ts: add the missing test file alongside
interactive.ts (per CLAUDE.md), covering tag/endpoint selection,
path/query parameter substitution, JSON body collection, cancel
paths, and >=400 response handling.
Co-Authored-By: nick.nisi@workos.com <nick.nisi@workos.com>
* fix(api): refuse JSON-mode interactive flow even in a TTY
If a user runs `workos api --json` from a real TTY, the previous
guard only fired when the environment was already non-interactive,
so the code fell through to apiInteractive() — which writes
human-readable preview lines (`console.log`) to stdout before
printResponse(), corrupting the JSON output contract.
Hoist the isJsonMode() check ahead of the TTY check so JSON mode
always exits with a structured tty_required error, regardless of
TTY status. Add a regression test for the JSON-in-TTY case and
adjust the existing JSON-mode tests so they don't queue
mockReturnValueOnce values that would never be consumed (which
leaked into later tests in the file).
Co-Authored-By: nick.nisi@workos.com <nick.nisi@workos.com>
* fix(api): expose --insecure-storage in help-json for the api command
The api command's yargs builder registers insecureStorageOption (so
the runtime accepts --insecure-storage), but the help-json schema
omitted it, breaking the machine-readable contract documented for
agents consuming `workos api --help --json`. Mirror the convention
used by every other credential-accessing command in help-json.ts.
Co-Authored-By: nick.nisi@workos.com <nick.nisi@workos.com>
* fix(api): tighten edge-case handling in body, path, and fetch flows
Three small but real fixes prompted by code-review questions:
* resolveBody: when --file - is used and stdin is empty, the previous
code returned an empty string, which runApiRequest then treated as a
valid body (silently flipping the inferred method to POST and firing
an empty-body request). Now exits with a structured empty_stdin_body
error so the user knows their pipe didn't deliver any data.
* interactive.ts: path placeholder substitution used String.replace,
which only replaces the first occurrence. The current WorkOS spec
doesn't reuse a parameter name within a single path, but the OpenAPI
format permits it; switch to replaceAll so a hypothetical path like
/users/{id}/links/{id} is fully resolved.
* request.ts: the fetch error handler discarded the original error
entirely, leaving users staring at a generic 'check your internet
connection' message even when the real cause was a DNS failure, TLS
cert mismatch, or timeout. Preserve the underlying message in the
thrown error and attach it as { cause } so it's surfaced when
printed and accessible programmatically.
Co-Authored-By: nick.nisi@workos.com <nick.nisi@workos.com>
* fix(api): URL-encode path param values in interactive mode
Greptile flagged a real P1: query param values were correctly wrapped
in encodeURIComponent, but path param values were substituted verbatim.
A user typing a value containing a space, '?', '#', '%', or '/' would
end up with an invalid URL like 'https://api.workos.com/users/user 42',
which Node's native fetch rejects with TypeError: Invalid URL — and our
catch handler in request.ts then surfaces as 'Failed to connect to
WorkOS API: Invalid URL', misleading the user into thinking it was a
network issue.
Apply encodeURIComponent to path param values before substitution and
add a regression test for reserved characters.
Co-Authored-By: nick.nisi@workos.com <nick.nisi@workos.com>
* fix(api): resolve $ref params and dedupe path/operation overlap
Greptile flagged two long-standing concerns in the OpenAPI catalog
parser:
1. $ref parameters are silently dropped. The WorkOS spec doesn't use
them today, but if it ever adopts shared parameter definitions
under components.parameters, the prompt for that parameter would
be skipped and the literal {param} placeholder would be sent to
the API. Resolve refs against components.parameters (with chain
following) and skip unresolvable refs explicitly.
2. Path-level + operation-level params with the same (name, in) pair
were concatenated, producing duplicate prompts in interactive mode.
Per the OpenAPI 3.x spec, operation-level params override path-level
ones for the same (name, in). Switch to a Map keyed on "in:name"
so the operation-level definition wins.
Tests added in catalog.spec.ts cover $ref resolution, unresolvable
refs (no placeholder leak), and path/operation override semantics.
Co-Authored-By: nick.nisi@workos.com <nick.nisi@workos.com>
* fix(api): switch catalog spec read to async to match CLAUDE.md
Devin Review flagged that loadCatalog() used readFileSync, violating
CLAUDE.md's 'Avoid Node-specific sync APIs (crypto, fs sync) unless
necessary' rule. Switch to fs/promises readFile and make loadCatalog
return Promise<Catalog>.
The cache now stores the in-flight Promise rather than the resolved
value, so concurrent callers reuse the same readFile/parse pass — this
also closes the (theoretical) race that Greptile and others kept
flagging earlier in this PR.
Cascading async-ification:
* runApiLs becomes async and is awaited from bin.ts.
* apiInteractive already awaited loadCatalog (was returning a sync
Catalog before — now it gets a Promise<Catalog>, which it correctly
awaits).
Spec file mocks switched from () => mockCatalog to async () =>
mockCatalog so they match the new return type, and the four runApiLs
tests now use await.
Co-Authored-By: nick.nisi@workos.com <nick.nisi@workos.com>
* fix(api): pass --api-key to interactive, require mandatory params and body
Three fixes for the interactive API builder:
- Thread --api-key from argv into runApiInteractive → apiInteractive so
the override actually reaches the request layer
- Split query param collection: always prompt for required params, gate
optional ones behind a confirm
- Add requestBodyRequired to the catalog; skip the "Provide a body?"
confirm when the OpenAPI spec marks the body as required
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>workos api gateway (#142)1 parent 64829e0 commit 6cfa55b
14 files changed
Lines changed: 2036 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| 52 | + | |
52 | 53 | | |
53 | 54 | | |
| 55 | + | |
54 | 56 | | |
55 | 57 | | |
56 | 58 | | |
57 | 59 | | |
| 60 | + | |
58 | 61 | | |
59 | 62 | | |
60 | 63 | | |
61 | 64 | | |
62 | 65 | | |
63 | 66 | | |
64 | | - | |
65 | | - | |
66 | 67 | | |
67 | 68 | | |
68 | 69 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
430 | 430 | | |
431 | 431 | | |
432 | 432 | | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
433 | 515 | | |
434 | 516 | | |
435 | 517 | | |
| |||
0 commit comments