Repository navigation
Space state webhook for instant open/closed, with opt-in ingress and pixel.pixelbar.nl - #146
Open
NinjaLikesCheez wants to merge 3 commits into
Open
NinjaLikesCheez wants to merge 3 commits into
NinjaLikesCheez wants to merge 3 commits into
Conversation
The space state service can POST /hooks/space-state when the space opens or closes. Pixel checks the HMAC-SHA256 signature (X-Spacestate-Timestamp, X-Spacestate-Signature, 5-minute window) and then only re-reads SpaceAPI: nothing in the body is trusted, logged or echoed. SpaceStatus.recheck() never reuses a check already under way, and a change found that way is confirmed after 5 s instead of a whole poll, so announcements go out in about 5 s rather than 30-60. Polling is unchanged and stays the fallback. The webhook listens on its own port (WEBHOOK_PORT, 8081) only when SPACESTATE_WEBHOOK_SECRET is set, so the health probes stay private. Terraform gains spacestate_webhook_enabled (external HTTPS ingress to 8081 only, plus the Key Vault secret spacestate-webhook-secret), optional webhook_allowed_ip_ranges and the spacestate_webhook_url output. Sentry now traces only incoming /hooks/ requests. See ADR 0018. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q6YEJeThAVV24LzGijYXTB
custom_domain and bind_custom_domain put the webhook ingress on a hostname of our own with an Azure-managed certificate. pixelbar.nl's DNS is at TransIP, so it takes two applies: the first only prints the asuid TXT and CNAME records (custom_domain_dns_records); the second issues the certificate (CNAME validation) and binds the hostname once they resolve. spacestate_webhook_url then uses the hostname. Both variables are validated: a domain needs the webhook ingress, binding needs a domain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q6YEJeThAVV24LzGijYXTB
Contributor
|
Merged |
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The space state service (pixelbar/spacestate) can now call
POST /hooks/space-statewhen the space opens or closes. Pixel then reads SpaceAPI straight away instead of waiting for the next 30-second poll. Open/closed announcements go out in about 5 s instead of 30–60 s. Polling is unchanged and stays the fallback.Design: ADR 0018.
The webhook is a nudge, not a source of truth
spaceStatus.recheck(). Nothing in the body is used, logged or echoed, so a forged or replayed call can cause one extra SpaceAPI read and nothing more. The webhook and SpaceAPI can't disagree about what gets announced.recheck()never reuses a check that was already under way, since that read may have started before the change. It waits for it, then asks again. Rechecks that arrive during the same check share one follow-up.PUSH_CONFIRM_MS(5 s) instead of a whole poll. A switch flicked back sends its own webhook, which resets the wait, so flicker protection still holds.Signed requests (contract for the sender)
202accepted.401for missing, malformed, stale (more than 5 minutes either way) or wrong signatures; the log has the reason, the reply doesn't.404for other paths,405for anything but POST,413above 4 KiB.src/adapters/webhook/signature.test.ts.Config
SPACESTATE_WEBHOOK_SECRET: a new app secret, at least 32 characters. Key Vaultspacestate-webhook-secret; entry added toinfra/secrets.manifest.yaml. Unset means no listener and polling only.WEBHOOK_PORT: default8081. It is its own port, neverHEALTH_PORT, and config refuses to make them the same, so/healthzand/readyzstay private.Azure
The Container App had no ingress, so nothing inbound could reach it.
spacestate_webhook_enabled(default off) adds external HTTPS ingress to port 8081 only, withallow_insecure_connections = false, and wires in the secret. min=max=1 is unchanged.webhook_allowed_ip_ranges(optional) adds Allow rules.spacestate_webhook_urloutput: the address to give the service.Custom domain (
infra/modules/pixel/domain.tf):custom_domain(prodpixel.pixelbar.nl, dev e.g.pixel-dev.pixelbar.nl) uses an Azure-managed certificate with CNAME validation. pixelbar.nl's DNS is at TransIP, so it takes two applies:custom_domain, apply, and read thecustom_domain_dns_recordsoutput: TXTasuid.pixeland CNAMEpixel.bind_custom_domain = trueand apply.After that,
spacestate_webhook_urlbecomeshttps://pixel.pixelbar.nl/hooks/space-state. The variables are cross-validated: a domain needs the ingress, and binding needs a domain.pixel.pixelbar.nldoesn't exist yet, and pixelbar.nl has no CAA records, so nothing blocks the certificate.Sentry
Only incoming
/hooks/requests are traced now (isUntracedRequest, which replacesisHealthProbe). Probes and internet scanners on the public port don't use up the trace quota.Testing
just checkequivalents pass: Biome,tsc, and all 2157 tests with coverage floors. New strict floors coversrc/adapters/webhook/{signature,server}.ts. The secrets-manifest drift test passes.recheck()ordering and sharing, push-confirm timing and the flick-back case, and config validation.terraform fmt -checkandvalidatepass for bootstrap, dev and prod (Terraform 1.16.5, locked azurerm 5.8.0, lockfiles unchanged).terraform testruns with a mocked provider checked ingress, env and secret wiring, both custom-domain phases, the URL switch-over, and the validation errors. I didn't commit them, because CI doesn't runterraform test.SpaceApiStatusagainst a fake SpaceAPI, using the runbook'scurlsnippet: wrong secret gives 401 pluswebhook.rejected bad-signature; the signed call gives 202 andspaceapi.changed via: push, with polling set to once an hour;/healthzon the public port gives 404.Rollout (nothing applied)
openssl rand -hex 32→.env.<env>asSPACESTATE_WEBHOOK_SECRET→just secrets-push <env>.spacestate_webhook_enabled = true(optionallycustom_domain) in tfvars →just push <env>→just tf-plan <env>. A maintainer applies; prod needsPIXEL_APPLY_PROD=1.bind_custom_domain = trueand apply.The sending side isn't in this PR.
pixelbar/spacestatestill has to send the webhook after a state change that actually changed something. Until it does, nothing changes: Pixel polls. Also,spaceapi.pixelbar.nlis still served by the old setup, so until it's cut over, a webhook can arrive before SpaceAPI shows the change, and the next poll catches it.Review notes
core/access*,core/dispatcher*, etc.)..env.example, example tfvars, env READMEs, and ADRs 0004/0009 marked as amended by 0018. CHANGELOG entry under Unreleased → Added.Generated by Claude Code