import { NextResponse } from "next/server"; export const dynamic = "force-dynamic"; /* Digital Asset Links — what lets the staff app open threadcount.tech/my links itself. * * Without this file, tapping "Approve" in a manager's email opens Chrome rather than the app they * installed. Android fetches it over HTTPS and checks that the certificate it names matches the * one the installed app was signed with. * * The fingerprint has to be **Play's app-signing certificate**, not the upload key — with Play App * Signing, Google re-signs the bundle, so the certificate on the device is theirs. Find it at * Play Console → Test and release → Setup → App signing → "SHA-256 certificate fingerprint", and * put it in the environment as ANDROID_APP_FINGERPRINTS. Several may be listed, comma-separated: * both apps, or an old and a new key during a rotation. * * Served from an env var rather than a static file on purpose. The fingerprint is only knowable * after the first upload, and a deploy is a cheaper way to add it than a code change — and if it * is ever rotated, nothing here needs editing. * * With no fingerprint configured this returns an empty statement list, which is the honest answer: * no app is authorised to handle these links yet, and Android falls back to the browser exactly as * it does today. It never returns a malformed or guessed fingerprint. */ const PACKAGES: { name: string; label: string }[] = [ { name: "tech.threadcount.staff", label: "ANDROID_APP_FINGERPRINTS_STAFF" }, { name: "tech.threadcount.app", label: "ANDROID_APP_FINGERPRINTS_COUNTER" }, ]; function fingerprints(specific: string): string[] { const raw = process.env[specific] || process.env.ANDROID_APP_FINGERPRINTS || ""; return raw .split(",") .map((f) => f.trim().toUpperCase()) // A SHA-256 fingerprint is 32 colon-separated hex pairs. Anything else is a paste error, and // shipping it would just make Android's verification fail silently. .filter((f) => /^([0-9A-F]{2}:){31}[0-9A-F]{2}$/.test(f)); } export async function GET() { const statements = PACKAGES.flatMap((p) => { const fps = fingerprints(p.label); if (!fps.length) return []; return [{ relation: [ // Opens threadcount.tech/my links in the app instead of the browser. "delegate_permission/common.handle_all_urls", // Credential sharing: the site and the app are one account system, so a password saved on // either autofills on the other. A staff member sets theirs once, on whichever surface the // printed slip's link happened to open on, and shouldn't have to remember which. "delegate_permission/common.get_login_creds", ], target: { namespace: "android_app", package_name: p.name, sha256_cert_fingerprints: fps }, }]; }); return NextResponse.json(statements, { headers: { "content-type": "application/json", // Android caches this; an hour is short enough that adding a fingerprint takes effect the // same day, and long enough that it isn't fetched on every link tap. "cache-control": "public, max-age=3600", }, }); }