feat(upstream): back-merge batch A — 2FA lockout columns, sign-in devices, widget deep-links, attachment tray - #31
Merged
Merged
Conversation
* feat(tags): restyle the create/edit tag dialog
The New tag dialog was a vertical stack with a disconnected preview,
a 32-swatch palette, and a switch squeezed against a paragraph.
Name now sits next to a live chip (no "PostTag" placeholder). Color is
eight presets plus hex, with the rest behind More colors. Visibility is
two cards — Portal / Internal — matching Create board. Description is
last and optional. Create stays disabled until a name is entered.
* fix(tags): stack the name preview under the field on small screens
The chip sat beside the input, so the placeholder truncated at phone
width. Stack below sm, keep the side-by-side row on desktop.
* feat(tags): pick tag color from a single swatch
Drop the inline 8-dot grid, hex row, and More colors disclosure. Color
is a circle next to the name that opens the same popover the list uses.
* feat(tags): render the settings list as chips with visibility
Each row is the tag as it appears on posts, not a color dot plus
plain text. Portal and Internal sit in an aligned column so public
vs team-only is scannable without opening the editor. The chip still
opens the color popover.
* test(tags): scope Portal/Internal assertions to the list row
closest('div') stopped at the chip wrapper after the list became a
colored badge, so the visibility label was outside the queried node.
* fix(tags): use a real radio group for visibility
Codex review: the Portal/Internal cards advertised radio semantics
without arrow-key or roving tabindex behaviour. Switch them to the
shared RadioGroup primitive. Also restore the dialog description
association that DialogContent otherwise strips.
* test(tags): cast list-row closest() to HTMLElement
within() wants an HTMLElement; closest('.group') is typed as Element.
* fix(tags): keep tag rows usable on narrow settings viewports
The chip column was a fixed 10rem and did not shrink, so combined with
the visibility label and action buttons it overflowed the
overflow-hidden settings card. Let the chip shrink, hide the
description below sm, and keep edit/delete visible on touch.
(cherry picked from commit 4ef5beb)
…IO#525) * fix(auth): identify new-sign-in devices by browser, not IP Rotating client IPs were hashed into the device fingerprint, so the same browser looked new on every network. Hash the normalised browser and OS instead; keep IP (and country when known) in the mail only. Seed the first recorded device silently so a hash-format change does not re-alert every account. * fix(auth): count the claimed device and isolate the new fingerprint set Postgres data-modifying CTEs share the outer snapshot, so liveCount missed the row just inserted and suppressed the second-device alert. UNION the RETURNING member into the count. Key devices as user:devices:v2 so leftover UA+/24 hashes cannot look like an extra browser. Point the password link at /settings/profile, which is where the personal password form actually lives. * fix(auth): serialize first-device claims on a set Two concurrent inserts into an empty device set could both see liveCount 1 and both skip the alert. Take an advisory xact lock on (workspace, set) before the insert so the second statement's snapshot includes the first member. * fix(auth): take the device-set lock in a prior statement READ COMMITTED snapshots at statement start, so an advisory lock in the same SQL as the counted insert does not help the waiter — it resumes with the empty snapshot it already took. Lock first in the transaction, then insert and count, so the second claim sees the first member. * fix(auth): revoke other sessions and use SSO-aware sign-in alerts changePassword now sets revokeOtherSessions: true so recovery from a new-sign-in alert signs out other sessions. Enforced SSO recipients are directed to their identity provider instead of a password CTA the profile page hides. * fix(auth): type the new-sign-in SSO mocks so web typecheck passes The empty-array mock inferred never[], and spreading unknown[] into getRegisteredOidcProviderIds failed TS2556. Both blocked the check job. * fix(auth): revoke other sessions when setting a first password OAuth and magic-link users recover via setPassword, which did not revoke sessions. The profile form now asks setPasswordFn to drop every session except the current one. complete-signup leaves the flag off so it does not kick the just-minted session. * fix(auth): revoke other sessions through Better Auth after setPassword Profile recovery now calls auth.api.revokeOtherSessions after a first password so the attacker session is dropped. complete-signup still leaves the flag off. The new-sign-in mail covers set-or-change. * fix(auth): slide known-device TTL and point SSO recovery at admins A daily sign-in on a known browser never called markDeviceSeen, so the member expired after 90 days and the next browser was a silent seed. Known claims now refresh the set TTL. SSO alert copy tells the recipient to ask a workspace admin to sign out other sessions, because IdP logout does not delete the local session row. * fix(auth): do not revive expired device fingerprints on TTL slide kvSetTouch updated every row in the set, including expired members the sweeper had not yet deleted. A known-device sign-in then resurrected old browsers and suppressed later alerts. Touch now only slides live rows. (cherry picked from commit fb3c1a9)
…kIO#529) * feat(auth): identify new-sign-in devices by a signed cookie Bowser/UA is too coarse for "is this the same browser?" — the cookie is the claim key, and the email still shows browser, OS, and IP as context. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(auth): type the device-set TTL query for tsc The member-touch test cast postgres.js rows after the fact; tsc rejects that overlap. Use the same generic on testSql() as the rest of the harness. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> (cherry picked from commit 7aac548)
* fix(editor): let Enter insert a newline in comments Restore enterAsHardBreak on the comment preset and stop parent forms from treating Enter as submit, so a line break no longer requires Shift. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(editor): type the Enter DOM handler CI typecheck failed because the handleDOMEvents keydown params were implicit any. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> (cherry picked from commit 1c8d144)
* fix(widget): honour documented open() deep-links
The SDK already sent view/title/board/body and post/article/changelog targets; the iframe dropped new-post after the composer merge and never applied the other fields.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(widget): resolve boards after identify and article TypeIDs
Identify now refetches the visitor-visible board list so open({ board })
can select members-only slugs, and articleId accepts a kb_article TypeID.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(widget): load articles like posts and auth identity feeds
open({ articleId }) now stores the TypeID or slug and lets help-detail
fetch with Bearer + sessionVersion, accepting article_ and kb_article_.
Popular board feeds pass the widget identity so members-only pills are
not empty after identify.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(widget): isolate open() feeds from identity and board filter drift
Similar-post hits are cached per session so a later identify cannot
keep showing another visitor's private titles. Popular Ideas keeps the
SDK ?board= filter across session re-keys, and article detail no longer
reuses another actor's content as placeholder data.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(widget): honour identity and locale on open() detail views
Changelog entryId fetches with the widget Bearer and sessionVersion so
an authenticated-audience entry is not treated as anonymous. Article
detail passes the active widget locale through the same query key as
help search, so a translated article is not swapped for the default.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(widget): isolate open() cache, auth changelog list, and article locale fallback
Keep similar-search hits session-scoped and bounded, send Bearer on the
changelog feed, fall back to the default-locale article, and stop Home
from advertising a board filter or overwriting a visitor board pick.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(widget): share query-key and similar-search helpers
Deduplicate session-cache reset and match placeholders by key prefix
instead of queryKey slot indexes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(widget): clear identity placeholders and attach help/changelog OTT
Logout no longer keeps the previous visitor's boards or search hits, help
collections refetch with Bearer + sessionVersion, and View on portal
transfers an identified OTT for articles and changelog entries.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(widget): auth help search, localize portal article URLs, drop lost board filters
Help search sends the widget Bearer and is session-scoped; article "View on
portal" uses hcArticlePath with the resolved locale; Popular Ideas clears a
filter the current session can no longer see.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(widget): abort stale help searches and reset compose after logout
In-flight KB search is aborted when sessionVersion changes, messenger
suggestions re-key with identity, and a compose board the live list no
longer contains falls back to the default.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(widget): auth Ask AI and leave gated help collections after logout
SDK-only visitors can retrieve the same gated articles they can browse,
and a replacement visitor is not left on a members-only category.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(widget): type the Ask AI fetch stub so header assertions typecheck
The mock was inferred as taking no arguments, so reading the request
init from mock.calls failed CI typecheck.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(widget): keep manual compose boards and drop stale changelog filters
A replacement visitor who picked another board after open() is no longer
overwritten when a restricted slug reappears, and a gated changelog
category clears once the new session feed does not contain it.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 43df5c9)
* feat(ids): serialize help-center articles as article_ Generate and emit article_ TypeIDs from the same UUID; accept retired kb_article_ ids as an inbound alias. No database migration — prefixes are applied at the ORM boundary. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ids): match legacy kb_article_ text in citations and redirects Copilot citation joins and redirect-rule cleanup now treat kb_article_ and article_ as the same UUID, so pre-prefix-flip JSON and text targets are not dropped after emit changes. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ids): fold kb_article_ citations before the top-cited limit The Copilot report now rewrites retired prefixes in SQL so one article cited as both article_ and kb_article_ is ranked and capped as a single source. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ids): canonicalize article TypeIDs and batch redirect-rule labels typeIdSchema now rewrites kb_article_ to article_ so writers cannot persist the retired prefix, and redirect-rule lists load labels in two queries instead of one per row. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ids): keep isTypeId exact so alias narrowing stays sound isValidTypeId still accepts kb_article_ as an inbound article id; the TypeId<'article'> guard only matches the canonical prefix. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(ids): reuse core TypeID helpers in schema and isTypeId typeIdSchema now validates and rewrites through isValidTypeId/ensureTypeId, and isTypeId composes those checks instead of re-parsing. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(help-center): canonicalize legacy admin article route IDs Bookmarked kb_article_ editor URLs now redirect to article_, and the detail query key is the same either way so publish/update cache the active editor instead of a stale alias key. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> (cherry picked from commit a720add)
better-auth 1.6.30 writes failed_verification_count and locked_until on every TOTP verify. The two_factor table never declared them, so Drizzle emitted `update "two_factor" set where …` and enrolment / sign-in 500'd. Add the columns and pin them in a schema test. Fixes QuackbackIO#432 (cherry picked from commit bcd4e6b)
…O#538) * feat(widget): two-step install with a lazy signing secret Cloud and self-host users were stuck looking for QUACKBACK_WIDGET_SECRET. Mint the secret on first admin fetch, put the launcher first, and keep regenerate on the install page. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(widget): keep install from sending users hunting for an env var Critic pass: launcher prompt forbids QUACKBACK_WIDGET_SECRET, Show on your website lives on the install page, regenerate writes the new secret into cache, and async confirm stays open until the rotate finishes. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(widget): harden install confirm and visibility feedback Keep async ConfirmDialog submits single-shot, toast when the site toggle fails, and stop sending detected installs to another settings page. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> (cherry picked from commit 1f795b0)
…t tray (QuackbackIO#539) Route conversation paste/drop/paperclip through the attachment tray so landscape screenshots stay in aspect. Old inline images lift onto attachments on read; posts and changelog still inline. (cherry picked from commit 19c3bd1)
The two_factor lockout migration picked from upstream (QuackbackIO#536) turns two gates red on purpose until a human confirms what it writes: the post-0248 replay span in the migrator gate and the contract ledger's scan count. 0278 is expand-only (two ADD COLUMN IF NOT EXISTS), so destructive DDL stays at 33 and the span gains one entry. Upstream did the same in QuackbackIO#537, which does not apply here because our span lists our own migrations. MATRIX.md gains one ungated entry point, resolvePublicArticleRefFn, which QuackbackIO#531 added so a widget deep-link can resolve a public article. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
resolvePrefix and prefixMatches looked a prefix up in ID_PREFIX_ALIASES with a
bare bracket access. The table is a plain object, so a prefix that happens to
be the name of an inherited Object.prototype member was answered with that
member: resolvePrefix('__proto__') returned Object.prototype itself and
resolvePrefix('valueOf') a function, where the contract says an unknown
prefix resolves to nothing. The prefix comes off the wire in every id the API
accepts, so only the table's own keys may count.
Found by the A2 property test written for the upstream article_ pick
(a720add); the test fails without this change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A confirm action that threw synchronously closed the dialog as if it had succeeded: the catch swallowed the error and returned without preventing the click, and Radix dismisses the dialog on any click it was allowed to handle. A rejected async action already left the dialog open. To the person clicking, a failed confirm is the same as one still running, so the dialog now stays in both cases. Found by the I4 test written for the upstream widget-install pick (QuackbackIO#538). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The nine upstream commits picked in this batch left 307 added lines that no test executed, almost all in the widget open() deep-links (QuackbackIO#531) and the conversation attachment tray (QuackbackIO#539). These suites pin them to a contract written in domain language before the tests: W1-W11 widget deep-links and identity-scoped feeds, A1-A4 article ids, C1-C8 attachment tray, E1-E2 editor, I1-I4 widget install, T1 tag dialog, S1-S3 sign-in devices. Every test names its number; the group text sits verbatim in each module's header. Two of the tests found production bugs, fixed in the two commits before this one. The pre-existing upstream tests in the three extended widget suites were numbered as well. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
UPSTREAM.md lists every upstream commit since the fork point with what we did with it: picked (fork PR and deploy tag), skipped (with the reason), or planned (with its batch). It carries the command that recomputes the picked/open split from the cherry-pick trailers, so a row that disagrees with the history is visible. CLAUDE.md points at it and asks a pick to update its row in the same pull request. SELF-IMPROVE gains what this batch cost: the load-flake entry is at 9x, with split coverage directories as the working pattern; the vitest 4 entry records that -u swallows the next positional argument and that an empty filter list runs the whole suite; and four findings from the test-writing pass are new or bumped entries (route mounting via a mocked createFileRoute, parallel coverage runs deleting each other's .tmp, Image never loading under the test DOM, and a file-wide vi.mock blocking a suite from testing the hook it mocked). Co-Authored-By: Claude Fable 5.1 <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.
What changes in the running service
Nine upstream commits, cherry-picked with
-xin upstream order, plus two fork fixes their tests found.failed_verification_countandlocked_untilon every TOTP verify; ourtwo_factortable did not have the columns, so enrolment and sign-in returned 500 (upstream issue 2FA sign-in fails: better-auth 1.6.30 lockout writes columns two_factor doesn't declare QuackbackIO/quackback#432). Migration0278_two_factor_lockout, expand-only.open()deep-links work as documented:new-postwith title/body/board,post,article,changelog,help,messenger,tickets,messages,home; help, changelog and Ask AI feeds carry the visitor identity and reset on identify/logout (upstream fix(widget): honour documented open() deep-links QuackbackIO/quackback#531).article_;kb_article_ids stay accepted everywhere and are rewritten (upstream a720add). No migration.resolvePrefix('__proto__')returnedObject.prototype(alias lookup had no own-key guard); the confirm dialog closed on a confirm action that threw synchronously.Skipped on purpose: upstream ce6951a (migration
0277_widget_chat_to_messenger, only relevant to a database with pre-messenger chat settings); skipping it keeps0278–0280in step with upstream. Upstream QuackbackIO#537 is done by hand because our replay-gate span lists our own migrations.Docs
UPSTREAM.mdis new: every upstream commit since the fork point with its status here, and the command that recomputes it from the cherry-pick trailers. Linked fromCLAUDE.md.Gates
PASS, 717 of 717 executable added lines executed (started at 307 uncovered; contract W1–W11, A1–A4, C1–C8, E1–E2, I1–I4, T1, S1–S3 confirmed before the tests were written).PASS: this change touched no file the manifest declares(touched files are listed by name).PASS; typecheck 0 errors; lint 0 errors (threemax-lineswarnings on upstream files that were already over the limit);db:check-driftno drift.settings.test.ts,help-center-article.service.test.ts); both green alone. SELF-IMPROVE updated.🤖 Generated with Claude Code