Skip to content

chore(release): pending release v0.85.0#1156

Merged
marcusrbrown merged 14 commits into
releasefrom
next
Jul 11, 2026
Merged

chore(release): pending release v0.85.0#1156
marcusrbrown merged 14 commits into
releasefrom
next

Conversation

@fro-bot

@fro-bot fro-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Pending Release: v0.85.0

This PR tracks changes pending release. Released on the next auto-release cycle (Sunday/Wednesday) or via manual dispatch.

Merge this PR to trigger a release. Releases also run automatically on Sunday/Wednesday at 20:00 UTC, or via manual workflow dispatch.

Commits Since Last Release


Auto-generated by the release pipeline. Updated: 2026-07-11 00:19 UTC

)

* chore(gateway): remove stale planning taxonomy from config comments

Strip internal unit/requirement identifiers (leftover planning-doc
references) from gateway config comments and test titles; the comments
now describe behavior directly.

* feat(gateway): add operator push contract and VAPID config foundation

Add the Gateway-side operator Web Push contract DTOs (VAPID public key,
subscription metadata, subscribe/unsubscribe requests) and the VAPID
configuration surface, disabled by default. No dispatch, routes, or
web-push dependency yet.

The VAPID private key is validated by fixed-reason validators and held as
a non-enumerable config property so it never lands in a serialized config
object or a thrown error. The operator contract version is intentionally
unchanged: push adds an additive REST surface that the dashboard SSE
drift gate does not observe, so the exact-match contract version stays put.

* fix(gateway): harden operator-push config parsing and add contract-guard tests

* fix(gateway): reject addressless VAPID subject and identical rotation keypair
@fro-bot fro-bot Bot requested review from fro-bot and marcusrbrown as code owners July 9, 2026 17:36
Co-authored-by: fro-bot[bot] <109017866+fro-bot[bot]@users.noreply.github.com>
* feat(gateway): add durable operator push subscription store

Add the Gateway-owned subscription record store: one durable record per
browser endpoint, bound to one operator, with endpoint/p256dh/auth held as
write-only secret fields never returned by any listing or export surface.

Ownership transfer between operators sharing a device is a single atomic
compare-and-swap that bumps a monotonic ownership generation, and the
dispatch path re-validates ownership immediately before send so a stale
read cannot deliver one operator's notification to another.

Privacy delete writes a secret-free tombstone at a separate key and
physically removes the subscription object; the active-read path excludes
any tombstoned endpoint, so restoring a subscription object alone cannot
resurrect a deleted record. A startup compare-and-swap self-test fails the
push surface closed on a store without real conditional-write semantics.

* fix(gateway): harden subscription store ownership, delete safety, and error classification

- markDead now enforces the record owner, closing a cross-account
  deactivation path before the dispatch layer wires it.
- deleteForOperator only counts a record deleted once the object is
  physically gone, rolls back its tombstone if the record was transferred
  mid-delete, and reports skipped records so a partial privacy delete is
  observable rather than silent.
- Error classification checks structured object-store error fields
  (status/code) before falling back to message matching, so a CAS conflict
  or missing key can't be misread from error wording.
- deactivateForOperator reports skipped records on retry exhaustion; the
  startup self-test key moves outside the subscription scan prefix; and
  single-clock capture keeps updatedAt and deactivatedAt consistent.

* fix(gateway): roll back delete tombstone whenever the raced record stays active

