Skip to content

fix(core): include success field in API responses#2098

Open
rhaegalll wants to merge 3 commits into
emdash-cms:mainfrom
rhaegalll:fix/api-response-success-field
Open

fix(core): include success field in API responses#2098
rhaegalll wants to merge 3 commits into
emdash-cms:mainfrom
rhaegalll:fix/api-response-success-field

Conversation

@rhaegalll

Copy link
Copy Markdown

What does this PR do?

The REST API reference documents a discriminated response envelope { success: true, data } on success and { success: false, error } on error (see Response format and Plugin API routes). In practice the success field was never emitted: apiSuccess/apiError returned bare { data } / { error }, and the generated OpenAPI schema (successEnvelope / apiErrorSchema) declared the same shape. Clients couldn't rely on the documented discriminant, and the published OpenAPI spec disagreed with the docs.

This adds success to both the runtime response helpers and the OpenAPI envelope schemas so responses match the reference. The existing data and error fields are unchanged, so this is backward compatible — existing consumers that read data/error keep working.

Notably, the internal ApiResult<T> type already carries the success discriminant; it was only being dropped at the HTTP boundary.

Closes #

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8

The REST API reference documents a discriminated response envelope
(`{ success: true, data }` on success, `{ success: false, error }` on
error), but apiSuccess/apiError and the generated OpenAPI schema both
omitted the `success` field, so clients could not rely on the documented
discriminant. Add it to the runtime helpers and the OpenAPI success/error
envelopes so responses match the reference. The existing data and error
fields are unchanged, keeping this backward compatible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bb34ce3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet cla: needed labels Jul 17, 2026
@rhaegalll

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Jul 17, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The approach is correct: adding the documented success discriminant to the shared apiSuccess/apiError helpers and the OpenAPI envelope schemas is the right fix, and the new unit tests pin the helper output. However, the PR is incomplete. EmDash has many hand-rolled JSON responses that bypass these helpers, so after this change the API is inconsistent: the OpenAPI spec now declares success everywhere, but a meaningful slice of real responses (auth/CSRF errors, validation errors, scope errors, /_emdash/api/auth/mode, /_emdash/api/manifest, /_emdash/api/admin/plugins/updates, etc.) still emit bare { data } or { error }.

This is both a correctness issue (the published spec disagrees with runtime behavior for those endpoints) and an AGENTS.md convention violation ("Use the shared utilities -- don't roll your own" for API responses). I would either fix the remaining call sites before merge or narrow the PR claims/changeset; as it stands the changeset says "Fixes API responses to include the success field", which over-promises.

