mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4f3fe10332
commit
d81eba3b37
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
## Frontend
|
## 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
|
- [ ] Check for potential dependency upgrades
|
||||||
- [ ] In settings, figure out automatic save
|
- [ ] In settings, figure out automatic save
|
||||||
|
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ fn default_relay_url() -> String {
|
|||||||
// enable push: with `method = "push"` and no `[notifications.push]` section, this default is
|
// 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
|
// 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`.
|
// 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)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
@@ -623,7 +623,10 @@ mod tests {
|
|||||||
assert!(settings.notifications.push.is_none());
|
assert!(settings.notifications.push.is_none());
|
||||||
assert!(settings.validate().is_ok());
|
assert!(settings.validate().is_ok());
|
||||||
// The sender falls back to the default when the section is absent.
|
// 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]
|
#[test]
|
||||||
@@ -652,7 +655,10 @@ mod tests {
|
|||||||
"
|
"
|
||||||
);
|
);
|
||||||
let settings = parse(&toml);
|
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());
|
assert_eq!(push.relay_url, default_relay_url());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,8 @@
|
|||||||
androidSdk
|
androidSdk
|
||||||
jdk17
|
jdk17
|
||||||
nodejs_22 # push relay (push_relay/): runs npm install/test/build; bundles npm
|
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
|
claude-code
|
||||||
clippy # Rust linter
|
clippy # Rust linter
|
||||||
pythonEnv
|
pythonEnv
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ Key properties:
|
|||||||
`firebase-admin` SDK
|
`firebase-admin` SDK
|
||||||
(`messaging().sendEach(...)`, batched multicast) → return per-token results
|
(`messaging().sendEach(...)`, batched multicast) → return per-token results
|
||||||
(`ok` / `unregistered` / `invalid`) so the caller can prune dead tokens.
|
(`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
|
- FCM client: the `firebase-admin` SDK authenticates via the function's runtime
|
||||||
service account — **no service-account JSON to manage, no OAuth-token minting
|
service account — **no service-account JSON to manage, no OAuth-token minting
|
||||||
or caching code**. `sendEach` returns per-token success/error, giving us
|
or caching code**. `sendEach` returns per-token success/error, giving us
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"default": "oott-push"
|
||||||
|
},
|
||||||
|
"targets": {},
|
||||||
|
"etags": {}
|
||||||
|
}
|
||||||
@@ -29,7 +29,8 @@ full design and rationale.
|
|||||||
"body": "..." } }`. Returns `{ "results": [{ "token": "...", "status":
|
"body": "..." } }`. Returns `{ "results": [{ "token": "...", "status":
|
||||||
"ok" | "unregistered" | "invalid" | "error" }] }`. The backend prunes tokens
|
"ok" | "unregistered" | "invalid" | "error" }] }`. The backend prunes tokens
|
||||||
reported `unregistered` or `invalid`.
|
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
|
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.
|
reach OOTT app installs), per-source-IP rate limiting, and a billing cap.
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ export function createApp(deps: AppDependencies): Express {
|
|||||||
app.set("trust proxy", true);
|
app.set("trust proxy", true);
|
||||||
app.use(express.json({ limit: "256kb" }));
|
app.use(express.json({ limit: "256kb" }));
|
||||||
|
|
||||||
// Liveness probe — no side effects, never rate limited.
|
// Liveness probe — no side effects, never rate limited. Note: the path
|
||||||
app.get("/healthz", (_req: Request, res: Response) => {
|
// `/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");
|
res.status(200).send("ok");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,6 @@ const rateLimitStore = createFirestoreRateLimitStore(
|
|||||||
|
|
||||||
const app = createApp({ messenger, rateLimitStore });
|
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.
|
// 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);
|
export const relay = onRequest({ region: "us-central1", maxInstances: 10 }, app);
|
||||||
|
|||||||
Reference in New Issue
Block a user