1bc2de655a
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
66 lines
3.1 KiB
TypeScript
66 lines
3.1 KiB
TypeScript
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",
|
|
},
|
|
});
|
|
}
|