From d81eba3b3799bc5d96bd3c881f035ff304b243d5 Mon Sep 17 00:00:00 2001 From: rzuasti Date: Mon, 8 Jun 2026 14:00:09 -0400 Subject: [PATCH] Deploy push relay: dev-shell tooling, real relay URL, /health route - flake.nix: add nodejs_22, firebase-tools, and google-cloud-sdk to the dev shell so the relay can be tested, built, deployed, and administered locally - backend settings: point default_relay_url at the deployed relay - rename the relay liveness route /healthz -> /health: Google Front End reserves /healthz and returns its own 404 before the request reaches Cloud Run, so the probe was unreachable (verified live; /v1/push and the FCM path work end-to-end) Co-Authored-By: Claude Opus 4.8 --- TODO.md | 2 +- backend/src/settings.rs | 12 +++++++++--- flake.nix | 2 ++ push_notifications.md | 3 ++- push_relay/.firebaserc | 7 +++++++ push_relay/README.md | 3 ++- push_relay/src/app.ts | 6 ++++-- push_relay/src/index.ts | 2 +- 8 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 push_relay/.firebaserc diff --git a/TODO.md b/TODO.md index a97737c..7db020a 100644 --- a/TODO.md +++ b/TODO.md @@ -24,7 +24,7 @@ ## Frontend -- [ ] In the status screen, the "listening for" property of passive scanners should change to days and months (now its always minutes) +- [x] In the status screen, the "listening for" property of passive scanners should change to days and months (now its always minutes) - [ ] Check for potential dependency upgrades - [ ] In settings, figure out automatic save diff --git a/backend/src/settings.rs b/backend/src/settings.rs index 1de39de..92cb7d2 100644 --- a/backend/src/settings.rs +++ b/backend/src/settings.rs @@ -182,7 +182,7 @@ fn default_relay_url() -> String { // enable push: with `method = "push"` and no `[notifications.push]` section, this default is // used. The concrete URL is filled in once the relay Cloud Function is deployed (see // push_relay/README.md); a self-hoster can always override it via `relay_url`. - "https://oott-push-relay.example.com/v1/push".to_string() + "https://relay-dzhbmmulaq-uc.a.run.app/v1/push".to_string() } #[derive(Debug, Deserialize, Clone)] @@ -623,7 +623,10 @@ mod tests { assert!(settings.notifications.push.is_none()); assert!(settings.validate().is_ok()); // The sender falls back to the default when the section is absent. - assert_eq!(settings.notifications.push.unwrap_or_default().relay_url, default_relay_url()); + assert_eq!( + settings.notifications.push.unwrap_or_default().relay_url, + default_relay_url() + ); } #[test] @@ -652,7 +655,10 @@ mod tests { " ); let settings = parse(&toml); - let push = settings.notifications.push.expect("push section should parse when present"); + let push = settings + .notifications + .push + .expect("push section should parse when present"); assert_eq!(push.relay_url, default_relay_url()); } diff --git a/flake.nix b/flake.nix index 69a9cee..d705cb5 100644 --- a/flake.nix +++ b/flake.nix @@ -69,6 +69,8 @@ androidSdk jdk17 nodejs_22 # push relay (push_relay/): runs npm install/test/build; bundles npm + firebase-tools # push relay: firebase CLI for emulator (serve) and deploy + google-cloud-sdk # push relay: gcloud for Cloud Run/IAM admin (e.g. invoker) claude-code clippy # Rust linter pythonEnv diff --git a/push_notifications.md b/push_notifications.md index e7f4360..ef9f40e 100644 --- a/push_notifications.md +++ b/push_notifications.md @@ -97,7 +97,8 @@ Key properties: `firebase-admin` SDK (`messaging().sendEach(...)`, batched multicast) → return per-token results (`ok` / `unregistered` / `invalid`) so the caller can prune dead tokens. - - `GET /healthz` — liveness. + - `GET /health` — liveness. (Not `/healthz`: Google Front End reserves that + path and returns its own 404 before the request reaches the function.) - FCM client: the `firebase-admin` SDK authenticates via the function's runtime service account — **no service-account JSON to manage, no OAuth-token minting or caching code**. `sendEach` returns per-token success/error, giving us diff --git a/push_relay/.firebaserc b/push_relay/.firebaserc new file mode 100644 index 0000000..2b1b413 --- /dev/null +++ b/push_relay/.firebaserc @@ -0,0 +1,7 @@ +{ + "projects": { + "default": "oott-push" + }, + "targets": {}, + "etags": {} +} \ No newline at end of file diff --git a/push_relay/README.md b/push_relay/README.md index b014930..af699d8 100644 --- a/push_relay/README.md +++ b/push_relay/README.md @@ -29,7 +29,8 @@ full design and rationale. "body": "..." } }`. Returns `{ "results": [{ "token": "...", "status": "ok" | "unregistered" | "invalid" | "error" }] }`. The backend prunes tokens reported `unregistered` or `invalid`. -- `GET /healthz` — liveness, returns `200 ok`. +- `GET /health` — liveness, returns `200 ok`. (Not `/healthz`: that path is + reserved by Google Front End and never reaches the function.) Protection (Phase 1, no shared secret): FCM project scoping (the relay can only reach OOTT app installs), per-source-IP rate limiting, and a billing cap. diff --git a/push_relay/src/app.ts b/push_relay/src/app.ts index 272fec4..79ae05a 100644 --- a/push_relay/src/app.ts +++ b/push_relay/src/app.ts @@ -30,8 +30,10 @@ export function createApp(deps: AppDependencies): Express { app.set("trust proxy", true); app.use(express.json({ limit: "256kb" })); - // Liveness probe — no side effects, never rate limited. - app.get("/healthz", (_req: Request, res: Response) => { + // Liveness probe — no side effects, never rate limited. Note: the path + // `/healthz` is reserved by Google Front End (it returns its own 404 before + // the request reaches Cloud Run), so this uses `/health`. + app.get("/health", (_req: Request, res: Response) => { res.status(200).send("ok"); }); diff --git a/push_relay/src/index.ts b/push_relay/src/index.ts index 856e30e..ed09c34 100644 --- a/push_relay/src/index.ts +++ b/push_relay/src/index.ts @@ -25,6 +25,6 @@ const rateLimitStore = createFirestoreRateLimitStore( const app = createApp({ messenger, rateLimitStore }); -// Single HTTP function hosting both routes (POST /v1/push, GET /healthz). Scales to zero, so there +// Single HTTP function hosting both routes (POST /v1/push, GET /health). Scales to zero, so there // is no idle cost and no server/OS to patch; the platform provides TLS and a stable HTTPS URL. export const relay = onRequest({ region: "us-central1", maxInstances: 10 }, app);