Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/routes/api/v1/health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { createFileRoute } from "@tanstack/react-router";

import { apiSuccess, handleApiRequest } from "@/server/api/response";
import { getVersionInfo } from "@/server/api/version";
import { methodGuard } from "@/server/api/methodGuard";

export const Route = createFileRoute("/api/v1/health")({
server: {
handlers: {
handlers: methodGuard({
GET: ({ request }) =>
handleApiRequest(request, () =>
apiSuccess(request, {
Expand All @@ -14,8 +15,8 @@ export const Route = createFileRoute("/api/v1/health")({
status: "ok",
version: "v1",
versions: getVersionInfo(),
}),
})
),
},
}),
},
});
5 changes: 3 additions & 2 deletions src/routes/api/v1/openapi[.]json.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { createFileRoute } from "@tanstack/react-router";

import { openApiDocument } from "@/server/api/openapi";
import { methodGuard } from "@/server/api/methodGuard";

export const Route = createFileRoute("/api/v1/openapi.json")({
server: {
handlers: {
handlers: methodGuard({
GET: () =>
new Response(JSON.stringify(openApiDocument), {
headers: {
"cache-control": "public, max-age=300",
"content-type": "application/json; charset=utf-8",
},
}),
},
}),
},
});
5 changes: 3 additions & 2 deletions src/routes/api/v1/policies/$owner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { getApiContext } from "@/server/api/context";
import { getMailboxPolicy, setMailboxPolicy } from "@/server/api/policy-service";
import { parseJsonBody } from "@/server/api/request";
import { apiSuccess, handleApiRequest } from "@/server/api/response";
import { methodGuard } from "@/server/api/methodGuard";

export const Route = createFileRoute("/api/v1/policies/$owner")({
server: {
handlers: {
handlers: methodGuard({
GET: ({ request, params }) =>
handleApiRequest(request, async () => {
const owner = stellarAddressSchema.parse(params.owner);
Expand All @@ -28,6 +29,6 @@ export const Route = createFileRoute("/api/v1/policies/$owner")({
const result = await setMailboxPolicy((await getApiContext()).repository, owner, policy);
return apiSuccess(request, result);
}),
},
}),
},
});
1 change: 1 addition & 0 deletions src/routes/api/v1/policies/$owner/senders/$sender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { senderRuleSchema, stellarAddressSchema } from "@/server/api/domain";
import { getSenderRule, setSenderRule } from "@/server/api/policy-service";
import { parseJsonBody } from "@/server/api/request";
import { apiSuccess, handleApiRequest } from "@/server/api/response";
import { methodGuard } from "@/server/api/methodGuard";

const ruleBodySchema = z.object({ rule: senderRuleSchema.exclude(["default"]) });

Expand Down
5 changes: 3 additions & 2 deletions src/routes/api/v1/policies/evaluate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { stellarAddressSchema, stroopAmountSchema } from "@/server/api/domain";
import { evaluateMailboxPolicy } from "@/server/api/policy-service";
import { parseJsonBody } from "@/server/api/request";
import { apiSuccess, handleApiRequest } from "@/server/api/response";
import { methodGuard } from "@/server/api/methodGuard";

const evaluationSchema = z.object({
owner: stellarAddressSchema,
Expand All @@ -16,7 +17,7 @@ const evaluationSchema = z.object({

export const Route = createFileRoute("/api/v1/policies/evaluate")({
server: {
handlers: {
handlers: methodGuard({
POST: ({ request }) =>
handleApiRequest(request, async () => {
const input = await parseJsonBody(request, evaluationSchema);
Expand All @@ -39,6 +40,6 @@ export const Route = createFileRoute("/api/v1/policies/evaluate")({

return apiSuccess(request, decision);
}),
},
}),
},
});
1 change: 1 addition & 0 deletions src/routes/api/v1/postage/$messageId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getApiContext } from "@/server/api/context";
import { hash32Schema } from "@/server/api/domain";
import { assertPostageParticipant, getPostage } from "@/server/api/postage-service";
import { apiSuccess, handleApiRequest } from "@/server/api/response";
import { methodGuard } from "@/server/api/methodGuard";

export const Route = createFileRoute("/api/v1/postage/$messageId")({
server: {
Expand Down
20 changes: 18 additions & 2 deletions src/server/api/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ registerRecordSchema("postage", postageSchema);
registerRecordSchema("receipt", receiptSchema);
registerRecordSchema("idempotencyRecord", idempotencyRecordSchema);

import { AsyncLocalStorage } from "node:async_hooks";

export interface RequestContext {
requestId: string;
method?: string;
route?: string;
}

export const requestContextStorage = new AsyncLocalStorage<RequestContext>();

export function getRequestContext(): RequestContext | undefined {
return requestContextStorage.getStore();
}

interface ApiContext {
repository: ApiRepository;
}
Expand Down Expand Up @@ -65,7 +79,7 @@ export function validateApiConfig(config: ApiConfig): void {

export async function getApiContext(): Promise<ApiContext> {
if (!import.meta.env.PROD) {
globalApi.__stealthApiRepository ??= new MemoryApiRepository();
globalApi.__stealthApiRepository ??= new ValidatedApiRepository(new MemoryApiRepository());
return { repository: globalApi.__stealthApiRepository };
}

Expand Down Expand Up @@ -95,7 +109,9 @@ export async function getApiContext(): Promise<ApiContext> {
}

const { HybridApiRepository } = await import("./kv-repository");
const repo = new HybridApiRepository(env.STEALTH_KV, env.STEALTH_COORDINATOR);
const repo = new ValidatedApiRepository(
new HybridApiRepository(env.STEALTH_KV, env.STEALTH_COORDINATOR),
);
globalApi.__stealthApiRepository = repo;
return { repository: repo };
}
42 changes: 41 additions & 1 deletion src/server/api/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,30 @@ export function normalizeValidationError(error: ZodError): ValidationErrorDetail
};
}

export function normalizeApiError(error: unknown): ApiError {
export interface UnexpectedErrorContext {
requestId?: string;
routeId?: string;
}

export type UnexpectedErrorReporter = (
error: unknown,
context: UnexpectedErrorContext,
) => void | Promise<void>;

let activeReporter: UnexpectedErrorReporter | null = null;

export function registerErrorReporter(reporter: UnexpectedErrorReporter): void {
activeReporter = reporter;
}

export function getErrorReporter(): UnexpectedErrorReporter | null {
return activeReporter;
}

export function normalizeApiError(
error: unknown,
context?: UnexpectedErrorContext,
): ApiError {
if (error instanceof ApiError) return error;

if (error instanceof ZodError) {
Expand All @@ -255,5 +278,22 @@ export function normalizeApiError(error: unknown): ApiError {
);
}

if (activeReporter) {
try {
const result = activeReporter(error, {
requestId: context?.requestId,
routeId: context?.routeId,
});
if (result instanceof Promise) {
result.catch((reporterError) => {
console.error("Async error reporter failure:", reporterError);
});
}
} catch (reporterError) {
console.error("Sync error reporter failure:", reporterError);
}
}

return new ApiError(500, "internal_error", "An unexpected server error occurred");
}

Loading
Loading