Skip to content

fix(share): invalidate stale short links - #1425

Merged
backnotprop merged 4 commits into
mainfrom
codex-fix-798-stale-short-link
Aug 31, 2026
Merged

fix(share): invalidate stale short links#1425
backnotprop merged 4 commits into
mainfrom
codex-fix-798-stale-short-link

Conversation

@backnotprop

@backnotprop backnotprop commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Closes #798 for stale-snapshot correctness. Follow-up #1427 tracks the remaining small-plan short-link creation limitation described below.

What changed

  • Preserve an incoming short URL while its encrypted snapshot hydrates, including concurrent development Strict Mode loads.
  • Associate incoming and locally generated short URLs with the exact shareable-content revision they represent.
  • Clear the displayed short URL after plan/document, annotation, attachment, raw HTML, HTML revision, or share configuration changes, without updating or deleting the original paste.
  • Discard stale in-flight short-link responses and require explicit creation of a fresh immutable snapshot.
  • Keep short-link generation errors attached to the content revision that failed, then clear them after a material change.
  • Run the new DOM-gated lifecycle regression in the Test workflow.

Root cause

The previous shared-session guard skipped short-link invalidation for the lifetime of an imported session, so Export could continue presenting the original immutable paste after local edits.

Product limitation and follow-up

This PR prevents Export from presenting an incorrect short URL. A small markdown plan now falls back to its correct fresh full hash URL after invalidation, but ExportModal still only offers Create short link when the full URL exceeds 2048 characters or hash sharing is unavailable. Therefore the literal request in #798 to always send a new short link back is only partially resolved for small plans. #1427 tracks adding an explicit replacement-short-link action without expanding this focused lifecycle fix.

Verification

  • DOM_TESTS=1 bun test packages/ui/hooks/useSharing.shortUrlLifecycle.test.tsx packages/ui/hooks/useSharing.contentRevision.test.tsx — 6 passed, 44 assertions
  • Lifecycle test passed 8/8 consecutive reruns
  • bun run --cwd packages/ui typecheck
  • bun run --cwd apps/portal build
  • test.yml parsed successfully; lifecycle DOM test registered exactly once
  • git diff --check
  • Local browser smoke with an in-memory paste mock: incoming link survived hydration; adding an annotation removed it; creating a new short link produced a distinct paste; original ciphertext stayed unchanged.

Security and compatibility

Hosted pastes remain immutable and ciphertext-only. This change performs no update or delete request. Existing incoming links and payload formats remain compatible; only the client-side presentation lifecycle changes.

@backnotprop

Copy link
Copy Markdown
Owner Author

Reviewed the diagnosis and the fix. The root cause is real and I reproduced it independently rather than reading it off the description.

On main, useSharing.ts:300-307 sets isSharedRef once isSharedSession flips true, and isSharedSession is never set back to false, so short-link invalidation is dead for the whole life of an imported session. Since the paste is immutable by design (apps/paste-service/core/handler.ts has no update or delete route), the displayed URL keeps pointing at the original ciphertext no matter what you annotate. That is exactly the loop @grncdr described in #798.

To make sure the new test guards the bug and not just the incidental StrictMode change, I applied only the incomingShortUrl capture to main's hook and kept main's old guard. The test still fails at the "short URL cleared after adding an annotation" assertion, so it is pinning the real regression.

Layer and compatibility both look right to me. Nothing outside packages/ui is touched, so the Bun and Pi mirror rule is not engaged. sharing.ts is untouched, which means SharePayload, toShareable/fromShareable and the deflate plus base64url plus #key= pipeline are all unchanged, so existing links in the wild keep restoring. Guide share links and the PLANNOTATOR_SHARE=disabled path are unaffected. No dependency, lockfile, endpoint or new URL changes. The postCount assertions are the best part of the test: they pin that invalidation never uploads and never mutates the original paste.

Three things I would like before merge:

  1. The new test never runs in CI. useSharing.shortUrlLifecycle.test.tsx is DOM-gated, and DOM-gated files only run if they are listed in .github/workflows/test.yml. Its sibling useSharing.contentRevision.test.tsx is listed at line 154; the new file is not, so the plain bun test step skips the whole describe block. One line beside line 154 fixes it. I confirmed it passes in the same process as the neighbouring UI DOM files, and it is stable over eight reruns.

  2. shortUrlError is no longer cleared when content changes. useSharing.ts:330 early-returns for the none tag with an empty shortShareUrl, which is exactly the state a failed generation leaves behind, so the setShortUrlError('') at :335 is skipped. I checked this with a scratch harness: fail a generation, then edit the document. On main the error clears, on this branch it persists, and ExportModal.tsx:344-346 renders it beside the Create short link button, so the user sees a stale "(Short URL service unavailable)" for content that was never attempted. Either hoist the error clear above the early returns or add a !shortUrlError term to the :330 guard.

  3. Worth a follow-up rather than a blocker: for a small markdown plan the reviewer now gets no short-link button at all after invalidation, because ExportModal only offers Create short link when urlIsLarge (shareUrl over 2048 chars) or hashUnavailable. They do get a correct long hash URL, and the header Copy share link path now returns correct content, so this is strictly better than the wrong short link. But step 4 of the original report, sending a short link back, is still out of reach unless the URL happens to be large.

