feat(calendar): relay Google Calendar push to local stacks via dev - #5593
feat(calendar): relay Google Calendar push to local stacks via dev#5593gbirman wants to merge 9 commits into
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe change adds domain models and ports for Google Calendar watch-channel teardown. The calendar service stops active channels, clears successful records, caches tokens per authorization link, and reports failures. Google and PostgreSQL adapters implement provider stopping and guarded persistence cleanup. The email service adds a Redis-backed relay, authenticated SSE endpoints, and a reconnecting subscriber. Local tooling configures per-instance calendar-push overlays with enable, disable, and status actions. Documentation covers local setup and verification. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1⚔️ Resolve merge conflicts 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
services/email_service/src/calendar_watch_relay.rs (2)
91-94: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low valueAlign the doc comment with the actual comparison.
Sha256::digest(...) == Sha256::digest(...)compares twoGenericArrayvalues with a normal byte-wise equality that can return early. The doc comment states the comparison has no early exit. Hashing first removes the practical timing leak of the raw secret, so the behavior is acceptable, but the wording is inaccurate. Either correct the comment or use a constant-time equality helper.♻️ Suggested comment correction
-/// Compare two secrets without early exit on the first differing byte. +/// Compare two secrets by SHA-256 digest, so a timing difference cannot +/// reveal a prefix of the expected secret. pub fn secrets_match(presented: &str, expected: &str) -> bool { Sha256::digest(presented.as_bytes()) == Sha256::digest(expected.as_bytes()) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/email_service/src/calendar_watch_relay.rs` around lines 91 - 94, Update the doc comment for secrets_match to accurately describe the existing SHA-256 digest comparison, removing the claim that it avoids early exit; do not change the comparison implementation.
121-131: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider reusing one multiplexed connection for publishing.
publishcallsget_multiplexed_async_connectionon every notification, so each relayed delivery pays a connection setup.RedisWatchRelayBusis cloneable and long-lived, so it can hold a lazily createdMultiplexedConnectioninstead. Traffic is dev-only today, so this is optional.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/email_service/src/calendar_watch_relay.rs` around lines 121 - 131, Update RedisWatchRelayBus and its publish method to lazily initialize and reuse a single cloneable multiplexed Redis connection across notifications instead of calling get_multiplexed_async_connection on every publish. Preserve the existing serialization, channel, publish, and error propagation behavior.services/email_service/src/api/calendar_watch.rs (1)
79-108: 🔒 Security & Privacy | 🔵 TrivialConsider rate limiting the unauthenticated relay publish path on dev.
/calendar/notificationsaccepts any caller. When the relay serves, any request that carries the fourx-goog-*headers causes a RedisPUBLISHunder the hash of the presented token. A caller that learns a local instance token can inject a synthetic notification and force that stack to re-arm a sync job, and unauthenticated traffic can drive Redis publish load on the dev deployment. The relay is inert in production, so this is dev-only exposure. A simple per-IP or per-token rate limit on the relay branch contains both effects.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/email_service/src/api/calendar_watch.rs` around lines 79 - 108, Apply a simple rate limit to the relay branch of the calendar notification handler before calling `relay.bus.publish`, keyed by caller IP or the presented token. Reject requests exceeding the limit without publishing, while preserving the existing validation and successful notification flow for allowed requests.services/email_service/src/pubsub/calendar_watch_subscriber.rs (1)
127-145: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueConsider decoding SSE bytes without
from_utf8_lossyper chunk.
String::from_utf8_lossy(&chunk)decodes each chunk independently. A multi-byte UTF-8 character split across two chunks becomes replacement characters, which corrupts the payload. The relayed fields are Google header values and are ASCII in practice, so the risk is low today. Feeding bytes into the parser, or buffering an incomplete trailing sequence, removes the failure mode.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/email_service/src/pubsub/calendar_watch_subscriber.rs` around lines 127 - 145, Update the SSE parsing loop around SseDataParser::push to avoid decoding each stream chunk independently with String::from_utf8_lossy. Feed bytes directly into a byte-aware parser or retain incomplete trailing UTF-8 sequences between iterations, ensuring multi-byte characters split across chunks are decoded intact before serde_json parses each RelayedWatchNotification.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/calendar_events/src/domain/service.rs`:
- Around line 667-678: Update the watch-channel cleanup flow around
clear_watch_channel so cleanup errors are not discarded or counted as
successfully stopped: increment summary.failed (or the established separate
cleanup-failure counter) when clearing bookkeeping fails, while retaining
summary.stopped only for successful cleanup. Add a test covering a
clear_watch_channel failure and verify the summary reflects the failure.
In `@services/email_service/src/pubsub/calendar_watch_subscriber.rs`:
- Around line 115-126: Wrap the subscribe request’s send future in a bounded
timeout so stalled response headers return control to the reconnect loop for
backoff and retry. Update the request flow around the visible
client.get(...).send().await call, add the request-timeout constant alongside
the existing timeout constants, and propagate or handle the timeout error
consistently with the surrounding reconnect logic.
In `@tooling/xtask/crates/xtask_local/src/local/calendar_push.rs`:
- Around line 37-42: Update the persisted-token loading logic in the token
helper around std::fs::read_to_string so generation occurs only when the path is
absent. Propagate permission and other read errors, and return an error when an
existing token file is empty; preserve the existing-token return path for
non-empty content and avoid minting or overwriting a replacement token in
failure cases.
In `@tooling/xtask/crates/xtask_local/src/local/env_layer.rs`:
- Around line 115-126: Update the merged-environment validation in resolve so
calendar push remains inactive when CALENDAR_WATCH_RELAY_SECRET is absent,
empty, or whitespace-only. Validate the secret’s trimmed content rather than
using contains_key; on failure, return an error or remove the relay
configuration, ensuring calendar_push::enable cannot leave
CALENDAR_WATCH_RELAY_URL armed without a valid secret.
---
Nitpick comments:
In `@services/email_service/src/api/calendar_watch.rs`:
- Around line 79-108: Apply a simple rate limit to the relay branch of the
calendar notification handler before calling `relay.bus.publish`, keyed by
caller IP or the presented token. Reject requests exceeding the limit without
publishing, while preserving the existing validation and successful notification
flow for allowed requests.
In `@services/email_service/src/calendar_watch_relay.rs`:
- Around line 91-94: Update the doc comment for secrets_match to accurately
describe the existing SHA-256 digest comparison, removing the claim that it
avoids early exit; do not change the comparison implementation.
- Around line 121-131: Update RedisWatchRelayBus and its publish method to
lazily initialize and reuse a single cloneable multiplexed Redis connection
across notifications instead of calling get_multiplexed_async_connection on
every publish. Preserve the existing serialization, channel, publish, and error
propagation behavior.
In `@services/email_service/src/pubsub/calendar_watch_subscriber.rs`:
- Around line 127-145: Update the SSE parsing loop around SseDataParser::push to
avoid decoding each stream chunk independently with String::from_utf8_lossy.
Feed bytes directly into a byte-aware parser or retain incomplete trailing UTF-8
sequences between iterations, ensuring multi-byte characters split across chunks
are decoded intact before serde_json parses each RelayedWatchNotification.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 88e989f2-5b5f-4ad8-afb1-0909e5acca57
⛔ Files ignored due to path filters (2)
.sqlx/query-a6ea220aaff4d5b84963280a24d51034b8f16fc6dbd288ebbd027cfde86b7872.jsonis excluded by!**/.sqlx/**.sqlx/query-ec4d63f3ed765a0f289331e66a95171e097fa6ff3676cc64fdcbebf517bea83b.jsonis excluded by!**/.sqlx/**
📒 Files selected for processing (23)
crates/calendar_events/src/domain/models.rscrates/calendar_events/src/domain/ports.rscrates/calendar_events/src/domain/service.rscrates/calendar_events/src/domain/service/test.rscrates/calendar_events/src/outbound/google.rscrates/calendar_events/src/outbound/pg.rscrates/calendar_events/src/outbound/pg/test.rsdocs/RUNNING_LOCALLY.mdservices/email_service/src/api/calendar_watch.rsservices/email_service/src/api/context.rsservices/email_service/src/bin/pubsub_workers/pubsub_workers.rsservices/email_service/src/calendar_watch_relay.rsservices/email_service/src/calendar_watch_relay/test.rsservices/email_service/src/lib.rsservices/email_service/src/main.rsservices/email_service/src/openapi.rsservices/email_service/src/pubsub/calendar_watch_subscriber.rsservices/email_service/src/pubsub/mod.rstooling/just/xtask.justtooling/xtask/crates/xtask_local/src/local/calendar_push.rstooling/xtask/crates/xtask_local/src/local/cli.rstooling/xtask/crates/xtask_local/src/local/env_layer.rstooling/xtask/crates/xtask_local/src/local/mod.rs
| repository | ||
| .clear_watch_channel(channel.calendar_id, &channel.channel_id) | ||
| .await | ||
| .inspect_err(|error| { | ||
| tracing::warn!( | ||
| error=?error, | ||
| calendar_id=%channel.calendar_id, | ||
| "stopped a watch channel but failed to clear its bookkeeping" | ||
| ); | ||
| }) | ||
| .ok(); | ||
| summary.stopped += 1; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Count bookkeeping cleanup failures as failed.
At Line 667, clear_watch_channel errors are discarded. At Line 678, the code then increments summary.stopped. This reports the channel as cleared when its bookkeeping remains retained.
Increment failed for cleanup errors, or add a separate cleanup-failure count. Add a test for this path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/calendar_events/src/domain/service.rs` around lines 667 - 678, Update
the watch-channel cleanup flow around clear_watch_channel so cleanup errors are
not discarded or counted as successfully stopped: increment summary.failed (or
the established separate cleanup-failure counter) when clearing bookkeeping
fails, while retaining summary.stopped only for successful cleanup. Add a test
covering a clear_watch_channel failure and verify the summary reflects the
failure.
| let response = client | ||
| .get(format!("{}/calendar/relay/subscribe", config.url)) | ||
| .header("x-relay-secret", &config.secret) | ||
| .header("x-relay-token", token) | ||
| .send() | ||
| .await?; | ||
| if response.status() != reqwest::StatusCode::OK { | ||
| anyhow::bail!( | ||
| "relay subscription rejected with status {}", | ||
| response.status() | ||
| ); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Bound the initial request so a stalled response cannot stop reconnects.
The client sets only connect_timeout, and READ_TIMEOUT applies after the body stream starts. If the relay accepts the TCP connection but never sends response headers, send().await waits without limit. The reconnect loop then stays parked on that future, and the local stack receives no relayed notifications until it shuts down. Wrap the request in a timeout so the loop can back off and retry.
🐛 Proposed fix
- let response = client
- .get(format!("{}/calendar/relay/subscribe", config.url))
- .header("x-relay-secret", &config.secret)
- .header("x-relay-token", token)
- .send()
- .await?;
+ let request = client
+ .get(format!("{}/calendar/relay/subscribe", config.url))
+ .header("x-relay-secret", &config.secret)
+ .header("x-relay-token", token)
+ .send();
+ let response = match tokio::time::timeout(CONNECT_TIMEOUT, request).await {
+ Ok(result) => result?,
+ Err(_) => anyhow::bail!("relay subscription request timed out"),
+ };Add the constant next to the other timeouts:
/// Bound on the subscribe request, which must return headers promptly even
/// though the body stays open.
const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let response = client | |
| .get(format!("{}/calendar/relay/subscribe", config.url)) | |
| .header("x-relay-secret", &config.secret) | |
| .header("x-relay-token", token) | |
| .send() | |
| .await?; | |
| if response.status() != reqwest::StatusCode::OK { | |
| anyhow::bail!( | |
| "relay subscription rejected with status {}", | |
| response.status() | |
| ); | |
| } | |
| let request = client | |
| .get(format!("{}/calendar/relay/subscribe", config.url)) | |
| .header("x-relay-secret", &config.secret) | |
| .header("x-relay-token", token) | |
| .send(); | |
| let response = match tokio::time::timeout(CONNECT_TIMEOUT, request).await { | |
| Ok(result) => result?, | |
| Err(_) => anyhow::bail!("relay subscription request timed out"), | |
| }; | |
| if response.status() != reqwest::StatusCode::OK { | |
| anyhow::bail!( | |
| "relay subscription rejected with status {}", | |
| response.status() | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@services/email_service/src/pubsub/calendar_watch_subscriber.rs` around lines
115 - 126, Wrap the subscribe request’s send future in a bounded timeout so
stalled response headers return control to the reconnect loop for backoff and
retry. Update the request flow around the visible client.get(...).send().await
call, add the request-timeout constant alongside the existing timeout constants,
and propagate or handle the timeout error consistently with the surrounding
reconnect logic.
| use std::io::Read; | ||
| use std::path::PathBuf; | ||
|
|
||
| use anyhow::{Context, Result}; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- target file ---'
wc -l tooling/xtask/crates/xtask_local/src/local/calendar_push.rs
cat -n tooling/xtask/crates/xtask_local/src/local/calendar_push.rs
printf '%s\n' '--- manifests ---'
fd -i 'Cargo.toml' tooling/xtask/crates/xtask_local tooling/xtask | head -40
printf '%s\n' '--- target manifest ---'
cat -n tooling/xtask/crates/xtask_local/Cargo.toml
printf '%s\n' '--- rootcause and anyhow usage in xtask_local ---'
rg -n -C 2 '\b(rootcause|anyhow)\b' tooling/xtask/crates/xtask_local --glob '*.rs' --glob 'Cargo.toml'Repository: macro-inc/macro
Length of output: 28940
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- rootcause declarations ---'
rg -n -C 3 'rootcause' --glob 'Cargo.toml' --glob '*.lock' --glob '*.rs' | head -300
printf '%s\n' '--- applicable instruction files ---'
find tooling/xtask -name 'CLAUDE.md' -o -name 'AGENTS.md' -o -name 'CONTRIBUTING.md' | sort
printf '%s\n' '--- workspace manifest context ---'
rg -n -C 4 '^\[workspace.dependencies\]|^anyhow|^rootcause' Cargo.toml tooling/xtask/Cargo.toml
printf '%s\n' '--- local crate module declarations ---'
cat -n tooling/xtask/crates/xtask_local/src/local/mod.rs | sed -n '1,80p'Repository: macro-inc/macro
Length of output: 618
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- rootcause references in tracked files ---'
git grep -n -i 'rootcause' -- ':!*.lock' || true
printf '%s\n' '--- workspace dependency section ---'
awk '
/^\[workspace.dependencies\]/ {show=1}
show {print NR ":" $0}
show && /^\[/ && $0 != "[workspace.dependencies]" {exit}
' Cargo.toml
printf '%s\n' '--- xtask manifest ---'
cat -n tooling/xtask/Cargo.toml | sed -n '1,100p'
printf '%s\n' '--- applicable instruction files ---'
git ls-files | grep -E '(^|/)(CLAUDE|AGENTS|CONTRIBUTING)\.md$' | grep '^tooling/xtask/' || true
printf '%s\n' '--- local module declarations ---'
cat -n tooling/xtask/crates/xtask_local/src/local/mod.rs | sed -n '1,90p'Repository: macro-inc/macro
Length of output: 50371
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- calendar_push call sites and declarations ---'
git grep -n -C 3 -E 'calendar_push|calendar push' -- tooling/xtask/crates/xtask_local/src tooling/xtask/crates/xtask_local/Cargo.toml || true
printf '%s\n' '--- enclosing return types ---'
git grep -n -C 2 -E 'fn (enable|disable|status)|calendar_push::(enable|disable|status)' -- tooling/xtask/crates/xtask_local/src || true
printf '%s\n' '--- change metadata ---'
git status --short -- tooling/xtask/crates/xtask_local/src/local/calendar_push.rs tooling/xtask/crates/xtask_local/Cargo.toml tooling/xtask/crates/xtask_local/src/local/mod.rs
git diff --stat -- tooling/xtask/crates/xtask_local/src/local/calendar_push.rs tooling/xtask/crates/xtask_local/Cargo.toml tooling/xtask/crates/xtask_local/src/local/mod.rsRepository: macro-inc/macro
Length of output: 12108
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- rootcause/anyhow boundary patterns ---'
git grep -n -C 2 -E 'anyhow::Error::from\(.*rootcause|rootcause::Report.*anyhow|anyhow::Result.*rootcause|Result<.*rootcause::Report' -- '*.rs' | head -240 || true
printf '%s\n' '--- cli return boundary ---'
cat -n tooling/xtask/crates/xtask_local/src/local/cli.rs | sed -n '235,282p'
cat -n tooling/xtask/crates/xtask_local/src/main.rs | sed -n '1,45p'
printf '%s\n' '--- rootcause ResultExt usage examples ---'
git grep -n -C 2 'rootcause::prelude::ResultExt' -- '*.rs' | head -120 || trueRepository: macro-inc/macro
Length of output: 23647
Use rootcause for this module's new error handling.
Add the workspace dependency and use rootcause::Report with rootcause::prelude::ResultExt for the module's fallible functions.
Source: Coding guidelines
| if let Ok(existing) = std::fs::read_to_string(&path) { | ||
| let existing = existing.trim(); | ||
| if !existing.is_empty() { | ||
| return Ok(existing.to_owned()); | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Do not rotate the persisted token after an arbitrary read failure.
if let Ok(existing) treats permission and I/O errors as a missing token file. enable can then mint and overwrite a new token. Existing Google channels still use the old token, which can orphan their deliveries. Generate only when the path is absent. Return errors for other read failures and for an existing empty token.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tooling/xtask/crates/xtask_local/src/local/calendar_push.rs` around lines 37
- 42, Update the persisted-token loading logic in the token helper around
std::fs::read_to_string so generation occurs only when the path is absent.
Propagate permission and other read errors, and return an error when an existing
token file is empty; preserve the existing-token return path for non-empty
content and avoid minting or overwriting a replacement token in failure cases.
| // The relay secret rides a later layer (Doppler or --env-file), so only | ||
| // the fully merged map can tell whether calendar push will actually arm. | ||
| if env.contains_key("CALENDAR_WATCH_RELAY_URL") | ||
| && !env.contains_key("CALENDAR_WATCH_RELAY_SECRET") | ||
| { | ||
| eprintln!( | ||
| "warning: calendar push is enabled but CALENDAR_WATCH_RELAY_SECRET is missing — \ | ||
| the stack will open watch channels without subscribing to their deliveries; \ | ||
| supply the secret via Doppler or --env-file" | ||
| ); | ||
| } | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Do not arm calendar push with a missing or blank relay secret.
tooling/xtask/crates/xtask_local/src/local/calendar_push.rs::enable writes CALENDAR_WATCH_RELAY_URL before this check. When CALENDAR_WATCH_RELAY_SECRET is absent, resolve only warns and returns success, so the next stack can open Google watch channels without a subscriber. contains_key also treats CALENDAR_WATCH_RELAY_SECRET= and whitespace-only values as configured. Return an error or omit the relay variables when the secret is missing or blank.
The PR objective requires the relay to remain inactive until its required configuration is present. Based on learnings, validate required secret content instead of checking only key presence.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tooling/xtask/crates/xtask_local/src/local/env_layer.rs` around lines 115 -
126, Update the merged-environment validation in resolve so calendar push
remains inactive when CALENDAR_WATCH_RELAY_SECRET is absent, empty, or
whitespace-only. Validate the secret’s trimmed content rather than using
contains_key; on failure, return an error or remove the relay configuration,
ensuring calendar_push::enable cannot leave CALENDAR_WATCH_RELAY_URL armed
without a valid secret.
Source: Learnings
Local stacks open events.watch channels against dev's verified webhook address with a per-instance token; dev forwards non-matching tokens over Redis pub/sub to an SSE subscribe endpoint, and the local pubsub workers consume that stream and re-arm sync jobs. Graceful shutdown now stops the stack's open channels at Google (channels.stop). `just calendar-push enable` arms it per instance.
…blank A present-but-empty CALENDAR_WATCH_RELAY_SECRET previously passed the contains_key check, leaving the overlay's watch config armed so the stack opened channels nobody subscribes to. Resolve now validates the trimmed secret and strips the overlay's contribution (keeping later-layer overrides); hand-armed relay config still just warns.
A stopped channel whose watch columns survive is not fully torn down — the summary previously reported it as stopped. Kept bookkeeping means a later pass retries; channels.stop tolerates already-gone channels, so the retry converges.
…not park reconnects Only connect_timeout was set; a server that accepts TCP but never sends response headers left send() pending forever, and READ_TIMEOUT only starts once the body streams.
A permission or I/O error reading the token file was treated as a missing token, minting a replacement and orphaning channels opened under the old one. Only NotFound mints; other failures (and a corrupt empty file) now error out.
… serving from email_service The relay's serving side moves to its own dev-only service, so email_service reverts to the plain webhook handler; the wire model, SSE parsing, secret comparison, and subscriber config move to a shared crate consumed by both the tunnel service and the subscriber worker.
Stateless axum service that is the public webhook address for local stacks' watch channels: POST /calendar/notifications routes deliveries by channel token through an in-memory fan-out to secret-gated SSE subscribers on /calendar/relay/subscribe. Strays with no live subscriber are acknowledged and dropped.
ECS Fargate + ALB + Route53 A-alias at calendar-event-local-tunnel-dev.macro.com, copied from the unfurl stack. Pinned to a single task with no autoscaling because subscriber fan-out is in-memory; long ALB idle timeout for the held SSE connections. The prod stack is an empty no-op so environment-wide deploys succeed.
591fa14 to
1e82076
Compare
Makes real Google Calendar push deliverable to
run_localstacks with no tunnel daemon or DNS: a new dev-onlycalendar-event-local-tunnelservice (ECS + ALB atcalendar-event-local-tunnel-dev.macro.com, wired through services-config/nix/doppler-projects in this PR) is the public webhook address for local watch channels and fans deliveries out by channel token to secret-gated SSE subscribers; each stack's pubsub workers subscribe out and re-arm sync jobs (just calendar-push enable, recipe in docs/RUNNING_LOCALLY.md). Graceful local shutdown stops the stack's open channels at Google via the newchannels/stopadapter. Before first deploy:pulumi upthe doppler-projects stack, putCALENDAR_WATCH_RELAY_SECRETin the new project's dev config and inlocal/lcl_personal, and init themacro-inc/calendar-event-local-tunnel/dev+/prodPulumi stacks (prod is an empty no-op).