krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

spikes/apns-worker/src/index.ts

58 lines · 2393 bytes

 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}