Send sign-in mail as HTML so quoted-printable wrapping cannot break the verify URL.
Staging was delivering magic links that looked expired because the token query string was encoded and wrapped in plaintext MIME. Also read rate-limit retry from the API error body, mention spam in the success copy, and document SES relay DNS. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+23
-36
@@ -27,6 +27,26 @@ function readApiErrorMessage(data: unknown): string {
|
||||
return "Request failed";
|
||||
}
|
||||
|
||||
function retryAfterFromResponse(
|
||||
res: Response,
|
||||
data: unknown,
|
||||
): number | undefined {
|
||||
if (res.status !== 429) return undefined;
|
||||
if (data && typeof data === "object" && "details" in data) {
|
||||
const d = (data as { details?: unknown }).details;
|
||||
if (d && typeof d === "object" && "retryAfterMs" in d) {
|
||||
const ms = (d as { retryAfterMs?: unknown }).retryAfterMs;
|
||||
if (typeof ms === "number" && ms > 0) return ms;
|
||||
}
|
||||
}
|
||||
const h = res.headers.get("retry-after");
|
||||
if (h) {
|
||||
const sec = Number.parseInt(h, 10);
|
||||
if (!Number.isNaN(sec)) return sec * 1000;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function fetchAuthSession(): Promise<{
|
||||
user: { id: string; email: string } | null;
|
||||
}> {
|
||||
@@ -54,13 +74,12 @@ export async function requestMagicLink(
|
||||
...(draft && Object.keys(draft).length > 0 ? { draft } : {}),
|
||||
}),
|
||||
});
|
||||
const data = await parseJson<{ error?: string; retryAfterMs?: number }>(res);
|
||||
const data: unknown = await parseJson(res);
|
||||
if (!res.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
error: readApiErrorMessage(data),
|
||||
retryAfterMs:
|
||||
typeof data.retryAfterMs === "number" ? data.retryAfterMs : undefined,
|
||||
retryAfterMs: retryAfterFromResponse(res, data),
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
@@ -85,22 +104,10 @@ export async function requestEmailChange(
|
||||
});
|
||||
const data: unknown = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
let retryAfterMs: number | undefined;
|
||||
if (
|
||||
res.status === 429 &&
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
"details" in data
|
||||
) {
|
||||
const d = (data as { details?: { retryAfterMs?: unknown } }).details;
|
||||
if (d && typeof d.retryAfterMs === "number") {
|
||||
retryAfterMs = d.retryAfterMs;
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: readApiErrorMessage(data),
|
||||
retryAfterMs,
|
||||
retryAfterMs: retryAfterFromResponse(res, data),
|
||||
};
|
||||
}
|
||||
return { ok: true };
|
||||
@@ -438,26 +445,6 @@ export type RuleStakeholderMutationResult =
|
||||
| { ok: true }
|
||||
| { ok: false; error: string; status: number; retryAfterMs?: number };
|
||||
|
||||
function retryAfterFromResponse(
|
||||
res: Response,
|
||||
data: unknown,
|
||||
): number | undefined {
|
||||
if (res.status !== 429) return undefined;
|
||||
if (data && typeof data === "object" && "details" in data) {
|
||||
const d = (data as { details?: unknown }).details;
|
||||
if (d && typeof d === "object" && "retryAfterMs" in d) {
|
||||
const ms = (d as { retryAfterMs?: unknown }).retryAfterMs;
|
||||
if (typeof ms === "number" && ms > 0) return ms;
|
||||
}
|
||||
}
|
||||
const h = res.headers.get("retry-after");
|
||||
if (h) {
|
||||
const sec = Number.parseInt(h, 10);
|
||||
if (!Number.isNaN(sec)) return sec * 1000;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function addRuleStakeholder(
|
||||
ruleId: string,
|
||||
email: string,
|
||||
|
||||
Reference in New Issue
Block a user