Two smaller notes. The StrictMode rationale in the comment at :154-157 is development-only behavior; apps/portal does wrap in StrictMode, but the deployed production build never double-invokes the mount effect. The capture is still correct and is what makes the test deterministic, so this is a comment wording thing only. And the incoming-hydration tag at :316-323 is consume-on-next-effect; it is safe today only because fromShareable and setGlobalAttachments always return fresh arrays so the hydration commit always changes the request context. One line of comment there would keep a future refactor from quietly swallowing an invalidation.

What I ran: the two useSharing files under DOM_TESTS=1 (5 pass, 33 assertions), the new file eight times for flake, the full typecheck across all nine projects including the strict-consumer config, the portal build, and the related registered DOM files (45 pass). The full bun test run has 12 failures, all in review-workspace.test.ts and the Pi server test; I checked out main in the same worktree and the same ones fail there, so they are environmental (no sem or but binary present) and unrelated to this change.

Recommendation: take with changes. Items 1 and 2 before merge; item 3 as a follow-up issue so #798 can be closed the way the reporter described it.

AI-assisted (Claude) under maintainer direction.

@backnotprop

Copy link
Copy Markdown
Owner Author

Addressed every item from the maintainer review in commit bcf8935:

  1. CI registration: added useSharing.shortUrlLifecycle.test.tsx beside the existing content-revision test in .github/workflows/test.yml. The workflow parses and the path is registered exactly once.
  2. Error semantics: added an explicit failed lifecycle state tied to the request context. A failure remains visible for the content that failed—even when an older short URL is cleared—then a material content/config change clears it. Existing request-context checks still discard stale async completions.
  3. Regression coverage: added a success-then-failure case that verifies the stale URL is removed, the new error survives an unchanged render, the next content edit clears it, and no extra POST occurs. Focused result: 6 tests, 44 assertions; lifecycle file also passed 8/8 reruns.
  4. Small-plan limitation: kept out of this focused PR per the review wording. Opened Allow minting a replacement short link for small shared plans #1427 and updated the PR description to state explicitly that [bug?] Short link on share.plannotator.ai is not updated with new annotations. #798 is only partially resolved for minting replacement short links on plans below the 2048-character threshold.
  5. Smaller notes: rewrote the URL-capture comment to distinguish concurrent loads from development-only Strict Mode behavior, and documented the fresh-array identity invariant behind the consume-on-next-effect hydration handoff.

There were no inline review threads in the PR API; this comment responds to the single posted maintainer review item-by-item. Typecheck and portal build pass. Fresh CI is running now.

@backnotprop

Copy link
Copy Markdown
Owner Author

CI follow-up: registering the lifecycle file in the existing 83-file Bun DOM invocation exposed deterministic Linux cross-file global-state interference in unrelated theme/highlight tests (the code-block test received another test's Kanagawa palette; both #798 lifecycle tests passed). The same 83-file command passed locally, but GitHub reproduced the unrelated failures on a failed-job rerun.

Commit d570d93 keeps the new regression mandatory in the same test job but runs it in an adjacent isolated Bun invocation. Local verification: the unchanged shared lane passes 674/674, and the isolated short-URL lifecycle suite passes 3/3. This avoids retrying/masking the global-state leak while ensuring the requested guard genuinely runs in CI.

@backnotprop

Copy link
Copy Markdown
Owner Author

Final CI resolution (superseding the adjacent-invocation detail in my prior comment): commit 4d0be150 keeps useSharing.shortUrlLifecycle.test.tsx directly beside useSharing.contentRevision.test.tsx in the existing command and adds Bun --isolate for fresh per-file globals. This addresses the Linux theme/storage/DOM state leak without weakening the maintainer's requested shared CI registration.

Final evidence: the exact isolated 83-file workflow command passes locally (685 tests, 2,916 assertions), GitHub Test passes, Release test/build/package and all cross-platform smoke jobs pass, and security checks pass. The PR is open, mergeable, and was not merged.

@backnotprop

backnotprop commented Aug 31, 2026

Copy link
Copy Markdown
Owner Author

Follow-up to the review in #1425 (comment)

Recovery is complete on commit 4d0be150.

  • packages/ui/hooks/useSharing.shortUrlLifecycle.test.tsx is registered exactly once in the DOM-gated list in .github/workflows/test.yml, beside the existing content-revision coverage.
  • The registered DOM batch now uses Bun per-file global isolation. This fixes the Linux-only theme/storage state leakage that made the previous CI run fail while keeping the lifecycle test in the required shared batch.
  • Material content changes clear shortUrlError and invalidate only content-associated/generated short URLs. Incoming hydration, generation, association, and failure transitions remain explicit.
  • Regression coverage exercises generated-link invalidation, incoming-link hydration, and error clearing after material content changes.
  • The Strict Mode duplicate-effect guard and incoming-hydration behavior are clarified in comments.
  • Existing incoming links remain compatible. Paste records remain immutable; no overwrite/update path was introduced.

Verification on the final head:

Important scope note: this does not fully resolve #798. Small plans still cannot mint a replacement short link after invalidation because ExportModal gates creation by URL size. That product limitation is tracked in focused follow-up #1427: #1427

No merge was performed.

@backnotprop
backnotprop merged commit 42978fe into main Aug 31, 2026
28 checks passed
@backnotprop
backnotprop deleted the codex-fix-798-stale-short-link branch August 31, 2026 16:11
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.

[bug?] Short link on share.plannotator.ai is not updated with new annotations.

1 participant