I checked the diff, the changed helpers/schemas, traced Response.json / new Response(JSON.stringify call sites across packages/core/src, and verified the client only reads data/error so existing consumers remain compatible. Tests compile against the old { data } casts but still pass because the new envelope is a superset.

Headline: mergeable once the hand-rolled responses are brought into line with the new envelope, or once the PR scope is honestly narrowed to "helpers and schemas only".


Findings

  • [needs fixing] packages/core/src/api/parse.ts:116

    The validation failure path in parseBody/parseQuery returns a raw { error: { code: "VALIDATION_ERROR", ... } } object without the success: false discriminant. Because almost every route uses these parsers, this creates a large class of 400 responses that contradict the updated OpenAPI apiErrorSchema.

    	return apiError(
    		"VALIDATION_ERROR",
    		"Invalid request data",
    		400,
    		{ issues },
    	);
    

    (The manual Response.json and cache headers can then be removed.)

  • [needs fixing] packages/core/src/astro/middleware/auth.ts:63

    csrfRejectedResponse() hand-rolls an error body without success: false. The same file also hand-rolls mcpUnauthorizedResponse() (line 77), the setup-route CSRF rejection (line 227), the invalid-bearer responses (lines 263 and 402), the passkey NOT_AUTHENTICATED/NOT_FOUND responses (lines 665 and 683), and the INSUFFICIENT_SCOPE responses in enforceTokenScope (lines 798 and 813). All of these are EmDash API error responses and should use apiError(...) so they match the documented { success: false, error } envelope and include consistent cache headers.

  • [needs fixing] packages/core/src/auth/scopes.ts:24

    requireScope() returns a hand-rolled { error: { code: "INSUFFICIENT_SCOPE", ... } } response without the success discriminant. It duplicates the scope logic in auth.ts and should delegate to apiError("INSUFFICIENT_SCOPE", ...) for consistency with the OpenAPI error schema.

  • [needs fixing] packages/core/src/astro/routes/api/admin/plugins/updates.ts:64

    This route returns Response.json({ data: { items } }), omitting success: true and the standard cache headers. The comment above it also becomes outdated after this PR. Use apiSuccess:

    	return apiSuccess({ items });
    
  • [needs fixing] packages/core/src/astro/routes/api/auth/mode.ts:43

    The public auth-mode endpoint returns a bare { data: { authMode, signupEnabled, providers } } envelope without success: true. It should use apiSuccess(...) to align with the OpenAPI success envelope.

  • [needs fixing] packages/core/src/astro/routes/api/manifest.ts:97

    The admin manifest endpoint returns Response.json({ data: manifest }, ...), missing the success: true discriminant. Use apiSuccess(manifest) and, if needed, set the X-Schema-Version-style header on the returned response.

  • [needs fixing] packages/core/src/astro/routes/api/admin/bylines/[id]/translations.ts:68

    The source-byline 404 is hand-rolled as { error: { code: "NOT_FOUND", ... } } without success: false. Since neighbouring code uses apiError via unwrapResult and handleError, this should be:

    		return apiError("NOT_FOUND", "Byline not found", 404);
    
  • [suggestion] packages/core/tests/integration/openapi/openapi.test.ts:122

    The "uses consistent error response shape across all error codes" test only asserts that an error response has a schema. Since the PR adds success: false to apiErrorSchema, consider also asserting that every 4xx/5xx JSON schema has a success property (or is a $ref to ApiError), mirroring the success-envelope check you added above.

rhaegalll and others added 2 commits July 17, 2026 19:03
Adding the success discriminant to the response envelope meant several
hand-rolled JSON responses no longer matched the documented shape: request
validation, the auth-middleware CSRF/scope/bearer rejections, and the
auth-mode, manifest, plugin-updates, and byline-translations routes still
emitted bare { data } / { error }. Route them through apiError/apiSuccess so
every response carries the success discriminant and consistent cache headers,
per the shared-utility convention. No auth logic changes -- response shape only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MCP unauthorized and invalid-bearer-token responses carry a
WWW-Authenticate header for OAuth discovery, so they could not simply be
swapped for apiError without losing it. Build the response via apiError to
get the { success: false, error } envelope, then set the WWW-Authenticate
header on it, preserving the OAuth discovery handshake. Standards-based
OAuth token/registration and well-known responses are intentionally left
untouched -- they follow their own RFC formats, not the EmDash envelope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review size/L and removed size/M review/needs-review No maintainer or bot review yet labels Jul 17, 2026
@rhaegalll

Copy link
Copy Markdown
Author

The initial PR only added success to the shared helpers and the OpenAPI schema, which left hand-rolled responses inconsistent (as the automated review caught). I've expanded it so the envelope is consistent everywhere:

  • Routed the remaining hand-rolled responses through apiError/apiSuccess — request validation, the auth-middleware rejections (CSRF, bearer, passkey, scope), and the auth/mode, manifest, plugins/updates, and bylines/[id]/translations routes.
  • Preserved the WWW-Authenticate header on the MCP/OAuth bearer 401s (built via apiError, header set on the response).
  • Left the standards-based OAuth token/registration and .well-known responses in their RFC formats (not the EmDash envelope).

Behavior is unchanged apart from the added success field, with this exception: three responses that previously sent no Cache-Control header (scopes.ts 403, bylines 404, plugins/updates 200) now carry the standard private, no-store like every other API response.

@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review of PR #2098. The author addressed all of the hand-rolled JSON response sites from the previous review: parseBody/parseQuery validation failures, auth middleware CSRF/token/scope errors, requireScope, /_emdash/api/admin/plugins/updates, /_emdash/api/auth/mode, /_emdash/api/manifest, and the byline translations 404 now all go through apiError/apiSuccess. The updated OpenAPI tests also assert the success discriminant on both success and error schemas.

The approach is still correct: adding the documented discriminant to the shared helpers and schemas, plus converting the bypass sites, makes the runtime envelope consistent with the REST API reference and generated spec while remaining backward compatible (existing data/error consumers are unaffected).

I traced remaining Response.json / JSON.stringify calls in packages/core/src and only found OAuth/well-known protocol endpoints, which legitimately use their RFC-mandated shapes rather than the EmDash envelope, and a few plain-text external-auth middleware responses. No other hand-rolled EmDash API envelopes remain.

Two small documentation/comment inconsistencies are still worth cleaning up before merge:

  1. The generated OpenAPI spec's own info.description still says responses are "wrapped in a { data } envelope", but the spec now declares { success, data }. The description should match the schema it documents.
  2. A comment in visual-editing/toolbar.ts still refers to a { data } envelope (the code already handles the new shape correctly).

Headline: ready to merge once the generated spec description is updated.


Findings

  • [needs fixing] packages/core/src/api/openapi/document.ts:2354

    The generated OpenAPI spec now declares the { success, data } success envelope and { success, false, error } error schema, but its own info.description still tells readers responses are "wrapped in a { data } envelope". This contradicts the schema and the PR's goal of making the spec agree with the docs.

    			description:
    				"REST API for the EmDash CMS. All endpoints require authentication and return responses wrapped in a `{ success, data }` envelope.",
    
  • [suggestion] packages/core/src/visual-editing/toolbar.ts:733

    The comment still describes the manifest response as a { data } envelope. The code below it (manifestCache = m && m.data ? m.data : m) already handles the current { success, data } shape, so the comment is stale.

            // The manifest endpoint wraps the payload in a { success, data } envelope (ApiResponse shape).
    

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core cla: signed review/awaiting-author Reviewed; waiting on the author to respond size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants