From 8459c169c32c7f4b00a5b040ad5223155696b3f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 20:01:17 +0000 Subject: [PATCH 1/3] fix(rest): converge the record-sharing family onto the ADR-0112 D5 envelope (#8111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `registerSharingEndpoints` answered two retired dialects across its nine refusal arms: `respond501` was flat `{ code, message }`, and the five mapped verdicts (400/403/404/409/422) plus the three verb-specific 500s were `{ code, error: '' }`. `body.error.code` — the one position ADR-0112 D5 declares — read `undefined` on all nine. Every arm now emits through the shared `sendError` from `@objectstack/types` (imported here as `sendEnvelopeError`; this module has a local `sendError` of its own for thrown errors), so the family agrees by construction rather than by nine literals that happen to match. No status code moves and no code VALUE changes. The 409 arm's `CONFLICT` was registered in neither `StandardErrorCode` nor `ERROR_CODE_LEDGER` — so `ApiErrorSchema`, whose `code` is a closed enum, would have rejected that body — and is now registered under `@objectstack/rest`, keeping the emitted value byte-identical. Renaming it onto `RESOURCE_CONFLICT` would change what clients read and is filed separately. The `CODE:` message prefix stays: censused as a server-internal service→REST derivation, stripped before the response is written, never on the wire. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V --- .changeset/sharing-envelope-convergence.md | 39 ++ .../client/src/shares-envelope-compat.test.ts | 187 ++++++++++ packages/rest/src/rest-server.ts | 66 +++- packages/rest/src/rest.test.ts | 13 +- packages/rest/src/sharing-envelope.test.ts | 340 ++++++++++++++++++ .../spec/src/api/error-code-ledger.zod.ts | 10 + scripts/check-route-envelope.mjs | 17 +- 7 files changed, 658 insertions(+), 14 deletions(-) create mode 100644 .changeset/sharing-envelope-convergence.md create mode 100644 packages/client/src/shares-envelope-compat.test.ts create mode 100644 packages/rest/src/sharing-envelope.test.ts diff --git a/.changeset/sharing-envelope-convergence.md b/.changeset/sharing-envelope-convergence.md new file mode 100644 index 0000000000..64021f1686 --- /dev/null +++ b/.changeset/sharing-envelope-convergence.md @@ -0,0 +1,39 @@ +--- +"@objectstack/rest": patch +"@objectstack/spec": patch +--- + +fix(rest): one error envelope across the record-sharing family (#8111) + +`registerSharingEndpoints` — `GET/POST /data/:object/:id/shares` and +`DELETE /data/:object/:id/shares/:shareId` — answered two retired dialects across +its nine refusal arms: the 501 was flat `{ code, message }`, and the five mapped +verdicts (400 / 403 / 404 / 409 / 422) plus the three verb-specific 500s were +`{ code, error: 'a bare string' }`. So `body.error.code`, the one position +ADR-0112 D5 declares, read `undefined` on all nine — while the adjacent +`/security` registrars, converged in #7981 and #8073, already answered the +declared shape. + +Every arm now emits `{ success: false, error: { code, message } }` through the +shared `sendError` from `@objectstack/types` — the same builder every conformant +route module writes through — so the family agrees by construction rather than by +nine literals happening to match. + +No status code moves and no code VALUE changes. One code did change status in the +REGISTRY rather than on the wire: the 409 arm's `CONFLICT` was registered in +neither `StandardErrorCode` nor `ERROR_CODE_LEDGER`, so `ApiErrorSchema` — whose +`code` is a closed enum — would have rejected that body. It is now registered +under `@objectstack/rest`, keeping the emitted value byte-identical; consolidating +it onto the standard catalog's `RESOURCE_CONFLICT` would change what clients read +and is filed separately for the maintainer. + +The `CODE:` message prefix the service uses to signal its verdict is untouched: it +is a server-internal service→REST derivation, stripped before the response is +written and never present on the wire, so no consumer can read it (censused at +claim). `ObjectStackClient` reads both envelopes' declared spots +(`errorBody?.code ?? errorBody?.error?.code`, and a bare-string limb for the +message), so `client.shares.list()`, `.grant()` and `.revoke()` keep throwing +identical `err.code`, `err.message`, `err.httpStatus`, `err.category`, +`err.retryable` and `err.fields` — re-measured against these call paths rather +than inherited from #7981 or #8073. `err.details` does change on every refusal: +its last fallback is the whole response body, and the body is what moved. diff --git a/packages/client/src/shares-envelope-compat.test.ts b/packages/client/src/shares-envelope-compat.test.ts new file mode 100644 index 0000000000..0ff1609f11 --- /dev/null +++ b/packages/client/src/shares-envelope-compat.test.ts @@ -0,0 +1,187 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#8111] SDK compatibility across the record-sharing envelope convergence. + * + * `registerSharingEndpoints` moved its nine refusal arms from two retired + * dialects onto the ADR-0112 D5 envelope. This measures what a caller of + * `client.shares.*` actually observes across that move — re-run against THESE + * consumers rather than inherited from #7981 or #8073, because a compatibility + * claim taken on one call path is not evidence about another. + * + * The method: drive the REAL `ObjectStackClient` against a stubbed transport + * answering the OLD body and then the NEW body for each arm, and compare every + * property the client attaches to the thrown error. `client.shares.*` goes + * through `ObjectStackClient.fetch`, whose error path reads BOTH envelopes' + * declared spots on purpose (`errorBody?.code ?? errorBody?.error?.code`, plus + * a bare-string limb for the message), which is what makes the move safe. + * + * ## The measured result + * + * `code`, `message`, `httpStatus`, `category`, `retryable` and `fields` are + * IDENTICAL on every arm. `details` CHANGES on every arm — its last fallback + * is the whole response body (`errorBody?.details ?? errorBody?.error?.details + * ?? errorBody`), and the body is exactly what this card reshapes. Neither + * envelope carries a `details`, so both fall through to that last limb; the + * value differs because the body differs. + * + * That is discharged by census, not by assertion: no in-repo consumer reads + * `err.details` off a `shares.*` call, and `details` is documented as + * unstructured debugging context. It is pinned here rather than left implicit + * so the next reader sees the one property that moved and why it was accepted. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { ObjectStackClient } from './index'; + +/** A client whose transport answers exactly one refusal body. */ +function clientAnswering(status: number, body: any) { + const fetchMock = vi.fn().mockResolvedValue({ + ok: false, + status, + statusText: 'Error', + json: async () => body, + headers: new Headers(), + }); + return new ObjectStackClient({ baseUrl: 'http://localhost:3000', fetch: fetchMock }); +} + +/** Every property `ObjectStackClient.fetch` attaches to the error it throws. */ +function observed(err: any) { + return { + message: err.message, + code: err.code, + httpStatus: err.httpStatus, + category: err.category, + retryable: err.retryable, + fields: err.fields, + details: err.details, + }; +} + +async function refusalFrom(status: number, body: any, call: (c: ObjectStackClient) => Promise) { + const client = clientAnswering(status, body); + try { + await call(client); + throw new Error('expected the call to reject, but it resolved'); + } catch (err: any) { + if (err?.httpStatus === undefined) throw err; + return observed(err); + } +} + +const LIST = (c: ObjectStackClient) => c.shares.list('lead', 'rec1'); +const GRANT = (c: ObjectStackClient) => c.shares.grant('lead', 'rec1', { + recipientType: 'user', recipientId: 'u1', accessLevel: 'read', +}); +const REVOKE = (c: ObjectStackClient) => c.shares.revoke('lead', 'rec1', 'sh1'); + +/** + * Every arm, in both dialects. + * + * `old` is what the arm emitted before this card — the flat `{ code, message }` + * for the 501 and `{ code, error: '' }` for the rest. `next` is + * what the shared `sendError` writes now. + */ +const ARMS: Array<{ + label: string; + status: number; + code: string; + message: string; + old: any; + call: (c: ObjectStackClient) => Promise; +}> = [ + { + label: '501 NOT_IMPLEMENTED', + status: 501, code: 'NOT_IMPLEMENTED', + message: 'Sharing service is not configured on this deployment', + old: { code: 'NOT_IMPLEMENTED', message: 'Sharing service is not configured on this deployment' }, + call: LIST, + }, + { + label: '400 VALIDATION_FAILED', + status: 400, code: 'VALIDATION_FAILED', message: 'recipientId is required', + old: { code: 'VALIDATION_FAILED', error: 'recipientId is required' }, + call: GRANT, + }, + { + label: '403 PERMISSION_DENIED', + status: 403, code: 'PERMISSION_DENIED', message: 'you do not hold canManageShares on this record', + old: { code: 'PERMISSION_DENIED', error: 'you do not hold canManageShares on this record' }, + call: LIST, + }, + { + label: '404 NOT_FOUND', + status: 404, code: 'NOT_FOUND', message: 'record lead/rec1 does not exist', + old: { code: 'NOT_FOUND', error: 'record lead/rec1 does not exist' }, + call: LIST, + }, + { + label: '409 CONFLICT', + status: 409, code: 'CONFLICT', message: "share sh1 is materialised by source 'rule'", + old: { code: 'CONFLICT', error: "share sh1 is materialised by source 'rule'" }, + call: REVOKE, + }, + { + label: '422 SHARING_NOT_ENABLED', + status: 422, code: 'SHARING_NOT_ENABLED', message: "'lead' bypasses record sharing", + old: { code: 'SHARING_NOT_ENABLED', error: "'lead' bypasses record sharing" }, + call: GRANT, + }, + { + label: '500 SHARE_GRANT_FAILED', + status: 500, code: 'SHARE_GRANT_FAILED', message: 'boom', + old: { code: 'SHARE_GRANT_FAILED', error: 'boom' }, + call: GRANT, + }, +]; + +const nextBody = (code: string, message: string) => ({ success: false, error: { code, message } }); + +describe('[#8111] client.shares.* observes the same error across the envelope move', () => { + for (const arm of ARMS) { + it(`${arm.label} — code, message, httpStatus, category, retryable, fields unchanged`, async () => { + const before = await refusalFrom(arm.status, arm.old, arm.call); + const after = await refusalFrom(arm.status, nextBody(arm.code, arm.message), arm.call); + + // The properties a caller branches on are identical, arm by arm. + expect(after.code).toBe(before.code); + expect(after.code).toBe(arm.code); + expect(after.message).toBe(before.message); + expect(after.message).toBe(arm.message); + expect(after.httpStatus).toBe(before.httpStatus); + expect(after.category).toBe(before.category); + expect(after.retryable).toBe(before.retryable); + expect(after.fields).toBe(before.fields); + }); + } + + it('`details` is the ONE property that moves — measured, not assumed', async () => { + // Both envelopes fall through to the same last limb (`?? errorBody`), + // because neither carries a `details`. So `err.details` is the whole + // response body in both cases — and the body is what this card + // reshapes. Discharged by census: nothing in-repo reads `err.details` + // off a `shares.*` call. + const arm = ARMS[1]; + const before = await refusalFrom(arm.status, arm.old, arm.call); + const after = await refusalFrom(arm.status, nextBody(arm.code, arm.message), arm.call); + + expect(before.details).toEqual({ code: 'VALIDATION_FAILED', error: 'recipientId is required' }); + expect(after.details).toEqual({ + success: false, + error: { code: 'VALIDATION_FAILED', message: 'recipientId is required' }, + }); + expect(after.details).not.toEqual(before.details); + }); + + it('the NEW envelope alone satisfies the whole read surface — no dialect straddle', async () => { + // Read on its own terms rather than only relative to the old body: a + // caller on a converged server gets the pair, not a half-populated error. + for (const arm of ARMS) { + const after = await refusalFrom(arm.status, nextBody(arm.code, arm.message), arm.call); + expect(after.code, `${arm.label} lost its code`).toBe(arm.code); + expect(after.message, `${arm.label} lost its message`).toBe(arm.message); + expect(after.httpStatus, `${arm.label} lost its status`).toBe(arm.status); + } + }); +}); diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 17fda035aa..6c39932666 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -9459,16 +9459,55 @@ export class RestServer { try { return await this.sharingServiceProvider(environmentId); } catch { return undefined; } }; - const respond501 = (res: any) => res.status(501).json({ - code: 'NOT_IMPLEMENTED', - message: 'Sharing service is not configured on this deployment', - }); + /** + * [#8111] The ONE refusal emitter for the record-sharing family — the + * 501, all five mapped verdicts and all three 500s go through it, so + * "list, grant and revoke answer the same shape" is a property of the + * code rather than of nine literals that happen to agree. + * + * Before this, the family carried BOTH dialects ADR-0112 D5 retires: + * `respond501` was flat `{ code, message }` and every other arm was + * `{ code, error: '' }`, so `body.error.code` — the one + * position D5 declares — read `undefined` on all nine. #7035 + * (PR #7293) had already removed both from this file's `/meta` + * refusals, #7981 (PR #8071) from `registerSecurityEndpoints` and + * #8073 (PR #8174) from the `/security/explain` pair. + * + * Emitted through the SHARED builder (`sendError` from + * `@objectstack/types`, imported as `sendEnvelopeError` because this + * module has a local `sendError` of its own — the sanitizing responder + * for THROWN errors, a different thing). That is what makes this the + * reference shape by construction rather than a tenth local literal + * agreeing with the nine it replaced, and it types `code` to the + * closed ADR-0112 vocabulary for free. + * + * ⛔ Status codes are untouched and no code VALUE moves: only the + * POSITION of `code` and `message` changes. + */ + const respondError = ( + res: any, + status: number, + code: ErrorCode, + message: string, + ): void => sendEnvelopeError(res, status, code, message); + + const respond501 = (res: any) => respondError( + res, 501, 'NOT_IMPLEMENTED', + 'Sharing service is not configured on this deployment', + ); // [ADR-0111] The service enforces authorization (D1/D4/D5/D7) and // signals the verdict via message prefixes, the plugin's established // error idiom — this maps them onto HTTP. Returns true when handled. + // + // [#8111] The prefix is a SERVER-INTERNAL service→REST derivation: it + // is stripped below and never reaches the wire, so no consumer can + // read it (censused at claim — the only in-repo `startsWith(CODE)` + // readers are this file's own route mappings plus one + // `plugin-approvals` check on an error it threw itself in-process). + // It therefore stays exactly as it is; only the response SHAPE moved. const respondSharingError = (res: any, error: any): boolean => { const msg = String(error?.message ?? error ?? ''); - const map: Array<[string, number]> = [ + const map: Array<[ErrorCode, number]> = [ ['VALIDATION_FAILED', 400], ['PERMISSION_DENIED', 403], ['NOT_FOUND', 404], @@ -9477,10 +9516,10 @@ export class RestServer { ]; for (const [code, status] of map) { if (msg.startsWith(code)) { - res.status(status).json({ - code, - error: msg.replace(new RegExp(`^${code}:\\s*`), ''), - }); + respondError( + res, status, code, + msg.replace(new RegExp(`^${code}:\\s*`), ''), + ); return true; } } @@ -9504,7 +9543,10 @@ export class RestServer { } catch (error: any) { if (respondSharingError(res, error)) return; logError('[REST] List shares error:', error); - res.status(500).json({ code: 'SHARES_LIST_FAILED', error: String(error?.message ?? error).slice(0, 500) }); + // The 500 arms keep their 500-char cap: an unexpected + // fault's message is not a contract, and truncating it + // stays a sanitization step — only the position moves. + respondError(res, 500, 'SHARES_LIST_FAILED', String(error?.message ?? error).slice(0, 500)); } }, metadata: { summary: 'List per-record sharing grants', tags: ['sharing'] }, @@ -9538,7 +9580,7 @@ export class RestServer { } catch (error: any) { if (respondSharingError(res, error)) return; logError('[REST] Grant share error:', error); - res.status(500).json({ code: 'SHARE_GRANT_FAILED', error: String(error?.message ?? error).slice(0, 500) }); + respondError(res, 500, 'SHARE_GRANT_FAILED', String(error?.message ?? error).slice(0, 500)); } }, metadata: { summary: 'Grant a per-record share to a principal', tags: ['sharing'] }, @@ -9567,7 +9609,7 @@ export class RestServer { } catch (error: any) { if (respondSharingError(res, error)) return; logError('[REST] Revoke share error:', error); - res.status(500).json({ code: 'SHARE_REVOKE_FAILED', error: String(error?.message ?? error).slice(0, 500) }); + respondError(res, 500, 'SHARE_REVOKE_FAILED', String(error?.message ?? error).slice(0, 500)); } }, metadata: { summary: 'Revoke a per-record share by id', tags: ['sharing'] }, diff --git a/packages/rest/src/rest.test.ts b/packages/rest/src/rest.test.ts index 106955c032..272ab2e6fe 100644 --- a/packages/rest/src/rest.test.ts +++ b/packages/rest/src/rest.test.ts @@ -1506,7 +1506,18 @@ describe('RestServer', () => { const res = { json: vi.fn(), status: vi.fn().mockReturnThis() }; await route!.handler({ params: { object: 'account', id: 'a1' }, body: {} } as any, res as any); expect(res.status).toHaveBeenCalledWith(400); - expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: 'VALIDATION_FAILED' })); + // [#8111] Migrated from the retired FLAT position to the ADR-0112 D5 one. + // Same code, same 400 — `body.error.code` is where it is now declared. + // This arm carried the OTHER retired dialect too (`{ code, error: '' }`), so its message now lives at `body.error.message` rather + // than being `body.error` itself; asserting the code position alone does + // not catch a bare string sitting where the object belongs. + expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ + error: expect.objectContaining({ + code: 'VALIDATION_FAILED', + message: 'recipientId is required', + }), + })); }); it('DELETE revokes and returns 204', async () => { diff --git a/packages/rest/src/sharing-envelope.test.ts b/packages/rest/src/sharing-envelope.test.ts new file mode 100644 index 0000000000..7b0ebbd79f --- /dev/null +++ b/packages/rest/src/sharing-envelope.test.ts @@ -0,0 +1,340 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#8111] ONE error envelope across the record-sharing family (ADR-0112 D5). + * + * ## What was wrong + * + * `registerSharingEndpoints` — `GET/POST /data/:object/:id/shares` and + * `DELETE /data/:object/:id/shares/:shareId` — answered TWO retired dialects + * across its nine refusal arms, both of which #7035 (PR #7293) had already + * removed from this file's `/meta` refusals, #7981 (PR #8071) from + * `registerSecurityEndpoints` and #8073 (PR #8174) from the explain pair: + * + * | arms | shape | + * | :-------------------------------------- | :--------------------------------------- | + * | 501 `NOT_IMPLEMENTED` (`respond501`) | `{ code, message }` — flat | + * | 400/403/404/409/422 (`respondSharingError`) | `{ code, error: '' }` | + * | three 500s (list/grant/revoke) | `{ code, error: '' }` | + * + * So `body.error.code` — the one position ADR-0112 D5 declares — read + * `undefined` on all nine. + * + * ## The `CODE:` message prefix is NOT the wire contract + * + * `respondSharingError` recovers the verdict by parsing the service's message + * text (`msg.startsWith(CODE)`) and then STRIPS that prefix before answering. + * The prefix is a server-internal service→REST derivation that never reaches + * the wire, so no consumer can read it — censused at claim. It is therefore + * untouched here: this card moved the response SHAPE only. `stripsThePrefix` + * below pins that the stripping still happens, so a future reader does not + * "restore" a prefix the wire never carried. + * + * ## What these cases assert, and why not `toThrow` + * + * These handlers *send*; they never throw. A `rejects.toThrow()`-shaped + * assertion would report "the promise resolved" and could not separate + * "refused with the wrong envelope" from "did not refuse at all" — and the + * wrong envelope IS the defect. So every case asserts the ADR-0112 **pair**, + * HTTP `status` AND nested `body.error.code`, plus both retired dialects' + * absence: no top-level `code` sibling, and `error` an object rather than a + * bare string. + * + * ## The cross-arm pin is DERIVED + * + * Nine hand-written literal expectations that agree today is how this defect + * started — each arm was individually defensible and nobody compared them. So + * `shapeOf()` reduces a body to its structural skeleton (key paths + value + * types) and the family case asserts every arm reduces to the SAME skeleton + * without naming what that skeleton is. A third dialect added to any arm fails + * there even if someone also adds a matching literal case. + * + * ## What does NOT move + * + * No status code, and no code VALUE. `NOT_IMPLEMENTED` and `PERMISSION_DENIED` + * are `StandardErrorCode`; `VALIDATION_FAILED`, `NOT_FOUND`, `SHARES_LIST_FAILED`, + * `SHARE_GRANT_FAILED` and `SHARE_REVOKE_FAILED` are in `ERROR_CODE_LEDGER`'s + * `@objectstack/rest` block; `SHARING_NOT_ENABLED` in `@objectstack/plugin-sharing`'s + * (the union is flat). `CONFLICT` was registered by this card — it was the one + * code this emitter has always put on the wire while being declared NOWHERE, + * so `ApiErrorSchema` would have rejected the 409 body. Registering the + * existing value keeps the wire byte-identical; renaming it onto the standard + * catalog's `RESOURCE_CONFLICT` would change what clients read and is filed + * separately for the maintainer. + */ + +import { describe, it, expect, vi } from 'vitest'; +// `.js` on purpose — NodeNext resolution requires the extension, and this +// package's TEST_DEBT ceiling has no margin for another TS2835 (#7248). +import { RestServer } from './rest-server.js'; +import { ApiErrorSchema } from '@objectstack/spec/api'; + +const LIST = '/api/v1/data/:object/:id/shares'; +const REVOKE = '/api/v1/data/:object/:id/shares/:shareId'; + +function mockServer() { + return { + get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn(), patch: vi.fn(), + use: vi.fn(), + listen: vi.fn().mockResolvedValue(undefined), + close: vi.fn().mockResolvedValue(undefined), + }; +} + +function mockRes() { + const res: any = { + statusCode: 200, + json: vi.fn(function (this: any, body: any) { this._body = body; return this; }), + send: vi.fn(function (this: any) { return this; }), + end: vi.fn(function (this: any) { return this; }), + setHeader: vi.fn(function (this: any) { return this; }), + status: vi.fn(function (this: any, code: number) { this.statusCode = code; return this; }), + header: vi.fn(function (this: any) { return this; }), + }; + return res; +} + +type Answer = { status: number; body: any }; + +/** + * @param service what `sharingServiceProvider` resolves to. `undefined` leaves + * the provider unset, which is the 501 arm. + */ +function boot(service?: any) { + const rest = new RestServer( + mockServer() as any, + { getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: {} }) } as any, + { api: { requireAuth: false } } as any, + undefined, undefined, undefined, undefined, undefined, + undefined, + service === undefined ? undefined : (async () => service) as any, + ); + (rest as any).resolveExecCtx = async () => ({ userId: 'u_admin' }); + rest.registerRoutes(); + + const route = (method: string, path: string) => { + const found = (rest as any).getRoutes().find( + (r: any) => r.method === method && r.path === path, + ); + if (!found) throw new Error(`route not registered: ${method} ${path}`); + return found; + }; + + const drive = async ( + method: string, + path: string, + req: Record = {}, + ): Promise => { + const res = mockRes(); + await route(method, path).handler( + { + method, path, headers: {}, query: {}, body: {}, + params: { object: 'account', id: 'a1', shareId: 'shr_X' }, + ...req, + } as any, + res, + ); + return { status: res.statusCode, body: res.json.mock.calls.at(-1)?.[0] }; + }; + + return { + list: () => drive('GET', LIST), + grant: (body: Record = {}) => drive('POST', LIST, { body }), + revoke: () => drive('DELETE', REVOKE), + }; +} + +/** A service whose every verb rejects with `err`. */ +function throwingService(err: unknown) { + return { + listShares: vi.fn().mockRejectedValue(err), + grant: vi.fn().mockRejectedValue(err), + revoke: vi.fn().mockRejectedValue(err), + }; +} + +/** The prefixed messages the real `sharing-service` throws (verbatim idiom). */ +const prefixed = (code: string, rest: string) => new Error(`${code}: ${rest}`); + +/** + * The full ADR-0112 assertion for one refusal: the PAIR (status + code) at the + * nested position, and both retired dialects absent. + */ +function expectNestedEnvelope(answer: Answer, status: number, code: string) { + expect( + answer.status, + `expected ${status}, got ${answer.status} with body ${JSON.stringify(answer.body)}`, + ).toBe(status); + // The pair ADR-0112 D5 declares — nested, because the flat position is the defect. + expect(answer.body?.error?.code).toBe(code); + expect(typeof answer.body?.error?.message).toBe('string'); + // Dialect 1 retired: `code` as a sibling of `error` (all nine arms had it). + expect(answer.body).not.toHaveProperty('code'); + // Dialect 2 retired: `error` as a bare string, which is what made + // `error.code` and `error.message` both read `undefined`. + expect(typeof answer.body?.error).toBe('object'); +} + +/** + * A body reduced to its STRUCTURE: every leaf key path with the type of its + * value, sorted. Values are dropped on purpose — arms legitimately differ in + * code and message, and the claim under test is that they agree in shape. + */ +function shapeOf(body: unknown): string { + const walk = (node: unknown, prefix: string): string[] => { + if (node === null || typeof node !== 'object' || Array.isArray(node)) { + return [`${prefix}:${Array.isArray(node) ? 'array' : node === null ? 'null' : typeof node}`]; + } + return Object.entries(node as Record) + .flatMap(([k, v]) => walk(v, prefix ? `${prefix}.${k}` : k)); + }; + return walk(body, '').sort().join('|'); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 1. Each arm, on its own terms — status AND nested code, per ADR-0112 +// ───────────────────────────────────────────────────────────────────────────── + +describe('[#8111] the record-sharing family answers the ADR-0112 D5 envelope', () => { + it('501 NOT_IMPLEMENTED — no sharing service provider is wired', async () => { + const api = boot(undefined); + for (const answer of [await api.list(), await api.grant(), await api.revoke()]) { + expectNestedEnvelope(answer, 501, 'NOT_IMPLEMENTED'); + } + }); + + it('400 VALIDATION_FAILED — the service refuses the grant input', async () => { + const api = boot(throwingService(prefixed('VALIDATION_FAILED', 'recipientId is required'))); + const answer = await api.grant(); + expectNestedEnvelope(answer, 400, 'VALIDATION_FAILED'); + expect(answer.body.error.message).toBe('recipientId is required'); + }); + + it('403 PERMISSION_DENIED — the caller does not manage the record (ADR-0111 D1)', async () => { + const api = boot(throwingService( + prefixed('PERMISSION_DENIED', 'you do not hold canManageShares on this record'), + )); + expectNestedEnvelope(await api.list(), 403, 'PERMISSION_DENIED'); + }); + + it('404 NOT_FOUND — the record is missing or invisible (indistinguishable by design)', async () => { + const api = boot(throwingService(prefixed('NOT_FOUND', 'record account/a1 does not exist'))); + expectNestedEnvelope(await api.list(), 404, 'NOT_FOUND'); + }); + + it('409 CONFLICT — revoke on a rule-materialised share (ADR-0111 D4)', async () => { + const api = boot(throwingService( + prefixed('CONFLICT', "share shr_X is materialised by source 'rule' and would be re-granted"), + )); + const answer = await api.revoke(); + expectNestedEnvelope(answer, 409, 'CONFLICT'); + expect(answer.body.error.message).toBe( + "share shr_X is materialised by source 'rule' and would be re-granted", + ); + }); + + it('422 SHARING_NOT_ENABLED — grant on an object the gates never consult', async () => { + const api = boot(throwingService( + prefixed('SHARING_NOT_ENABLED', "'account' bypasses record sharing"), + )); + expectNestedEnvelope(await api.grant(), 422, 'SHARING_NOT_ENABLED'); + }); + + it('500 — an unexpected fault on each verb keeps its own code', async () => { + const api = boot(throwingService(new Error('boom'))); + const cases: Array<[Answer, string]> = [ + [await api.list(), 'SHARES_LIST_FAILED'], + [await api.grant(), 'SHARE_GRANT_FAILED'], + [await api.revoke(), 'SHARE_REVOKE_FAILED'], + ]; + for (const [answer, code] of cases) { + expectNestedEnvelope(answer, 500, code); + // The bare-string dialect put this text at `body.error`; it is the + // declared `message` now, and the 500-char cap is kept. + expect(answer.body.error.message).toBe('boom'); + } + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// 2. The cross-arm claim, derived rather than restated +// ───────────────────────────────────────────────────────────────────────────── + +describe('[#8111] the whole sharing family reduces to ONE skeleton', () => { + it('every refusal arm of all three routes has the same structural shape', async () => { + const arms: Array<[string, Answer]> = [ + ['501 list', await boot(undefined).list()], + ['501 revoke', await boot(undefined).revoke()], + ['400 grant', await boot(throwingService(prefixed('VALIDATION_FAILED', 'x'))).grant()], + ['403 list', await boot(throwingService(prefixed('PERMISSION_DENIED', 'x'))).list()], + ['404 list', await boot(throwingService(prefixed('NOT_FOUND', 'x'))).list()], + ['409 revoke', await boot(throwingService(prefixed('CONFLICT', 'x'))).revoke()], + ['422 grant', await boot(throwingService(prefixed('SHARING_NOT_ENABLED', 'x'))).grant()], + ['500 list', await boot(throwingService(new Error('x'))).list()], + ['500 grant', await boot(throwingService(new Error('x'))).grant()], + ['500 revoke', await boot(throwingService(new Error('x'))).revoke()], + ]; + const [, reference] = arms[0]; + for (const [label, answer] of arms) { + expect(shapeOf(answer.body), `${label} drifted: ${JSON.stringify(answer.body)}`) + .toBe(shapeOf(reference.body)); + } + }); + + it('every arm parses as the DECLARED ApiErrorSchema, code vocabulary included', async () => { + const arms: Answer[] = [ + await boot(undefined).list(), + await boot(throwingService(prefixed('VALIDATION_FAILED', 'x'))).grant(), + await boot(throwingService(prefixed('PERMISSION_DENIED', 'x'))).list(), + await boot(throwingService(prefixed('NOT_FOUND', 'x'))).list(), + // The 409's `CONFLICT` was registered NOWHERE before this card, so + // this parse is what would have caught it: `ApiErrorSchema.code` is + // a closed enum and an unregistered code fails it. + await boot(throwingService(prefixed('CONFLICT', 'x'))).revoke(), + await boot(throwingService(prefixed('SHARING_NOT_ENABLED', 'x'))).grant(), + await boot(throwingService(new Error('x'))).grant(), + ]; + for (const answer of arms) { + const parsed = ApiErrorSchema.safeParse(answer.body.error); + expect( + parsed.success, + `body.error failed ApiErrorSchema: ${JSON.stringify(answer.body)} → ${ + parsed.success ? '' : JSON.stringify(parsed.error.issues)}`, + ).toBe(true); + } + }); + + it('stripsThePrefix — the `CODE:` derivation stays server-internal', async () => { + // The prefix is how the service signals the verdict to the route. It is + // stripped before answering and has never been on the wire, so nothing + // can read it. Pinned so a later reader neither "restores" it nor + // assumes the message is the raw throw. + const api = boot(throwingService(prefixed('NOT_FOUND', 'record account/a1 does not exist'))); + const answer = await api.list(); + expect(answer.body.error.message).toBe('record account/a1 does not exist'); + expect(answer.body.error.message.startsWith('NOT_FOUND')).toBe(false); + }); + + it('an unprefixed service error falls through to the 500 arm, not a verdict', async () => { + // `respondSharingError` returns false when no prefix matches — the + // fall-through that keeps an unexpected fault from being reported as a + // permission verdict. + const api = boot(throwingService(new Error('connection reset'))); + const answer = await api.list(); + expectNestedEnvelope(answer, 500, 'SHARES_LIST_FAILED'); + expect(answer.body.error.message).toBe('connection reset'); + }); + + it('the healthy paths are untouched — only refusals moved', async () => { + const api = boot({ + listShares: vi.fn().mockResolvedValue([{ id: 'shr_1', recipient_id: 'bob' }]), + grant: vi.fn(async (input: any) => ({ id: 'shr_2', ...input })), + revoke: vi.fn().mockResolvedValue(undefined), + }); + const listed = await api.list(); + expect(listed.body).toEqual({ data: [{ id: 'shr_1', recipient_id: 'bob' }] }); + const granted = await api.grant({ recipientId: 'bob', accessLevel: 'edit' }); + expect(granted.status).toBe(201); + expect(granted.body).toEqual(expect.objectContaining({ id: 'shr_2', recipientId: 'bob' })); + }); +}); diff --git a/packages/spec/src/api/error-code-ledger.zod.ts b/packages/spec/src/api/error-code-ledger.zod.ts index e5112adfe3..a28551e313 100644 --- a/packages/spec/src/api/error-code-ledger.zod.ts +++ b/packages/spec/src/api/error-code-ledger.zod.ts @@ -102,6 +102,16 @@ export const ERROR_CODE_LEDGER = { 'BATCH_UNRESOLVED_REF', 'BLANK_MATCH_KEY', 'CONCURRENT_UPDATE', + // [#8111] `respondSharingError`'s 409 arm — `revoke` on a rule-materialised + // share (`source != 'manual'`), thrown by plugin-sharing's `sharing-service` + // and documented at `content/docs/kernel/runtime-services/sharing-service.mdx`. + // REGISTERED, not renamed: this is the value the arm has always put on the + // wire, and #8111 converged its POSITION only. Consolidating it onto the + // standard catalog's `RESOURCE_CONFLICT` would change what clients read, so + // it is a deliberate wire change for the maintainer, filed separately — + // exactly the shape this block's existing generic synonyms (`NOT_FOUND`, + // `FORBIDDEN`, `INTERNAL`) already carry. + 'CONFLICT', 'CONFLICTING_MAPPING', 'DATASET_INVALID', 'DELEGABLE_SCOPE_FAILED', diff --git a/scripts/check-route-envelope.mjs b/scripts/check-route-envelope.mjs index 76dee8072f..6c59510da8 100644 --- a/scripts/check-route-envelope.mjs +++ b/scripts/check-route-envelope.mjs @@ -258,7 +258,22 @@ const MODULES = { // COMPUTED message (`msg.slice(0, 500)`), which that counter cannot see — // the six flat `{ code, message }` arms converted alongside them were never // counted by either dialect, having no `error` key at all. - siblingCode: 73, + // + // 73 → 69 (#8111): `registerSharingEndpoints` — the record-sharing family — + // followed. Its four `{ code, error }` sites (the shared `respondSharingError` + // literal feeding 400/403/404/409/422, plus the three verb-specific 500s + // `SHARES_LIST_FAILED` / `SHARE_GRANT_FAILED` / `SHARE_REVOKE_FAILED`) now + // emit through the shared `sendError` from `@objectstack/types`. + // Measured: merge-base (2473cd2d3) siblingCode=73, branch head=69; the four + // vanished sites are merge-base lines 9480, 9507, 9541, 9570, all inside + // that one function, and the head has NO site left in that range while all + // 69 survivors map 1:1 onto a head line by the edit's own line shift (0 + // before the emitter, +37 after it). `stringError` + // is unmoved at 44 by construction: all four arms carried a COMPUTED message + // (`msg.replace(…)`, `String(…).slice(0, 500)`), which that counter cannot + // see — and `respond501`, converted alongside them, was never counted by + // either dialect, having no `error` key at all. + siblingCode: 69, }, }; From 73eb5716b158a4511b5a8417172e61c618ba278c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 20:45:42 +0000 Subject: [PATCH 2/3] docs(spec): regenerate api references for the CONFLICT ledger entry (#8111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registering `CONFLICT` in `ERROR_CODE_LEDGER` grows `ErrorCode` — the union `ApiErrorSchema.code` parses against — by exactly one member, and every generated reference page that renders an `ApiError`-shaped field prints that enum as a truncated summary with a "+N more" count. So one ledger row moves 134 counter lines across 11 pages, plus the two pages that list the vocabulary in full gain a `CONFLICT` bullet. Generated output only (`gen:schema` + `gen:docs`), never hand-edited. Measured as a genuine cascade, not absorbed drift: a pristine `origin/main` tree regenerates completely clean under the identical commands, and every one of the 136 changed lines here is either the `+264 more` → `+265 more` counter (134) or a `CONFLICT` bullet (2) — nothing else. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V --- content/docs/references/api/analytics.mdx | 6 +-- content/docs/references/api/auth.mdx | 4 +- .../docs/references/api/automation-api.mdx | 18 ++++----- content/docs/references/api/batch.mdx | 4 +- content/docs/references/api/contract.mdx | 15 ++++---- .../docs/references/api/error-code-ledger.mdx | 1 + content/docs/references/api/export.mdx | 12 +++--- content/docs/references/api/metadata.mdx | 38 +++++++++---------- content/docs/references/api/package-api.mdx | 16 ++++---- content/docs/references/api/protocol.mdx | 6 +-- content/docs/references/api/storage.mdx | 16 ++++---- 11 files changed, 69 insertions(+), 67 deletions(-) diff --git a/content/docs/references/api/analytics.mdx b/content/docs/references/api/analytics.mdx index 9180f81f1c..2917e8cf66 100644 --- a/content/docs/references/api/analytics.mdx +++ b/content/docs/references/api/analytics.mdx @@ -44,7 +44,7 @@ const result = AnalyticsEndpoint.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; title?: string; measures: object[]; dimensions: object[] }[]` | ✅ | Available cubes, each as the `CubeMeta` discovery projection — the cube name, its title, and the measures/dimensions a client may name in a query. A bare array: there is no `cubes` wrapper object, and no cube `sql` is published. | @@ -79,7 +79,7 @@ const result = AnalyticsEndpoint.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ rows: Record[]; fields: object[]; sql?: string }` | ✅ | | @@ -93,7 +93,7 @@ const result = AnalyticsEndpoint.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ sql: string; params: any[] }` | ✅ | | diff --git a/content/docs/references/api/auth.mdx b/content/docs/references/api/auth.mdx index 2c2009660d..604206b6e2 100644 --- a/content/docs/references/api/auth.mdx +++ b/content/docs/references/api/auth.mdx @@ -117,7 +117,7 @@ const result = AuthProvider.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ session: object; user: object; token?: string }` | ✅ | | @@ -153,7 +153,7 @@ const result = AuthProvider.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ id: string; email: string; emailVerified: boolean; name: string; … }` | ✅ | | diff --git a/content/docs/references/api/automation-api.mdx b/content/docs/references/api/automation-api.mdx index 5a9735be46..7b61d4bb75 100644 --- a/content/docs/references/api/automation-api.mdx +++ b/content/docs/references/api/automation-api.mdx @@ -119,7 +119,7 @@ const result = AutomationApiErrorCode.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` | ✅ | The created flow definition | @@ -144,7 +144,7 @@ const result = AutomationApiErrorCode.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; deleted: boolean }` | ✅ | | @@ -187,7 +187,7 @@ const result = AutomationApiErrorCode.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` | ✅ | Full flow definition | @@ -213,7 +213,7 @@ const result = AutomationApiErrorCode.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ id: string; flowName: string; flowVersion?: integer; status: Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| … +2 more>; … }` | ✅ | Full execution log with step details | @@ -241,7 +241,7 @@ const result = AutomationApiErrorCode.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ flows: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | | @@ -269,7 +269,7 @@ const result = AutomationApiErrorCode.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ runs: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | | @@ -295,7 +295,7 @@ const result = AutomationApiErrorCode.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; enabled: boolean }` | ✅ | | @@ -325,7 +325,7 @@ const result = AutomationApiErrorCode.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ success: boolean; output?: any; error?: string; durationMs?: number }` | ✅ | | @@ -351,7 +351,7 @@ const result = AutomationApiErrorCode.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; label: string; description?: string; successMessage?: string; … }` | ✅ | The updated flow definition | diff --git a/content/docs/references/api/batch.mdx b/content/docs/references/api/batch.mdx index 7b24fda07b..6b65527425 100644 --- a/content/docs/references/api/batch.mdx +++ b/content/docs/references/api/batch.mdx @@ -55,7 +55,7 @@ const result = BatchConfigSchema.parse(data); | :--- | :--- | :--- | :--- | | **id** | `string` | optional | Record ID if operation succeeded | | **success** | `boolean` | ✅ | Whether this record was processed successfully | -| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }[]` | optional | Array of errors if operation failed. Branch on `errors[0].code` — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error (#4793). A NON-atomic batch that stopped (the `continueOnError: false` default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back (#7539). | +| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }[]` | optional | Array of errors if operation failed. Branch on `errors[0].code` — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error (#4793). A NON-atomic batch that stopped (the `continueOnError: false` default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back (#7539). | | **data** | `Record` | optional | Full record data (if returnRecords=true) | | **index** | `number` | optional | Index of the record in the request array | | **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]` | optional | Write-observability (#3407/#3431/#3455): caller-supplied fields LEGALLY stripped from THIS row before it was written — static `readonly` (#2948) / TRUE `readonlyWhen` (#3042) on update, or the #3043 create-ingress strip. Per-row because a batch can drop different fields on different rows (`readonlyWhen` is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit `X-ObjectStack-Dropped-Fields` for batches. Optional — omit-when-empty keeps the shape backward-compatible. | @@ -122,7 +122,7 @@ const result = BatchConfigSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **operation** | `Enum<'create' \| 'update' \| 'upsert' \| 'delete'>` | optional | Operation type that was performed | | **total** | `number` | ✅ | Total number of records in the batch | diff --git a/content/docs/references/api/contract.mdx b/content/docs/references/api/contract.mdx index 35e9ccfd40..4d1ae7b773 100644 --- a/content/docs/references/api/contract.mdx +++ b/content/docs/references/api/contract.mdx @@ -27,7 +27,7 @@ const result = ApiErrorSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +260 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +261 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) | | **message** | `string` | ✅ | Readable error message | | **category** | `string` | optional | Error category (e.g. validation, authorization) | | **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | @@ -115,6 +115,7 @@ const result = ApiErrorSchema.parse(data); * `CLOUD_UNCONFIGURED` * `COMMIT_NOT_FOUND` * `CONCURRENT_UPDATE` +* `CONFLICT` * `CONFLICTING_MAPPING` * `CONNECTOR_UPSTREAM_UNAVAILABLE` * `CREATE_FAILED` @@ -314,7 +315,7 @@ const result = ApiErrorSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | @@ -353,7 +354,7 @@ const result = ApiErrorSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ id?: string; success: boolean; errors?: object[]; index?: number; … }[]` | ✅ | Results for each item in the batch | @@ -395,7 +396,7 @@ const result = ApiErrorSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **id** | `string` | ✅ | ID of the deleted record | @@ -447,7 +448,7 @@ const result = ApiErrorSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `Record[]` | ✅ | Array of matching records | | **pagination** | `{ total?: number; limit?: number; offset?: number; cursor?: string; … }` | ✅ | Pagination info | @@ -463,7 +464,7 @@ const result = ApiErrorSchema.parse(data); | :--- | :--- | :--- | :--- | | **id** | `string` | optional | Record ID if processed | | **success** | `boolean` | ✅ | | -| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }[]` | optional | | +| **errors** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }[]` | optional | | | **index** | `number` | optional | Index in original request | | **data** | `any` | optional | Result data (e.g. created record) | @@ -502,7 +503,7 @@ Key-value map of record data | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `Record` | ✅ | The requested or modified record | diff --git a/content/docs/references/api/error-code-ledger.mdx b/content/docs/references/api/error-code-ledger.mdx index 3e233ebee2..0fab9ffe8a 100644 --- a/content/docs/references/api/error-code-ledger.mdx +++ b/content/docs/references/api/error-code-ledger.mdx @@ -187,6 +187,7 @@ const result = ErrorCode.parse(data); * `CLOUD_UNCONFIGURED` * `COMMIT_NOT_FOUND` * `CONCURRENT_UPDATE` +* `CONFLICT` * `CONFLICTING_MAPPING` * `CONNECTOR_UPSTREAM_UNAVAILABLE` * `CREATE_FAILED` diff --git a/content/docs/references/api/export.mdx b/content/docs/references/api/export.mdx index 184c910581..9b491b688f 100644 --- a/content/docs/references/api/export.mdx +++ b/content/docs/references/api/export.mdx @@ -57,7 +57,7 @@ const result = CreateExportJobRequestSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ jobId: string; status: Enum<'pending' \| 'processing' \| 'completed' \| 'failed' \| 'cancelled' \| 'expired'>; estimatedRecords?: integer; createdAt: string }` | ✅ | | @@ -157,7 +157,7 @@ const result = CreateExportJobRequestSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ jobId: string; status: Enum<'pending' \| 'processing' \| 'completed' \| 'failed' \| 'cancelled' \| 'expired'>; format: Enum<'csv' \| 'json' \| 'jsonl' \| 'xlsx' \| 'parquet'>; totalRecords?: integer; … }` | ✅ | | @@ -231,7 +231,7 @@ const result = CreateExportJobRequestSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ jobId: string; downloadUrl: string; fileName: string; fileSize: integer; … }` | ✅ | | @@ -449,7 +449,7 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ totalRecords: integer; validRecords: integer; invalidRecords: integer; duplicateRecords: integer; … }` | ✅ | | @@ -488,7 +488,7 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ jobs: object[]; nextCursor?: string; hasMore: boolean }` | ✅ | | @@ -546,7 +546,7 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ id: string; name: string; enabled: boolean; nextRunAt?: string; … }` | ✅ | | diff --git a/content/docs/references/api/metadata.mdx b/content/docs/references/api/metadata.mdx index 0d22a7363c..2f7000e6b8 100644 --- a/content/docs/references/api/metadata.mdx +++ b/content/docs/references/api/metadata.mdx @@ -51,7 +51,7 @@ const result = AppDefinitionResponseSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; label: string \| Record; description?: string \| Record; icon?: string; … }` | ✅ | Full App Configuration | @@ -65,7 +65,7 @@ const result = AppDefinitionResponseSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; label: string; icon?: string; description?: string }[]` | ✅ | List of available concepts (Objects, Apps, Flows) | @@ -92,7 +92,7 @@ const result = AppDefinitionResponseSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ total: integer; succeeded: integer; failed: integer; errors?: object[] }` | ✅ | Bulk operation result | @@ -117,7 +117,7 @@ const result = AppDefinitionResponseSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ type: string; name: string }` | ✅ | | @@ -131,7 +131,7 @@ const result = AppDefinitionResponseSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ sourceType: string; sourceName: string; targetType: string; targetName: string; … }[]` | ✅ | Items this item depends on | @@ -145,7 +145,7 @@ const result = AppDefinitionResponseSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ sourceType: string; sourceName: string; targetType: string; targetName: string; … }[]` | ✅ | Items that depend on this item | @@ -159,7 +159,7 @@ const result = AppDefinitionResponseSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `Record` | optional | Effective metadata with all overlays applied | @@ -173,7 +173,7 @@ const result = AppDefinitionResponseSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ exists: boolean }` | ✅ | | @@ -200,7 +200,7 @@ const result = AppDefinitionResponseSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `any` | ✅ | Exported metadata bundle | @@ -228,7 +228,7 @@ const result = AppDefinitionResponseSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ total: integer; imported: integer; skipped: integer; failed: integer; … }` | ✅ | Import result | @@ -242,7 +242,7 @@ const result = AppDefinitionResponseSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ type: string; name: string; definition: Record }` | ✅ | Metadata item | @@ -256,7 +256,7 @@ const result = AppDefinitionResponseSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `Record[]` | ✅ | Array of metadata definitions | @@ -270,7 +270,7 @@ const result = AppDefinitionResponseSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `string[]` | ✅ | Array of metadata item names | @@ -284,7 +284,7 @@ const result = AppDefinitionResponseSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ id: string; baseType: string; baseName: string; packageId?: string; … }` | optional | Overlay definition, undefined if none | @@ -348,7 +348,7 @@ Metadata query with filtering, sorting, and pagination | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ items: object[]; total: integer; page: integer; pageSize: integer }` | ✅ | Paginated query result | @@ -406,7 +406,7 @@ Metadata query with filtering, sorting, and pagination | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ type: string; label: string; description?: string; filePatterns: string[]; … }` | optional | Type info | @@ -420,7 +420,7 @@ Metadata query with filtering, sorting, and pagination | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `string[]` | ✅ | Registered metadata type identifiers | @@ -446,7 +446,7 @@ Metadata query with filtering, sorting, and pagination | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ valid: boolean; errors?: object[]; warnings?: object[] }` | ✅ | Validation result | @@ -460,7 +460,7 @@ Metadata query with filtering, sorting, and pagination | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ name: string; label?: string; pluralLabel?: string; description?: string; … }` | ✅ | Full Object Schema | diff --git a/content/docs/references/api/package-api.mdx b/content/docs/references/api/package-api.mdx index f286874750..eca93192ca 100644 --- a/content/docs/references/api/package-api.mdx +++ b/content/docs/references/api/package-api.mdx @@ -57,7 +57,7 @@ Get installed package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Installed package details | @@ -89,7 +89,7 @@ List installed packages response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ packages: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | | @@ -143,7 +143,7 @@ Install package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ package: object; dependencyResolution?: object; namespaceConflicts?: object[]; message?: string }` | ✅ | | @@ -185,7 +185,7 @@ Rollback package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ success: boolean; restoredVersion?: string; message?: string }` | ✅ | | @@ -220,7 +220,7 @@ Upgrade package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ success: boolean; phase: string; plan?: object; snapshotId?: string; … }` | ✅ | | @@ -250,7 +250,7 @@ Resolve dependencies response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ dependencies: object[]; canProceed: boolean; requiredActions: object[]; installOrder: string[]; … }` | ✅ | Dependency resolution result with topological sort | @@ -277,7 +277,7 @@ Uninstall package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ packageId: string; success: boolean; message?: string }` | ✅ | | @@ -309,7 +309,7 @@ Upload artifact response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ success: boolean; artifactRef?: object; submissionId?: string; message?: string }` | ✅ | | diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 46ad75b26b..e475128b5a 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -280,7 +280,7 @@ const result = AiAgentCapabilitiesSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **operation** | `Enum<'create' \| 'update' \| 'upsert' \| 'delete'>` | optional | Operation type that was performed | | **total** | `number` | ✅ | Total number of records in the batch | @@ -428,7 +428,7 @@ const result = AiAgentCapabilitiesSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **operation** | `Enum<'create' \| 'update' \| 'upsert' \| 'delete'>` | optional | Operation type that was performed | | **total** | `number` | ✅ | Total number of records in the batch | @@ -1528,7 +1528,7 @@ Uninstall package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **operation** | `Enum<'create' \| 'update' \| 'upsert' \| 'delete'>` | optional | Operation type that was performed | | **total** | `number` | ✅ | Total number of records in the batch | diff --git a/content/docs/references/api/storage.mdx b/content/docs/references/api/storage.mdx index 0b6b35d111..1f605cccfc 100644 --- a/content/docs/references/api/storage.mdx +++ b/content/docs/references/api/storage.mdx @@ -46,7 +46,7 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ fileId: string; key: string; size: integer; mimeType: string; … }` | ✅ | | @@ -72,7 +72,7 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ url: string }` | ✅ | | @@ -101,7 +101,7 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ path: string; name: string; size: integer; mimeType: string; … }` | ✅ | Uploaded file metadata | @@ -147,7 +147,7 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ uploadId: string; resumeToken: string; fileId: string; totalChunks: integer; … }` | ✅ | | @@ -161,7 +161,7 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ uploadUrl: string; downloadUrl?: string; fileId: string; method: Enum<'PUT' \| 'POST'>; … }` | ✅ | | @@ -175,7 +175,7 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ key: string }` | ✅ | | @@ -202,7 +202,7 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ chunkIndex: integer; eTag: string; bytesReceived: integer }` | ✅ | | @@ -216,7 +216,7 @@ const result = CompleteChunkedUploadRequestSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **success** | `boolean` | ✅ | Operation success status | -| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +264 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | +| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +265 more>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | | **data** | `{ uploadId: string; fileId: string; filename: string; totalSize: integer; … }` | ✅ | | From a23bfbed6b81a9ff9f7e5f9f4587f8313bcc2848 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 21:45:45 +0000 Subject: [PATCH 3/3] test(qa): migrate #8209's share-grant dogfood pin to the ADR-0112 D5 position (#8111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `federated-phantom-share-grant.dogfood.test.ts` (added by #8209 / PR #8209 for #8119) asserts the `/data/:object/:id/shares` refusal envelope. It was written against the FLAT dialect — `body.code` / `body.error` as a bare string — because that is what the route emitted when it landed. This PR converges that family onto `{ error: { code, message } }`, so the pin reads `undefined` and fails. Migrated, not loosened: the assertions move to `body.error.code` and `body.error.message` and assert the D5 position ALONE. Accepting either shape would re-admit the dialect this card retires. The CONTROL case's negative assertion moves too. It did not fail — a negative assertion on the vacated flat position passes for free (`undefined !== 'SHARING_NOT_ENABLED'`) — which is exactly why it had to move: left alone it would have gone on "passing" while reading a key no response carries any more. Why the queue caught this and PR CI did not: the merge queue runs the FULL suite, PR-side CI only the affected subset, and neither PR alone is red — #8209 was green before this convergence existed and this branch was green before #8209's pin existed. The interaction is only visible composed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01B3Kurx8qufrDzNjk4rag7V --- ...erated-phantom-share-grant.dogfood.test.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/qa/dogfood/test/federated-phantom-share-grant.dogfood.test.ts b/packages/qa/dogfood/test/federated-phantom-share-grant.dogfood.test.ts index e41e36379a..308b21dbee 100644 --- a/packages/qa/dogfood/test/federated-phantom-share-grant.dogfood.test.ts +++ b/packages/qa/dogfood/test/federated-phantom-share-grant.dogfood.test.ts @@ -307,11 +307,17 @@ describe('[#8119] federated phantom anchor: single-record gates + share posture' { recipientId: adminId, accessLevel: 'edit' }, ); expect(res.status).toBe(422); - const body = await res.json() as { code?: string; error?: string }; - expect(body.code).toBe('SHARING_NOT_ENABLED'); + // [#8111] MIGRATED, not loosened: this route family converged onto the + // ADR-0112 D5 envelope, so the pair lives at `body.error.{code,message}`. + // It was written against the retired dialect (`{ code, error: '' }`) + // because that is what the route emitted when #8119 landed. Asserting the + // D5 position ALONE is deliberate — accepting both shapes would re-admit + // the dialect this convergence retired. + const body = await res.json() as { error?: { code?: string; message?: string } }; + expect(body.error?.code).toBe('SHARING_NOT_ENABLED'); // The operator-facing half: "no owner_id field" would be false here and // would send them to add a column the platform already injected. - expect(body.error).toMatch(/federated/); + expect(body.error?.message).toMatch(/federated/); }); it('CONTROL: a LOCAL private record is NOT refused by the posture guard', async () => { @@ -339,7 +345,13 @@ describe('[#8119] federated phantom anchor: single-record gates + share posture' { recipientId: 'usr_grantee_8119', accessLevel: 'edit' }, ); expect(res.status).not.toBe(422); - expect((await res.json() as { code?: string }).code).not.toBe('SHARING_NOT_ENABLED'); + // [#8111] Migrated with its sibling above. This one did not FAIL after the + // envelope moved — a negative assertion on the vacated flat position + // passes for free (`undefined !== 'SHARING_NOT_ENABLED'`) — which is + // exactly why it had to move too: left alone it would have gone on + // "passing" while reading a key no response carries any more. + expect((await res.json() as { error?: { code?: string } }).error?.code) + .not.toBe('SHARING_NOT_ENABLED'); }); it('CONTROL: the grandfathered shipped object still refuses as PUBLIC, unchanged', async () => {