spikes/apns-worker/src/index.ts
58 lines · 2393 bytes
3 symbols in this file
1// Spike: does a deployed Worker reach APNs over HTTP/2? APNs rejects HTTP/1.1,
2// so any APNs response with an apns-id answers yes.
3
4import { send } from "./apns";
5
6interface Env {
7 APNS_KEY_ID: string;
8 APNS_TEAM_ID: string;
9 APNS_TOPIC: string;
10 APNS_KEY: string;
11 SPIKE_SECRET: string;
12}
13
14interface PushRequest {
15 token: string;
16 sandbox?: boolean;
17 title?: string;
18 body?: string;
19}
20
21export default {
22 async fetch(request: Request, env: Env): Promise<Response> {
23 const url = new URL(request.url);
24 if (request.method === "GET" && url.pathname === "/probe") return probe();
25 if (request.method !== "POST" || url.pathname !== "/push") return new Response("not found", { status: 404 });
26 if (request.headers.get("authorization") !== `Bearer ${env.SPIKE_SECRET}`) return new Response("unauthorized", { status: 401 });
27
28 const req = (await request.json()) as PushRequest;
29 if (!/^[0-9a-f]{64,200}$/i.test(req.token ?? "")) return new Response("bad token", { status: 400 });
30
31 const result = await send(
32 { keyId: env.APNS_KEY_ID, teamId: env.APNS_TEAM_ID, privateKey: env.APNS_KEY, topic: env.APNS_TOPIC },
33 req.token,
34 { aps: { alert: { title: req.title ?? "Moonrise", body: req.body ?? "Push from a Cloudflare Worker" } } },
35 req.sandbox ?? true,
36 );
37 return Response.json(result, { status: result.status === 200 ? 200 : 502 });
38 },
39} satisfies ExportedHandler<Env>;
40
41/** Sends with a throwaway key and token. Needs no Apple credentials: an APNs JSON
42 * error (e.g. 403 InvalidProviderToken) still proves the HTTP/2 connection worked. */
43async function probe(): Promise<Response> {
44 const pair = (await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign"])) as CryptoKeyPair;
45 const der = new Uint8Array((await crypto.subtle.exportKey("pkcs8", pair.privateKey)) as ArrayBuffer);
46 const pem = `-----BEGIN PRIVATE KEY-----\n${btoa(String.fromCharCode(...der))}\n-----END PRIVATE KEY-----`;
47 try {
48 const result = await send(
49 { keyId: "PROBE00000", teamId: "PROBE00000", privateKey: pem, topic: "org.example.probe" },
50 "0".repeat(64),
51 { aps: { alert: "probe" } },
52 true,
53 );
54 return Response.json({ http2: result.apnsId !== null || result.reason !== null, ...result });
55 } catch (e) {
56 return Response.json({ http2: false, error: String(e) }, { status: 502 });
57 }
58}