release: dev → main (21 commits — billing waves 0–5, deployments, auth, agent security) - #86
Draft
dylanroscover wants to merge 25 commits into
Draft
release: dev → main (21 commits — billing waves 0–5, deployments, auth, agent security)#86dylanroscover wants to merge 25 commits into
dylanroscover wants to merge 25 commits into
Conversation
…keyed quota
- customers/{uid} doc shape (Customer type, TRIAL_LENGTH_DAYS=14, doc factory);
minted with a live 14-day trial clock in bootstrapUser (created-branch only,
idempotent, never fails a signup)
- pure billing-state resolver (trialing/active/expired/canceled) with the
trialEndsAt:null pre-go-live sentinel and fail-open posture on corrupt data
- scripts/backfill-customers.mjs (idempotent, dry-run; not executed anywhere)
- functions quota model migrated off free/starter/pro/enterprise to the site
tier (core: 0, pro: 1 TiB); roost on core now 403s tier_insufficient;
readTier now reads the tier field the dashboard reads (old code read a
never-written plan field)
- roadmap billing line updated for the unified 14-day no-card trial
Billing entity is the site-owner user (no org entity exists). Verified: web
tsc clean + 2763 jest green; functions build + 242 node:test green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
stripe@22.4.0 — official typed SDK per the billing plan; wraps land in web/lib/stripe.server.ts with wave 1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…billing gates - createSite derives tier from the owner's billing state (trialing→pro, active→subscriptionTier??pro); subscriptionTier added to Customer; tierUpgradedAt on site docs + API projections + openapi - web quota surfaces unified on the functions model: quota route + whoami (a third hand-copied mirror) now read TIER_STORAGE_BYTES from siteTier.ts; roostAvailable replaces unlimited; dead unlimited-plan branch removed - billingGate.server.ts: requireActiveBilling (402 trial_expired) / requirePro (403 tier_insufficient) / siteTierOrThrow over an acyclic billingSnapshot leaf; api-key surface gated at all four _shared.ts scope resolvers (session auth untouched — dashboard stays readable when locked); error catalog entries in errors.mdx + openapi problem responses - known follow-up (task 0.8): cli/sdk quota typings still expect unlimited Verified: tsc clean; 163 suites / 2820 tests green; validate:api passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ook (waves 0.6/0.8/1.2-1.4)
- 13 pro-only API routes gated ({requirePro:true} at scope resolvers;
account-scoped requireProAccount for api-key creation); control-plane
lockout central in authorizedSiteHandler via BILLING_LOCKED_CAPABILITIES
(mutations only — viewing, bill-reduction and security actions stay open);
billing_locked deny audits
- stripe.server.ts lazy singleton (mode from key prefix, 10s timeout, key
never logged) + linkStripeCustomer on first signup (metadata.uid contract,
email-list adoption rules, zero network while unconfigured) + backfill
script (not executed)
- POST /api/billing/portal (session-auth, owner-only) + lowercase "manage
billing" row in account settings
- stripe webhook: signature-verified fail-closed, claim-first event dedupe,
status normalization + price→tier mapping per plan's pinned decisions,
transactional customer writes, invoice mirror, secret-free logging
- cli + node/python SDK quota typings aligned to roostAvailable (breaking —
rc bumps owed before any 1.0.0 final)
- chunking 10k perf smoke given an explicit 180s budget (was the suite's one
load-sensitive flake, failing --bail pre-commit runs on green code)
- env-manifest: 7 STRIPE_* keys registered (metadata only)
- pre-commit hook jest timeout 90s→300s (user-approved; suite outgrew it)
Verified: tsc clean; validate:api 13 warnings (delta 0); 166 suites /
2968 tests green; cli + node sdk + python sdk suites green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
393565b's message claims the webhook handler landed; it did not. The harvest command chained the new-file copies behind a patch apply that errored, the && chain aborted before the copies ran, and the omission was masked by hand-fixing only the visible failure (env-manifest). The source worktree had already been cleaned up, so the five files were re-emitted by the original agent from its transcript (replay of its Write/Edit payloads, cross-checked against edit markers, test counts, and export counts) and re-verified here: tsc clean, 6 billing suites / 162 tests green against the real stripe.server.ts from wave 1.2. Contents are as originally reviewed: signature-verified fail-closed route, claim-first event dedupe ledger, status normalization + price→ tier mapping per the plan's pinned decisions, transactional customer writes, invoice mirror, secret-free logging. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lling tab
- checkout (2.1): tier-picker dialog + subscription-mode Stripe Checkout;
metered line items (no quantity), payment_method_collection always,
metadata {uid,tier} on session + subscription; entitlement-write-free
(webhook remains the single writer, test-pinned); roost pro-gate CTA
now opens the dialog
- trial lifecycle cron (2.2): /api/cron/billing-trial-lifecycle — expiry
flips via resolveBillingState, day-10/13/expiry reminder emails through
the existing resend transport (latest-due-wins, send-before-stamp),
post-expiry alert cutoff at +30d cleared instantly on conversion; one
customers read per owner in the health-check send loop
- usage metering (2.3+2.4): /api/cron/billing-usage-report — Stripe
Billing Meters v1 (legacy usage records verified REMOVED from stripe@22);
three meter event names, daily snapshots (meters must aggregate 'last'),
max(3,n) pro minimum, GiB-floored pro-only storage overage, dual-layer
idempotency (firestore ledger + deterministic identifier)
- billing tab (2.5): account-settings billing section — trial countdown,
per-site projections, storage bars, choose-a-plan/manage-billing; fed by
session-auth'd GET /api/billing/snapshot (admin-side reads — NO
firestore.rules change); wire contract in lib/types/billingSnapshot.ts
leaf; pricing constants centralized in lib/billing/pricing.ts
- docs: scheduled-endpoints table + checklist now cover both billing crons
(registration on cron-job.org is a ship-time step)
Verified: tsc clean; validate:api 13 warnings (delta 0); 178 suites /
3246 tests green; all 21 agent-created files audited present pre-commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aces - trial banner (3.1): announcement (config/billing goLiveAt, set at 5.1) / final-week countdown / non-dismissable expired state; read-only notice beside the machines list (signage only — metrics stay visible per the lockout matrix); dismissal in devicePrefs (7-day window), owner-scoped, renders nothing when absent - pro-tier gates (3.2): ProTierGate card+inline variants across roost, api-keys page, account-settings api section, webhooks page; core accounts with existing keys/webhooks keep list + revoke/rotate fully visible — only create affordances gate (revocation is a security action; matches the server's deliberately untier-gated GET/DELETE) - billing warning (3.3): X-Owlette-Billing-Warning on api-key responses while trialing (silent pre-go-live), emitted at the existing advisory- header sink; 6 routes that never attached advisories now do; CLI warns once per invocation on stderr; node + python SDKs expose an optional callback (additive, minor) - /api/keys POST now carries the same requireProAccount gate as its public sibling — it was the ungated second door into key creation (3.2 finding); wiring pinned by tests, GET deliberately stays open for audit Verified: tsc clean; 186 suites / 3354 tests green; validate:api 13 (sequential after jest — a jest fixture creates a transient route dir, so the two must never run concurrently); cli 9/9 new + build clean; node sdk green; python sdk 75 green incl. 6 new. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, docs; go-live minting gate - admin billing (4.1/4.2): /admin/customers (extend trial / comp / force- expire, confirm dialogs) + /admin/billing ops dashboard (state+tier counts, MRR floor from usage mirrors, conversion, storage top-list); three superadmin routes behind authorizedPlatformHandler; new BILLING_MANAGE capability + billing_mutated audit kind (web + functions); extension anchors at max(end, now), re-arms reminder markers consistently with the cron's planner, clears the alert mute; comp carries falsifiable provenance (compedTier/At/By/Note) - health alerts (4.3): canceled / payment-failed / past-due-transition emails to owner + ops (ADMIN_EMAIL convention), per-kind 24h cooldown stamped only after a successful send; alert failure never fails webhook processing - docs (4.4): customer billing page (dashboard/billing.mdx, honest about unshipped storage metering) + docs/internal/billing-runbook.md (state resolution, failure modes, env map, gap table) - go-live minting gate (4.4's audit finding, fixed): bootstrapUser now mints the null trial sentinel until config/billing.goLiveAt has passed — a pre-go-live signup can never resolve expired while checkout is unavailable; post-go-live signups self-start; runbook rows reconciled Verified: tsc clean; 192 suites / 3500 tests green; validate:api 13 (sequential after jest); functions build + 242 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ve script
- webhook delivery gating (2.6): fireWebhooks pauses for expired/canceled
owners (state-named debug log, no failCount creep, fail-open on read
errors, per-run memoized cache); functions retry pump gated via a
mirrored billingLogic resolver — paused records stay byte-identical so
conversion resumes them next tick; dead emitWebhook fan-out documented,
not gated; tier deliberately not consulted (paid core keeps webhooks)
- core one-site limit (2.7): createSite rejects a second site for
subscribed-core owners with the tier_insufficient shape ("core includes
one site — upgrade to pro for unlimited sites"); dialog error path
verified to surface it verbatim; trialing/pro unaffected
- billing-go-live.mjs (5.3 tooling): announce / stamp / status modes with
refusal rails (missing customers docs, unset/future goLiveAt), goLiveAt-
anchored clocks, idempotent-by-construction stamps, wouldExpire warning
for late runs; NOT executed anywhere; BETA_DEFAULT_TIER constants
deliberately untouched until the stamp actually runs (T0 deploy step,
with the billing-golive-copy branch)
- 61 script tests incl. drift guards cross-checking the .mjs hand-mirrors
against their TS sources; runbook + customer docs updated (delivery row,
site-limit note, go-live invocations)
Verified: tsc clean; 193 suites / 3586 tests green; validate:api 13;
functions build + 256 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…yments Agents 2.11.1+ (9dccd12) refuse install_software commands without sha256_checksum, but the dashboard never collected one — every UI-created deployment failed on up-to-date fleet machines. The server now streams the installer from its URL at authoring time and pins the SHA-256 automatically; nobody hashes files by hand. - computeInstallerChecksum action: streaming hash, 10 GiB cap, SSRF guard (webhook validator) with per-redirect-hop re-validation - routes: POST /api/sites/{siteId}/deployments/checksum (DEPLOYMENT_MANAGE) + POST /api/platform/installer-checksum (SYSTEM_PRESET_MANAGE) - useInstallerChecksum hook + InstallerChecksumStatus row shared by DeploymentDialog and SystemPresetDialog: debounced auto-compute, stored checksum adoption, manual-entry fallback (reachable from computing/error states), deploy/save blocked until a checksum exists - sha256_checksum persisted through deployment templates + system presets (types, hooks, actions with 64-hex validation, route serializers); legacy docs self-heal on next save - openapi: document the agent-side refusal on the public deployments API (field stays optional there — no CLI/SDK contract break) - tests: compute-core suite (redirect re-validation, private-ip rejection, size cap, abort mapping) + checksum cases in template/preset action tests; e2e specs updated to the required-checksum flow and now assert the checksum lands on the deployment doc and install command Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ialog UX
The dashboard's "retry failed" never called the retry API — it cloned a new
"<name> (Retry)" deployment without a checksum, so it failed on every agent.
Retry now goes through POST .../deployments/{id}/retry and retries IN PLACE:
the same deployment record flips to in_progress and failed targets reset to
pending, instead of accumulating duplicate clone records.
- retry route self-heals legacy deployments: computes the sha256 from the
installer url, pins it on the doc, stamps it into every re-issued command;
a compute failure returns the error instead of queuing commands agents
would refuse
- retry route accepts an optional `machines` filter (validated) so a single
failed target can be retried on its own
- per-target retry icon on failed rows (spinner + disabled while in flight),
backed by the new useDeployments.retryDeployment(deploymentId, machineIds?)
- silent flags: single-line Input -> auto-growing Textarea in both the deploy
and system-preset dialogs; typed/pasted newlines collapse to spaces so the
stored value stays a valid single command line
- fix invisible "in progress" status pill (only status with no color mapped,
fell through to a transparent default) -> cyan, matching its spinner icon
- tests: 5 retry-route cases (machines filter, self-heal, stored-checksum
passthrough, compute failure, malformed filter); retry e2e spec rewritten
for the in-place contract + a per-row-icon test; api-sprint retry seed
pinned so it stays network-free
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A prospective customer reached owlette.app/register from LinkedIn's iOS in-app browser, tapped "continue with Google", and got auth/popup-blocked (Sentry OWLETTE-WEB-45) — surfaced as two stacked toasts both reading "An error occurred. Please try again", then nothing. Signup lost. Federated Google sign-in cannot work in an embedded webview and no configuration changes that: iOS WKWebView returns null from window.open() unless the HOST app opts in, and Google's OAuth endpoint independently rejects embedded user-agents (disallowed_useragent). signInWithRedirect is not a fallback — it would turn a loud error into a silent hang, since our authDomain is *.firebaseapp.com while we serve from owlette.app, the exact cross-origin case WebKit storage partitioning breaks. So the fix is UI-side: - detect the host app (inapp-spy, MIT, zero deps) and pre-empt the Google button with inline remediation before a tap is wasted on it - keep a reactive branch on auth/popup-blocked for webviews the UA doesn't identify, and for ordinary browsers with a popup blocker - force-expand the email fallback the notice points at, rather than leaving it collapsed behind a focus gesture - offer a best-effort escape (x-safari:// on iOS, intent:// on Android) plus copy-link, always alongside manual "open in browser" instructions — the iOS scheme is undocumented and already dead in several host apps, so it is a shortcut, never the mechanism - stop the double toast: AuthContext now rethrows these codes silently so the page owns the messaging - preserve the Sentry signal with the raw user-agent attached; the parsed browser family collapses every unrecognised iOS webview to one label that does not name the host app, so this was previously unmeasurable Also hides the passkey button inside a detected webview: the ceremony can only use passkeys for the HOST app's associated domain, so browserSupportsWebAuthn() reports true there while sign-in cannot succeed. Three defects found by adversarial review and fixed here: - an iOS Home Screen web app drops the Version/ and Safari/ tokens exactly as a webview does, so inapp-spy's catch-all iOS rule matched our own installed PWA — it would have been told "google sign-in doesn't work in this in-app browser" inside its own window and lost its passkey button. Guarded on navigator.standalone, and only when no host app was named so a positively identified LinkedIn is still detected. Deliberately NOT display-mode:standalone — chrome-less webviews match that media query. - the "open in browser" button was rendered on the popup-blocked path in ordinary browsers, where it resolved to a same-URL reload that dropped the notice and returned the user to the button that had just failed. Escape affordances and the ••• instructions are now gated on actually being in a host app. - copyToClipboard could orphan a focusable off-screen textarea if the legacy execCommand path threw; removal moved to a finally. Verified in an isolated worktree: tsc clean, eslint clean, 197 suites / 3647 tests pass, production build exit 0. Detection asserted against the verbatim production user-agent plus real-Safari, desktop, Android and standalone-PWA negatives. Known open: Turnstile gates the email fallback we now promote. Whether it renders inside LinkedIn's webview is unverified and needs a device test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported as "google sign-in is stuck on dev": authenticate in the popup,
end up back at /login?redirect=%2Fdashboard, nothing else happens.
Google OAuth signs up and signs in through the same popup, so the sign-in
had actually SUCCEEDED — a user who already had an account was simply
logged in. /register then pushed to /dashboard the instant the popup
resolved, with no wait for the __session cookie that AuthContext mints
asynchronously from onAuthStateChanged and does not await. proxy.ts saw an
unauthenticated request and bounced to /login?redirect=%2Fdashboard, which
is exactly what the user saw.
/login already had a settle-and-poll guard for this. /register never got
one — the two pages drifted, and that drift is the whole bug. So:
- lib/postSignIn.ts now owns the landing resolution and is shared by all
four sign-in paths (login email / google / passkey, register google).
The passkey path passes settleMs 0: its verify route mints the cookie
before returning, so settling there is dead latency on the one flow whose
appeal is feeling instant.
- signInWithGoogle returns { isNewUser } from Firebase's
getAdditionalUserInfo, so /register can say "welcome back — signing you
in" instead of congratulating people on an account it did not create. An
absent flag is read as "returning": a generic greeting is a cheaper error
than claiming a signup that never happened.
- auth/email-already-in-use on the email path renders inline with routes to
sign in or reset the password, instead of a toast that expires and leaves
the user on a form that cannot succeed. AuthContext stays quiet for that
code (the page owns the message) but still reports it — that reporting is
how we learned real users hit this.
- ui/inline-notice extracted so the two inline notices cannot drift the way
the two pages did; InAppBrowserNotice moved onto it.
This also fixes brand-new Google signups, which were racing the same cookie
and which nobody had noticed. The comment claiming "AuthContext will
redirect to /setup-2fa for new users" was stale — dashboard/page.tsx does
it, so a new user has to reach /dashboard first, and losing the race sent
them to /login instead. The email signup path had the same race against
proxy-protected /setup-2fa (already worked around in e2e as a known flake)
and is now resolved too.
Verified in an isolated worktree: tsc clean, eslint clean, 199 suites /
3660 tests pass, production build exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
OWLETTE-WEB-46: a real user signed up, reached the mandatory 2FA step, hit "cancel", and landed on /register — the form they had just submitted. Being authenticated but staring at a blank signup form, they concluded the account had not been created, filled it all in again, and got auth/email-already-in-use with nowhere to go. Three things had to line up: - /setup-2fa's cancel was `router.back()`. For a brand-new signup the previous history entry is /register by construction, so cancel always led there. - proxy.ts does bounce an authenticated user off /register, but only on a server request. A client-side history pop replays the page out of the App Router cache with no round trip, so the proxy is never consulted — the breadcrumbs show no /register RSC fetch after the back navigation. - /register had no client-side guard of its own, so it rendered the form. So: - /setup-2fa cancel now picks a destination instead of guessing from history. When setup is mandatory (`requiresMfaSetup`, set at bootstrap for every new signup) /dashboard is a loop, not an exit — dashboard/page.tsx pushes straight back — so the honest way out is ending the session, and the button says "sign out" rather than pretending otherwise. A voluntary visit from account settings goes to /dashboard, matching what "finish" already does. The page's unauthenticated effect now replaces rather than pushes: leaving /setup-2fa one back-press away is the same trap in miniature. - hooks/useRedirectIfAuthenticated mirrors the proxy rule client-side, shared by /login and /register so the two cannot drift again. It skips while the page is itself signing someone in — signUp() populates `user` before the handler has resolved where to send them, and the guard would otherwise beat the /setup-2fa push. Latched in a ref, not `loading`, which clears in the handlers' `finally` while the push is still in flight. - On /login the guard reads the `redirect` param during render rather than the state synced by an effect: effects flush in declaration order, so the state version fires once against the '/dashboard' default and a signed-in visitor to /login?redirect=%2Froost would land on the dashboard instead. Param validation is now one `safeRedirect` helper shared by both readers. Verified the two "does not pre-empt" tests are not vacuous by removing the skip: both fail with spurious replace() calls, then pass once restored. eslint clean, tsc --noEmit clean, 201 suites / 3672 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clears three Dependabot advisories against the agent's pinned cryptography 44.x: CVE-2026-69247 (PKCS#7 EnvelopedData Bleichenbacher oracle), CVE-2026-69249 (exponential cert path-building DoS), and CVE-2026-69248 (wildcard DNS escaping permittedSubtrees). None are reachable from owlette. The agent's entire cryptography surface is Fernet (secure_storage.py) plus hashes/AESGCM/HKDF (auth_manager.py); it never calls the PKCS#7 decryption or x509.verification routines the advisories cover, and no transitive dependency does either. Verified before landing: - cp311-abi3-win_amd64 wheel exists; full requirements set resolves clean - Fernet, HKDF and AESGCM round-trip bidirectionally between 44.0.3 and 50.0.0, so existing .tokens.enc files keep decrypting and rollback is safe - secure_storage and auth_manager import and exercise correctly on the bundled 3.11 interpreter with the Rust/PyO3 bindings initialized The wheel also moves its statically-linked OpenSSL 3.4.1 -> 4.0.1. CPython's ssl module links its own OpenSSL independently, so TLS for requests/urllib3 is unaffected. requirements.txt is a build-time input, so 2.12.19 must be rebuilt for this to reach the fleet; the existing dev artifact still bundles 44.0.3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cryptography, claude-agent-sdk and sentry-sdk were the only entries in requirements.txt declared as ranges rather than exact versions, so every installer build re-resolved them against whatever PyPI had that day. Two builds of the same commit could ship different binaries with no record of which was which, which is why the 2.12.19 rebuild could not be diffed against the artifact already on dev. Pinned to exactly what the verified build resolved. The rebuilt payload is byte-for-byte identical in package set to the pre-pin build (79 packages), so this freezes the tested state rather than changing it. Also documents why claude-agent-sdk sits at 0.1.69 while 0.2.132 is current: 0.2.x requires mcp>=1.23.0, and every mcp from 1.23.0 on requires pywin32>=310, which our pywin32==306 pin forbids. Under the old range pip silently backtracked 63 releases and reported success; the pin makes that visible instead. Raising pywin32 is what unblocks the upgrade. This is a partial fix. 55 transitive dependencies remain unpinned, so builds are still not fully reproducible; a generated lockfile is the real remedy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The embedded interpreter's python311._pth enables `import site` so that pip works, which also puts this machine's user site-packages on sys.path for every python and pip call in the build. The build machine's unrelated packages therefore participate in dependency resolution: the 2.12.19 build log reported conflicts against fastapi, gradio and huggingface-hub, none of which ship in the installer, and `pip list` against the built tree described 160 packages when the real payload is 79. Setting PYTHONNOUSERSITE=1 for the whole script drops the user-site entry from sys.path (verified: 160 visible packages -> 77). build_installer_quick needs no change; it reuses the tree rather than invoking pip. Also corrects the 2.12.19 changelog date in both changelogs. The entry was dated 2026-07-15, when the work was finished, but the release did not actually happen until today - the installer API recorded it at 2026-08-07T15:24:22Z. Keep a Changelog dates entries by release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hero led with "attention is all you need" — a transformer-paper riff that read as brand voice but never named a problem the visitor has. The subhead beneath it was a rotating verb/noun animation that listed features rather than stating a value. - hero h1 -> "never miss a beat"; static subhead -> "owlette keeps your installations running 24/7". "installations" is the operator's own noun for exhibits, signage, kiosks and show rigs, and naming owlette gives the sentence a subject instead of leaving a dangling participle. - TAGLINE -> "keep your installation running": a standalone brand line for the login card and /for-ai.json, which render it with no subhead nearby to supply context. - page/OG/Twitter titles -> "owlette — keep every windows machine running", carrying the search keywords the h1 gives up. Descriptions realigned to SUMMARY so the hero, search results, /llms.txt and /for-ai.json all open on the same clause. - specs retargeted to the new copy; the rotator's reserved subhead height is gone, so the hero no longer holds 3em of empty space. RotatingWord is now unreferenced. Left in place pending a decision on whether it returns for per-load headline rotation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ch devices Touch support and reduced-motion were read in lazy useState initializers guarded with `typeof window`. A lazy initializer runs during the client's FIRST render — which is the hydration render — so on any touch device it returned true where the server had returned false. The component then rendered its static branch against server HTML containing the animated branch, and React discarded and re-rendered the subtree (error #418) on every mobile visit to the landing page. Desktop was unaffected. Both capabilities now come from useSyncExternalStore with a getServerSnapshot of false. That snapshot is used for SSR *and* for hydration, so the first client pass matches the HTML exactly and React re-renders with the real values only once hydration has committed — without the post-mount setState the original comment was avoiding. Reduced-motion additionally subscribes to matchMedia's change event, so toggling the OS setting now takes effect live rather than only on reload. Verified in-browser: 320x568 and 390x844 with touch emulation both go from 1 console error to 0 and still render the static branch; 1280x900 stays on the animated mouse-following branch, also clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One headline can only say one thing, and repeat visitors saw the same line every time. The hero now rolls one of nine curated phrases per request — "never miss a beat", "always watching", "never blink", "eyes on everything", "no blind spots", "it doesn't sleep", "awake at 3am", "the night shift", "always the first to know". The phrase is chosen in the page's server component and passed to HeroSection as a prop. Choosing inside HeroSection would roll once on the server and again during hydration, giving React two different strings for the same node; page.tsx already awaits headers(), so it renders per request and the phrase genuinely re-rolls rather than freezing into a prerendered page. Typed as HeroHeadline, so only a curated phrase compiles. lib/heroHeadlines.ts is the single source of truth, imported by both the component and e2e/helpers/landing.ts, which builds the specs' match pattern from it — a hand-copied list in the specs would silently stop covering phrases added later. The hardcoded `<br className="sm:hidden" />` is replaced by text-balance: it was tuned to break "never miss / a beat" and would have split most of the other phrases in the wrong place. Scope is the <h1> only. TAGLINE, the page title, and the OG tags stay fixed so the machine-readable surfaces don't churn. Verified across 320/360/390/430 with touch emulation: no headline or document overflow for any of the nine phrases, and server-rendered HTML matched the hydrated DOM 6/6 across separate requests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment added in 8dd3db6 was wrong in two ways, and both understated the problem. A 12-way review caught it; the corrected facts are verified against PyPI metadata directly: - The pywin32 boundary is mcp 1.11.0, not 1.23.0. Every mcp from 1.11.0 on requires pywin32>=310, so pywin32==306 freezes mcp at 1.10.1 - the last release with no pywin32 dependency - not somewhere in the 1.22.x range. - The binding constraint appears at claude-agent-sdk 0.1.70 (mcp>=1.19.0), not at 0.2.x (mcp>=1.23.0). 0.1.70 is already blocked; this was never a 0.2.x-only problem. Also records two facts a future reader needs before acting on it: pywin32 exactly 310 is sufficient (311 is only required by mcp 2.0.0, which claude-agent-sdk 0.2.132 caps out of range), and the upgrade adds roughly 42 MB to the installer because the SDK vendors the Claude CLI - 0.1.69 carries CLI 2.1.121, 0.2.132 carries 2.1.224. Comment-only change; the pins and the resolved dependency set are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| } catch (error) { | ||
| // Fail open — see the module header. | ||
| console.error( | ||
| `[webhooks] billing lookup failed for site ${siteId}; delivering anyway:`, |
| } catch (error) { | ||
| // Fail open — see the module header. | ||
| console.error( | ||
| `[webhooks] billing lookup failed for site ${siteId}; delivering anyway:`, |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
…ss real Two defects in step 5, both of which reached every installed machine. --ignore-installed skipped pip's uninstall step, so the pinned versions were written over get-pip's bootstrap while the bootstrap's .dist-info survived. The 2.12.19 payload shipped two setuptools dist-infos (75.6.0 + 83.0.0) and two packaging (23.1 + 26.3), with 36 files present on disk that belong only to the newer RECORD. importlib.metadata then resolved by directory scan order, so a scanner on a fleet machine could read the newer, patched version while the older, vulnerable code was what actually ran - a false negative in exactly the "operators who run vulnerability scanners" case 2.12.19's changelog invokes. The flag was pure downside: step 1 wipes build/ every run, so there was never an installed tree to ignore. Verified: payload goes from 79 dist-info dirs / 77 unique to 77 / 77, with no duplicates, and the build log now shows "Successfully uninstalled packaging-26.3" where it previously orphaned it. Second, GPUtil has never published a wheel, so --only-binary=:all: failed on every build and the "retry with source builds" fallback was the real install - meaning arbitrary source builds were permitted for all 77 packages and the warning had become background noise. Naming GPUtil as the single --no-binary exception makes the pass succeed first time and restores the guarantee for the other 76: a missing wheel now fails the build instead of widening it. pip check still reports one genuine conflict (wheel 0.47.0 wants packaging>=24.0 against our packaging==23.1) and is still swallowed as non-fatal. Addressed separately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…urllib3 Removes google-auth, google-api-python-client and google-auth-oauthlib. They are leftovers from the firebase-admin removal: no agent code imports google.* anywhere in the repo, nothing else declares a dependency on them (the only match across our tree is sentry-sdk's google-genai, under an extra we do not install), and the only remaining references are stale PyInstaller hiddenimports that also name firebase_admin - a package this repo explicitly forbids. They were dependency roots, so dropping three lines drops 19 packages from the installer. The consequence that matters most is transitive: google-auth 2.22.0 carried `urllib3 (<2.0)` and was the sole constraint holding the agent on urllib3 1.26.20 - the final 1.26.x release, from 2024, effectively end-of-line. Removing it moves the HTTP transport to 2.7.0. That was a bigger real-world exposure than any of the advisories below, and it was invisible because nothing reports a dependency that is merely held back. Dependency bumps, all verified unreachable in this codebase before bumping - these are hygiene, not incident response: - setuptools 75.6.0 -> 83.0.0: CVE-2025-47273 (PackageIndex.download path traversal) and the MANIFEST.in sdist bypass. Neither reachable - nothing invokes package_index and the agent never builds an sdist. Note 83.0.0 no longer ships pkg_resources (removed in 82.0.0); the only consumer in the payload is sentry_sdk's <py3.8 fallback, which is doubly guarded and dead on 3.11. - requests 2.31.0 -> 2.34.2: the verify=False pool-reuse, .netrc leak and extract_zipped_paths advisories. None reachable - no verify= in the agent, no .netrc on a LocalSystem profile, and extract_zipped_paths short-circuits because certifi's bundle is a real file. 2.34.0 also fixes greedy no_proxy matching and URI path slash-stripping, which do affect a fleet agent behind a corporate proxy using signed URLs. - packaging 23.1 -> 24.2: not a CVE. It was the standing conflict against the wheel that get-pip bootstraps, and clearing it is what lets pip check gate. Nothing constrained it below 24 - customtkinter is the only dependent and declares no version. With the tree clean, pip check is now fatal instead of a swallowed WARNING. It had been non-fatal for as long as that conflict existed, which meant a conflict introduced by any future bump would have shipped silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Version sync plus the 2.12.20 changelog entry in both changelogs, landed before the installer build per the release order in .claude/CLAUDE.md - the build bakes the version into the exe filename and binary. Covers the two dependency commits on dev: the build fix that stops shipping orphaned dist-info, and the dependency wave that drops the unused google stack, clears the setuptools/requests advisories, and unpins urllib3. Copy is deliberately outcome-first rather than a version table. Operators care that a scanner against their machine now reports one accurate version per library and that the HTTP layer is maintained again; the package numbers are in the commits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two errors in the entry, both found by installing the build on a real machine rather than reasoning about it. The count was wrong: 17 packages are removed, not 19. The 19 came from counting tokens in a pip --dry-run line; 17 is the difference between the two installed trees (77 unique packages before, 60 after). The commit message on a97defb carries the same wrong figure and is left as-is rather than rewritten, since it is already part of the history this commit corrects. The scanner claim was worse, because it was backwards for most machines. The entry said this release lets operators scanning their own machines "see a clean bill". That holds only for a fresh install. Installing over an existing agent replaces files but deletes nothing, so an upgraded machine keeps every previous version's metadata: measured here, dist-info directories went from 176 to 189 across one upgrade, and importlib.metadata then reported urllib3 1.26.20 / requests 2.31.0 / cryptography 41.0.7 while the code actually running was urllib3 2.7.0 / requests 2.34.2 / cryptography 50.0.0. A scanner on that machine reports vulnerable versions that are not what executes. The entry now claims only what a fresh install delivers and documents the upgrade limitation, including that uninstalling first preserves config, tokens and logs. The underlying defect - upgrade never clears {app}\python - is deliberate (the old uninstaller used to destroy user data) and needs its own fix; this commit stops the changelog from overstating what shipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Draft. Opened as a review surface — merging this deploys to owlette.app (prod), and
mainis not branch-protected, so the merge is the deploy.What's in it — 21 commits
Billing (beta exit) — waves 0–5. Tier derivation and quota unification, Stripe customer lifecycle + portal + webhook, checkout, trial lifecycle, usage metering, billing tab, trial banner and pro gates, admin overrides, ops dashboard, health alerts, delivery gate, core site limit.
Deployments. Auto-computed sha256 checksums for installer deployments; working retry (in-place, checksum self-healing) + dialog UX.
Auth. Google sign-in inside in-app browsers; returning users landing on
/register;/setup-2facancel no longer drops users back on/register.Landing. Problem-solving hero copy, hydration-mismatch fix on touch devices, per-load headline rotation.
Agent security + build. cryptography 44.0.3 → 50.0.0; the three range-pinned deps hard-pinned; user site-packages sealed out of the installer build; pin rationale corrected.
Billing is inert on merge
Billing is gated on a Firestore doc
config/billing.goLiveAt, written separately byscripts/billing-go-live.mjs.trialClockStarted()inweb/lib/actions/bootstrapUser.server.tsreturnsfalsewhen the doc is missing, whengoLiveAtis null or future-dated, and when the read throws — fail-safe by design ("the failure mode of that choice is a too-long trial, never a premature lockout").So this merge deploys billing code. It does not start anyone's clock. Go-live remains a separate, deliberate step against prod Firestore.
Dependabot impact
Alerts are computed against the default branch, so nothing on
devhas closed anything yet. This merge closes 3 of 8 openagent/requirements.txtalerts — CVE-2026-69247, CVE-2026-69249, CVE-2026-69248, all cryptography.The other 5 remain open on both branches: setuptools #5 (high,
PackageIndexpath traversal) / #8 (medium), and requests #4 / #6 / #7. All five were individually verified unreachable in this codebase — noverify=False, noextract_zipped_pathscaller, no.netrc,package_indexnever invoked, no sdist ever built. PR #80 addresses them and should land in 2.12.20 rather than here.Known defects this does NOT fix
--ignore-installedinbuild_installer_full.batships orphaned dist-info. Every installer carries twosetuptoolsand twopackagingdist-infos; scanners read the newer (patched) metadata while the older (vulnerable) code executes. Highest-value build fix outstanding.google-auth==2.22.0pinsurllib3<2.0, holding the agent on urllib3 1.26.20 (EOL). Thegoogle-*trio is unused by agent code; dropping it removes 17 packages and unpins the transport.agent/src/cleanup_commands.pyimportsfirebase_adminat module scope, violating a repo guardrail. Dead code, but shipped.Verification
playwright e2egreen on every web-scope commit throughd8334db.sha256 4081945d726d75c9b1d4a17690dd1d9f9e9a24e1e54a0d4dec36b0021092afea. Prod fleet remains on 2.12.18; promotion is a separate step.🤖 Generated with Claude Code