-
Notifications
You must be signed in to change notification settings - Fork 61
Expand file tree
/
Copy patherrors.ts
More file actions
188 lines (177 loc) · 6.23 KB
/
Copy patherrors.ts
File metadata and controls
188 lines (177 loc) · 6.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
export type ErrorKind =
| "no-wallet"
| "wrong-chain"
| "user-rejected"
| "expired"
| "duplicate-nonce"
| "invalid-signature"
| "rate-limited"
| "ai-timeout"
| "ai-unavailable"
| "verifier-timeout"
| "verifier-unavailable"
| "network"
| "unknown";
export type ClassifiedError = {
kind: ErrorKind;
title: string;
message: string;
detail?: string;
recoverable: boolean;
};
export type ErrorContext = {
status?: number;
bodyText?: string;
};
const COPY: Record<ErrorKind, { title: string; message: string }> = {
"no-wallet": {
title: "No wallet detected",
message: "Install MetaMask, Rabby, or Coinbase Wallet, then refresh this page.",
},
"wrong-chain": {
title: "Wrong network",
message:
"Your wallet isn't on the expected network. Use the switch button in the wallet widget to fix it.",
},
"user-rejected": {
title: "You cancelled the signature",
message: "No payment was sent. Try again whenever you're ready.",
},
expired: {
title: "Payment context expired",
message: "The signed challenge took too long to return. Retry to get a fresh one.",
},
"duplicate-nonce": {
title: "Signature already used",
message: "This payment context was already submitted. Each challenge can only be used once. Retry to get a fresh one.",
},
"invalid-signature": {
title: "Signature was rejected",
message:
"The verifier didn't accept the signature. Double-check your wallet account hasn't changed mid-flow, then retry.",
},
"rate-limited": {
title: "Rate limited",
message: "Too many requests recently. Wait a moment and try again.",
},
"ai-timeout": {
title: "AI provider timed out",
message:
"Payment was verified, but the AI provider didn't respond in time. Your wallet wasn't charged. Retry.",
},
"ai-unavailable": {
title: "AI provider unavailable",
message: "The upstream model is down right now. Try again in a minute.",
},
"verifier-timeout": {
title: "Verifier timed out",
message:
"The signature verifier didn't respond in time. Your signature wasn't accepted — no payment occurred. Retry.",
},
"verifier-unavailable": {
title: "Verifier unavailable",
message:
"The signature verifier is down. Your signature wasn't accepted — no payment occurred. Retry in a moment.",
},
network: {
title: "Network error",
message: "Couldn't reach the gateway. Check your connection and retry.",
},
unknown: {
title: "Something broke",
message: "An unexpected error happened. Retry — and if it persists, check the console.",
},
};
function build(kind: ErrorKind, detail?: string): ClassifiedError {
return {
kind,
...COPY[kind],
detail: sanitizeDetail(detail),
recoverable: kind !== "no-wallet",
};
}
function sanitizeDetail(raw: string | undefined): string | undefined {
if (!raw) return undefined;
try {
const parsed = JSON.parse(raw);
if (parsed.error && typeof parsed.error === "string") {
const code = parsed.error;
if (parsed.correlation_id) {
return `[${code}] correlation_id=${parsed.correlation_id}`;
}
return `[${code}]`;
}
} catch {}
return raw.length > 200 ? raw.slice(0, 200) + "..." : raw;
}
// Maps gateway HTTP status + sanitized error body to a user-facing kind.
// The gateway's public error codes (gateway/errors.go) are stable strings
// like "chain_id_mismatch", "invalid_timestamp", "verifier_timeout",
// "upstream_timeout", "nonce_already_used".
function statusToKind(status: number, body: string): ErrorKind {
if (status === 400) {
if (body.includes("chain_id_mismatch")) return "wrong-chain";
if (body.includes("invalid_timestamp")) return "expired";
return "invalid-signature";
}
if (status === 402) return "expired";
if (status === 403) return "invalid-signature";
if (status === 408) return "ai-timeout";
if (status === 504) {
// Verifier timeout means we never reached the AI — different copy.
return body.includes("verifier_timeout") ? "verifier-timeout" : "ai-timeout";
}
if (status === 409) {
return body.includes("nonce_already_used") ? "duplicate-nonce" : "invalid-signature";
}
if (status === 429) return "rate-limited";
if (status === 502) {
// Gateway returns 502 + verification_unavailable when the verifier didn't
// respond (gateway/main.go:409,419) — signing never succeeded. The
// upstream_unavailable case is post-payment AI-provider failure.
return body.includes("verification_unavailable") ? "verifier-unavailable" : "ai-unavailable";
}
if (status >= 500) return "ai-unavailable";
return "unknown";
}
function looksWrongChain(message: string): boolean {
const m = message.toLowerCase();
return (
m.includes("wrong network") ||
m.includes("wrong chain") ||
m.includes("chain mismatch") ||
m.includes("unsupported chain") ||
m.includes("chain not supported") ||
m.includes("incorrect network") ||
m.includes("did not switch to chain")
);
}
function looksRejected(message: string, code?: number | string): boolean {
if (code === 4001 || code === "ACTION_REJECTED") return true;
const m = message.toLowerCase();
return (
m.includes("user rejected") ||
m.includes("user denied") ||
m.includes("rejected the request") ||
m.includes("action_rejected")
);
}
export function classifyError(err: unknown, ctx?: ErrorContext): ClassifiedError {
if (ctx?.status !== undefined && ctx.status !== 0) {
return build(statusToKind(ctx.status, ctx.bodyText ?? ""), ctx.bodyText);
}
if (typeof err === "object" && err !== null) {
const e = err as { message?: string; code?: number | string; shortMessage?: string };
const message = e.shortMessage ?? e.message ?? "";
if (looksRejected(message, e.code)) return build("user-rejected", message);
if (looksWrongChain(message)) return build("wrong-chain", message);
if (message.toLowerCase().includes("no wallet") || message.toLowerCase().includes("no crypto wallet")) {
return build("no-wallet", message);
}
if (message.toLowerCase().includes("failed to fetch") || message.toLowerCase().includes("networkerror")) {
return build("network", message);
}
return build("unknown", message);
}
return build("unknown", String(err));
}