Sync upstream/main (hypercerts-org) — 320 commits, releases 0.3.0 → 0.6.3#1
Merged
Conversation
The email-mode submit button said "Sending verification code..." while submitting, but the demo is a pure OAuth client — it hands off to the auth service and has no visibility into whether a verification code will actually be sent. In the HYPER-268 session-reuse path no OTP email goes out at all, so showing "Sending verification code..." momentarily was misleading. Matches the handle-mode button (and the shared SignInButton component) which already use "Redirecting..." for exactly this reason. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three bugs blocked chooser enrichment on the real /oauth/authorize chooser (the one cross-client session reuse lands on): 1. Route filter only matched /account*. Upstream renders the inline chooser at /oauth/authorize too. isChooserRequest now matches both. 2. Script only intercepted window.__deviceSessions. The inline chooser at /oauth/authorize hydrates from window.__sessions instead. Both are intercepted via a shared interceptGlobal() helper. 3. Middleware spliced in immediately after expressInit, so compression's wrapped res.end ran on top of ours — we only ever saw gzipped bytes and the <script> never injected. Reuse findInsertionIndex from client-css-injection to land it after compression, same as CSS injection. DOM rewrite also reworked: TreeWalker finds the deepest element whose own text matches a known handle/sub, so we don't depend on upstream class names. The handle-wrap (a flex-row) is flipped to flex-column and the email label appended as a sibling of the handle, giving handle + email stacked in a shrink-wrapping left column with the chevron snug against the wider of the two lines in the right column. Stable classes epds-handle-label / epds-email-label let branding CSS restyle or reorder the pair. Verified end-to-end via Chromium driven through the docker-compose stack: trusted-demo sign-in, then untrusted-demo sign-in, lands on /oauth/authorize with both handle and email stacked correctly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
On pull_request, `actions/checkout`'s default is the GitHub-synthesized merge commit (refs/pull/N/merge = base + head). Railway, however, deploys the PR branch head — Railway has no native way to deploy merge commits (https://docs.railway.com/deployments/github-autodeploys). The mismatch means when main advances between rebases, E2E test code from the merge commit can assert against UI/behaviour that exists in main but not yet in the PR branch's Railway deployment. Symptom: tests newly added on main fail on unrelated PRs until the PR is rebased (e.g. the "Current Handle:" row introduced by hypercerts-org#99 broke hypercerts-org#93's CI). Pin checkout to `github.event.pull_request.head.sha` so test code and deployed runtime share one SHA. Empty on push / workflow_dispatch falls back to actions/checkout's default (GITHUB_SHA for the triggering event), verified against the v6.0.2 source. Trade-off: PR CI no longer catches main-incompatibilities until the branch is rebased, but rebase was the mitigation anyway — this just makes the contract explicit instead of surfacing as confusing unrelated failures.
Adds the 4 previously-pending scenarios: - Add + verify backup email - Recover via verified backup email - Non-existent backup shows same UI (anti-enumeration) - Remove backup email + confirm recovery no longer works Emails are generated per-scenario so multiple runs against a shared Railway preview don't collide. Backup verification + recovery both exercise the real UI (click verification link, navigate from login page's recovery link) rather than internal APIs, since none exist for fast-seeding backup emails. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…il Given The "the test user has a verified backup email" step was calling getPage(this) before resetBrowserContext(), which returned a stale reference to the page on the context that resetBrowserContext was about to close. The subsequent page.goto() then failed with "Target page, context or browser has been closed". Move the getPage() call below the reset so we get a live page on the fresh context. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three Sonar security hotspots flagged on the new step file: - Math.random() for unique test-email suffixes. Swap to crypto.randomBytes — still non-crypto in intent but avoids the pseudorandom-not-safe warning. - Two /[.>"',]+$/ trailing-punctuation regexes used to trim stray punctuation from a URL captured out of an email body. Sonar flags them as potential ReDoS even though the input is already bounded by the preceding \S+ match. Replace with an explicit while-loop over a small Set — linear time is then syntactically obvious, and both callsites now share one helper (extractVerifyUrl) instead of each re-doing the match + trim. No behaviour change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The /auth/complete bridge looked up the session email via the PDS account-by-email endpoint, which only indexes primary account email. Recovery sessions hold the verified backup email — so the lookup returned null and the user was treated as new, sent to the handle picker instead of back to the originating client. When the direct lookup fails, fall back to the auth-service backup_email table to get the account's DID, then resolve the primary email via the existing /_internal/account-by-handle endpoint (which accepts DIDs). The downstream signCallback then signs the primary email, matching the existing login contract with pds-core. Exercised by the "User recovers account via verified backup email" cucumber scenario added in this PR. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Moves the backup-email → primary-email translation out of the /auth/complete handler into its own lib/ helper and adds 6 unit tests (happy path, case-folding, missing backup row, null email from PDS, non-OK status, network throw). Covers the branch added in the previous commit, restoring the coverage percentage. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…review-endpoint feat(preview): in-browser email previews
Rename and de-reference files so external readers — who don't have access to our Linear workspace — aren't confronted with opaque `HYPER-268` tokens. - Rename `.changeset/hyper-268-cross-client-session-reuse.md` to `cross-client-session-reuse.md`; also unwrap the hard-wrapped body paragraphs and drop the internal-doc reference line. - Rename `.changeset/hyper-268-fix-headers-sent-crash.md` to `fix-post-flush-headers-sent-crash.md`; scrub the inline HYPER-268 mention from the body. - Scrub the HYPER-268 mention from `demo-neutral-submitting-copy.md`. - Rename `docs/design/hyper-268-session-reuse.md` to `cross-client-session-reuse.md`; demote `HYPER-268` from the H1 title to a "Tracking issue:" subtitle so contributors can still find the ticket without making it the first thing anyone sees. - Update the code-comment pointer in `pds-core/src/index.ts` to the new design-doc path. No user-visible behavior change; this is purely a naming / language cleanup.
Add three new High Risk entries to docs/design/pds-white-boxing.md covering the upstream couplings introduced by the cross-client OAuth session-reuse work: - Item 15: pds-core cookie-domain middleware hardcodes the upstream dev-id/ses-id cookie names (and their :hash sidecars) to rewrite Set-Cookie for a shared parent Domain attribute. - Item 16: pds-core chooser-enrichment rewrites the /account and /oauth/authorize HTML responses, reading upstream's __sessions / __deviceSessions globals and the React-rendered DOM structure. - Item 17: auth-service session-reuse sniffs the dev-id cookie on incoming /oauth/authorize requests with the same rename-risk as item 15. Docs-only; no runtime behaviour changes.
…ite-boxing-session-reuse
The bare auth-service hostname previously returned a 404 "Cannot GET /" page, which was confusing for users bookmarking or mistyping the URL. Mount a tiny router that 303-redirects / to /account, matching the redirect status used elsewhere in this service. /account itself enforces auth and bounces to /account/login when unauthenticated, so the redirect chain ends somewhere useful regardless of session state. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GitHub's merge queue creates a temporary `gh-readonly-queue/main/pr-<N>-...` branch when a PR is enqueued and fires a `merge_group` event. The required status checks (`format`, `lint`, `typecheck`, `test`) live in this workflow, but it only triggered on `push` and `pull_request`, so those checks never ran on the queue branch and the entry sat in `AWAITING_CHECKS` forever (observed with PR hypercerts-org#105 at position 1). Adding `merge_group` to `on:` runs the same jobs against the merge-queue branch so the queue can progress. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…migration-70fb816
Introduce a single styled error template (lib/render-error.ts) and route every error response through it. Replaces bare <p> responses across complete, recovery, choose-handle, and account-settings, and removes three duplicate local renderError functions (login-page, choose-handle, recovery). Also wires catch-all 404 and 500 middleware so unmatched paths and uncaught errors render the same styled card instead of Express's default text output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Express 4.x guidance, a custom error-handling middleware must call next(err) (not return bare) once res.headersSent is true, so the default handler closes the connection and fails the request instead of leaving a partially-streamed response dangling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The @typescript-eslint/no-confusing-void-expression rule forbids returning the void result of next(err). Call next(err) and return separately so we still delegate to Express's default handler without returning a void expression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related changes to the shared error-response path:
1. The 404 / 500 handlers in auth-service/src/index.ts used
req.accepts('html'), which returns truthy for Accept: */* and so
sent HTML to fetch/curl clients. Switched to
req.accepts(['json','html']) === 'html' so HTML is only returned
when the client explicitly prefers it.
2. Error responses calling res.send(renderError(...)) now chain
.type('html') before .send(), matching the convention documented
in AGENTS.md ("res.status(N).type('html').send(renderError(...))
for HTML pages"). Applied across complete.ts, recovery.ts,
account-settings.ts, and the new index.ts error handlers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers both the user-visible styled 404/500 pages and the content-negotiation change that makes programmatic clients (Accept: */*) receive JSON error bodies instead of HTML. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ware Add unit tests for the shared renderError helper (default title, title override, HTML escaping, inline styles) and integration tests for the trailing 404 + 500 middleware in createAuthService, mirroring the real content-negotiation path (JSON for Accept: */*, HTML for browser Accept) and the headersSent guard that delegates to Express's default handler. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Moves the trailing content-negotiating handlers out of createAuthService into src/lib/error-middleware.ts so they can be imported directly from unit tests, eliminating the duplicated handler code between index.ts and the integration test. Also disables x-powered-by on the test fixture app to match defensive defaults. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
node: built-in imports should precede external package imports per the documented four-group ordering. CodeRabbit flagged the same file once already (thread resolved without a fix landing); Claude Code reviewer re-flagged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both handlers branch on req.accepts(['json','html']) but omitted a
Vary header, risking CDN cache-poisoning where a JSON 404 served to
an API client could later be served to a browser navigating the same
URL. Add res.vary('Accept') in both and assert it in the tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a second @otp-expiry scenario that backdates the auth_flow row via the existing /_internal/test/expire-auth-flow hook, then submits a still-valid OTP. After PR hypercerts-org#154's reactive abort gate the OTP form pings /auth/ping before submitting; with the auth_flow row dead the ping reports `flow_expired`, the gate navigates to /auth/abort, and cleanExit serves its Tier-2 styled "Sign-in session expired" fallback page (the OAuth client redirect path needs the dead row's clientId, which is exactly what's missing here). The scenario asserts both signals — the ping reason (proving auth_flow specifically tripped, not PAR) and the abort fallback page — so a regression that, say, swaps which timer the gate honours would still be caught. Without this guardrail nothing in CI would notice if AUTH_FLOW_TTL_MS were quietly shortened back to the OTP TTL; the existing scenario only proves the 10-min-and-resend path works, not that the 60-min boundary is enforced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-386-bug-missing-space-between-account-and-email-address fix(pds-core): add email label spacing
…-395-align-the-login-complete-page Add login complete preview
…elease/main prepare for release v0.6.3
Add a deployment_status workflow that syncs successful staging and production Railway deployments to Linear release pipelines without changing the existing Changesets release-notes flow. Co-Authored-By: Claude <noreply@anthropic.com>
Pin the Linear release action to a full commit SHA so SonarCloud does not flag the workflow dependency as an unreviewed security hotspot. Co-Authored-By: Claude <noreply@anthropic.com>
Gate the Linear release reporting jobs on Railway's deployment creator so unrelated deployments with matching environment names are not synced to Linear. Co-Authored-By: Claude <noreply@anthropic.com>
…release-workflow ci: report deployments to Linear releases
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…enance-skill docs(skills): add Instatus maintenance scheduling skill
…llel-3 ci: run e2e suite with three Cucumber workers
Adds a `handle-login-url` row to `/preview/validate` so client devs get a warning when their metadata is missing the hand-off URL that gates the "Or sign in with ATProto/Bluesky" button — and an error when the URL is set but isn't a parseable http(s) URL (which would silently disable the button on real flows). Mirrors the runtime isSafeHttpUrl gate in auth-service's login-page, so http:// localhost dev clients still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ClientMetadata previously stated the field must be `https://`, but both the runtime `isSafeHttpUrl` gate in auth-service's login page and the new preview validation accept `http://` for localhost / dev clients. Bring the docstring in line with the actual behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clarify two minor docs/behavior mismatches surfaced in review: - ClientMetadata.epds_handle_login_url JSDoc claimed the URL "Must be ... on the client's own origin", but neither the login page's isSafeHttpUrl gate nor the /preview/validate check enforces same-origin. Relax to "should be on the client's own origin" with an explicit note that origin is not validated at runtime. - Both the JSDoc and the preview-validation success message described the handoff as "?handle=<value> appended". The login page uses URLSearchParams.set, so the separator is "?" only when the URL has no existing query string, else "&". Rephrase to "with a handle=<value> query param" to match actual behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_url `isSafeHttpUrl` in auth-service and `/preview/validate` both accept any absolute http(s):// URL regardless of environment, so saying https is "required in production" overstates what the runtime enforces. Reword to "expected in production" and call out that the scheme gate is not environment-aware. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…idate-handle-login-url feat(shared): preview validation flags missing epds_handle_login_url
Sync brings in 320 upstream commits covering releases 0.3.0 → 0.6.3. Major pickups: - OAuth/auth-flow lifecycle hardening (PRs hypercerts-org#122, hypercerts-org#128, hypercerts-org#134, hypercerts-org#154, hypercerts-org#169): session-expired dead-end fixes, OTP-resend-after-PAR-expiry, slow-OTP timeout handling. - Cross-client OAuth session reuse (PR hypercerts-org#96): sign-in once in a browser works across all apps on the same ePDS. - Security fixes: branded email templates gated on trustedClients, favicon-injection hardening, preview CSS-injection limits, hydration <script> XSS escaping. - Account recovery fixes (PRs hypercerts-org#98, hypercerts-org#141): backup-email recovery now completes OAuth instead of dropping to signup. - Auth-service error pages: styled errors replace raw JSON; structured JSON responses for API callers via content negotiation. - Preview endpoints for emails (browser-render without OAuth walk). Preserved attpslabs additions: - Cloudflare Tunnel deployment config (Caddyfile, docker-compose.yml). - Headless OTP + per-client API keys (/_internal/* endpoints). - CORS allowances for Authorization and DPoP headers. - Dual MIT/Apache-2.0 license. - Reserved-handle check in /_internal/check-handle. Branding preservation (Approach A): self.surf visual surface left byte-identical to pre-sync state. Upstream's restyled login page, "Powered by Certified" footer, and Certified favicons are NOT shipped. Visual route files (login-page, account-login, account-settings, choose-handle, recovery) are taken from pre-sync main verbatim. POWERED_BY_HTML / POWERED_BY_CSS / renderFaviconTag in lib/page-helpers.ts are neutralised to empty strings so non-overridden consumers don't crash. Trade-off: this sync intentionally skips the in-file OTP UX improvements that landed inside the restyled login page (double-submit fix, false-error flash fix, OTP-box clear on error). These can be cherry-picked later if needed without re-enabling Certified branding. Conflict resolutions: - Caddyfile, docker-compose.yml: kept ours (self.surf prod config, not upstream-compatible). - .env.example: concatenated both appends. - vitest.config.ts: kept our lower coverage floors (upstream's thresholds would fail given the visual-file overrides). - packages/auth-service/src/index.ts: kept our headless-OTP router mounted before CSRF/rate-limit middleware; layered upstream's new test-hooks router around it. Dropped upstream's /favicon.ico route (depends on stripped SVG assets) and createPreviewRouter (depends on internal route exports that ours don't expose). Files removed (upstream-only, depend on branding/restyle internals): - src/__tests__/heartbeat-toggle.test.ts - src/__tests__/login-page-prompt-login.test.ts - src/__tests__/error-handlers.test.ts - src/__tests__/favicon.test.ts - src/routes/preview.ts - public/{certified-brandmark,certified-text-monochrome,favicon,favicon-dark}.svg Verification: - pnpm build: clean across all packages. - pnpm test: 990/990 passing. - pnpm lint, prettier --check: clean. Co-Authored-By: Claude Opus 4.7 (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.
Summary
Sync of 320 commits from hypercerts-org/ePDS (releases 0.3.0 → 0.6.3). Last sync was 2026-04-16.
What we gain
<script>XSS escapingBranding preservation (Approach A — strict)
Self.surf visual surface is left byte-identical to pre-sync state:
POWERED_BY_HTML/POWERED_BY_CSS/renderFaviconTagneutralised to empty stringspublic//favicon.icoroute dropped (depended on stripped SVGs)createPreviewRouterdropped (depended on internal route exports our pre-sync routes don't expose)Trade-off: this sync intentionally skips the in-file OTP UX improvements that landed inside the restyled login page. These can be cherry-picked later without re-enabling Certified branding.
Preserved attpslabs additions
/_internal/otp/*)/_internal/check-handleConflict resolutions
Caddyfiledocker-compose.yml.env.examplevitest.config.tspackages/auth-service/src/index.tsTest plan
pnpm buildclean across all packagespnpm test— 990/990 passingpnpm lintcleanpnpm exec prettier --check .clean/healthreturns 200,/oauth/authorizerenders 200 with zero Certified branding in HTML,/_internal/otp/sendreturns 401 (CSRF bypass works),/account/loginrenders 200 with no brandingDeploy notes
epds.sqliteis safe (verified)pre-upstream-sync-2026-05-27for easy revertgit revert -m 1 <merge-sha>on main, then pull-and-rebuild on the server🤖 Generated with Claude Code