Widen the lost-physical-delete rollback so it fires whenever the re-read
record is active, not only on an ownership change. A same-operator
re-subscribe that won the delete race left an active record shadowed by
the just-written tombstone until a later prune; the invariant is that a
tombstone exists only when the endpoint has no active record for any
operator. Add a same-owner race regression test, an eventual-prune
reclaim test, and a transfer-clears-tombstone test, plus comments noting
the direct-scan paths deliberately skip the read-side tombstone filter.
Co-authored-by: fro-bot[bot] <109017866+fro-bot[bot]@users.noreply.github.com>
- Added 'Gateway Image Smoke Test' and 'Workspace Image Smoke Test' to the list of required status checks.
- Modified 'Test' job to only depend on 'setup'.
- Adjusted 'Release' job dependencies to include only 'lint', 'test', and 'build'.
…d self-test (#1160)

* feat(gateway): add authenticated operator push routes behind a fail-closed self-test

Register GET /operator/push/vapid-key, POST /operator/push/subscriptions,
POST /operator/push/subscriptions/unsubscribe, and GET
/operator/push/subscriptions as privileged operator routes, mounted only
when push config is present and the subscription store's startup
compare-and-swap self-test passes. A failed self-test disables the push
surface without crashing gateway startup.

Subscribe validates the browser endpoint URL against an SSRF-conservative
posture (HTTPS only; no loopback, private, or link-local hosts; no bare
local names) before any value reaches the store, and never echoes the
endpoint in a response or log. Mutations carry the same CSRF, origin, and
rate-limit posture as the launch and cancel routes, and every denial
returns the same no-oracle response.

* fix(gateway): close push endpoint SSRF gaps, harden self-test gate, cap subscriptions

- validate-endpoint.ts: reject IPv4-mapped IPv6 loopback/private/link-local/
  unspecified addresses in both dotted and WHATWG hex-compressed form,
  0.0.0.0, IPv6 ::, and trailing-dot FQDNs (metadata.google.internal.);
  document the parse-time-only boundary vs DNS rebinding
- program.ts: replace Effect.promise with Effect.tryPromise around
  pushStore.selfTestCas() so a throwing adapter degrades to the same
  fail-closed 'push disabled' path as a Result-typed self-test failure,
  instead of crashing the whole gateway at startup
- subscription-store.ts: wrap the self-test cleanup getObject read in
  try/catch so a throw during cleanup cannot turn a successful self-test
  into a rejected promise
- add parseOperatorPushUnsubscribeRequest contract guard, export it from
  the operator-contract barrel, and switch subscription-route.ts to use it
  in place of the local parseUnsubscribeBody
- subscription-route.ts: add a per-operator active-subscription cap
  (MAX_ACTIVE_SUBSCRIPTIONS_PER_OPERATOR = 20), exempting re-subscribes of
  an endpoint the operator already owns
- add coverage: hardened SSRF rejection/pass cases, program.ts self-test
  success/failure/throw/absent branches, subscription-route store-error
  and unauthenticated-unsubscribe paths, and per-level (info/warn/error/
  debug) endpoint-non-logging assertions
- fix a pre-existing program.test.ts type error (selfTestCasMock's return
  type inferred as always-success, rejecting the failure-branch fixture)

* fix(gateway): reject all IPv6-literal push endpoints, soften subscription cap wording

* refactor(gateway): make IPv6 endpoint classifier total, drop dead guards
Co-authored-by: fro-bot[bot] <109017866+fro-bot[bot]@users.noreply.github.com>
Co-authored-by: fro-bot[bot] <109017866+fro-bot[bot]@users.noreply.github.com>
…rd (#1162)

* feat(gateway): add web-push dependency and guarded push sender adapter

* feat(gateway): add push payload builder, trigger policy, and dedupe cache

* feat(gateway): add push dispatcher with linearizable transfer check

* fix(gateway): harden push dispatch — exhaustive relay switch, per-operator dedupe, SSRF label fix

* build: regenerate third-party notices for web-push dependency tree

* build: sync dist bundle chunk hashes after dependency updates

* fix(gateway): handle autoSelectFamily all:true in push SSRF lookup guard
…logout (#1163)

* feat(gateway): add listAllActiveRecords to push subscription store

* refactor(gateway): reshape push dispatcher to broadcast to all active subscriptions

* feat(gateway): dispatch operator push on pending approvals, failed runs, and logout

* fix(gateway): guard push dedupe on broadcast failure, surface revoke deactivation gaps

* chore: remove stray review artifact and ignore root output.json
…1164)

* chore: strip plan-unit taxonomy from source comments and test labels

* chore: correct approval-detail comment and restore cancel.ts docblock indent
Co-authored-by: fro-bot[bot] <109017866+fro-bot[bot]@users.noreply.github.com>
marcusrbrown and others added 2 commits July 10, 2026 17:17
… audit events (#1165)

* feat(gateway): add coarse audit events for the operator push lifecycle

* feat(gateway): emit push audit events on subscribe, dispatch, deactivate, and disable

* docs: add operator push privacy/retention policy and surface documentation

* test(gateway): harden push audit tests — assert event absence on denied paths, real key-leak check

- dispatcher.ts: add default/never exhaustiveness guard to the broadcast outcome tally switch
- dispatcher.test.ts: replace tautological 'auth-value' substring check with distinctive endpoint/p256dh/auth secret values that actually catch a leak into logs or audit events
- subscription-route.test.ts: assert push.subscribed/push.unsubscribed audit events are absent on all denied subscribe/unsubscribe gate paths
- program.test.ts: assert push.subscription.deactivated is absent on the skipped (updated=0) and failed deactivation paths
- server.ts: document that the push route mount guard requires auditLogger, guaranteeing every push route action is audited

* test(gateway): pin all-skipped broadcast emits a zero-count dispatch event
@marcusrbrown marcusrbrown merged commit 802b679 into release Jul 11, 2026
1 check passed
@marcusrbrown marcusrbrown deleted the next branch July 11, 2026 00:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant