From c687e44e2cba64a6f1634bcc400e4208bd366634 Mon Sep 17 00:00:00 2001 From: Nascimento95 Date: Wed, 21 Oct 2026 14:34:44 +0700 Subject: [PATCH 1/2] chore(shared): add the link_expired and account_required error codes Co-Authored-By: Claude Sonnet 5.5 --- packages/shared/src/schemas/error.test.ts | 34 +++++++++++++++++++++++ packages/shared/src/schemas/error.ts | 4 +++ 2 files changed, 38 insertions(+) create mode 100644 packages/shared/src/schemas/error.test.ts diff --git a/packages/shared/src/schemas/error.test.ts b/packages/shared/src/schemas/error.test.ts new file mode 100644 index 00000000..ff7329e3 --- /dev/null +++ b/packages/shared/src/schemas/error.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import { ApiError, apiErrorCodes } from './error'; + +/** The stable codes of the wiki API › Conventions, with `internal_error` of an unexpected 500. */ +const wikiCodes = [ + 'validation_error', + 'unauthorized', + 'forbidden', + 'not_found', + 'conflict', + 'rate_limited', + 'coherence_blocked', + 'link_expired', + 'premium_required', + 'route_removed', + 'account_required', + 'internal_error', +]; + +describe('apiErrorCodes', () => { + it('holds the stable codes of the wiki, `internal_error`, and no other', () => { + expect([...apiErrorCodes].toSorted()).toEqual(wikiCodes.toSorted()); + }); +}); + +describe('ApiError', () => { + it.each(['link_expired', 'account_required'])('accepts the code %s', (code) => { + expect(ApiError.parse({ code, message: 'Refused' })).toEqual({ code, message: 'Refused' }); + }); + + it('refuses a code the wiki does not list', () => { + expect(ApiError.safeParse({ code: 'link_gone', message: 'Refused' }).success).toBe(false); + }); +}); diff --git a/packages/shared/src/schemas/error.ts b/packages/shared/src/schemas/error.ts index 94db069a..f3d3ad38 100644 --- a/packages/shared/src/schemas/error.ts +++ b/packages/shared/src/schemas/error.ts @@ -9,6 +9,8 @@ export const apiErrorCodes = [ 'conflict', 'rate_limited', 'coherence_blocked', + /** 410 of a confirmation link that expired, was already used or was cancelled (D-062). */ + 'link_expired', 'premium_required', /** 410 of a withdrawn route, with `details.removedAt` (`RouteRemovedDetails`). */ 'route_removed', @@ -20,6 +22,8 @@ export const apiErrorCodes = [ 'account_suspended', /** 409 on a moderation decision: another member of the team decided first (D-087). */ 'already_decided', + /** 403 of a route that needs an account, called with the token of an anonymous account (D-099). */ + 'account_required', 'internal_error', ] as const; From fe282c4aa026c9b05b5028eee6e510ae78d22317 Mon Sep 17 00:00:00 2001 From: Paul Thiberville Date: Wed, 21 Oct 2026 19:28:01 +0200 Subject: [PATCH 2/2] test(shared): compare the error codes to those of the admin as well Co-Authored-By: Claude Opus 5.5 --- packages/shared/src/schemas/error.test.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/schemas/error.test.ts b/packages/shared/src/schemas/error.test.ts index ff7329e3..9ad79318 100644 --- a/packages/shared/src/schemas/error.test.ts +++ b/packages/shared/src/schemas/error.test.ts @@ -17,9 +17,20 @@ const wikiCodes = [ 'internal_error', ]; +/** + * The codes of the admin (wiki Admin-Web), not yet cited by API › Conventions: until that page + * lists them (#618), the test compares to both lists. + */ +const adminCodes = [ + 'mfa_required', + 'recent_sign_in_required', + 'account_suspended', + 'already_decided', +]; + describe('apiErrorCodes', () => { - it('holds the stable codes of the wiki, `internal_error`, and no other', () => { - expect([...apiErrorCodes].toSorted()).toEqual(wikiCodes.toSorted()); + it('holds the stable codes of the wiki, those of the admin, `internal_error`, and no other', () => { + expect([...apiErrorCodes].toSorted()).toEqual([...wikiCodes, ...adminCodes].toSorted()); }); });