diff --git a/apps/admin/package.json b/apps/admin/package.json
index 2a37978..23a5aca 100644
--- a/apps/admin/package.json
+++ b/apps/admin/package.json
@@ -14,6 +14,7 @@
"test:ci": "vitest run --coverage"
},
"dependencies": {
+ "@gonext/api-types": "workspace:*",
"@gonext/blocks-editor": "workspace:*",
"@gonext/blocks-sdk": "workspace:*",
"@radix-ui/react-avatar": "^1.1.2",
diff --git a/apps/admin/src/app/(authenticated)/comments/CommentListClient.test.tsx b/apps/admin/src/app/(authenticated)/comments/CommentListClient.test.tsx
index a99df37..b0da3d0 100644
--- a/apps/admin/src/app/(authenticated)/comments/CommentListClient.test.tsx
+++ b/apps/admin/src/app/(authenticated)/comments/CommentListClient.test.tsx
@@ -251,6 +251,53 @@ describe('CommentListClient', () => {
expect(href).toContain('/posts/p1');
});
+ // ── Regression locks for PR #523 ──────────────────────────────────
+ //
+ // Before #523 the comments table's "On post" column built its href
+ // via `${postId}/edit`, which 404'd because no nested editor route
+ // exists — the [id] segment IS the editor. The fix re-pointed all
+ // call sites to `postEditHref()` (which returns the bare /posts/{id}
+ // path despite the legacy name). These tests pin the contract so a
+ // future rename or copy-paste can't silently re-introduce /edit.
+
+ it('TestCommentList_PostLinkOmitsEditSuffix_Issue523: every comment-row post link omits /edit', () => {
+ // SAMPLE has rows on two distinct post ids (p1, p2). All four
+ // rows must use bare /posts/{id}.
+ render();
+
+ const postLinks = [
+ ...screen.getAllByRole('link', { name: /hello world/i }),
+ ...screen.getAllByRole('link', { name: /^second$/i }),
+ ];
+ // 4 comment rows total, but we may have one link per row, so
+ // check we found at least the two distinct post titles.
+ expect(postLinks.length).toBeGreaterThanOrEqual(2);
+ for (const link of postLinks) {
+ const href = link.getAttribute('href') ?? '';
+ expect(href).not.toMatch(/\/edit($|[?#])/);
+ expect(href).toMatch(/^\/posts\/[^/]+$/);
+ }
+ });
+
+ it('TestCommentList_RowPostLinkPointsToBarePostId_Issue523: link href matches /posts/{postId} exactly', () => {
+ // Pinned constants: row "c1" is on post "p1" → expect /posts/p1.
+ // Locks down the literal route shape so a refactor to e.g.
+ // /admin/posts/{id} wouldn't pass either.
+ render();
+ const link = screen.getAllByRole('link', { name: /hello world/i })[0];
+ expect(link?.getAttribute('href')).toBe('/posts/p1');
+ });
+
+ it('TestCommentList_PostLinkUsesPostIdNotCommentId_Issue523: the column links to the post, not the comment', () => {
+ // Sanity: if someone wires the link to comment.id by accident
+ // (c1, c2, …) the regression test fails loudly.
+ render();
+ const link = screen.getAllByRole('link', { name: /hello world/i })[0];
+ const href = link?.getAttribute('href') ?? '';
+ expect(href).not.toMatch(/\/posts\/c\d+/);
+ expect(href).toContain('p1');
+ });
+
it('all-rows checkbox selects every row', () => {
render();
const selectAll = screen.getByLabelText(/select all comments/i);
diff --git a/apps/admin/src/app/(authenticated)/media/components/FolderTree.test.tsx b/apps/admin/src/app/(authenticated)/media/components/FolderTree.test.tsx
index 9220617..26e039a 100644
--- a/apps/admin/src/app/(authenticated)/media/components/FolderTree.test.tsx
+++ b/apps/admin/src/app/(authenticated)/media/components/FolderTree.test.tsx
@@ -98,6 +98,31 @@ describe('FolderTree', () => {
await waitFor(() => expect(onMediaMoved).toHaveBeenCalled());
});
+ // ── Regression locks for PR #523 (data:null tolerance) ────────────
+ //
+ // listCollections used to return `{ data: null }` for an empty
+ // folder set (Postgres NULL → Go nil-slice → JSON null). The
+ // FolderTree wrapped the result with `Array.isArray(res.data) ?
+ // res.data : []` so the empty-state path doesn't blow up. Lock the
+ // band-aid here in case the API regresses on a future build.
+
+ it('TestFolderTree_TolerateListCollectionsDataNull_Issue523: renders empty tree without crash when API returns data:null', async () => {
+ mocks.listCollections.mockReset().mockResolvedValue({
+ data: null as unknown as MediaCollection[],
+ });
+
+ expect(() =>
+ render(),
+ ).not.toThrow();
+
+ await waitFor(() => expect(mocks.listCollections).toHaveBeenCalled());
+ // The synthetic leaves ("All", "Unfiled") still render — they
+ // come from the component itself, not the API. Their presence
+ // proves the tree mounted and didn't crash on the null payload.
+ expect(screen.getByTestId('folder-leaf-all')).toBeInTheDocument();
+ expect(screen.getByTestId('folder-leaf-unfiled')).toBeInTheDocument();
+ });
+
it('drops onto Unfiled with collection_id null', async () => {
mocks.listCollections.mockResolvedValueOnce({ data: [] });
mocks.moveMediaToCollection.mockResolvedValueOnce({ moved: 1 });
diff --git a/apps/admin/src/app/(authenticated)/media/components/MediaGrid.test.tsx b/apps/admin/src/app/(authenticated)/media/components/MediaGrid.test.tsx
index f8289b7..692093a 100644
--- a/apps/admin/src/app/(authenticated)/media/components/MediaGrid.test.tsx
+++ b/apps/admin/src/app/(authenticated)/media/components/MediaGrid.test.tsx
@@ -142,4 +142,62 @@ describe('MediaGrid', () => {
expect(screen.getByTestId('tile-edit-a')).toBeInTheDocument();
expect(screen.getByTestId('tile-delete-a')).toBeInTheDocument();
});
+
+ // ── Regression locks for PR #523 (data:null tolerance) ────────────
+ //
+ // Postgres' nil-slice + Go's omitempty quirk meant the admin list
+ // endpoints would emit `data: null` instead of `data: []` for an
+ // empty result set. PR #523 added a router-level coerce, but the
+ // client-side defensive band-aid (Array.isArray(...) ? ... : [])
+ // must stay so the grid keeps rendering against older API builds.
+
+ it('TestMediaGrid_TolerateInitialDataNull_Issue523: renders without crash when initialData.data is null', async () => {
+ // Cast through unknown to bypass the static `data: MediaAsset[]`
+ // type — at runtime the API used to actually emit null, and the
+ // band-aid in MediaGrid is what saves us when it does.
+ const initialWithNullData = {
+ data: null as unknown as never,
+ pagination: { next_cursor: '' },
+ } as unknown as MediaListResponse;
+
+ mocks.listMedia.mockResolvedValue({
+ data: [] as MediaAsset[],
+ pagination: { next_cursor: '' },
+ });
+
+ expect(() =>
+ render(),
+ ).not.toThrow();
+
+ // After the post-mount refetch settles, the empty-state surface
+ // appears — proves the grid recovered without crashing.
+ await waitFor(() => {
+ expect(screen.getByTestId('empty-state')).toBeInTheDocument();
+ });
+ });
+
+ it('TestMediaGrid_TolerateFetchDataNull_Issue523: refetch returning data:null does not crash', async () => {
+ // The first refetch (issued on mount when filter changes /
+ // hydrated flag flips) used to throw "Cannot read properties of
+ // null (reading 'length')" if the API emitted null. The
+ // band-aid coerces null → [].
+ mocks.listMedia.mockResolvedValue({
+ data: null as unknown as MediaAsset[],
+ pagination: { next_cursor: '' },
+ });
+
+ render(
+ ,
+ );
+
+ fireEvent.click(screen.getByTestId('filter-chip-image'));
+
+ // The grid is still mounted, no crash banner appears, and the
+ // empty-state survives the null payload.
+ await waitFor(() => {
+ const lastCall = mocks.listMedia.mock.calls.at(-1);
+ expect(lastCall?.[0]).toMatchObject({ type: 'image' });
+ });
+ expect(screen.getByTestId('empty-state')).toBeInTheDocument();
+ });
});
diff --git a/apps/admin/src/app/(authenticated)/media/page.test.tsx b/apps/admin/src/app/(authenticated)/media/page.test.tsx
new file mode 100644
index 0000000..8a0a325
--- /dev/null
+++ b/apps/admin/src/app/(authenticated)/media/page.test.tsx
@@ -0,0 +1,146 @@
+/**
+ * Media page — server-component coercion regression tests (issue #523).
+ *
+ * The admin media list endpoint historically returned `data: null` for
+ * empty libraries (Postgres NULL → Go nil-slice → JSON null). The page
+ * coerces null → [] before handing the result to the
+ * client island, because the client assumes `data` is always an array
+ * and calling `.length` on null throws.
+ *
+ * PR #523 added a router-level fix on the API side (router.Page[T]
+ * coerces nil Data to []), but the page-side band-aid stays in place
+ * so older API builds keep working. These tests lock the coercion
+ * logic.
+ *
+ * Note: media/page.tsx is a Server Component and exports only the
+ * default async function. We cannot import `fetchInitial` directly,
+ * so we test by mocking `@/lib/server-api`, awaiting the page's
+ * default export, and verifying the resulting JSX prop tree carries a
+ * safe array shape into the MediaGrid client.
+ */
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+// MediaGrid is a complex client component with hooks; we don't need
+// to render it for these tests. Replace it with a transparent shim
+// so we can read the props the server component passed in.
+const mediaGridProps: { value: unknown } = { value: undefined };
+vi.mock('./components/MediaGrid', () => ({
+ MediaGrid: (props: unknown) => {
+ mediaGridProps.value = props;
+ // Render nothing — props introspection is what we care about.
+ return null;
+ },
+}));
+
+const serverApiFetchMock = vi.fn();
+vi.mock('@/lib/server-api', () => ({
+ serverApiFetch: (...args: unknown[]) => serverApiFetchMock(...args),
+}));
+
+import MediaPage from './page';
+
+beforeEach(() => {
+ serverApiFetchMock.mockReset();
+ mediaGridProps.value = undefined;
+});
+
+afterEach(() => {
+ vi.useRealTimers();
+});
+
+/** Build a Response shim for serverApiFetch to return. */
+function jsonRes(body: unknown, status = 200): Response {
+ return new Response(JSON.stringify(body), {
+ status,
+ headers: { 'Content-Type': 'application/json' },
+ });
+}
+
+describe('Media page server coercion (issue #523)', () => {
+ it('TestMediaPage_CoercesDataNullToEmptyArray_Issue523: API data:null is normalised to []', async () => {
+ // Reproduce the failure mode: the API returned `data: null` and
+ // the client crashed reading `.length`. The page-level coerce in
+ // page.tsx is what saves it.
+ serverApiFetchMock.mockResolvedValueOnce(
+ jsonRes({ data: null, pagination: { next_cursor: '' } }),
+ );
+
+ // Awaiting an async server component is the only way to drive
+ // its rendering in vitest without a full React server runtime.
+ const tree = await MediaPage();
+ // Render the resulting JSX through React's virtual tree just to
+ // trigger the MediaGrid shim's prop-capture.
+ const { render } = await import('@testing-library/react');
+ render(tree);
+
+ const props = mediaGridProps.value as {
+ initialData: { data: unknown[] };
+ };
+ expect(props.initialData.data).toEqual([]);
+ expect(Array.isArray(props.initialData.data)).toBe(true);
+ });
+
+ it('TestMediaPage_PassesThroughArrayData_Issue523: real arrays are not mutated', async () => {
+ // Sanity: when the API returns the canonical shape the page
+ // does not corrupt the data on its way to the client.
+ const data = [
+ {
+ id: 'a',
+ filename: 'a.png',
+ mime_type: 'image/png',
+ byte_size: 100,
+ alt_text: '',
+ caption: '',
+ storage_key: 'k/a',
+ uploader_id: 'u',
+ created_at: '2026-05-17T00:00:00Z',
+ updated_at: '2026-05-17T00:00:00Z',
+ tags: [],
+ },
+ ];
+ serverApiFetchMock.mockResolvedValueOnce(
+ jsonRes({ data, pagination: { next_cursor: '' } }),
+ );
+
+ const tree = await MediaPage();
+ const { render } = await import('@testing-library/react');
+ render(tree);
+
+ const props = mediaGridProps.value as {
+ initialData: { data: unknown[] };
+ };
+ expect(props.initialData.data).toHaveLength(1);
+ });
+
+ it('TestMediaPage_FallbackOnFetchFailure_Issue523: non-2xx surfaces an empty list (no crash)', async () => {
+ // Server-side fetch failure path. The page returns null from
+ // fetchInitial and falls through to a safe `{data:[]}` default.
+ serverApiFetchMock.mockResolvedValueOnce(
+ new Response('forbidden', { status: 403 }),
+ );
+
+ const tree = await MediaPage();
+ const { render } = await import('@testing-library/react');
+ render(tree);
+
+ const props = mediaGridProps.value as {
+ initialData: { data: unknown[] };
+ };
+ expect(props.initialData.data).toEqual([]);
+ });
+
+ it('TestMediaPage_FallbackOnFetchThrow_Issue523: thrown network error surfaces an empty list', async () => {
+ // serverApiFetch can also throw (DNS, abort, etc.). The page's
+ // try/catch is the last line of defence.
+ serverApiFetchMock.mockRejectedValueOnce(new Error('ECONNRESET'));
+
+ const tree = await MediaPage();
+ const { render } = await import('@testing-library/react');
+ render(tree);
+
+ const props = mediaGridProps.value as {
+ initialData: { data: unknown[] };
+ };
+ expect(props.initialData.data).toEqual([]);
+ });
+});
diff --git a/apps/admin/src/app/(authenticated)/posts/PostListClient.test.tsx b/apps/admin/src/app/(authenticated)/posts/PostListClient.test.tsx
index 9665ede..053bb91 100644
--- a/apps/admin/src/app/(authenticated)/posts/PostListClient.test.tsx
+++ b/apps/admin/src/app/(authenticated)/posts/PostListClient.test.tsx
@@ -12,7 +12,7 @@
*/
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { act, fireEvent, render, screen } from '@testing-library/react';
-import type { Post, PostListResponse } from './columns';
+import { postEditHref, type Post, type PostListResponse } from './columns';
// Hoisted router stubs so we can inspect calls from inside the
// vi.mock factory and from each test body.
@@ -279,6 +279,40 @@ describe('PostListClient', () => {
).toBeInTheDocument();
});
+ // ── Regression locks for PR #523 ──────────────────────────────────
+ //
+ // The post edit href was renamed in PR #523 from
+ // `/posts/{id}/edit` → bare `/posts/{id}` because the single-id
+ // route IS the editor (no nested /edit segment exists). The change
+ // touched both the posts list and the comments table. These tests
+ // pin the contract on the producer side so any call site that
+ // re-adopts the `/edit` suffix fails loud.
+
+ it('TestPostEditHref_OmitsEditSuffix_Issue523: returns bare /posts/{id} (no /edit segment)', () => {
+ // Locks the literal shape postEditHref returns. A regression that
+ // re-appends /edit (e.g. via a copy-paste from older code) fails
+ // here, in CommentListClient.test.tsx, AND in the row-render
+ // tests below.
+ expect(postEditHref('p1')).toBe('/posts/p1');
+ expect(postEditHref('p1')).not.toMatch(/\/edit$/);
+ });
+
+ it('TestPostEditHref_PercentEncodesSpecials_Issue523: id is URL-encoded', () => {
+ // Defensive: post ids today are UUIDs but the API contract
+ // tolerates arbitrary strings, so the helper must encode them.
+ expect(postEditHref('a/b')).toBe('/posts/a%2Fb');
+ });
+
+ it('TestPostListRow_TitleLinkOmitsEditSuffix_Issue523: rendered row href matches postEditHref output', () => {
+ // Sanity: the row in PostListClient must consume postEditHref, not
+ // build its own URL. If the row goes back to template-string
+ // concatenation a future refactor could re-introduce /edit.
+ render();
+ const link = screen.getByRole('link', { name: /hello world/i });
+ expect(link.getAttribute('href')).toBe(postEditHref('p1'));
+ expect(link.getAttribute('href')).not.toMatch(/\/edit$/);
+ });
+
it('shows an inline error when "Load more" fetcher rejects', async () => {
const fetcher = vi.fn(async () => {
throw new Error('boom');
diff --git a/apps/admin/src/app/(authenticated)/posts/page.test.tsx b/apps/admin/src/app/(authenticated)/posts/page.test.tsx
index fe93314..91b325a 100644
--- a/apps/admin/src/app/(authenticated)/posts/page.test.tsx
+++ b/apps/admin/src/app/(authenticated)/posts/page.test.tsx
@@ -1,16 +1,79 @@
/**
- * Posts list — page head snapshot tests.
+ * Posts list — page head snapshot tests + adapter envelope regression locks.
*
* The page itself is a server component that hits the network, so we
* can't render the whole tree here. Instead we extract the page-head
* fragment by rendering a minimal harness and verifying that the
* Headline composition is correct (italic-serif accent on "posts").
+ *
+ * In addition to head + adapter unit tests, this file regression-locks
+ * the wire-envelope handling that PR #523 cleaned up: the page must
+ * tolerate both `{data:[...]}` and the legacy `{posts:[...]}` envelopes,
+ * survive `data:null` (the old "empty results" shape), and render the
+ * FetchFailureState on HTTP 400 / malformed JSON. Because
+ * `fetchInitialPosts` is a file-local function we exercise the contract
+ * via a faithful re-implementation that mirrors the source's envelope
+ * extraction precedence. If a refactor breaks that precedence these
+ * tests pin the behavior issue #523 fixed.
*/
import { describe, expect, it } from 'vitest';
import { render } from '@testing-library/react';
import { Headline } from '@/components/ui/headline';
import { adaptApiPost, type ApiPost } from './page';
+/**
+ * Mirror of the envelope-extraction logic in `page.tsx::fetchInitialPosts`.
+ *
+ * Kept literally identical to the source — every change to the source
+ * MUST mirror here or these tests stop locking the right contract.
+ * The shape and precedence rules are documented in PR #523:
+ *
+ * 1. Body parses as JSON → try `data`, then `posts`, else [].
+ * 2. Non-2xx → returns FetchFailureState (HTTP NNN).
+ * 3. JSON.parse throws → caught by outer try, returns
+ * FetchFailureState (parse message).
+ */
+type ApiEnvelope = {
+ data?: ApiPost[] | null;
+ posts?: ApiPost[] | null;
+ pagination?: { next_cursor?: string; nextCursor?: string };
+ nextCursor?: string;
+ total?: number;
+};
+
+function extractEnvelopeRows(json: ApiEnvelope): ApiPost[] {
+ // Mirror of the precedence chain in fetchInitialPosts: data wins
+ // over posts, and Array.isArray rejects null + undefined.
+ return Array.isArray(json.data)
+ ? json.data
+ : Array.isArray(json.posts)
+ ? json.posts
+ : [];
+}
+
+async function fetchInitialPostsHarness(res: Response): Promise<{
+ rows: ApiPost[];
+ errorReason: string | null;
+}> {
+ // This re-implements the body of fetchInitialPosts() literally
+ // (minus the URL fetch + adapter mapping — those are tested in their
+ // own describe blocks). The goal is to regression-lock the envelope
+ // handling cleaned up in PR #523, including the catch-all that
+ // converts thrown JSON errors into a "Couldn't load" reason string.
+ try {
+ if (!res.ok) {
+ return { rows: [], errorReason: `HTTP ${res.status}` };
+ }
+ const json = (await res.json()) as ApiEnvelope;
+ return { rows: extractEnvelopeRows(json), errorReason: null };
+ } catch (err) {
+ return {
+ rows: [],
+ errorReason: err instanceof Error ? err.message : 'network error',
+ };
+ }
+}
+
describe('adaptApiPost — author display fallback (issue #515)', () => {
it('falls back to last 8 chars of the UUID when the API omits a display name', () => {
// 36-char UUID — the realistic shape coming back from the list
@@ -51,6 +114,114 @@ describe('adaptApiPost — author display fallback (issue #515)', () => {
});
});
+describe('fetchInitialPosts envelope handling (issue #523)', () => {
+ it('TestPostsPage_AcceptsDataEnvelopeShape_Issue523: pulls rows out of the {data:[...]} envelope', async () => {
+ const body: ApiEnvelope = {
+ data: [
+ { id: 'p1', title: 'A', status: 'publish', author_id: 'aaaa-1111' },
+ { id: 'p2', title: 'B', status: 'draft', author_id: 'bbbb-2222' },
+ ],
+ total: 2,
+ };
+ const res = new Response(JSON.stringify(body), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ const { rows, errorReason } = await fetchInitialPostsHarness(res);
+
+ expect(errorReason).toBeNull();
+ expect(rows).toHaveLength(2);
+ expect(rows.map((r) => r.id)).toEqual(['p1', 'p2']);
+ });
+
+ it('TestPostsPage_AcceptsLegacyEnvelopeShape_Issue523: pulls rows out of the {posts:[...]} legacy envelope', async () => {
+ // PR #523 made the adapter accept both `data` (the current REST
+ // shape) and `posts` (the legacy shape some older fixtures still
+ // emit). Removing the `posts` fallback would silently break any
+ // caller still on the old shape.
+ const body: ApiEnvelope = {
+ posts: [
+ { id: 'legacy-1', title: 'L1', status: 'publish', author_id: 'aaaa-1111' },
+ ],
+ total: 1,
+ };
+ const res = new Response(JSON.stringify(body), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ const { rows, errorReason } = await fetchInitialPostsHarness(res);
+
+ expect(errorReason).toBeNull();
+ expect(rows).toHaveLength(1);
+ expect(rows[0]?.id).toBe('legacy-1');
+ });
+
+ it('TestPostsPage_TreatsDataNullAsEmpty_Issue523: coerces data:null into an empty list (no crash)', async () => {
+ // Before PR #523 the API would emit `data: null` on empty result
+ // sets, which Array.isArray rejects — the page must coerce that to
+ // an empty list rather than crashing. The router-level fix in
+ // PR #523 stops emitting null, but the band-aid here must stay so
+ // older API builds keep working.
+ const body = { data: null, total: 0 } as unknown as ApiEnvelope;
+ const res = new Response(JSON.stringify(body), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ const { rows, errorReason } = await fetchInitialPostsHarness(res);
+
+ expect(errorReason).toBeNull();
+ expect(rows).toEqual([]);
+ });
+
+ it('TestPostsPage_HTTP400_RendersCouldntLoadState_Issue523: surfaces an HTTP NNN reason on non-2xx', async () => {
+ // status=any was a 400-trigger before PR #523's queryparse fix.
+ // The page must short-circuit to FetchFailureState rather than
+ // attempt to parse the body.
+ const res = new Response('{"error":"bad request"}', { status: 400 });
+
+ const { rows, errorReason } = await fetchInitialPostsHarness(res);
+
+ expect(errorReason).toBe('HTTP 400');
+ expect(rows).toEqual([]);
+ });
+
+ it('TestPostsPage_MalformedJSON_SurfacesParseError_Issue523: catches JSON.parse failures into the reason string', async () => {
+ // 2xx + bad body — fetchInitialPosts's outer try/catch must
+ // convert the SyntaxError into the FetchFailureState reason.
+ const res = new Response('not json {[', {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ const { rows, errorReason } = await fetchInitialPostsHarness(res);
+
+ // jsdom + node both throw a SyntaxError with the word "JSON" in it.
+ expect(errorReason).toBeTruthy();
+ expect(rows).toEqual([]);
+ });
+
+ it('TestPostsPage_DataWinsOverPosts_Issue523: precedence — data envelope beats legacy posts envelope when both are present', async () => {
+ // Defensive: if a server ever emits BOTH (e.g. a wrapper that
+ // dual-writes the shape during a rollout), the current envelope
+ // must win. Locks the literal precedence chain in fetchInitialPosts.
+ const body: ApiEnvelope = {
+ data: [{ id: 'new', title: 'New', status: 'publish', author_id: 'a' }],
+ posts: [{ id: 'old', title: 'Old', status: 'publish', author_id: 'b' }],
+ };
+ const res = new Response(JSON.stringify(body), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ const { rows } = await fetchInitialPostsHarness(res);
+
+ expect(rows.map((r) => r.id)).toEqual(['new']);
+ });
+});
+
describe('Posts page head', () => {
it('renders the brand "All posts." headline with the italic accent', () => {
const { container } = render(
diff --git a/apps/admin/src/app/(authenticated)/posts/page.tsx b/apps/admin/src/app/(authenticated)/posts/page.tsx
index 5dfaee1..210fe15 100644
--- a/apps/admin/src/app/(authenticated)/posts/page.tsx
+++ b/apps/admin/src/app/(authenticated)/posts/page.tsx
@@ -34,6 +34,7 @@
import Link from 'next/link';
import { Suspense, type ReactElement } from 'react';
import { Download, Plus } from 'lucide-react';
+import type { paths } from '@gonext/api-types';
import { serverApiFetch } from '@/lib/server-api';
import { Headline } from '@/components/ui/headline';
import { Button } from '@/components/ui/button';
@@ -56,15 +57,25 @@ function shortAuthorId(id: string): string {
return id.slice(-8);
}
-/** Wire shape we expect from `GET /api/v1/posts`. */
-export type ApiPost = {
+/**
+ * Wire shape we expect from `GET /api/v1/posts`.
+ *
+ * Pilot migration (issue #514): the base shape is derived from the
+ * OpenAPI spec via `@gonext/api-types` instead of hand-typed. We treat
+ * every field as optional (`Partial<>`) because the list endpoint
+ * doesn't currently emit the full single-post projection — and the
+ * admin must not crash when a field the spec marks required is missing
+ * (the adapter below already falls back defensively).
+ *
+ * The `author` extension is admin-side only — the server returns an
+ * `author_id` and the admin attempts to enrich with a display name
+ * when available. That field isn't modelled in the OpenAPI spec yet
+ * (tracked under issue #515), so it stays as a local intersection.
+ */
+type PostSchema =
+ paths['/api/v1/posts']['get']['responses']['200']['content']['application/json']['data'][number];
+export type ApiPost = Partial & {
id: string;
- title: string;
- status: string;
- published_at?: string | null;
- updated_at?: string;
- created_at?: string;
- author_id?: string;
author?: { id?: string; display_name?: string; displayName?: string } | null;
};
diff --git a/apps/api/cmd/server/adapters_test.go b/apps/api/cmd/server/adapters_test.go
new file mode 100644
index 0000000..22957c4
--- /dev/null
+++ b/apps/api/cmd/server/adapters_test.go
@@ -0,0 +1,297 @@
+// Regression tests for the login adapters in adapters.go (issue #521).
+//
+// PR #496 (and its follow-up in PR #523) wired users.meta.roles into the
+// session principal via the SQL lookup adapters in adapters.go. Without
+// the COALESCE + JSON unmarshal on the SELECT, super_admin users lost
+// their role on every sign-in — the session would mint, but every
+// capability check would deny.
+//
+// These tests pin the projection: both userLookupByEmail (the password
+// path) and userLookupByID (the TOTP finalize path) must populate
+// UserRecord.Roles from users.meta.roles. They use a testcontainers
+// Postgres instance because the COALESCE-on-jsonb shape isn't worth
+// faking — pgxmock would require us to hand-roll the JSON encoding and
+// would not catch a planner regression.
+//
+// Skipped under `go test -short`; nightly-full-tests covers the full
+// run.
+
+package main
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/jackc/pgx/v5/pgxpool"
+
+ "github.com/Singleton-Solution/GoNext/apps/api/internal/auth/login"
+ "github.com/Singleton-Solution/GoNext/packages/go/testutil/containers"
+)
+
+// adaptersSchemaSQL is the slice of the migration tree these tests
+// need. Kept in sync with `migrations/000001_*` (users + user_passwords)
+// and the role-meta column already present on users since the bootstrap
+// migration.
+//
+// We deliberately omit unrelated tables — the lookup queries only touch
+// `users` and `user_passwords` so anything else would just add noise to
+// the failure mode if a column ever renames.
+const adaptersSchemaSQL = `
+CREATE EXTENSION IF NOT EXISTS pgcrypto;
+CREATE EXTENSION IF NOT EXISTS citext;
+
+CREATE OR REPLACE FUNCTION gen_uuid_v7() RETURNS uuid LANGUAGE sql AS $$
+ SELECT gen_random_uuid();
+$$;
+
+CREATE TABLE IF NOT EXISTS users (
+ id UUID PRIMARY KEY DEFAULT gen_uuid_v7(),
+ email CITEXT NOT NULL UNIQUE,
+ handle CITEXT NOT NULL UNIQUE,
+ status TEXT NOT NULL DEFAULT 'active',
+ meta JSONB NOT NULL DEFAULT '{}'::jsonb,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE TABLE IF NOT EXISTS user_passwords (
+ user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
+ password_hash TEXT NOT NULL,
+ params_version INTEGER NOT NULL DEFAULT 1,
+ last_changed_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+`
+
+func setupAdaptersPostgres(t *testing.T) *pgxpool.Pool {
+ t.Helper()
+ dsn := containers.Postgres(t)
+ if dsn == "" {
+ // containers.Postgres already called t.Skip; bail out.
+ return nil
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ pool, err := pgxpool.New(ctx, dsn)
+ if err != nil {
+ t.Fatalf("pgxpool.New: %v", err)
+ }
+ if _, err := pool.Exec(ctx, adaptersSchemaSQL); err != nil {
+ pool.Close()
+ t.Fatalf("apply schema: %v", err)
+ }
+ t.Cleanup(pool.Close)
+ return pool
+}
+
+// insertUser inserts a user with the given email + roles meta and an
+// optional password hash. Returns the generated UUID as a string.
+func insertUser(t *testing.T, pool *pgxpool.Pool, email, handle, status string, rolesJSON string, hash string) string {
+ t.Helper()
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ var id string
+ q := `
+ INSERT INTO users (email, handle, status, meta)
+ VALUES ($1::citext, $2::citext, $3, jsonb_build_object('roles', $4::jsonb))
+ RETURNING id::text
+ `
+ if err := pool.QueryRow(ctx, q, email, handle, status, rolesJSON).Scan(&id); err != nil {
+ t.Fatalf("insert user: %v", err)
+ }
+ if hash != "" {
+ if _, err := pool.Exec(ctx, `
+ INSERT INTO user_passwords (user_id, password_hash) VALUES ($1::uuid, $2)
+ `, id, hash); err != nil {
+ t.Fatalf("insert user_password: %v", err)
+ }
+ }
+ return id
+}
+
+// TestUserLookupByEmail_ProjectsRolesFromMeta_Issue521 pins the contract
+// PR #496 + PR #523 fixed: the by-email adapter MUST populate Roles
+// from users.meta.roles. Without this projection a super_admin who
+// signs in via password loses their role on session creation.
+func TestUserLookupByEmail_ProjectsRolesFromMeta_Issue521(t *testing.T) {
+ pool := setupAdaptersPostgres(t)
+ if pool == nil {
+ return
+ }
+ insertUser(t, pool,
+ "admin@example.com",
+ "admin",
+ "active",
+ `["super_admin","editor"]`,
+ "$argon2id$v=19$m=65536,t=3,p=4$YWFh$YWFhYWFh",
+ )
+
+ lookup := userLookupByEmail(pool)
+ rec, err := lookup(context.Background(), "admin@example.com")
+ if err != nil {
+ t.Fatalf("lookup: unexpected error %v", err)
+ }
+ if rec.Email != "admin@example.com" {
+ t.Errorf("Email = %q, want %q", rec.Email, "admin@example.com")
+ }
+ if rec.Status != "active" {
+ t.Errorf("Status = %q, want %q", rec.Status, "active")
+ }
+ if rec.Hash == "" {
+ t.Error("Hash should be populated (user_passwords row exists)")
+ }
+ if got, want := len(rec.Roles), 2; got != want {
+ t.Fatalf("len(Roles) = %d, want %d (roles: %v)", got, want, rec.Roles)
+ }
+ wantRoles := map[string]bool{"super_admin": true, "editor": true}
+ for _, r := range rec.Roles {
+ if !wantRoles[r] {
+ t.Errorf("unexpected role %q in %v", r, rec.Roles)
+ }
+ delete(wantRoles, r)
+ }
+ if len(wantRoles) != 0 {
+ t.Errorf("missing roles: %v (have %v)", wantRoles, rec.Roles)
+ }
+}
+
+// TestUserLookupByID_ProjectsRolesFromMeta_Issue521 is the TOTP-path
+// sibling. The TOTP finalize handler only has a user id (recovered from
+// the intermediate token), so a separate by-id lookup exists. The two
+// MUST be case-equivalent — same projection, same Status, same Roles.
+// Without this the TOTP path silently dropped roles even after the
+// password path was fixed.
+func TestUserLookupByID_ProjectsRolesFromMeta_Issue521(t *testing.T) {
+ pool := setupAdaptersPostgres(t)
+ if pool == nil {
+ return
+ }
+ id := insertUser(t, pool,
+ "totp@example.com",
+ "totp",
+ "active",
+ `["super_admin"]`,
+ "$argon2id$v=19$m=65536,t=3,p=4$YWFh$YWFhYWFh",
+ )
+
+ lookup := userLookupByID(pool)
+ rec, err := lookup(context.Background(), id)
+ if err != nil {
+ t.Fatalf("lookup by id: unexpected error %v", err)
+ }
+ if rec.ID != id {
+ t.Errorf("ID = %q, want %q", rec.ID, id)
+ }
+ if rec.Email != "totp@example.com" {
+ t.Errorf("Email = %q, want %q", rec.Email, "totp@example.com")
+ }
+ if rec.Status != "active" {
+ t.Errorf("Status = %q, want %q", rec.Status, "active")
+ }
+ if len(rec.Roles) != 1 || rec.Roles[0] != "super_admin" {
+ t.Errorf("Roles = %v, want [super_admin]", rec.Roles)
+ }
+}
+
+// TestUserLookupByEmail_EmptyRolesArray_Issue521 covers the case
+// where meta.roles is the default empty array. The lookup must return
+// a zero-length Roles slice (not nil-vs-empty matters: the session
+// data map encodes []string verbatim, and downstream code reads .len).
+func TestUserLookupByEmail_EmptyRolesArray_Issue521(t *testing.T) {
+ pool := setupAdaptersPostgres(t)
+ if pool == nil {
+ return
+ }
+ insertUser(t, pool,
+ "noroles@example.com",
+ "noroles",
+ "active",
+ `[]`,
+ "",
+ )
+
+ lookup := userLookupByEmail(pool)
+ rec, err := lookup(context.Background(), "noroles@example.com")
+ if err != nil {
+ t.Fatalf("lookup: unexpected error %v", err)
+ }
+ if len(rec.Roles) != 0 {
+ t.Errorf("Roles = %v, want [] for user with empty meta.roles", rec.Roles)
+ }
+ // OAuth-only user (no password row) → empty Hash, NOT an error.
+ if rec.Hash != "" {
+ t.Errorf("Hash = %q, want empty for OAuth-only user", rec.Hash)
+ }
+}
+
+// TestUserLookupByEmail_MissingRolesKey_Issue521 covers the COALESCE
+// branch: when meta.roles is missing entirely (legacy rows), the SQL
+// projects '[]'::jsonb. The lookup must not error and must return a
+// zero-length Roles slice — the same behavior as an explicit empty
+// array.
+func TestUserLookupByEmail_MissingRolesKey_Issue521(t *testing.T) {
+ pool := setupAdaptersPostgres(t)
+ if pool == nil {
+ return
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ // Insert a user with meta = '{}' so meta->'roles' is NULL.
+ // We bypass insertUser's jsonb_build_object('roles', …) call so
+ // the meta is the empty object — the regression target.
+ var id string
+ err := pool.QueryRow(ctx, `
+ INSERT INTO users (email, handle, status, meta)
+ VALUES ('legacy@example.com'::citext, 'legacy'::citext, 'active', '{}'::jsonb)
+ RETURNING id::text
+ `).Scan(&id)
+ if err != nil {
+ t.Fatalf("insert legacy user: %v", err)
+ }
+
+ lookup := userLookupByEmail(pool)
+ rec, lookupErr := lookup(context.Background(), "legacy@example.com")
+ if lookupErr != nil {
+ t.Fatalf("lookup: unexpected error %v", lookupErr)
+ }
+ if len(rec.Roles) != 0 {
+ t.Errorf("Roles = %v, want [] for user with missing meta.roles", rec.Roles)
+ }
+}
+
+// TestUserLookupByEmail_NotFound_Issue521 pins the contract that an
+// unknown email returns login.ErrUserNotFound (sentinel), not a wrapped
+// pgx.ErrNoRows. The service uses errors.Is(err, ErrUserNotFound) for
+// the constant-time hash-or-not branch.
+func TestUserLookupByEmail_NotFound_Issue521(t *testing.T) {
+ pool := setupAdaptersPostgres(t)
+ if pool == nil {
+ return
+ }
+ lookup := userLookupByEmail(pool)
+ _, err := lookup(context.Background(), "ghost@example.com")
+ if err == nil {
+ t.Fatal("lookup: want error for missing user, got nil")
+ }
+ if err != login.ErrUserNotFound {
+ t.Errorf("err = %v, want login.ErrUserNotFound", err)
+ }
+}
+
+// TestUserLookupByID_NotFound_Issue521 is the by-id sibling — same
+// sentinel contract.
+func TestUserLookupByID_NotFound_Issue521(t *testing.T) {
+ pool := setupAdaptersPostgres(t)
+ if pool == nil {
+ return
+ }
+ lookup := userLookupByID(pool)
+ // A syntactically valid UUID that doesn't exist in the table.
+ _, err := lookup(context.Background(), "00000000-0000-0000-0000-000000000000")
+ if err == nil {
+ t.Fatal("lookup by id: want error for missing user, got nil")
+ }
+ if err != login.ErrUserNotFound {
+ t.Errorf("err = %v, want login.ErrUserNotFound", err)
+ }
+}
diff --git a/apps/api/internal/auth/login/service_test.go b/apps/api/internal/auth/login/service_test.go
index c611893..8259a9e 100644
--- a/apps/api/internal/auth/login/service_test.go
+++ b/apps/api/internal/auth/login/service_test.go
@@ -792,6 +792,148 @@ func TestAuthenticate_RecordsFailureForKnownUserOnly(t *testing.T) {
}
}
+// TestAuthenticate_PasswordPath_ThreadsRolesIntoSession_Issue521 is the
+// non-TOTP sibling of TestFinalizeTOTP_ThreadsRolesFromUserByID.
+//
+// PR #496 / PR #523 wired role projection into BOTH login paths: the
+// regular password Lookup (already returns a UserRecord with Roles
+// populated by the SQL adapter), and the TOTP finalize path which
+// re-fetches via UserByID. The TOTP regression test already exists;
+// this test pins the password-path side so a refactor that drops the
+// `user.Roles` argument from the s.completeLogin(...) call sites in
+// service.go fails loud, not silently as a runtime "super_admin lost
+// admin on next sign-in" report.
+func TestAuthenticate_PasswordPath_ThreadsRolesIntoSession_Issue521(t *testing.T) {
+ f := newFixture(t)
+ f.addUser(t, "u-1", "alice@example.com", "pwd", "active")
+
+ // Replace the default Lookup to project a Roles slice onto the
+ // UserRecord. We can't reach into addUser's map directly because
+ // the fixture's Lookup re-fetches by email + sets Hash, so wrap.
+ original := f.deps.Lookup
+ f.deps.Lookup = func(ctx context.Context, email string) (UserRecord, error) {
+ rec, err := original(ctx, email)
+ if err != nil {
+ return rec, err
+ }
+ rec.Roles = []string{"super_admin"}
+ return rec, nil
+ }
+ svc := f.service(t)
+
+ res, err := svc.Authenticate(context.Background(), Input{
+ Email: "alice@example.com",
+ Password: "pwd",
+ IP: "10.0.0.1",
+ })
+ if err != nil {
+ t.Fatalf("Authenticate: unexpected err %v", err)
+ }
+ if res.Token == "" {
+ t.Fatal("Token empty — login should have completed")
+ }
+
+ data := f.sessionCreator.lastData()
+ if data == nil {
+ t.Fatal("session data is nil; expected roles to be stamped in")
+ }
+ rolesAny, ok := data["roles"]
+ if !ok {
+ t.Fatalf("session data missing 'roles' key: %#v", data)
+ }
+ roles, ok := rolesAny.([]string)
+ if !ok {
+ t.Fatalf("roles type: got %T, want []string", rolesAny)
+ }
+ if len(roles) != 1 || roles[0] != "super_admin" {
+ t.Errorf("roles: got %v, want [super_admin]", roles)
+ }
+}
+
+// TestAuthenticate_PasswordPath_TOTPInline_ThreadsRoles_Issue521 covers
+// the "TOTP code supplied in the first call" branch. This is the
+// less-trodden combined-request path (some API clients submit the code
+// alongside the password rather than going through the intermediate
+// token round-trip). The roles must still come from the first-call
+// Lookup, not from a nil pointer.
+func TestAuthenticate_PasswordPath_TOTPInline_ThreadsRoles_Issue521(t *testing.T) {
+ f := newFixture(t)
+ f.addUser(t, "u-1", "alice@example.com", "pwd", "active")
+ sec, err := totp.Generate("GoNext", "alice@example.com")
+ if err != nil {
+ t.Fatalf("totp.Generate: %v", err)
+ }
+ f.addTOTP(t, "u-1", sec.Base32)
+
+ original := f.deps.Lookup
+ f.deps.Lookup = func(ctx context.Context, email string) (UserRecord, error) {
+ rec, lookupErr := original(ctx, email)
+ if lookupErr != nil {
+ return rec, lookupErr
+ }
+ rec.Roles = []string{"editor"}
+ return rec, nil
+ }
+ svc := f.service(t)
+
+ code, err := generateCurrentTOTP(sec.Base32)
+ if err != nil {
+ t.Fatalf("generate code: %v", err)
+ }
+ res, err := svc.Authenticate(context.Background(), Input{
+ Email: "alice@example.com",
+ Password: "pwd",
+ TOTPCode: code,
+ IP: "10.0.0.1",
+ })
+ if err != nil {
+ t.Fatalf("Authenticate: %v", err)
+ }
+ if res.Token == "" {
+ t.Fatal("Token empty — combined-call login should have completed")
+ }
+
+ data := f.sessionCreator.lastData()
+ if data == nil {
+ t.Fatal("session data is nil; expected roles to be stamped in")
+ }
+ roles, ok := data["roles"].([]string)
+ if !ok {
+ t.Fatalf("roles type: got %T", data["roles"])
+ }
+ if len(roles) != 1 || roles[0] != "editor" {
+ t.Errorf("roles: got %v, want [editor]", roles)
+ }
+}
+
+// TestAuthenticate_PasswordPath_EmptyRoles_NoDataKey_Issue521 documents
+// the contract for users without role grants: the session data map is
+// nil (no roles key) rather than `data = map[string]any{"roles": []}`.
+// The completeLogin code path checks `if len(rolesFromLookup) > 0`
+// before allocating the map — locking this behavior avoids accidental
+// "roles: []" map entries leaking into session storage.
+func TestAuthenticate_PasswordPath_EmptyRoles_NoDataKey_Issue521(t *testing.T) {
+ f := newFixture(t)
+ f.addUser(t, "u-1", "alice@example.com", "pwd", "active")
+ // Default Lookup returns Roles = nil for the fixture.
+ svc := f.service(t)
+
+ res, err := svc.Authenticate(context.Background(), Input{
+ Email: "alice@example.com",
+ Password: "pwd",
+ IP: "10.0.0.1",
+ })
+ if err != nil {
+ t.Fatalf("Authenticate: %v", err)
+ }
+ if res.Token == "" {
+ t.Fatal("Token empty")
+ }
+ if got := f.sessionCreator.lastData(); got != nil {
+ t.Errorf("session data: got %v, want nil for user with no roles", got)
+ }
+}
+
// TestFinalizeTOTP_ThreadsRolesFromUserByID is the regression test for
// issue #496: when a user with TOTP enabled completes the two-step
// login, the post-2FA session MUST carry the user's roles. Before the
diff --git a/packages/ts/api-types/README.md b/packages/ts/api-types/README.md
new file mode 100644
index 0000000..d1ce6ec
--- /dev/null
+++ b/packages/ts/api-types/README.md
@@ -0,0 +1,30 @@
+# @gonext/api-types
+
+TypeScript types generated from `apps/api/openapi/openapi.yaml` — the hand-authored source of truth for every GoNext REST endpoint. Consumers (`apps/admin`, `packages/ts/sdk`) import wire shapes from here so we stop hand-writing types that drift from the server.
+
+## Usage
+
+```ts
+import type { paths, components } from '@gonext/api-types';
+
+// Pick a response shape by path/method/status/content-type:
+type PostListResponse =
+ paths['/api/v1/posts']['get']['responses']['200']['content']['application/json'];
+
+// Or pull a schema directly out of components:
+type Post = components['schemas']['Post'];
+```
+
+## Regenerating
+
+The generated output is committed at `src/generated.ts` so `pnpm install` doesn't need to run codegen. After editing `apps/api/openapi/openapi.yaml`, regenerate from this package:
+
+```sh
+pnpm --filter @gonext/api-types generate
+```
+
+That runs `openapi-typescript ../../../apps/api/openapi/openapi.yaml -o src/generated.ts`. Commit the resulting diff alongside the spec change so reviewers see both halves of the contract update.
+
+## Why we commit the generated file
+
+Two reasonable patterns exist — regenerate on every install, or commit the generated output. We commit because (a) `pnpm install` stays fast and side-effect-free, (b) PR diffs make spec changes auditable, and (c) consumers can typecheck immediately without a separate codegen step.
diff --git a/packages/ts/api-types/package.json b/packages/ts/api-types/package.json
new file mode 100644
index 0000000..03de00e
--- /dev/null
+++ b/packages/ts/api-types/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@gonext/api-types",
+ "version": "0.0.0",
+ "private": true,
+ "description": "TypeScript types generated from apps/api/openapi/openapi.yaml. Single source of truth for the GoNext REST API wire format — consumers (apps/admin, packages/ts/sdk) import paths/components from here instead of hand-writing types that drift from the server. Regenerate after every openapi.yaml change with `pnpm generate`.",
+ "license": "FSL-1.1-Apache-2.0",
+ "type": "module",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js",
+ "default": "./dist/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "dist",
+ "src",
+ "README.md"
+ ],
+ "scripts": {
+ "generate": "openapi-typescript ../../../apps/api/openapi/openapi.yaml -o src/generated.ts",
+ "build": "tsc",
+ "dev": "tsc --watch",
+ "lint": "echo '@gonext/api-types lint: not yet implemented'",
+ "typecheck": "tsc --noEmit"
+ },
+ "devDependencies": {
+ "openapi-typescript": "^7.4.0",
+ "typescript": "^5.6.0"
+ }
+}
diff --git a/packages/ts/api-types/src/generated.ts b/packages/ts/api-types/src/generated.ts
new file mode 100644
index 0000000..ba2f9e0
--- /dev/null
+++ b/packages/ts/api-types/src/generated.ts
@@ -0,0 +1,3179 @@
+/**
+ * This file was auto-generated by openapi-typescript.
+ * Do not make direct changes to the file.
+ */
+
+export interface paths {
+ "/": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Server identity
+ * @description Returns the binary's name, version, and commit. Mounted by cmd/server/main.go.
+ */
+ get: operations["getRoot"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/healthz": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Liveness probe
+ * @description Returns 200 unconditionally once the process has started serving. Must
+ * never depend on external state (DB, Redis); a failure here means the
+ * binary itself is unhealthy.
+ */
+ get: operations["getHealthz"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/readyz": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Readiness probe
+ * @description Returns 200 only when every registered readiness check (DB, Redis)
+ * succeeds. Returns 503 with a per-check breakdown otherwise. Drives
+ * Kubernetes traffic gating.
+ */
+ get: operations["getReadyz"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/openapi.json": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * OpenAPI 3.1 document
+ * @description Returns the embedded OpenAPI 3.1 description as application/json.
+ * Conditional requests are honored via ETag and Last-Modified.
+ */
+ get: operations["getOpenapiJSON"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/openapi.yaml": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * OpenAPI 3.1 document (YAML)
+ * @description Same content as /openapi.json, served as YAML. Identical caching
+ * semantics. Convenience surface for tooling that prefers YAML.
+ */
+ get: operations["getOpenapiYAML"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/auth/login": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Password + TOTP login
+ * @description Exchanges email + password (and optionally TOTP / recovery code) for
+ * a session cookie. The session token is delivered exclusively via the
+ * Set-Cookie header — never echoed in the response body so HttpOnly
+ * keeps its meaning. Returns a 200 with the user id and expiry on
+ * success, or a 200 with an intermediate token when 2FA is required.
+ */
+ post: operations["login"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/auth/logout": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Revoke the current session */
+ post: operations["logout"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/auth/refresh": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Refresh the bearer JWT
+ * @description Trades a refresh cookie for a short-lived (15m) JWT access token
+ * consumed by the dashboard's API client.
+ */
+ post: operations["refreshToken"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/auth/sessions": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List the caller's live sessions */
+ get: operations["listSessions"];
+ put?: never;
+ post?: never;
+ /** Revoke every session belonging to the caller */
+ delete: operations["revokeAllSessions"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/auth/sessions/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description SessionView.id from listSessions. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ /** Revoke a single session */
+ delete: operations["revokeSession"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/auth/verify/send": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Send an email-verification link
+ * @description Generates a fresh verification token, persists its hash in Redis
+ * with a 24h TTL, and queues the verification email. Rate-limited to
+ * one send per minute per user.
+ */
+ post: operations["sendVerificationEmail"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/auth/verify": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Consume an email-verification token
+ * @description Validates the token from the link the user clicked. Marks the
+ * underlying user row as verified on success. Tokens are single-use.
+ */
+ get: operations["verifyEmail"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/auth/tokens": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List personal access tokens */
+ get: operations["listTokens"];
+ put?: never;
+ /**
+ * Mint a new personal access token
+ * @description Creates a long-lived PAT. The secret is returned ONCE in the response;
+ * subsequent reads only see metadata.
+ */
+ post: operations["createToken"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/auth/tokens/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ /** Revoke a personal access token */
+ delete: operations["revokeToken"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/posts": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List posts */
+ get: operations["listPosts"];
+ put?: never;
+ /** Create a post */
+ post: operations["createPost"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/posts/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /** Get a single post */
+ get: operations["getPost"];
+ put?: never;
+ post?: never;
+ /**
+ * Trash a post
+ * @description Soft-deletes the post (status → trash).
+ */
+ delete: operations["trashPost"];
+ options?: never;
+ head?: never;
+ /** Update a post */
+ patch: operations["updatePost"];
+ trace?: never;
+ };
+ "/api/v1/posts/{id}/autosave": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /** Read the current autosave for a post */
+ get: operations["getPostAutosave"];
+ put?: never;
+ /** Write or replace the autosave for a post */
+ post: operations["putPostAutosave"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/pages": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List pages */
+ get: operations["listPages"];
+ put?: never;
+ /** Create a page */
+ post: operations["createPage"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/pages/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /** Get a page */
+ get: operations["getPage"];
+ put?: never;
+ post?: never;
+ /** Trash a page */
+ delete: operations["trashPage"];
+ options?: never;
+ head?: never;
+ /** Update a page */
+ patch: operations["updatePage"];
+ trace?: never;
+ };
+ "/api/v1/users": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List users */
+ get: operations["listUsers"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/users/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /** Get a user */
+ get: operations["getUser"];
+ put?: never;
+ post?: never;
+ /** Delete a user */
+ delete: operations["deleteUser"];
+ options?: never;
+ head?: never;
+ /** Update a user */
+ patch: operations["updateUser"];
+ trace?: never;
+ };
+ "/api/v1/comments": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List comments */
+ get: operations["listComments"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/comments/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /** Get a comment */
+ get: operations["getComment"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/terms": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List taxonomy terms */
+ get: operations["listTerms"];
+ put?: never;
+ /** Create a term */
+ post: operations["createTerm"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/terms/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /** Get a term */
+ get: operations["getTerm"];
+ put?: never;
+ post?: never;
+ /** Delete a term */
+ delete: operations["deleteTerm"];
+ options?: never;
+ head?: never;
+ /** Update a term */
+ patch: operations["updateTerm"];
+ trace?: never;
+ };
+ "/api/v1/media": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List media library items */
+ get: operations["listMedia"];
+ put?: never;
+ /**
+ * Upload a media file
+ * @description Accepts a multipart/form-data upload. Persists the binary in the
+ * configured object store, computes derived sizes, and returns the
+ * resulting Media row. This endpoint is in flight — see issue #341.
+ */
+ post: operations["uploadMedia"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/media/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /** Get a media item */
+ get: operations["getMedia"];
+ put?: never;
+ post?: never;
+ /** Delete a media item */
+ delete: operations["deleteMedia"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/admin/plugins": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List installed plugins */
+ get: operations["listPlugins"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/admin/plugins/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /** Get a plugin manifest */
+ get: operations["getPlugin"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/admin/plugins/{id}/activate": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Activate a plugin */
+ post: operations["activatePlugin"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/admin/plugins/{id}/deactivate": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Deactivate a plugin */
+ post: operations["deactivatePlugin"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/_/plugins/dev/install": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Dev-only plugin hot-install
+ * @description Hot-installs a plugin from a local manifest. Only mounted when
+ * GONEXT_PLUGINS_DEV_MODE=true. Used by the `gonext plugin dev` CLI's
+ * watch loop. Storage is in-memory: not persisted across restarts.
+ */
+ post: operations["installPluginDev"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/admin/jobs/dlq": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List archived (dead-letter) jobs */
+ get: operations["listDeadLetterJobs"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/admin/jobs/dlq/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /** Get an archived job */
+ get: operations["getDeadLetterJob"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/admin/jobs/dlq/{id}/replay": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Re-enqueue an archived job */
+ post: operations["replayDeadLetterJob"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/admin/jobs/dlq/{id}/discard": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Permanently delete an archived job */
+ post: operations["discardDeadLetterJob"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/admin/jobs/dlq/{id}/redact": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Redact fields in an archived job payload */
+ post: operations["redactDeadLetterJob"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/admin/status": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Aggregate operational status
+ * @description Returns the per-axis health report (DB, Redis, queues, disk, themes,
+ * plugins). Status is always 200; per-source failures are reported in
+ * the body.
+ */
+ get: operations["getAdminStatus"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/admin/rum/percentiles": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** RUM Web Vitals percentiles */
+ get: operations["getRumPercentiles"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/admin/rum/slow-routes": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Slowest routes by RUM metric */
+ get: operations["getRumSlowRoutes"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/_/rum/beacon": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * RUM beacon endpoint
+ * @description Public, unauthenticated beacon used by the theme JS to ship Web
+ * Vitals. Returns 204 on accept. Drops malformed payloads silently.
+ */
+ post: operations["rumBeacon"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/settings": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get site settings */
+ get: operations["getSettings"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ /** Patch site settings */
+ patch: operations["updateSettings"];
+ trace?: never;
+ };
+ "/api/v1/search": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Cross-resource search */
+ get: operations["search"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/webhooks": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List webhook subscriptions */
+ get: operations["listWebhooks"];
+ put?: never;
+ /** Create a webhook subscription */
+ post: operations["createWebhook"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/webhooks/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /** Get a webhook */
+ get: operations["getWebhook"];
+ put?: never;
+ post?: never;
+ /** Delete a webhook */
+ delete: operations["deleteWebhook"];
+ options?: never;
+ head?: never;
+ /** Update a webhook */
+ patch: operations["updateWebhook"];
+ trace?: never;
+ };
+ "/wp-json/wp/v2/posts": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** WordPress-compatible posts list */
+ get: operations["wpListPosts"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/wp-json/wp/v2/posts/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
+ };
+ /** WordPress-compatible post fetch */
+ get: operations["wpGetPost"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+}
+export type webhooks = Record;
+export interface components {
+ schemas: {
+ /** @description Server identity returned by GET /. Mirrors cmd/server/main.go. */
+ Identity: {
+ /** @example gonext */
+ name: string;
+ /**
+ * @example v0.1.0
+ * @example dev
+ */
+ version: string;
+ /**
+ * @example abc1234
+ * @example unknown
+ */
+ commit: string;
+ };
+ Health: {
+ /** @enum {string} */
+ status: "ok" | "degraded" | "fail";
+ checks?: {
+ [key: string]: {
+ status?: string;
+ detail?: string;
+ };
+ };
+ };
+ Pagination: {
+ next_cursor: string;
+ prev_cursor: string;
+ };
+ /** @description RFC 7807-flavoured error envelope. */
+ Error: {
+ error: {
+ /** Format: uri */
+ type?: string;
+ title: string;
+ status: number;
+ code: string;
+ detail?: string;
+ fields?: {
+ [key: string]: string[];
+ };
+ trace_id?: string;
+ };
+ };
+ ErrorEnvelope: components["schemas"]["Error"];
+ LoginRequest: {
+ /** Format: email */
+ email: string;
+ /** Format: password */
+ password: string;
+ totp_code?: string;
+ recovery_code?: string;
+ intermediate_token?: string;
+ };
+ LoginResponse: {
+ user_id: string;
+ /** Format: date-time */
+ expires_at: string;
+ };
+ TOTPChallenge: {
+ intermediate_token: string;
+ requires: ("totp" | "recovery")[];
+ };
+ AccessToken: {
+ access_token: string;
+ /** @enum {string} */
+ token_type: "Bearer";
+ /** @description Seconds until expiry. */
+ expires_in: number;
+ };
+ SessionView: {
+ id: string;
+ /** Format: date-time */
+ created_at: string;
+ /** Format: date-time */
+ last_seen_at: string;
+ device_label: string;
+ ip: string;
+ current: boolean;
+ };
+ SessionList: {
+ sessions: components["schemas"]["SessionView"][];
+ };
+ VerifyResult: {
+ verified: boolean;
+ user_id: string;
+ };
+ /** @description Personal access token metadata. The secret is never echoed. */
+ Token: {
+ id: string;
+ name: string;
+ /** Format: date-time */
+ created_at: string;
+ /** Format: date-time */
+ last_used_at: string | null;
+ /** Format: date-time */
+ expires_at?: string | null;
+ scopes: string[];
+ };
+ TokenCreate: {
+ name: string;
+ scopes: string[];
+ /** Format: date-time */
+ expires_at?: string | null;
+ };
+ TokenWithSecret: components["schemas"]["Token"] & {
+ /**
+ * @description The raw token string. Echoed exactly once in the create
+ * response — clients MUST store it now; subsequent reads only
+ * see metadata.
+ */
+ secret: string;
+ };
+ /** @description Native REST projection of a row in the posts table. */
+ Post: {
+ id: string;
+ post_type: string;
+ parent_id?: string | null;
+ author_id: string;
+ /** @enum {string} */
+ status: "draft" | "publish" | "future" | "pending" | "private" | "trash";
+ title: string;
+ slug: string;
+ excerpt?: string;
+ /** @description Canonical block-tree JSON. */
+ content_blocks: Record;
+ /** @enum {string} */
+ comment_status: "open" | "closed";
+ /** @enum {string} */
+ ping_status: "open" | "closed";
+ menu_order: number;
+ meta: Record;
+ /** Format: date-time */
+ published_at?: string;
+ /** Format: date-time */
+ scheduled_for?: string;
+ /** Format: date-time */
+ created_at: string;
+ /** Format: date-time */
+ updated_at: string;
+ version: number;
+ protected: boolean;
+ };
+ PostCreate: {
+ parent_id?: string | null;
+ status?: string;
+ title?: string;
+ slug?: string;
+ excerpt?: string;
+ content_blocks?: Record;
+ password?: string;
+ comment_status?: string;
+ ping_status?: string;
+ menu_order?: number;
+ meta?: Record;
+ /** Format: date-time */
+ published_at?: string;
+ /** Format: date-time */
+ scheduled_for?: string;
+ };
+ PostUpdate: components["schemas"]["PostCreate"];
+ PostPage: {
+ data: components["schemas"]["Post"][];
+ pagination: components["schemas"]["Pagination"];
+ };
+ Page: components["schemas"]["Post"];
+ PageCreate: components["schemas"]["PostCreate"];
+ PageUpdate: components["schemas"]["PostCreate"];
+ PagePage: {
+ data: components["schemas"]["Page"][];
+ pagination: components["schemas"]["Pagination"];
+ };
+ Autosave: {
+ post_id: string;
+ content_blocks: Record;
+ title?: string;
+ excerpt?: string;
+ /** Format: date-time */
+ saved_at: string;
+ };
+ AutosaveInput: {
+ content_blocks: Record;
+ title?: string;
+ excerpt?: string;
+ };
+ User: {
+ id: string;
+ /** Format: email */
+ email: string;
+ display_name: string;
+ slug?: string;
+ /** Format: uri */
+ avatar_url?: string;
+ roles: string[];
+ /** Format: date-time */
+ verified_at?: string | null;
+ /** Format: date-time */
+ created_at: string;
+ };
+ UserUpdate: {
+ display_name?: string;
+ slug?: string;
+ /** Format: uri */
+ avatar_url?: string;
+ roles?: string[];
+ };
+ UserPage: {
+ data: components["schemas"]["User"][];
+ pagination: components["schemas"]["Pagination"];
+ };
+ Comment: {
+ id: string;
+ post_id: string;
+ author_id?: string | null;
+ author_name: string;
+ /** Format: email */
+ author_email?: string;
+ /** Format: uri */
+ author_url?: string;
+ content: string;
+ /** @enum {string} */
+ status: "approved" | "pending" | "spam" | "trash";
+ parent_id?: string | null;
+ /** Format: date-time */
+ created_at: string;
+ };
+ CommentPage: {
+ data: components["schemas"]["Comment"][];
+ pagination: components["schemas"]["Pagination"];
+ };
+ Term: {
+ id: string;
+ /** @enum {string} */
+ taxonomy: "category" | "post_tag";
+ name: string;
+ slug: string;
+ description?: string;
+ parent_id?: string | null;
+ count: number;
+ };
+ TermCreate: {
+ /** @enum {string} */
+ taxonomy: "category" | "post_tag";
+ name: string;
+ slug?: string;
+ description?: string;
+ parent_id?: string | null;
+ };
+ TermUpdate: {
+ name?: string;
+ slug?: string;
+ description?: string;
+ parent_id?: string | null;
+ };
+ TermPage: {
+ data: components["schemas"]["Term"][];
+ pagination: components["schemas"]["Pagination"];
+ };
+ Media: {
+ id: string;
+ mime: string;
+ /** Format: uri */
+ url: string;
+ byte_size: number;
+ width?: number;
+ height?: number;
+ alt?: string;
+ caption?: string;
+ sha256?: string;
+ uploaded_by?: string;
+ /** Format: date-time */
+ created_at: string;
+ };
+ MediaPage: {
+ data: components["schemas"]["Media"][];
+ pagination: components["schemas"]["Pagination"];
+ };
+ Plugin: {
+ id: string;
+ name: string;
+ version: string;
+ active: boolean;
+ author?: string;
+ description?: string;
+ /** Format: date-time */
+ installed_at?: string;
+ };
+ PluginManifest: {
+ id: string;
+ version: string;
+ entrypoint: string;
+ manifest?: Record;
+ };
+ ArchivedTask: {
+ id: string;
+ queue: string;
+ type: string;
+ payload_preview: string;
+ /**
+ * Format: byte
+ * @description Present on detail responses; nil on list.
+ */
+ payload?: string;
+ last_error: string;
+ /** Format: date-time */
+ failed_at?: string;
+ retried: number;
+ max_retry: number;
+ redacted: boolean;
+ redacted_fields?: string[];
+ };
+ RedactRequest: {
+ fields: string[];
+ };
+ /** @description Free-form site options. Keys are stable strings. */
+ Settings: {
+ [key: string]: unknown;
+ };
+ SearchResult: {
+ /** @enum {string} */
+ type: "post" | "page" | "user" | "term" | "media";
+ id: string;
+ title: string;
+ excerpt: string;
+ /** Format: uri */
+ url?: string;
+ score?: number;
+ };
+ SearchResults: {
+ results: components["schemas"]["SearchResult"][];
+ total?: number;
+ };
+ Webhook: {
+ id: string;
+ /** Format: uri */
+ url: string;
+ events: string[];
+ active: boolean;
+ /** @description First 4 chars only. */
+ secret_preview?: string;
+ /** Format: date-time */
+ created_at: string;
+ };
+ WebhookCreate: {
+ /** Format: uri */
+ url: string;
+ events: string[];
+ secret?: string;
+ /** @default true */
+ active: boolean;
+ };
+ WebhookUpdate: {
+ /** Format: uri */
+ url?: string;
+ events?: string[];
+ active?: boolean;
+ };
+ RumBeacon: {
+ route: string;
+ /** @enum {string} */
+ metric: "lcp" | "fcp" | "cls" | "inp" | "ttfb";
+ value: number;
+ device?: string;
+ connection?: string;
+ /** Format: date-time */
+ timestamp?: string;
+ };
+ RumPercentiles: {
+ [key: string]: {
+ p50?: number;
+ p75?: number;
+ p95?: number;
+ p99?: number;
+ samples?: number;
+ };
+ };
+ RumSlowRoute: {
+ route: string;
+ value: number;
+ samples?: number;
+ };
+ StatusReport: {
+ /** Format: date-time */
+ generated_at: string;
+ sections: {
+ [key: string]: {
+ ok?: boolean;
+ detail?: Record;
+ error?: string;
+ };
+ };
+ };
+ AuditEvent: {
+ id: string;
+ event_type: string;
+ actor_id: string;
+ subject_type?: string;
+ subject_id?: string;
+ meta?: Record;
+ /** Format: date-time */
+ created_at: string;
+ };
+ /** @description WordPress-shaped post envelope for /wp-json clients. */
+ WPPost: {
+ id: number;
+ /** Format: date-time */
+ date?: string;
+ /** Format: date-time */
+ date_gmt?: string;
+ /** Format: date-time */
+ modified?: string;
+ /** Format: date-time */
+ modified_gmt?: string;
+ slug: string;
+ status: string;
+ type: string;
+ /** Format: uri */
+ link?: string;
+ title: {
+ rendered?: string;
+ };
+ content: {
+ rendered?: string;
+ protected?: boolean;
+ };
+ excerpt?: {
+ rendered?: string;
+ };
+ author?: number;
+ comment_status?: string;
+ ping_status?: string;
+ };
+ };
+ responses: {
+ /** @description Malformed request. */
+ BadRequest: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Authentication required or credentials are invalid. */
+ Unauthorized: {
+ headers: {
+ "WWW-Authenticate"?: string;
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Caller is authenticated but lacks the capability required. */
+ Forbidden: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Resource not found. */
+ NotFound: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Rate limit exceeded. */
+ TooManyRequests: {
+ headers: {
+ "Retry-After"?: number;
+ "RateLimit-Limit"?: number;
+ "RateLimit-Remaining"?: number;
+ "RateLimit-Reset"?: number;
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ /** @description Unexpected server error. Includes a trace_id. */
+ InternalError: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ parameters: {
+ /** @description Opaque pagination cursor. */
+ Cursor: string;
+ /** @description Maximum number of items to return. */
+ Limit: number;
+ };
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+export type $defs = Record;
+export interface operations {
+ getRoot: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Identity payload. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Identity"];
+ };
+ };
+ 500: components["responses"]["InternalError"];
+ };
+ };
+ getHealthz: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Process is alive. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Health"];
+ };
+ };
+ };
+ };
+ getReadyz: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description All dependencies healthy. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Health"];
+ };
+ };
+ /** @description One or more dependencies are unhealthy. */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Health"];
+ };
+ };
+ };
+ };
+ getOpenapiJSON: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OpenAPI document. */
+ 200: {
+ headers: {
+ ETag?: string;
+ "Cache-Control"?: string;
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": Record;
+ };
+ };
+ /** @description ETag matched — caller has a fresh copy. */
+ 304: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Method not allowed. */
+ 405: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ getOpenapiYAML: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OpenAPI document, YAML serialization. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/yaml": string;
+ };
+ };
+ /** @description ETag matched. */
+ 304: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ login: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["LoginRequest"];
+ };
+ };
+ responses: {
+ /**
+ * @description Either an authenticated session was established (LoginResponse) or
+ * 2FA is required (TOTPChallenge). The client distinguishes by the
+ * shape of the payload.
+ */
+ 200: {
+ headers: {
+ /** @description Session cookie on a successful login. */
+ "Set-Cookie"?: string;
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["LoginResponse"] | components["schemas"]["TOTPChallenge"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 429: components["responses"]["TooManyRequests"];
+ 500: components["responses"]["InternalError"];
+ };
+ };
+ logout: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Session cookie cleared. */
+ 204: {
+ headers: {
+ "Set-Cookie"?: string;
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 401: components["responses"]["Unauthorized"];
+ };
+ };
+ refreshToken: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description New access token. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["AccessToken"];
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ };
+ };
+ listSessions: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Every live session for the authenticated principal. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SessionList"];
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ 500: components["responses"]["InternalError"];
+ };
+ };
+ revokeAllSessions: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description All sessions revoked. The caller's session is gone too. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 401: components["responses"]["Unauthorized"];
+ };
+ };
+ revokeSession: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description SessionView.id from listSessions. */
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Session revoked. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ sendVerificationEmail: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Send accepted. The email is dispatched asynchronously. */
+ 202: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 401: components["responses"]["Unauthorized"];
+ 429: components["responses"]["TooManyRequests"];
+ /** @description Rate limiter unavailable — fail-closed. */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ verifyEmail: {
+ parameters: {
+ query: {
+ token: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Email verified. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["VerifyResult"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 404: components["responses"]["NotFound"];
+ /** @description Token expired or already consumed. */
+ 410: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ listTokens: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description PAT metadata for the authenticated principal. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ tokens: components["schemas"]["Token"][];
+ };
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ };
+ };
+ createToken: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["TokenCreate"];
+ };
+ };
+ responses: {
+ /** @description Token minted. Secret is in the response body — store it. */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TokenWithSecret"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ };
+ };
+ revokeToken: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Token revoked. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 401: components["responses"]["Unauthorized"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ listPosts: {
+ parameters: {
+ query?: {
+ /** @description Opaque pagination cursor. */
+ cursor?: components["parameters"]["Cursor"];
+ /** @description Maximum number of items to return. */
+ limit?: components["parameters"]["Limit"];
+ status?: "draft" | "publish" | "future" | "pending" | "private" | "trash";
+ author?: string;
+ search?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description A page of posts. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["PostPage"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 500: components["responses"]["InternalError"];
+ };
+ };
+ createPost: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["PostCreate"];
+ };
+ };
+ responses: {
+ /** @description Post created. */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Post"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ };
+ };
+ getPost: {
+ parameters: {
+ query?: never;
+ header?: {
+ /** @description Password for protected posts. See HeaderPostPassword. */
+ "X-Post-Password"?: string;
+ };
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The requested post. */
+ 200: {
+ headers: {
+ ETag?: string;
+ "X-Version"?: number;
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Post"];
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ trashPost: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Post trashed. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ updatePost: {
+ parameters: {
+ query?: never;
+ header?: {
+ /** @description Optimistic concurrency token (X-Version from last read). */
+ "If-Match"?: string;
+ };
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["PostUpdate"];
+ };
+ };
+ responses: {
+ /** @description Post updated. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Post"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ /** @description Optimistic concurrency check failed. */
+ 412: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getPostAutosave: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Autosave payload. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Autosave"];
+ };
+ };
+ /** @description No autosave exists for this post. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 401: components["responses"]["Unauthorized"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ putPostAutosave: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AutosaveInput"];
+ };
+ };
+ responses: {
+ /** @description Autosave stored. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ };
+ };
+ listPages: {
+ parameters: {
+ query?: {
+ /** @description Opaque pagination cursor. */
+ cursor?: components["parameters"]["Cursor"];
+ /** @description Maximum number of items to return. */
+ limit?: components["parameters"]["Limit"];
+ status?: "draft" | "publish" | "future" | "pending" | "private" | "trash";
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description A page of pages. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["PagePage"];
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ };
+ };
+ createPage: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["PageCreate"];
+ };
+ };
+ responses: {
+ /** @description Page created. */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Page"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ };
+ };
+ getPage: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The requested page. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Page"];
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ trashPage: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Page trashed. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 401: components["responses"]["Unauthorized"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ updatePage: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["PageUpdate"];
+ };
+ };
+ responses: {
+ /** @description Page updated. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Page"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ listUsers: {
+ parameters: {
+ query?: {
+ /** @description Opaque pagination cursor. */
+ cursor?: components["parameters"]["Cursor"];
+ /** @description Maximum number of items to return. */
+ limit?: components["parameters"]["Limit"];
+ search?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description A page of users. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["UserPage"];
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ };
+ };
+ getUser: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The requested user. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["User"];
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ deleteUser: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description User deleted (soft). */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ updateUser: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UserUpdate"];
+ };
+ };
+ responses: {
+ /** @description User updated. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["User"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ listComments: {
+ parameters: {
+ query?: {
+ /** @description Opaque pagination cursor. */
+ cursor?: components["parameters"]["Cursor"];
+ /** @description Maximum number of items to return. */
+ limit?: components["parameters"]["Limit"];
+ /** @description Filter by post id. */
+ post?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description A page of comments. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["CommentPage"];
+ };
+ };
+ };
+ };
+ getComment: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The requested comment. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Comment"];
+ };
+ };
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ listTerms: {
+ parameters: {
+ query?: {
+ /** @description Opaque pagination cursor. */
+ cursor?: components["parameters"]["Cursor"];
+ /** @description Maximum number of items to return. */
+ limit?: components["parameters"]["Limit"];
+ taxonomy?: "category" | "post_tag";
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description A page of terms. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["TermPage"];
+ };
+ };
+ };
+ };
+ createTerm: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["TermCreate"];
+ };
+ };
+ responses: {
+ /** @description Term created. */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Term"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 403: components["responses"]["Forbidden"];
+ };
+ };
+ getTerm: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The requested term. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Term"];
+ };
+ };
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ deleteTerm: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Term deleted. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ updateTerm: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["TermUpdate"];
+ };
+ };
+ responses: {
+ /** @description Term updated. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Term"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ listMedia: {
+ parameters: {
+ query?: {
+ /** @description Opaque pagination cursor. */
+ cursor?: components["parameters"]["Cursor"];
+ /** @description Maximum number of items to return. */
+ limit?: components["parameters"]["Limit"];
+ /** @description Filter by MIME type (e.g. image/png). */
+ mime?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description A page of media items. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["MediaPage"];
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ };
+ };
+ uploadMedia: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "multipart/form-data": {
+ /** Format: binary */
+ file: string;
+ alt?: string;
+ caption?: string;
+ };
+ };
+ };
+ responses: {
+ /** @description Media uploaded. */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Media"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ /** @description Upload too large. */
+ 413: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ getMedia: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description The requested media item. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Media"];
+ };
+ };
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ deleteMedia: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Media deleted. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ listPlugins: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Plugin manifest list. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ plugins: components["schemas"]["Plugin"][];
+ };
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ };
+ };
+ getPlugin: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Plugin manifest. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Plugin"];
+ };
+ };
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ activatePlugin: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Plugin activated. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Plugin"];
+ };
+ };
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ /** @description Plugin already active or activation refused. */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Error"];
+ };
+ };
+ };
+ };
+ deactivatePlugin: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Plugin deactivated. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Plugin"];
+ };
+ };
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ installPluginDev: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["PluginManifest"];
+ };
+ };
+ responses: {
+ /** @description Plugin installed. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Plugin"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ /** @description Endpoint not mounted (dev mode disabled). */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ listDeadLetterJobs: {
+ parameters: {
+ query?: {
+ /** @description Maximum number of items to return. */
+ limit?: components["parameters"]["Limit"];
+ queue?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description A page of archived jobs. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ tasks: components["schemas"]["ArchivedTask"][];
+ };
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ };
+ };
+ getDeadLetterJob: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Archived task detail. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["ArchivedTask"];
+ };
+ };
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ replayDeadLetterJob: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Job re-enqueued. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ discardDeadLetterJob: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Job discarded. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ redactDeadLetterJob: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["RedactRequest"];
+ };
+ };
+ responses: {
+ /** @description Redaction applied. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 400: components["responses"]["BadRequest"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ getAdminStatus: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Operational status report. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["StatusReport"];
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ };
+ };
+ getRumPercentiles: {
+ parameters: {
+ query?: {
+ route?: string;
+ from?: string;
+ to?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description p50/p75/p95/p99 of LCP, CLS, INP per route. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["RumPercentiles"];
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ };
+ };
+ getRumSlowRoutes: {
+ parameters: {
+ query?: {
+ metric?: "lcp" | "inp" | "cls" | "fcp" | "ttfb";
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Routes ranked by the requested metric. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ routes: components["schemas"]["RumSlowRoute"][];
+ };
+ };
+ };
+ };
+ };
+ rumBeacon: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["RumBeacon"];
+ };
+ };
+ responses: {
+ /** @description Beacon accepted. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 400: components["responses"]["BadRequest"];
+ };
+ };
+ getSettings: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Settings map. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Settings"];
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ };
+ };
+ updateSettings: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["Settings"];
+ };
+ };
+ responses: {
+ /** @description Settings updated. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Settings"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ };
+ };
+ search: {
+ parameters: {
+ query: {
+ q: string;
+ type?: ("post" | "page" | "user" | "term" | "media")[];
+ /** @description Maximum number of items to return. */
+ limit?: components["parameters"]["Limit"];
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Mixed-type search results. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SearchResults"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ };
+ };
+ listWebhooks: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Webhook subscriptions for this site. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ webhooks: components["schemas"]["Webhook"][];
+ };
+ };
+ };
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ };
+ };
+ createWebhook: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["WebhookCreate"];
+ };
+ };
+ responses: {
+ /** @description Webhook subscription created. */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Webhook"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ };
+ };
+ getWebhook: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Webhook. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Webhook"];
+ };
+ };
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ deleteWebhook: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Webhook deleted. */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ updateWebhook: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["WebhookUpdate"];
+ };
+ };
+ responses: {
+ /** @description Webhook updated. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Webhook"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 404: components["responses"]["NotFound"];
+ };
+ };
+ wpListPosts: {
+ parameters: {
+ query?: {
+ page?: number;
+ per_page?: number;
+ search?: string;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description WordPress-shaped posts collection. */
+ 200: {
+ headers: {
+ "X-WP-Total"?: number;
+ "X-WP-TotalPages"?: number;
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["WPPost"][];
+ };
+ };
+ };
+ };
+ wpGetPost: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description WordPress-shaped post. */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["WPPost"];
+ };
+ };
+ 404: components["responses"]["NotFound"];
+ };
+ };
+}
diff --git a/packages/ts/api-types/src/index.ts b/packages/ts/api-types/src/index.ts
new file mode 100644
index 0000000..eab081b
--- /dev/null
+++ b/packages/ts/api-types/src/index.ts
@@ -0,0 +1,13 @@
+/**
+ * @gonext/api-types — entry point.
+ *
+ * Re-exports everything from the generated module so consumers can write:
+ *
+ * import type { paths, components } from '@gonext/api-types';
+ *
+ * The generated file is committed (see `src/generated.ts`) so consumers
+ * don't need to run a codegen step on `pnpm install`. Whenever
+ * `apps/api/openapi/openapi.yaml` changes, run `pnpm generate` here and
+ * commit the diff alongside the spec change.
+ */
+export * from './generated';
diff --git a/packages/ts/api-types/tsconfig.json b/packages/ts/api-types/tsconfig.json
new file mode 100644
index 0000000..dbcb3f7
--- /dev/null
+++ b/packages/ts/api-types/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "strict": true,
+ "noImplicitAny": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "forceConsistentCasingInFileNames": true,
+ "skipLibCheck": true,
+ "declaration": true,
+ "declarationMap": true,
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 666d50c..06bcbd9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,9 @@ importers:
apps/admin:
dependencies:
+ '@gonext/api-types':
+ specifier: workspace:*
+ version: link:../../packages/ts/api-types
'@gonext/blocks-editor':
specifier: workspace:*
version: link:../../packages/ts/blocks-editor
@@ -294,6 +297,15 @@ importers:
specifier: ^1.6.0
version: 1.6.1(@types/node@22.19.19)(jsdom@24.1.3)
+ packages/ts/api-types:
+ devDependencies:
+ openapi-typescript:
+ specifier: ^7.4.0
+ version: 7.13.0(typescript@5.9.3)
+ typescript:
+ specifier: ^5.6.0
+ version: 5.9.3
+
packages/ts/blocks-core:
dependencies:
'@gonext/blocks-rich-text':
@@ -2139,6 +2151,16 @@ packages:
'@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+ '@redocly/ajv@8.11.2':
+ resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
+
+ '@redocly/config@0.22.0':
+ resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==}
+
+ '@redocly/openapi-core@1.34.15':
+ resolution: {integrity: sha512-HAwCnNyKcs5XGQqms+9t7OdAPM/5TDstmhF+0i7tdCFato2QKuYIlyWETwkXd8c5zbltr1oB+6y9NTeQLr2d6Q==}
+ engines: {node: '>=18.17.0', npm: '>=9.5.0'}
+
'@reduxjs/toolkit@2.12.0':
resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==}
peerDependencies:
@@ -2654,6 +2676,10 @@ packages:
ajv@8.20.0:
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
+ ansi-colors@4.1.3:
+ resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
+ engines: {node: '>=6'}
+
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@@ -2789,6 +2815,9 @@ packages:
brace-expansion@1.1.14:
resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==}
+ brace-expansion@2.1.1:
+ resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==}
+
brace-expansion@5.0.6:
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
engines: {node: 18 || 20 || >=22}
@@ -2850,6 +2879,9 @@ packages:
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+ change-case@5.4.4:
+ resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
+
character-entities-html4@2.1.0:
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
@@ -2893,6 +2925,9 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ colorette@1.4.0:
+ resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
+
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@@ -3593,6 +3628,10 @@ packages:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
+ index-to-position@1.2.0:
+ resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==}
+ engines: {node: '>=18'}
+
inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
@@ -3794,6 +3833,10 @@ packages:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
+ js-levenshtein@1.1.6:
+ resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
+ engines: {node: '>=0.10.0'}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -4134,6 +4177,10 @@ packages:
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
+ minimatch@5.1.9:
+ resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
+ engines: {node: '>=10'}
+
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
@@ -4249,6 +4296,12 @@ packages:
oniguruma-to-es@2.3.0:
resolution: {integrity: sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==}
+ openapi-typescript@7.13.0:
+ resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==}
+ hasBin: true
+ peerDependencies:
+ typescript: ^5.x
+
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -4276,6 +4329,10 @@ packages:
parse-entities@4.0.2:
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
+ parse-json@8.3.0:
+ resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
+ engines: {node: '>=18'}
+
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
@@ -4332,6 +4389,10 @@ packages:
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
+ pluralize@8.0.0:
+ resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
+ engines: {node: '>=4'}
+
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@@ -4823,6 +4884,10 @@ packages:
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
+ supports-color@10.2.2:
+ resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
+ engines: {node: '>=18'}
+
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -4965,6 +5030,10 @@ packages:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
+ type-fest@4.41.0:
+ resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
+ engines: {node: '>=16'}
+
typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
engines: {node: '>= 0.4'}
@@ -5034,6 +5103,9 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
+ uri-js-replace@1.0.1:
+ resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
+
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -5231,6 +5303,13 @@ packages:
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+ yaml-ast-parser@0.0.43:
+ resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
+
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
yjs@13.6.30:
resolution: {integrity: sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
@@ -5632,7 +5711,7 @@ snapshots:
'@eslint/eslintrc@2.1.4':
dependencies:
ajv: 6.15.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
espree: 9.6.1
globals: 13.24.0
ignore: 5.3.2
@@ -5667,7 +5746,7 @@ snapshots:
'@humanwhocodes/config-array@0.13.0':
dependencies:
'@humanwhocodes/object-schema': 2.0.3
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
minimatch: 3.1.5
transitivePeerDependencies:
- supports-color
@@ -6460,6 +6539,29 @@ snapshots:
'@radix-ui/rect@1.1.1': {}
+ '@redocly/ajv@8.11.2':
+ dependencies:
+ fast-deep-equal: 3.1.3
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+ uri-js-replace: 1.0.1
+
+ '@redocly/config@0.22.0': {}
+
+ '@redocly/openapi-core@1.34.15(supports-color@10.2.2)':
+ dependencies:
+ '@redocly/ajv': 8.11.2
+ '@redocly/config': 0.22.0
+ colorette: 1.4.0
+ https-proxy-agent: 7.0.6(supports-color@10.2.2)
+ js-levenshtein: 1.1.6
+ js-yaml: 4.1.1
+ minimatch: 5.1.9
+ pluralize: 8.0.0
+ yaml-ast-parser: 0.0.43
+ transitivePeerDependencies:
+ - supports-color
+
'@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6)':
dependencies:
'@standard-schema/spec': 1.1.0
@@ -6757,7 +6859,7 @@ snapshots:
'@typescript-eslint/types': 8.59.3
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.59.3
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
eslint: 8.57.1
typescript: 5.9.3
transitivePeerDependencies:
@@ -6767,7 +6869,7 @@ snapshots:
dependencies:
'@typescript-eslint/tsconfig-utils': 8.59.3(typescript@5.9.3)
'@typescript-eslint/types': 8.59.3
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -6786,7 +6888,7 @@ snapshots:
'@typescript-eslint/types': 8.59.3
'@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.3)
'@typescript-eslint/utils': 8.59.3(eslint@8.57.1)(typescript@5.9.3)
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
eslint: 8.57.1
ts-api-utils: 2.5.0(typescript@5.9.3)
typescript: 5.9.3
@@ -6801,7 +6903,7 @@ snapshots:
'@typescript-eslint/tsconfig-utils': 8.59.3(typescript@5.9.3)
'@typescript-eslint/types': 8.59.3
'@typescript-eslint/visitor-keys': 8.59.3
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
minimatch: 10.2.5
semver: 7.8.0
tinyglobby: 0.2.16
@@ -6891,7 +6993,7 @@ snapshots:
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 0.2.3
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.6
@@ -6910,7 +7012,7 @@ snapshots:
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 0.2.3
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.6
@@ -6984,6 +7086,8 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
+ ansi-colors@4.1.3: {}
+
ansi-regex@5.0.1: {}
ansi-styles@4.3.0:
@@ -7134,6 +7238,10 @@ snapshots:
balanced-match: 1.0.2
concat-map: 0.0.1
+ brace-expansion@2.1.1:
+ dependencies:
+ balanced-match: 1.0.2
+
brace-expansion@5.0.6:
dependencies:
balanced-match: 4.0.4
@@ -7199,6 +7307,8 @@ snapshots:
chalk@5.6.2: {}
+ change-case@5.4.4: {}
+
character-entities-html4@2.1.0: {}
character-entities-legacy@3.0.0: {}
@@ -7243,6 +7353,8 @@ snapshots:
color-name@1.1.4: {}
+ colorette@1.4.0: {}
+
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
@@ -7353,9 +7465,11 @@ snapshots:
dependencies:
ms: 2.1.3
- debug@4.4.3:
+ debug@4.4.3(supports-color@10.2.2):
dependencies:
ms: 2.1.3
+ optionalDependencies:
+ supports-color: 10.2.2
decimal.js-light@2.5.1: {}
@@ -7682,8 +7796,8 @@ snapshots:
'@typescript-eslint/parser': 8.59.3(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.10
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.5(eslint@8.57.1)
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
@@ -7702,10 +7816,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
+ eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
dependencies:
'@nolyfill/is-core-module': 1.0.39
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
eslint: 8.57.1
get-tsconfig: 4.14.0
is-bun-module: 2.0.0
@@ -7713,22 +7827,22 @@ snapshots:
tinyglobby: 0.2.16
unrs-resolver: 1.11.1
optionalDependencies:
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.59.3(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.10
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -7739,7 +7853,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.10
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.3(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
hasown: 2.0.3
is-core-module: 2.16.2
is-glob: 4.0.3
@@ -7824,7 +7938,7 @@ snapshots:
ajv: 6.15.0
chalk: 4.1.2
cross-spawn: 7.0.6
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
doctrine: 3.0.0
escape-string-regexp: 4.0.0
eslint-scope: 7.2.2
@@ -8205,14 +8319,14 @@ snapshots:
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
- https-proxy-agent@7.0.6:
+ https-proxy-agent@7.0.6(supports-color@10.2.2):
dependencies:
agent-base: 7.1.4
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
@@ -8239,6 +8353,8 @@ snapshots:
indent-string@4.0.0: {}
+ index-to-position@1.2.0: {}
+
inflight@1.0.6:
dependencies:
once: 1.4.0
@@ -8423,7 +8539,7 @@ snapshots:
istanbul-lib-source-maps@5.0.6:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
@@ -8446,6 +8562,8 @@ snapshots:
joycon@3.1.1: {}
+ js-levenshtein@1.1.6: {}
+
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
@@ -8467,7 +8585,7 @@ snapshots:
form-data: 4.0.5
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
- https-proxy-agent: 7.0.6
+ https-proxy-agent: 7.0.6(supports-color@10.2.2)
is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.23
parse5: 7.3.0
@@ -9028,7 +9146,7 @@ snapshots:
micromark@4.0.2:
dependencies:
'@types/debug': 4.1.13
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
decode-named-character-reference: 1.3.0
devlop: 1.1.0
micromark-core-commonmark: 2.0.3
@@ -9070,6 +9188,10 @@ snapshots:
dependencies:
brace-expansion: 1.1.14
+ minimatch@5.1.9:
+ dependencies:
+ brace-expansion: 2.1.1
+
minimist@1.2.8: {}
mlly@1.8.2:
@@ -9196,6 +9318,16 @@ snapshots:
regex: 5.1.1
regex-recursion: 5.1.1
+ openapi-typescript@7.13.0(typescript@5.9.3):
+ dependencies:
+ '@redocly/openapi-core': 1.34.15(supports-color@10.2.2)
+ ansi-colors: 4.1.3
+ change-case: 5.4.4
+ parse-json: 8.3.0
+ supports-color: 10.2.2
+ typescript: 5.9.3
+ yargs-parser: 21.1.1
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -9237,6 +9369,12 @@ snapshots:
is-decimal: 2.0.1
is-hexadecimal: 2.0.1
+ parse-json@8.3.0:
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ index-to-position: 1.2.0
+ type-fest: 4.41.0
+
parse5@7.3.0:
dependencies:
entities: 6.0.1
@@ -9277,6 +9415,8 @@ snapshots:
mlly: 1.8.2
pathe: 2.0.3
+ pluralize@8.0.0: {}
+
possible-typed-array-names@1.1.0: {}
postcss-import@15.1.0(postcss@8.5.14):
@@ -9912,6 +10052,8 @@ snapshots:
tinyglobby: 0.2.16
ts-interface-checker: 0.1.13
+ supports-color@10.2.2: {}
+
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@@ -10037,7 +10179,7 @@ snapshots:
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
esbuild: 0.27.7
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
@@ -10073,6 +10215,8 @@ snapshots:
type-fest@0.20.2: {}
+ type-fest@4.41.0: {}
+
typed-array-buffer@1.0.3:
dependencies:
call-bound: 1.0.4
@@ -10190,6 +10334,8 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
+ uri-js-replace@1.0.1: {}
+
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
@@ -10250,7 +10396,7 @@ snapshots:
vite-node@1.6.1(@types/node@22.19.19):
dependencies:
cac: 6.7.14
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
pathe: 1.1.2
picocolors: 1.1.1
vite: 5.4.21(@types/node@22.19.19)
@@ -10293,7 +10439,7 @@ snapshots:
'@vitest/utils': 1.6.1
acorn-walk: 8.3.5
chai: 4.5.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
execa: 8.0.1
local-pkg: 0.5.1
magic-string: 0.30.21
@@ -10328,7 +10474,7 @@ snapshots:
'@vitest/utils': 1.6.1
acorn-walk: 8.3.5
chai: 4.5.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@10.2.2)
execa: 8.0.1
local-pkg: 0.5.1
magic-string: 0.30.21
@@ -10445,6 +10591,10 @@ snapshots:
xmlchars@2.2.0: {}
+ yaml-ast-parser@0.0.43: {}
+
+ yargs-parser@21.1.1: {}
+
yjs@13.6.30:
dependencies:
lib0: 0.2.117