Tauri has no App Store target, so mas-package.sh covers the distance between the .app and something
App Store Connect will take. The order is load-bearing: the provisioning profile goes in before
codesign runs because the signature covers it, which is also why Tauri's own signing is switched
off for this build.
The sandbox costs three things, all Apple's rules rather than choices. The updater is gone, and
falls out for free because the plugin was already conditional on the config declaring it; only the
release overlay does. The Check for Updates menu item is gated on the same condition, since a menu
item that errors when clicked is its own rejection risk. The library moves into the container, and
the system spelling dictionary becomes unreadable.
Two things the first upload taught us. The profile is kept owner-only where it lives next to the
signing keys, and cp carried that mode into the bundle; Apple rejects a package containing anything
a non-root user cannot read. And altool exits 0 after printing UPLOAD FAILED, so its exit status
cannot be trusted and the transcript is the only reliable signal.
Claude-Session: https://claude.ai/code/session_018HwEWvoE1NkkUjJCMLSnup
An unsigned bundle on a current macOS opens to a malware warning with no obvious way past it, and
the way past it that does exist teaches people to click through exactly the warning worth reading.
The build now signs with a Developer ID certificate and notarizes with an App Store Connect API
key, which is also what does the App Store upload, so there is one credential to rotate.
The verification step is the point. codesign only says a signature is internally consistent;
spctl is what a person double-clicking the file actually meets, and it does not pass until the
notarization ticket is stapled.
A final job rewrites the version and sha256 in the tap's cask, using an SSH deploy key rather than
a token so a leak from a release job cannot reach the app repositories.
Also fixes Cargo.lock drifting a version behind on every release: the bump wrote Cargo.toml but
never staged the lock, so any fresh build dirtied the tree.
Claude-Session: https://claude.ai/code/session_018HwEWvoE1NkkUjJCMLSnup
The repo had no licence at all, which legally means all rights reserved. FSL grants free use for
anything except building a competing product, and each version becomes MIT two years after its
release. AGPL was the other candidate and is ruled out by the App Store, whose terms impose
restrictions the GPL forbids.
Cargo.toml still carried the Tauri scaffold defaults, and that description ends up in the deb and
rpm metadata.
Claude-Session: https://claude.ai/code/session_018HwEWvoE1NkkUjJCMLSnup
Move Check for Updates into the macOS app menu and add Settings (Cmd+,),
Save (Cmd+S), Find (Cmd+F), chapter navigation, and a Help > Report an
Issue link. Route new menu actions through menu-action events and trim
duplicate editor keybindings.
Replace native update dialogs with an in-app modal that shows a live
download progress bar and installing state, so updates no longer feel
abrupt. Drives the plugin's downloadAndInstall progress events through a
zustand store.
NSSpellChecker's implicit URL handling is unreliable for URLs alone on a
line (a bookmark list), flagging the whole URL as one misspelling. Request
the link checking type alongside spelling so the data detector claims URL
ranges, then keep only spelling results. Real misspellings are unaffected.
The bundled Hunspell dictionary (~49k SCOWL roots) lacked common words like
"cybersecurity", "scalable", and "assistantship", and checked words inside
URLs. On macOS, spelling now uses the system NSSpellChecker (same engine as
TextEdit/Safari): far better vocabulary, skips URLs, and respects words the
user has taught macOS. The custom/tech dictionaries and Harper grammar are
unchanged; non-macOS builds keep the Hunspell fallback.
include_str! embeds google-credentials.json at compile time, but the
file is gitignored, so CI checkouts had no file and the Rust crate
failed to compile on all three platforms. Write it from the
GOOGLE_CREDENTIALS secret, falling back to the committed example
placeholders so the build always succeeds.
Embed each bundled font the book uses (fetched in the webview) with its
own @font-face, point body text and headings at the chosen families, and
drop the hardcoded Literata injection in the packager. System fonts are
referenced by name with a serif fallback since they cannot be embedded.
Interpolate the book's body and heading families into the Typst
preamble, title page, cover, and contents. Embed the needed bundled
fonts and load any chosen system fonts (via fontdb) into the Typst
engine so PDF preview and export match the on-screen fonts.
Add a Typography section to book setup with one-click preset pairings,
an advanced body/heading picker, and a live sample. Enumerate installed
fonts via a new list_system_fonts Tauri command (fontdb) so they appear
alongside the bundled set, with a note that system fonts may not embed
in exports.
Split heading typography onto a new --font-heading token and set both
--font-book and --font-heading from the active book's fonts when it
opens or its setup changes. Declare @font-face for the new bundled
fonts.
Add EB Garamond, Lora, Source Serif 4, and Fraunces (OFL variable
fonts) and a font catalog with body/heading pairings. Store the chosen
fonts per book in BookSettings.fonts, defaulting to Literata so existing
books are unchanged.
* fix(persist): atomic+durable .margin writes
Route save_book, write_file, write_bytes through a shared atomic_write
helper: write to a temp file, fsync, then rename into place so a crash
mid-write can never leave a truncated or empty manuscript. Rotate the
prior copy to .bak for manuscripts (not exports) so one good copy
always survives.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(editor): isolate undo history per chapter
Key the editor by chapter id so each chapter gets a fresh TipTap
instance with its own history. Previously a single instance was reused
across chapters, so the content swap entered the undo stack and Ctrl+Z
after switching restored the prior chapter's doc into the current one,
which autosave then persisted.
With remount, save the cursor/scroll position on unmount (from a ref,
not the editor, which is destroyed by then) and load it on mount,
replacing the in-place chapter-switch bookkeeping.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* feat(editor): persist active chapter per book
Remember which chapter was open per book and restore it on reopen,
falling back to the first chapter when the saved id is no longer valid.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(persist): flush pending save on navigate-away and quit
Edits are saved on an 800ms debounce, so leaving a book within that
window lost the latest changes. Flush a dirty book before openBook /
closeBook swap state (covers All books, open-another, Cmd+N), and add a
window close-request handler that saves before destroying on quit.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(persist): guard markSaved against in-flight edits
markSaved cleared dirty unconditionally, so an edit made while a save
was in flight had its dirty flag wiped and its follow-up save dropped.
Only clear dirty when the book reference is unchanged since the save
started (immutable updates give a fresh reference per edit).
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(persist): persist new books on create
A freshly created book lived only in memory until the first edit, so
quitting beforehand lost it and it never appeared in the library. Save
it to disk on creation via a shared createAndOpenBook used by both the
Library card and the New Book menu action.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(export): correct list nesting, marks, line-start escaping, svg type
Four export-correctness fixes intermingled across the Typst and EPUB
renderers:
- H7: neutralize newlines in escaped text and apply leading-marker
escaping (= - + / and N.) to the first text node of every block
(paragraph, heading, list item, blockquote) and figure captions, even
when that run carries a mark, so author text can never inject Typst
markup or phantom headings/TOC entries.
- H9: render lists recursively so nested lists survive and multi-
paragraph list items keep their paragraph breaks (Typst functional
list()/enum(); EPUB nested <ul>/<ol> and multiple <p> in <li>).
- H10: render strike, underline and inline code marks in both exporters
(Typst #strike/#underline/#raw; EPUB <s>/<u>/<code> with css).
- H8b: normalize +-bearing image subtypes so an imported svg+xml cover
becomes cover.svg with the correct media type.
Validated the generated Typst constructs compile with typst 0.15.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* feat(export): surface typst warnings and glyph-coverage notice
Typst compile warnings were discarded. Capture them via the Warned
result and emit a pdf-warnings event (only from the export path, not
the live preview) that App shows as a non-fatal toast; the export still
succeeds. Also scan book text before reporting success and warn when it
contains scripts the embedded Latin fonts cannot render (CJK, Arabic,
Hebrew, Devanagari, Thai, emoji).
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(ui): handle library-open errors
Opening a book card swallowed failures: a corrupt or unreadable file
failed silently and normalizeBook could throw on a missing chapters
array. Surface load errors as a toast, throw a clear message on
unparseable JSON, and make normalizeBook tolerant of missing chapters,
metadata and settings.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(preview): destroy pdf.js document on recompile
The preview created a new PDF document on every recompile but never
destroyed the old one, leaking the document and its worker transport.
Keep the loading task and destroy it on effect cleanup.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(library): surface unreadable books
list_books silently skipped any .margin file it could not read, parse,
or that lacked an id, so a truncated manuscript just vanished. Emit a
corrupt placeholder entry instead and show it in the library as a
non-openable card with a recovery hint pointing at the .bak backup.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(persist): schema-version guard and editor content check
A book file's declared schema was never read, so a file from a newer
app version would be opened, normalized and autosaved over in the old
format. Refuse to open books whose schema is newer than supported.
Enable the editor content check so chapters with unreadable content
warn the user instead of silently loading stripped content that the
next edit would persist.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(editor): prune chapter positions on delete and guard quota
Saved cursor/scroll and active-chapter entries accumulated per book
forever and were never removed when a book was deleted. Add
clearPositions and call it from the delete flow, and guard localStorage
writes against quota or unavailable storage.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(security): validate book id paths
load/save/delete_book built paths from the raw id, and ids round-trip
from file JSON, so a crafted id could escape the library directory.
Route all three through a book_path helper that rejects anything but
[A-Za-z0-9_-].
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* chore(security): restrictive content security policy
security.csp was null (no CSP). Add a restrictive policy as
defense-in-depth: self-only by default, data:/blob: images for cover
and figure URLs, inline styles for React/inline style attributes, and
blob: workers for pdf.js. Needs verification against a production build
(covers, preview, export, IPC).
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(import): preserve link, underline, strike and code marks
EPUB import only mapped bold and italic; a, u, s/del/strike and code
elements lost their formatting (kept as plain text). Map them to the
matching marks, consistent with what the exporters now render.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(import): keep inline svg and unresolved image alt text
Inline svg elements were dropped entirely and an img whose source could
not be resolved was discarded. Serialize a standalone svg to a
data:image/svg+xml figure, and fall back to an unresolved image's alt
text as a paragraph instead of losing it.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(import): keep table rows together
Tables were flattened to one paragraph per cell, losing row grouping.
Join each row's cells into a single paragraph so the row structure
survives (the model has no table node).
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(import): import the cover image
The EPUB cover was discarded and replaced with the default generated
cover. Locate the cover via the manifest cover-image property or the
meta name="cover" reference, resolve it to a data URI, and import it as
an image cover.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(import): validate ISBN with checksum
The importer accepted any 9-13 digit run as the ISBN, so a UUID or
arbitrary number became a bogus ISBN. Prefer scheme-tagged identifiers
and accept only values that pass the ISBN-10/13 length and checksum
test, leaving it empty otherwise.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* fix(import): isolate bad chapters and bound nesting depth
A deeply nested document could overflow the stack and abort the entire
import. Bound block recursion depth and wrap each chapter build in a
try/catch so one unreadable chapter is skipped instead of failing the
whole import.
Claude-Session: https://claude.ai/code/session_01RzTWrLiy4zGuw2hZSQGamk
* changes
Positions were kept in an in-memory Map, so they were lost on quit and
the cursor jumped to the top on reopen. Store them in localStorage
(keyed by book + chapter, like theme/width) and restore on app open,
not just on in-session chapter switches. Also save the current chapter
continuously (debounced on selection + scroll) so closing the app
records where you left off, and re-apply scroll after fonts load.
proof_text loaded the dictionary and ran spell/grammar checks
synchronously inside an async command, blocking an async-runtime worker
and holding the engine mutex for the whole call. Move the work to
spawn_blocking so IPC stays responsive under load.
The render loop swallowed getDocument/render rejections, leaving the
preview silently blank with no signal about what failed. Surface the
error in the pane and recover on the next compile. Also drop the
redundant canvasContext arg (pdf.js v6 renders via the canvas itself).