Patch release: nested code fences parse correctly, reference link
labels must immediately follow the link text, and username@host is
recognized as part of a URL.
Scans compiled route handlers for the Unit coercion the compiler emits
when a respond is the value of an exhaustive when, which is the shape
that broke the bio endpoint and is invisible in source review.
Includes a positive control: a fixture written in the unsafe shape that
the detector must keep flagging, so a codegen change fails the test
instead of silently disabling it. Also asserts a floor on the number of
handlers scanned so a broken filter can't pass vacuously.
Adopt the Koin Kotlin compiler plugin (io.insert-koin.compiler.plugin 1.0.2)
and convert :common's DI from classic DSL to the plugin DSL
(org.koin.plugin.module.dsl), enabling compile-time DI graph validation
(compileSafety = true) while keeping the centralized module{} layout.
- Convert singleOf/factoryOf/scopedOf(::X) -> single<X>()/factory<X>()/scoped<X>()
across mainModule (root + ProjectDefScope block), migratorModule, and the
android/desktop/ios platformModule + exampleProjectModule actuals.
- Provider functions and when/config bodies use create(::fn) to keep auto-wiring
and validation; explicit-lambda and named() bindings stay classic.
- Add koin-annotations (BOM-managed) for @Provided; mark SandboxFileAccess
@Provided (its definition lives in the desktop app module, external to :common).
- Relocate the iOS startKoin into a plugin-less bootstrapKoin() helper in :base so
common's iOS compilation is no longer a full-graph (A3) aggregator.
Green with compileSafety=true on desktop, android, common metadata, and
desktopTest (1404 tests). iOS/Native is blocked by an upstream plugin bug: the
generated dsl_single hint functions encode named() qualifiers as parameter names,
which K/N's IdSignature ignores, so same-type qualified definitions (the three
CoroutineContext dispatchers) clash during klib serialization. Documented inline.
* Projects are now detected only if they have a project.toml file, other directories are ignored
* Update plugin ee.schimke.composeai.preview to v0.16.24 (#724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* Update dependency org.jetbrains:markdown to v0.7.6
---------
Co-authored-by: Adam Brown <adamwbrown@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* Add iOS UI smoke tests (XCUITest)
Adds an iOS UI smoke-test suite that drives the real app on a simulator via
XCUITest, the iOS analogue of the android/src/androidTest Compose UI tests.
The whole iOS UI is Compose Multiplatform, and CMP (1.8+) maps Compose testTags
to iOS accessibilityIdentifiers automatically, so the tests target the same
testTags the Android suite uses.
Workflows covered (all green on simulator):
- LaunchSmoke: app boots through the SwiftUI entry point, Koin + data migration
run, project selection renders.
- ProjectWorkflow: create + open a project (exercises the create dialog text
entry and navigation into the editor).
- SceneEditorWorkflow: create a scene and confirm it opens in the editor.
- NotesWorkflow: navigate to Notes and open the create-note screen.
Setup:
- ios/scripts/add_ui_test_target.rb idempotently creates the iosUITests
UI-testing target + shared scheme via the xcodeproj gem (the folder previously
had source files but no actual target).
- ios/scripts/disable_sim_hardware_keyboard.sh forces the software keyboard so
XCUITest text entry lands (Compose fields need it).
- ios-ui-tests CI job runs the suite on a simulator and uploads the xcresult on
failure.
- composeUi ProjectCreateDialog: tag the name field so the create flow is
targetable (also benefits Android).
Known limitation: the app's custom rich-text editors (scene body, note body via
MarkdownEditField) do not report keyboard focus to XCUITest, so their text entry
can't be driven (only standard Compose text fields can). The scene and notes
tests therefore stop at "editor opens" / "creation screen opens"; the full
edit/create-with-body paths remain covered by the Android suite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* iOS UI tests: type into note body via composetexteditor 2.3.0
composetexteditor 2.3.0 publishes text-editing accessibility semantics on its
editor, so XCUITest can now drive text entry into it. Bump the dependency and
promote the Notes UI test to the full Android-parity flow: create a note by
typing into the body, then assert its card appears.
The scene edit+save flow stays scoped to "scene opens" for now — text entry
works, but the scene editor's initial-buffer gating + dirty-driven save make
the save affordance unreliable to assert from an IME-driven edit; promoting it
is a follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the Keychain-backed AuthTokenStore for iOS, completing the
client-side token-at-rest encryption left as a TODO when Android and
desktop were done. The account-keyed token map is stored as a single
kSecClassGenericPassword item (encrypted by the OS) instead of the
plaintext file store. Backed by multiplatform-settings' KeychainSettings.
Matches the other stores' post-refactor shape: no plaintext-file
migration (that FileAuthTokenStore was never shipped); the genuine legacy
case - inline tokens in server.json - is handled by migrateInlineTokens
in the common ServerSettingsFilesystemDatasource.
Also unblocks and enables the iOS unit test suite in CI. iOS test
linking failed because okio-fakefilesystem references the deprecated
kotlinx.datetime.Clock typealias, which double-binds during Kotlin/Native
klib caching. Since commonTest declared okio-fakefilesystem, it polluted
the Native test classpath even though only JVM tests use FakeFileSystem.
Moved that dependency to jvmTest (still reaches desktopTest). The
ios-compile job now also runs :common:iosSimulatorArm64Test (110+ tests
across 11 suites that previously never ran).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Bump composetexteditor to 2.2.5, which keeps decoration spans
(spell-check underlines, find highlights) out of the undo history and
off the editOperations stream via the new RichSpanStyle.isDecoration
flag. This fixes undo/redo appearing dead in the scene/focus editors.
With decorations no longer emitting, the local isDecorationOnly() edit-
stream filter is redundant — remove it and its two collectors' filters.
Route DraftCompare diff highlights through updateRichSpans (the batch
overlay API), since they are overlays too.
* Stop spell-check spans being wiped on undo
toEditorSpellChecker() built a new EditorSpellChecker instance every
recomposition. rememberSpellCheckState keys its full-rescan LaunchedEffect
on that instance, so it restarted runFullSpellCheck on every edit —
removing all spell spans and re-adding them across suspend points. A
recomposition mid-rescan (now reachable since undo actually mutates text)
cancelled it after the remove-all, wiping every span document-wide with
no recovery.
Remember the EditorSpellChecker per dictionary so the rescan effect only
runs when the dictionary actually changes.
Mirrors the recently added RTF import with an RTF export option. RTF joins
Markdown/EPUB/PDF/DOCX in the export dialog and the export pipeline.
- Add ExportFormat.Rtf with the .rtf extension and application/rtf mime type
- New RtfStoryRenderer renders the title page, a linked Contents page, and
per-chapter sections, converting each chapter's markdown into formatted RTF
runs (bold/italic, headings, lists, quotes, code, links, horizontal rules).
Heading and link colors pick up the project theme accents, matching DOCX/PDF.
- Wire the format into ExportStoryUseCase, the export options dialog, and the
Android save-picker mime type; localized format label added for all locales
- Tests cover well-formed/balanced RTF output, metadata, bookmarks, escaping,
unicode escapes, theme colors, an end-to-end use-case run, and a round-trip
back through the RTF importer
Adds RTF as an import format alongside Markdown. RTF has no headings, so
the importer collects each paragraph with its formatting and splits into
scenes by one of three user-chosen strategies: formatting (outline level
/ font size / bold), a chapter-heading regex, or a single scene.
Shared scene/group folding is extracted into ImportStructure so both
importers reuse it, and a StoryImporterRegistry routes by file extension.
Importers now take ByteArray so RTF bytes aren't decoded prematurely.
Markdown gains an Auto detection mode (the new default) that reads a lone
leading heading as the project title and picks the chapter level by
frequency. The create-dialog help affordance becomes an icon button.
Replace the plaintext FileAuthTokenStore binding with platform-specific
encrypted stores behind the same AuthTokenStore interface, wired via a new
expect/actual authTokenStoreModule.
Android: EncryptedSharedPrefsAuthTokenStore backed by EncryptedSharedPreferences
with a Keystore-backed AES256_GCM master key (androidx.security:security-crypto).
Desktop: EncryptedFileAuthTokenStore writes the token-map JSON as AES/GCM/NoPadding
to the config directory. The key is derived (PBKDF2WithHmacSHA256) from the OS user
name and home dir plus a static salt, with no key file on disk, so a copied token
file is useless on another machine or user. A random 12-byte IV is prepended per
write and owner-only POSIX perms are applied best-effort. Decryption failure is
treated as no tokens rather than crashing. This guards against casual disk
scraping and off-machine copies, not same-user local malware that can re-derive
the key.
iOS: still uses the plaintext file store pending a Keychain-backed implementation
(TODO marker in the iOS binding).
Migration: a legacy plaintext auth_tokens.json from an intermediate build is
imported into the encrypted store and deleted on first access; existing encrypted
tokens win on key collision so a stale plaintext entry cannot clobber a fresh
session.
Every project-scoped client API built its request path by raw string
interpolation of projectName, e.g. "/api/project/$userId/$projectName/begin_sync".
That string reached the shared url() builder whose only path handling was
pathSegments = path.split("/"). Because the split ran on the already-interpolated
string, a projectName containing "/" was split into extra discrete path segments
and a ".." survived as a literal traversal dot-segment, so the outbound request
could target a different endpoint than the {userId}/{projectName}/{action}
template intended (e.g. a malicious sync server returning a project named
"p/../../../api/account/test_auth").
The shared ProjectNameValidator permits "/", "\" and "." (they are encoded to
disk-safe lookalikes only when used as a directory name), so a server-supplied
project name persists verbatim and then injects into every subsequent
project-scoped request under the same host with the bearer token attached.
Fix: each dynamic value is now percent-encoded into a single opaque path
segment via String.encodeUrlPathSegment() before interpolation, and the sink
sets encodedPath directly. Embedded "/" becomes %2F so it cannot create extra
segments, and an all-dots segment is encoded to %2E so a ".." name cannot act as
a traversal segment. The validator is intentionally left unchanged: tightening
it to reject "/" or "." would break syncing for already-valid existing project
names, so encoding is the backward-compatible fix and the on-disk
encodeForFilename behavior is untouched.
Adds a MockEngine test asserting a malicious projectName collapses to a single
encoded segment in the outbound URL across ProjectDataApi, ServerProjectApi and
WritingActivityApi.