1use worker::{Fetch, Headers, Method, Request, RequestInit, send::SendFuture};
4
5use super::Ctx;
6use crate::{
7 Error, Result,
8 push::{Subscription, VAPID_PRIVATE_KEY, VAPID_PUBLIC_KEY, VAPID_SUBJECT, VapidKeys, encrypt, vapid_authorization},
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Sent {
14 Delivered,
16 Gone,
19}
20
21pub fn send(
50 ctx: &Ctx,
51 subscription: &Subscription,
52 message: &serde_json::Value,
53 ttl: u32,
54) -> impl Future<Output = Result<Sent>> + Send + use<> {
55 let env = ctx.env().clone();
56 let subscription = subscription.clone();
57 let payload = message.to_string();
58 SendFuture::new(async move {
59 let var = |name: &str| env.var(name).ok().map(|value| value.to_string()).filter(|v| !v.trim().is_empty());
60 let missing = |name: &str| {
61 Error::internal(format!(
62 "web push needs {name} (not set). Fix: `ocre g push` writes a VAPID key pair to .dev.vars; in \
63 production set VAPID_PUBLIC_KEY and VAPID_SUBJECT in worker.env and push VAPID_PRIVATE_KEY with \
64 `ocre secrets push VAPID_PRIVATE_KEY --file .prod.vars`"
65 ))
66 };
67 let private_key = super::secrets::lookup(&env, VAPID_PRIVATE_KEY).await?.filter(|v| !v.trim().is_empty());
68 let keys = VapidKeys {
69 public_key: var(VAPID_PUBLIC_KEY).ok_or_else(|| missing(VAPID_PUBLIC_KEY))?,
70 private_key: private_key.ok_or_else(|| missing(VAPID_PRIVATE_KEY))?,
71 };
72 let subject = var(VAPID_SUBJECT).ok_or_else(|| missing(VAPID_SUBJECT))?;
73 let body = encrypt(&subscription.keys, payload.as_bytes())?;
74 let authorization = vapid_authorization(&subscription.endpoint, &subject, &keys, crate::now())?;
75 let headers = Headers::new();
76 headers.set("Authorization", &authorization)?;
77 headers.set("Content-Encoding", "aes128gcm")?;
78 headers.set("Content-Type", "application/octet-stream")?;
79 headers.set("TTL", &ttl.to_string())?;
80 let mut init = RequestInit::new();
81 let array = worker::js_sys::Uint8Array::from(body.as_slice());
82 init.with_method(Method::Post).with_headers(headers).with_body(Some(array.into()));
83 let mut response = Fetch::Request(Request::new_with_init(&subscription.endpoint, &init)?).send().await?;
84 match response.status_code() {
85 200..=299 => Ok(Sent::Delivered),
86 404 | 410 => Ok(Sent::Gone),
87 status => {
88 let text = response.text().await.unwrap_or_default();
89 Err(Error::internal(format!(
90 "the push service answered {status}: {}",
91 text.chars().take(200).collect::<String>()
92 )))
93 }
94 }
95 })
96}