src/unsubscribe-handler.tsTypeScript
@@ -0,0 +1,47 @@
1+
export interface UnsubscribeRequest {2+
method: string;3+
token: string;4+
}5+
6+
export interface UnsubscribeResponse {7+
status: number;8+
body?: string;9+
}10+
11+
interface TokenPayload {12+
email: string;13+
expiresAtMs: number;14+
signature: string;15+
}16+
17+
export interface UnsubscribeDependencies {18+
clock: { now(): number };19+
decoder: { decode(token: string): TokenPayload };20+
signer: {21+
sign(email: string, expiresAtMs: number): string;22+
verify(email: string, expiresAtMs: number, signature: string): boolean;23+
};24+
subscriptions: { unsubscribeMarketing(email: string): Promise<void> };25+
}26+
27+
export async function handleUnsubscribe(28+
request: UnsubscribeRequest,29+
dependencies: UnsubscribeDependencies,30+
): Promise<UnsubscribeResponse> {31+
const payload = dependencies.decoder.decode(request.token);32+
33+
if (dependencies.clock.now() > payload.expiresAtMs) {34+
return { status: 400, body: "Invalid unsubscribe link" };35+
}36+
37+
const expected = dependencies.signer.sign(38+
payload.email,39+
payload.expiresAtMs,40+
);41+
if (expected !== payload.signature) {42+
return { status: 400, body: "Invalid unsubscribe link" };43+
}44+
45+
await dependencies.subscriptions.unsubscribeMarketing(payload.email);46+
return { status: 204 };47+
}