refactor: enforce enum machine codes #18
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 11s
Frontend verify / verify (push) Successful in 9m31s

Use owner enums for known codes; preserve wire values and unknown fallbacks.

Verified npm ci and verify: 379 tests, 5 lint checks, app and Storybook builds.
This commit was merged in pull request #41.
This commit is contained in:
2026-09-28 16:36:40 +03:00
committed by ksv741
parent 12c3b72bfb
commit 5b69003516
26 changed files with 248 additions and 79 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ import createQueryClient from "openapi-react-query";
import type { paths } from "./generated/schema";
import { apiErrorCode } from "./problem-details";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
const HTTP_UNAUTHORIZED = 401;
@@ -101,7 +101,7 @@ async function fetchCurrent(
catch {
return response;
}
if (apiErrorCode(problem) !== "UNAUTHORIZED") return response;
if (!isApiErrorCode(apiErrorCode(problem), ApiErrorCode.Unauthorized)) return response;
if (!isCurrent()) throw new DOMException("Session changed", "AbortError");
if (!(await sessionState.handlers.refresh())) {
if (isCurrent()) sessionState.handlers.clear();
+1 -1
View File
@@ -9,7 +9,7 @@ export {
invalidateApiRequests,
} from "./client";
export type { paths } from "./generated/schema";
export { apiErrorCode } from "./problem-details";
export { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
export {
invalidateResourceAccess,
+13
View File
@@ -0,0 +1,13 @@
import { expect, it } from "vitest";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
it("matches known codes and preserves unknown wire codes for fallback handling", () => {
expect(isApiErrorCode(apiErrorCode({ code: "UNAUTHORIZED" }), ApiErrorCode.Unauthorized)).toBe(true);
const unknownCode = apiErrorCode({ code: "FUTURE_PROVIDER_ERROR" });
expect(unknownCode).toBe("FUTURE_PROVIDER_ERROR");
expect(isApiErrorCode(unknownCode, ApiErrorCode.Unauthorized)).toBe(false);
expect(isApiErrorCode(undefined, ApiErrorCode.Unauthorized)).toBe(false);
expect(apiErrorCode(null)).toBeUndefined();
expect(apiErrorCode({})).toBeUndefined();
});
+24 -1
View File
@@ -1,7 +1,30 @@
enum ApiErrorCode {
BankDependentRefunds = "BANK_DEPENDENT_REFUNDS",
BankIntentConflict = "BANK_INTENT_CONFLICT",
BankNotFound = "BANK_NOT_FOUND",
BankRefundLimit = "BANK_REFUND_LIMIT",
BankVersionConflict = "BANK_VERSION_CONFLICT",
BankWriteConflict = "BANK_WRITE_CONFLICT",
BondTickerAmbiguous = "BOND_TICKER_AMBIGUOUS",
Forbidden = "FORBIDDEN",
InvalidOperationsCursor = "INVALID_OPERATIONS_CURSOR",
InvalidTbankToken = "INVALID_TBANK_TOKEN",
NotFound = "NOT_FOUND",
StockTickerAmbiguous = "STOCK_TICKER_AMBIGUOUS",
TbankNotConnected = "TBANK_NOT_CONNECTED",
TbankUnavailable = "TBANK_UNAVAILABLE",
Unauthorized = "UNAUTHORIZED",
}
function apiErrorCode(error: unknown): string | undefined {
return typeof error === "object" && error !== null && "code" in error
? String(error.code)
: undefined;
}
export { apiErrorCode };
// Incoming codes are an open wire string; unknown values retain their fallback.
function isApiErrorCode(value: unknown, expected: ApiErrorCode): boolean {
return value === expected;
}
export { ApiErrorCode, apiErrorCode, isApiErrorCode };
+8 -7
View File
@@ -10,7 +10,7 @@ import type { components } from "./generated/schema";
import { useAuthenticatedQuery } from "./authenticated-query";
import { getAuthenticatedSessionSnapshot } from "./client";
import { apiErrorCode } from "./problem-details";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
const accessEpochs = new WeakMap<QueryClient, Map<string, number>>();
@@ -23,13 +23,14 @@ function invalidateResourceAccess(
group: string,
error: unknown,
) {
const errorCode = apiErrorCode(error);
if (
![
"FORBIDDEN",
"INVALID_TBANK_TOKEN",
"NOT_FOUND",
"TBANK_NOT_CONNECTED",
].includes(apiErrorCode(error) ?? "")
[
ApiErrorCode.Forbidden,
ApiErrorCode.InvalidTbankToken,
ApiErrorCode.NotFound,
ApiErrorCode.TbankNotConnected,
].every(code => !isApiErrorCode(errorCode, code))
)
return;
const epochs = accessEpochs.get(client) ?? new Map<string, number>();