Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
858 commits
Select commit Hold shift + click to select a range
2105086
data-browser: fix ? and \ hotkeys broken by react-hotkeys-hook v5
joepio Jul 8, 2026
4e331ec
Outbox fix: count debounced + cold-drain saves as pending (no silent …
Polleps Jul 8, 2026
c8cb9a2
desktop: Android Tauri app boots and syncs — TLS verifier init, gradl…
joepio Jul 8, 2026
ebbffe8
data-browser: shortcuts overlay - real filter input, scroll inside panel
joepio Jul 8, 2026
b42f69d
planning: device-pairing.md — one-scan server↔phone pairing UX + enve…
joepio Jul 8, 2026
120a0c2
data-browser: one floating-surface style for menus and popovers
joepio Jul 8, 2026
7971be2
data-browser: dropdown menu behaves like the popovers
joepio Jul 8, 2026
9bd9e24
data-browser: presence for canvas + tables on the drive presence channel
joepio Jul 8, 2026
335c4bf
planning: device-pairing — SaaS device directory for zero-scan pairing
joepio Jul 8, 2026
d043233
data-browser: client-side demo workspace + Meetings (follow + chat), …
joepio Jul 8, 2026
8590a6d
Fix three e2e flakes: sync offline-reconnect, table views, table cell…
joepio Jul 8, 2026
648717d
Device pairing: stop logging the agent secret; device-directory client
joepio Jul 8, 2026
2dd47ba
lib: atomic://pair envelope encode/decode (device pairing P1)
joepio Jul 8, 2026
a41933d
data-browser: QR pairing dialog + atomic://pair paste support
joepio Jul 8, 2026
f86d61f
planning: device-pairing — record P0/P1 render-side progress
joepio Jul 8, 2026
867a597
Demo polish: fix canvas draw crash, enrich team table, reactive say-h…
joepio Jul 9, 2026
01ae700
Demo: drop welcome-doc todo list; RTE: copy resource mentions as thei…
joepio Jul 9, 2026
6e67dd6
Device pairing: atomic:// deep link + import flow (phone-side receiver)
joepio Jul 9, 2026
5f260ca
Comments/chat: tighter, aligned message rows with compact timestamps
joepio Jul 9, 2026
44b6d01
Chat: avatars beside messages; compact, aligned follow-event lines
joepio Jul 9, 2026
1f0c47e
Navbar panel toggles show open state; meeting survives refresh
joepio Jul 9, 2026
ef541d3
Avatars: shared follow menu + online dot everywhere (incl. chat)
joepio Jul 9, 2026
51b7448
Device pairing: make atomic:// deep links actually work on Android
joepio Jul 9, 2026
b63c938
Kanban: custom vertical card ordering with a live drag preview
joepio Jul 9, 2026
07db293
Demo on Tauri: ClientDb becomes a platform default, not a hard block
joepio Jul 9, 2026
51042e3
Chat: rounded hover chip clear of the avatar; meeting trail dedups pe…
joepio Jul 9, 2026
737f055
Navbar: collapse actions to icons based on real overflow, not a fixed…
joepio Jul 9, 2026
72eb83e
Meeting: toast new chat messages when the meeting panel is closed
joepio Jul 9, 2026
81e3eb9
Android UX: swipe to open/close the sidebar; keyboard resizes the app
joepio Jul 9, 2026
40c51e9
Right panel: sidebar-style slide animation + overlay-on-small-screens
joepio Jul 9, 2026
6da5038
Fix ClientDb never starting on Android: stale SharedWorker guard
joepio Jul 9, 2026
043b49e
i18n: regenerate translation catalog (single-writer clean)
joepio Jul 9, 2026
f7c1cf9
Sync page: full device-to-device pairing (QR + copyable code + paste,…
joepio Jul 9, 2026
cc4e9c3
Sync UI: target the local server origin, not a bare path (fixes deskt…
joepio Jul 9, 2026
0eafe66
data-browser: live "typing…" presence for chat & comment composers
joepio Jul 9, 2026
7999fd5
Welcome screen: scrollable on phones (was clipped; buttons unreachable)
joepio Jul 9, 2026
9df9800
Drive UX redesign: user page owns drives, caret switcher, drop dead s…
joepio Jul 9, 2026
d43f9b9
Redesign the Sync page around unified connection cards
joepio Jul 9, 2026
b783b9a
Fix host-to-drive binding: /setup becomes /bind-drive and actually wr…
joepio Jul 9, 2026
cd2fb9b
Planning: tours/replayable meetings + DID-branch deployment strategy
joepio Jul 9, 2026
05befe2
Fix cargo test -p atomic_lib for every feature combination
joepio Jul 9, 2026
227acce
Sync page: honest local-only state + 'Sync this workspace' promote
joepio Jul 9, 2026
9998a0d
locales: extract Sync page redesign + promote strings
joepio Jul 9, 2026
218dc72
Fix 'Exit demo' getting stuck on 'Leaving…' forever
joepio Jul 9, 2026
8ce3c03
Demo v3 + presence: meeting narration, live nudges, de-duplicated fol…
joepio Jul 9, 2026
6cdab0e
Never store the agent's private key in plaintext
joepio Jul 9, 2026
f80b3f7
Scan pairing QRs with the device camera
joepio Jul 9, 2026
fd5fbdc
Introduce Android devices by name, not "localhost"
joepio Jul 9, 2026
48aed6c
Point a freshly signed-in device at the data it's missing
joepio Jul 9, 2026
ca18338
Meet a data-less sign-in with a way to reach the data
joepio Jul 10, 2026
dc3d6fb
Stop telling people their workspace isn't there while it's arriving
joepio Jul 10, 2026
5a68f10
Write one node identity, not two
joepio Jul 10, 2026
3f528c6
A pairing code can no longer hand over an account
joepio Jul 10, 2026
3ef88f7
Make room for the on-screen keyboard on the onboarding screens
joepio Jul 10, 2026
02a4575
Shrink the layout for the keyboard instead of scrolling the top bar away
joepio Jul 10, 2026
98ad6a3
Show pairing as steps in a dialog, not a status line and a toast
joepio Jul 10, 2026
30034f8
Land pairing on success or on a reason, and rebuild the Sync page's p…
joepio Jul 10, 2026
26c499b
Refuse peer sync from a different account, instead of syncing nothing
joepio Jul 10, 2026
ec90714
Make a device's node act as the signed-in user, not as its own server
joepio Jul 10, 2026
721b780
Ask whether the drive is here, not whether it exists somewhere
joepio Jul 10, 2026
c7bf3b2
Tell the local clients about changes no commit produced
joepio Jul 10, 2026
7feb467
Remember the drive a device syncs, so it reconnects on its own
joepio Jul 10, 2026
2e04f02
Sync the agent resource between a user's own devices
joepio Jul 10, 2026
a6f4930
Record the resolution: the node relays and serves, never signs as a p…
joepio Jul 10, 2026
b175448
Plan node-as-granted-replica: how autonomous private-drive replicatio…
joepio Jul 10, 2026
0a4901c
P2: classify a commit's authorization impact
joepio Jul 10, 2026
2e3c538
Add Resource::genesis_signer(): the forge-resistant creator from the …
joepio Jul 10, 2026
3c7bb9e
Golden cross-language vectors for the v1 genesis certificate
joepio Jul 10, 2026
dcd22b6
Bind the browser GenesisCert to the shared golden fixture
joepio Jul 10, 2026
9e93e33
Add signBytes to the crypto providers + Agent
joepio Jul 10, 2026
f11541f
Mint cert-based DIDs at creation in newResource; drop _new: from that…
joepio Jul 10, 2026
d31702b
Send the cert-DID subject in the genesis commit body
joepio Jul 10, 2026
232aca8
Cert-mint file uploads; move AI ids off the _new: scheme
joepio Jul 10, 2026
34caaba
Zero-scan: auto-connect the account's devices on sign-in
joepio Jul 10, 2026
68f5ffe
Add "Back up to Cloud Sync" for a locally-started drive
joepio Jul 11, 2026
482dde7
Sign in to Cloud Sync in-app instead of a dead-end toast
joepio Jul 12, 2026
0fa1f6b
Fix the Tauri dev server serving a stale bundle under nushell
joepio Jul 12, 2026
116fbe8
Simplify the welcome screen to a title and three buttons
joepio Jul 12, 2026
9e8935b
Make skipping the recovery backup a deliberate, warned choice
joepio Jul 12, 2026
1a7ef82
Tell an over-quota drive apart from an unenrolled one
joepio Jul 12, 2026
d90c6df
docs: state the no-phone-home guarantee for self-hosters
joepio Jul 12, 2026
8ec3e72
Send the backup CTA to a sign-in form, not the sales page
joepio Jul 13, 2026
a0aa17a
Let a server push one of its drives to another server
joepio Jul 13, 2026
08278cf
Let a user ask their server to replicate a drive elsewhere
joepio Jul 13, 2026
ad1ccfc
Demo improvements, and planning for importer
joepio Jul 13, 2026
12491b5
fix: restore DID-aware website templates
joepio Jul 14, 2026
d6b20cc
feat: classify Atomic DID subject kinds
joepio Jul 14, 2026
5ffa7d1
fix: read creation metadata from genesis certificates
joepio Jul 14, 2026
3c84a9a
fix: make scoped search resilient to index lag
joepio Jul 14, 2026
c6f1d88
fix: stabilize collaborative browser flows
joepio Jul 14, 2026
b0bf559
refactor: remove unused WebSocket frame encoders
joepio Jul 14, 2026
f98d706
docs: document template drive configuration
joepio Jul 14, 2026
a89e857
docs: expand importer and browser assistant plans
joepio Jul 14, 2026
f19ee5e
style: apply Rust formatting
joepio Jul 14, 2026
c61e110
fix: derive `drive` from `parent` so moving a resource revokes inheri…
joepio Jul 14, 2026
e750a35
feat: drafts and suggestions as a fork of a resource
joepio Jul 14, 2026
5782b01
feat: Edit as draft / Merge draft / Open original in the actions regi…
joepio Jul 14, 2026
a34e2b0
fix: a fork must not inherit the original's ACL; show drafts in their…
joepio Jul 14, 2026
453fbb7
style: make the draft bar a bar, not a card
joepio Jul 14, 2026
55d73c9
fix: three-way merge so a draft can't revert a concurrent edit to the…
joepio Jul 14, 2026
5442f93
test: draft flow e2e; fix DraftBar change count frozen by React Compiler
joepio Jul 15, 2026
66bd09f
docs: record that Loro-container classes aren't draftable yet
joepio Jul 15, 2026
49c9f5e
feat: surface pending drafts on the original, so a reviewer finds the…
joepio Jul 15, 2026
14fc694
feat: CRDT body merge for document drafts
joepio Jul 15, 2026
ed176c9
feat: meeting agenda/minutes feature (integrated from codex/meeting-a…
joepio Jul 15, 2026
6844828
refactor: split Fork (edit an existing resource) from Draft (new unpu…
joepio Jul 15, 2026
faf503e
docs: update the drafts/forks design doc for the Fork/Draft terminolo…
joepio Jul 15, 2026
f5be356
test: fix e2e typecheck drift so `pnpm typecheck` passes in the e2e p…
joepio Jul 15, 2026
a072fc7
fix: bootstrap the meeting and forks ontologies into the client store
joepio Jul 15, 2026
eddaeba
feat: record meeting join/leave in the chat, like start/end
joepio Jul 15, 2026
551a555
feat: meeting panel polish — Notes link, subtle Leave, title-to-edito…
joepio Jul 15, 2026
d9ee37a
fix: resolve the personal drive via getResource, not a raw server fetch
joepio Jul 15, 2026
be0b1b4
Dashboard view planning #1234
joepio Jul 15, 2026
ebd3e48
docs: update auth, genesis, and websocket docs to match shipped v2 be…
joepio Jul 15, 2026
eb253ef
docs: add the dashboards proposal (user- and LLM-composable views)
joepio Jul 15, 2026
bceef2b
test: offline agent persistence — save then resolve an agent with no …
joepio Jul 15, 2026
e85a7c8
feat(desktop): read-only NFS virtual drive — mount your Atomic drives
joepio Jul 15, 2026
323d099
feat(desktop): start/stop the virtual drive from Settings
joepio Jul 15, 2026
a14d2c5
feat(desktop): mount the virtual drive from the app, no terminal
joepio Jul 15, 2026
4d692b3
feat(desktop): virtual drive write path — edit files, changes sync back
joepio Jul 15, 2026
081c977
feat(desktop): persist the virtual-drive fileid map (stable ids acros…
joepio Jul 15, 2026
f248c77
feat(desktop): cache directory listings so readdir paging isn't O(N²)
joepio Jul 15, 2026
ab0bb3d
perf(desktop): skip the virtual-drive commit when a file's bytes are …
joepio Jul 15, 2026
4ffe853
feat(desktop): content-defined chunking for large virtual-drive files
joepio Jul 16, 2026
cddff43
perf(desktop): make virtual-drive listings fast on large stores
joepio Jul 16, 2026
028c21c
feat(desktop): surface all resources in the virtual drive, scoped to …
joepio Jul 16, 2026
b3dbc68
perf(desktop): reconstruct-once read cache + VFS benchmarks
joepio Jul 16, 2026
0f1782a
feat(server): implement GET /drive-usage (per-drive storage) + test
joepio Jul 16, 2026
0d25836
refactor(data-browser): unify server connect/switch inline on the Syn…
joepio Jul 16, 2026
ae8dc07
feat(data-browser): working server switch, remove-server, and Node ID…
joepio Jul 16, 2026
a28a238
feat(data-browser): accept bare host in connect box + restore explainer
joepio Jul 16, 2026
61158b8
fix(lib): don't error-toast benign redundant-genesis commit drops
joepio Jul 16, 2026
11d58ff
test(server): fresh client reads a replicated resource after source i…
joepio Jul 16, 2026
3018cf9
fix(lib): scope connection status to the active server + Sync card re…
joepio Jul 16, 2026
7cd7c62
chore(data-browser): extract new Sync page strings
joepio Jul 16, 2026
d243495
refactor(data-browser): one stable server list, active one marked not…
joepio Jul 16, 2026
4503338
chore(data-browser): re-extract Sync strings after the stable-list re…
joepio Jul 16, 2026
4e660fe
perf(lib): make per_drive_usage O(drive) instead of O(store)
joepio Jul 16, 2026
44aa33f
chore: fix lint + format drift already on the branch
joepio Jul 17, 2026
f6a2801
chore(build): drop dependency debuginfo from the dev profile
joepio Jul 17, 2026
5c203ef
refactor(lib): Endpoint handlers are closures, built with a builder
joepio Jul 17, 2026
9808028
feat(server): a node describes itself as an Atomic Server resource
joepio Jul 17, 2026
5844897
refactor(lib): endpoints declare the params they need, and share one …
joepio Jul 17, 2026
59c2907
fix(server): version links point at a path that exists
joepio Jul 17, 2026
cea4c39
fix(lib): a version id names the state its change produced
joepio Jul 17, 2026
ad14f07
feat(server): versioning reads Loro history, not the commit log
joepio Jul 17, 2026
f2d0317
feat(flutter): server settings in the shared views
joepio Jul 17, 2026
c7a1bef
feat(data-browser): show a pairing code for the server your drives li…
joepio Jul 17, 2026
2886b9b
feat(flutter): read the pairing codes the other Atomic apps show
joepio Jul 17, 2026
6d68c6e
fix: a LAN address is a dev server, not a public one
joepio Jul 17, 2026
21837f1
fix(flutter): let debug builds talk to a dev server
joepio Jul 17, 2026
04b345a
fix(data-browser): offer the code, not a server URL, when your data i…
joepio Jul 17, 2026
0a1c576
fix(data-browser): stop offering a pairing code that can never authen…
joepio Jul 17, 2026
61fc7cf
chore: refresh lockfile and strings after the pairing-code revert
joepio Jul 17, 2026
e9aacc2
feat(flutter): offer this device's workspace to a server
joepio Jul 17, 2026
f75d2bb
fix(flutter): make the generated bridge compile as generated
joepio Jul 17, 2026
cc24567
fix(flutter): stop asking where to sync an identity that has nothing yet
joepio Jul 17, 2026
8c15ebf
docs: a sync & onboarding guideline, and make the clients point at it
joepio Jul 17, 2026
6b782d2
fix(sync): rights decide who may sync, not whether they are you
joepio Jul 17, 2026
6926e29
feat: a server is a device you can scan, and we call it one
joepio Jul 17, 2026
f1497af
docs: correct the guideline where it taught my mistake
joepio Jul 17, 2026
9b6971e
refactor(data-browser): say it once
joepio Jul 17, 2026
5b68c14
fix(data-browser): show the code on the screen that needs it most
joepio Jul 17, 2026
d7ae65d
fix(sync): a push is not a failure, and neither is being in sync
joepio Jul 17, 2026
0388a28
docs: record how the fixture hid the flow
joepio Jul 17, 2026
f80d148
fix(flutter): say what the sync did, not how much it imported
joepio Jul 17, 2026
5ac5e33
feat(server): /server says which devices it syncs with
joepio Jul 17, 2026
a346b17
fix(data-browser): signing in cannot end in somebody else's workspace
joepio Jul 17, 2026
3cd86c2
fix: a pairing code names its drive, and the scanner adopts it
joepio Jul 17, 2026
d0e80d4
fix(data-browser): sign out instantly, and stop treating the server r…
joepio Jul 17, 2026
69fc5a7
fix(sync): always hand a peer your own agent resource on connect
joepio Jul 17, 2026
dda8c45
fix(flutter): settings speaks the browser's language
joepio Jul 17, 2026
aa218e9
fix(data-browser): find the workspace once a paired device pushes it
joepio Jul 17, 2026
a9c3d1c
fix(flutter): one Devices panel, in the browser's order
joepio Jul 17, 2026
4ae3db2
feat: sign-in secret field — hidden, paste-and-go, with a Paste button
joepio Jul 17, 2026
bda4e2b
perf(flutter): sign in without waiting on Iroh discovery
joepio Jul 17, 2026
4d5063b
fix(flutter): one Devices list, and delete the peer UI it replaced
joepio Jul 17, 2026
1e876fd
feat(data-browser): show the server's paired devices in the Devices list
joepio Jul 17, 2026
ead0db2
test(server): merge integration tests into one binary
joepio Jul 17, 2026
683a25d
fix(lib): authorize relayed sync by owned drive, not peer identity
joepio Jul 17, 2026
f80025d
feat(server): add signed POST /forget-peer to disconnect a paired device
joepio Jul 17, 2026
91c01da
feat(data-browser): disconnect a paired device from the Sync page
joepio Jul 17, 2026
8a015e1
feat(flutter): fold QR pairing into one Devices list with richer cards
joepio Jul 17, 2026
397dcfb
feat(flutter): make the gallery-root title a drive switcher
joepio Jul 17, 2026
435b1d9
fix(lib): persist device identity and paired peers durably
joepio Jul 17, 2026
c061e8a
fix(flutter): invalidate canvas edit cache on remote strokes to stop …
joepio Jul 17, 2026
313d7f9
fix(flutter): merge store state before a stroke so concurrent edits d…
joepio Jul 17, 2026
abd9300
fix(lib): enable mDNS local discovery so same-LAN devices dial directly
joepio Jul 17, 2026
16e5d87
feat(flutter): persistent, copyable error toasts
joepio Jul 17, 2026
ded7f2d
build(flutter): 16 KB-align the Rust native library for Android
joepio Jul 17, 2026
0f74190
fix(flutter): white screen on canvas after a remote stroke
joepio Jul 18, 2026
f4af102
fix(flutter): store the agent secret in secure storage, not plaintext…
joepio Jul 18, 2026
f797905
fix(data-browser): declutter the mobile topbar and unify navbar buttons
joepio Jul 18, 2026
382dfa7
fix(sync): stop RBSR dropping subjects at a range's low end
joepio Jul 20, 2026
2d10b0d
test(sync): cover device sync across real process boundaries
joepio Jul 20, 2026
799eb1c
fix(flutter): keep a peer's stroke through a whole-list rewrite, and …
joepio Jul 20, 2026
f104f11
test(browser): cover the pairing helpers and the sync page
joepio Jul 20, 2026
c12c713
fix(browser): stop shipping a developer's portal override
joepio Jul 20, 2026
fc78080
style: cargo fmt
joepio Jul 20, 2026
ff6f6f4
docs: map test coverage, and the blind spots
joepio Jul 20, 2026
7122dfb
test(server): cover forgetting a paired device
joepio Jul 20, 2026
7fbd938
test(sync): reproduce the concurrent commit vs peer apply data loss
joepio Jul 20, 2026
0585cf2
fix(sync): stop a local edit and a peer update clobbering each other
joepio Jul 20, 2026
3b447c3
test(browser): cover pasting a pairing code
joepio Jul 20, 2026
c431e53
test(browser): cover paired-device cards and forgetServerPeer
joepio Jul 20, 2026
761b896
Content localization and translations i18n #1069
joepio Jul 21, 2026
0472714
Website template locale routing + DID import fix for i18n content loc…
joepio Jul 22, 2026
882e7b0
fix(demo): stop the guest identity flashing "Error loading resource"
joepio Jul 22, 2026
e295505
fix(browser): give the meeting side panel title its own transition tag
joepio Jul 22, 2026
fc0c464
fix(onboarding): don't send self-hosted users to a dev portal URL
joepio Jul 22, 2026
a438d35
rename(managed): call the hosted product AtomicServer.eu, not AtomicC…
joepio Jul 22, 2026
a576fd9
feat(browser): add @tomic/edit-mode, guest-editable local-first page …
joepio Jul 22, 2026
7ceaffc
feat(browser): add marketing site seed/apply scripts and a demo recorder
joepio Jul 22, 2026
fffbdbc
Bump to v0.41.0-beta.1, update changelog
joepio Jul 22, 2026
55c5607
Merge develop into did, pulling in 4 security hotfixes
joepio Jul 22, 2026
fbc2654
fix(ci): unpin Flutter Dagger image from a version 20+ releases behind
joepio Jul 22, 2026
01fa296
Revert the internalId server-managed-property check — breaks local-fi…
joepio Jul 22, 2026
4010427
test(flutter): mock the secure-storage channel for AtomicSession.save()
joepio Jul 22, 2026
478ea20
fix(ci): satisfy oxlint padding-line-between-statements in record-dem…
joepio Jul 22, 2026
5ce8529
fix(ci): mount two repo-root fixture files the Dagger JS container wa…
joepio Jul 22, 2026
7245532
fix(cli): handle LocalizedText in the interactive `new` prompt's data…
joepio Jul 22, 2026
728ae22
Hide genesis
joepio Jul 23, 2026
ea8b5fc
fix(data-browser): DOM prop leaks, missing keys, and New Table Enter-…
joepio Jul 23, 2026
276c33b
feat(browser): per-agent encrypted OPFS databases for the ClientDb
joepio Jul 23, 2026
8bb6b3c
feat(browser): add /app/new-drive route for the managed portal's "+ N…
joepio Jul 23, 2026
49a4435
fix(client-db): give lock-steal recovery a realistic settle budget
joepio Jul 23, 2026
2b96f30
fix(data-browser): restore the New Meeting quick-create button
joepio Jul 23, 2026
4c80d2b
fix(data-browser): move editor-render-phase state updates into effects
joepio Jul 23, 2026
971cec2
fix(lib): release the subject lock before after-commit handlers run
joepio Jul 23, 2026
4a44fca
fix(server): repopulate base models, not just JSON ontologies, on ATO…
joepio Jul 23, 2026
39b9a27
chore(deps): bump wasmtime, quinn-proto, crossbeam-epoch, plist to pa…
joepio Jul 23, 2026
59e86b7
fix(browser): patch 45 real npm vulnerabilities via scoped pnpm overr…
joepio Jul 23, 2026
8a4b473
fix(data-browser): Kanban view polish — icons, header pill, add-card …
joepio Jul 23, 2026
b4fd025
feat(data-browser): colorful mode — tint the app chrome with the main…
joepio Jul 23, 2026
f241952
feat(data-browser): unify the sidebar drive header
joepio Jul 23, 2026
9144050
feat(data-browser): dark mode for the static loading screen
joepio Jul 23, 2026
4f6fe05
feat(data-browser): p2p video & audio calls in the meeting panel
joepio Jul 23, 2026
8bcc0ba
fix(data-browser): merge kanban column header into one rounded card
joepio Jul 23, 2026
5e9add3
feat(data-browser): meetings start from their page, not the top bar o…
joepio Jul 23, 2026
e5e83ce
Create android-data-reuse.md
joepio Jul 23, 2026
f5c3027
Create emoji-cover-images.md
joepio Jul 23, 2026
d2a43d8
feat(data-browser): dark-aware theme-color metas for OS/browser chrome
joepio Jul 24, 2026
d12607a
fix(data-browser): only list origins that answer /server as known ser…
joepio Jul 24, 2026
e5edc3e
Emoji v1 #1235
joepio Jul 24, 2026
1665992
fix(server): qualify propvals filter clauses so `/search?filters=` ac…
joepio Jul 24, 2026
4f88ae2
fix(data-browser): guard slash-menu suggestion handlers against an un…
joepio Jul 24, 2026
bde6ff7
fix(lib): stop `_new:` placeholder resources from looping stuck-commi…
joepio Jul 24, 2026
f235561
Optional vector search
joepio Jul 24, 2026
3578d08
perf(lib): rework query execution and indexes — shallow rows, typed s…
joepio Jul 24, 2026
2ae326f
docs(perf): correct index-rework numbers with verified paired measure…
joepio Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(flutter): server settings in the shared views
The Flutter SDK's server was set once, at login, as raw text, and never
shown again: no way to see which server you were on, let alone change it.
The data-browser grew all of that on its Sync page; `lib/atomic/` is meant
to be the reusable Dart SDK, so the same settings belong there.

Adds a Server section to the shared settings dialog: the servers this device
knows (stable order, the active one marked rather than moved), switch, add,
remove, and what the active node says about itself — version, node id, and
what this drive costs there.

Pure Dart, no bridge work. `/server` is a plain JSON-AD endpoint, and the
agent secret is already in the session, so `/drive-usage` can be signed here
— the same code on mobile and on web.

Signing is the risk in reimplementing a scheme in a third language, so Dart
now asserts the same cross-language golden vectors Rust and TS do
(`lib/src/genesis_test_vectors.json`). That caught this: `encode_base64` is
URL-safe and unpadded, and Dart was writing standard base64 with padding.
The server's decoder is tolerant enough that it would have worked by luck.

URL normalization is shared with login, so `localhost:9883` is accepted
wherever a server is typed.

Claude-Session: https://claude.ai/code/session_01VddwtxveM8Zqsf765aQSHY
  • Loading branch information
joepio committed Jul 17, 2026
commit f2d03173f8842dfc37d181600e77e64940448f1a
91 changes: 91 additions & 0 deletions flutter/lib/atomic/atomic_auth.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/// Request signing, for the endpoints that check who is asking.
///
/// Pure Dart on purpose: the same code signs on mobile and on web, and needs
/// nothing from the Rust bridge. The agent secret already lives in
/// [AtomicSession], so Dart can sign without asking Rust for the key.
///
/// The scheme is the one `atomic_lib`'s `get_authentication_headers` and
/// @tomic/lib's `signRequest` implement: sign `"<url> <timestamp>"` and send
/// the signature, public key, timestamp and agent alongside it. The server
/// rebuilds the message from the request it received — query string included —
/// so the URL signed must be the exact URL fetched.
library;

import 'dart:convert';
import 'dart:typed_data';

import 'package:ed25519_edwards/ed25519_edwards.dart' as ed;

/// Base64 as Atomic writes it: URL-safe, unpadded — matching `encode_base64`
/// in `atomic_lib`. Keys and signatures travel in URLs and DIDs, so the
/// standard alphabet's `+/=` would need escaping.
String encodeAtomicBase64(List<int> bytes) =>
base64Url.encode(bytes).replaceAll('=', '');

/// Base64 as Atomic reads it: either alphabet, padded or not.
///
/// Deliberately more tolerant than [encodeAtomicBase64] writes, mirroring
/// `decode_base64` — secrets and keys minted by older clients (or pasted by
/// hand) come in both alphabets, and a decoder is the wrong place to be strict.
Uint8List decodeAtomicBase64(String input) {
final standard = input.replaceAll('-', '+').replaceAll('_', '/');
final remainder = standard.length % 4;
final padded =
remainder == 0 ? standard : standard + '=' * (4 - remainder);

return base64Decode(padded);
}

/// An agent's keys, as carried in the one-string secret.
class AtomicAgent {
const AtomicAgent({
required this.subject,
required this.privateKeyB64,
required this.publicKeyB64,
});

final String subject;
final String privateKeyB64;
final String publicKeyB64;

/// Reads the base64-encoded JSON secret that `setup()` hands out.
factory AtomicAgent.fromSecret(String secret) {
final decoded = jsonDecode(utf8.decode(decodeAtomicBase64(secret)))
as Map<String, dynamic>;
final privateKeyB64 = decoded['privateKey'] as String;
final privateKey = ed.newKeyFromSeed(decodeAtomicBase64(privateKeyB64));

return AtomicAgent(
subject: decoded['subject'] as String,
privateKeyB64: privateKeyB64,
publicKeyB64: encodeAtomicBase64(ed.public(privateKey).bytes),
);
}
}

/// Signs `message` with the agent's private key.
String signMessage(String message, String privateKeyB64) =>
signBytes(Uint8List.fromList(utf8.encode(message)), privateKeyB64);

/// Signs raw bytes with the agent's private key.
String signBytes(Uint8List message, String privateKeyB64) {
final privateKey = ed.newKeyFromSeed(decodeAtomicBase64(privateKeyB64));
final signature = ed.sign(privateKey, message);

// ed.sign returns the 64-byte signature followed by the message itself.
return encodeAtomicBase64(signature.sublist(0, 64));
}

/// Headers proving to `url`'s server that this agent is asking.
///
/// `url` must be the whole URL being fetched, query string and all.
Map<String, String> signedHeaders(String url, AtomicAgent agent) {
final timestamp = DateTime.now().millisecondsSinceEpoch;

return {
'x-atomic-public-key': agent.publicKeyB64,
'x-atomic-signature': signMessage('$url $timestamp', agent.privateKeyB64),
'x-atomic-timestamp': '$timestamp',
'x-atomic-agent': agent.subject,
};
}
176 changes: 176 additions & 0 deletions flutter/lib/atomic/server_info.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/// What a server says about itself, and what a drive costs on it.
///
/// Plain HTTP against two endpoints every atomic-server has, so this works
/// against any node — self-hosted included — with no bridge call:
///
/// GET /server → a `Server` resource (JSON-AD). Public.
/// GET /drive-usage → a drive's resource count + bytes. Signed; the server
/// checks read access to the drive.
library;

import 'dart:convert';

import 'package:http/http.dart' as http;

import 'atomic_auth.dart';

/// Property URLs of the `Server` class. Handwritten rather than generated —
/// this describes the node, not anything in a drive. Keep in step with
/// `lib/src/urls.rs` and the data-browser's `serverOntology.ts`.
class ServerProps {
static const nodeId = 'https://atomicdata.dev/properties/server/nodeId';
static const version = 'https://atomicdata.dev/properties/server/version';
static const managed = 'https://atomicdata.dev/properties/server/managed';
static const portalUrl = 'https://atomicdata.dev/properties/server/portalUrl';
}

/// A node's own description. Fields are null when the node does not report
/// them: an older server, or one with no peer-to-peer transport running.
class ServerInfo {
const ServerInfo({
this.nodeId,
this.version,
this.managed = false,
this.portalUrl,
});

/// This node's `did:ad:node:...` identity, if its p2p transport is running.
final String? nodeId;
final String? version;

/// Whether the node reports to a control plane, rather than being self-hosted.
final bool managed;

/// Where a managed node is administered.
final String? portalUrl;

static const unknown = ServerInfo();

factory ServerInfo.fromJsonAd(Map<String, dynamic> json) {
String? read(String prop) {
final value = json[prop];

return value is String && value.isNotEmpty ? value : null;
}

return ServerInfo(
nodeId: read(ServerProps.nodeId),
version: read(ServerProps.version),
managed: json[ServerProps.managed] == true,
portalUrl: read(ServerProps.portalUrl),
);
}
}

/// What a drive stores on a node.
class DriveUsage {
const DriveUsage({
this.driveName,
required this.resourceCount,
required this.blobBytes,
required this.loroBytes,
});

final String? driveName;
final int resourceCount;

/// Bytes held by file contents.
final int blobBytes;

/// Bytes held by the CRDT documents behind the resources.
final int loroBytes;

int get totalBytes => blobBytes + loroBytes;
}

/// Asks `serverUrl` what it is. Returns [ServerInfo.unknown] when the node is
/// unreachable, or too old to describe itself.
Future<ServerInfo> fetchServerInfo(String serverUrl, {http.Client? client}) async {
if (serverUrl.isEmpty) {
return ServerInfo.unknown;
}

final http.Client httpClient = client ?? http.Client();

try {
final response = await httpClient.get(
Uri.parse('$serverUrl/server'),
headers: const {'Accept': 'application/ad+json'},
);

if (response.statusCode != 200) {
return ServerInfo.unknown;
}

return ServerInfo.fromJsonAd(
jsonDecode(response.body) as Map<String, dynamic>,
);
} catch (_) {
return ServerInfo.unknown;
} finally {
if (client == null) httpClient.close();
}
}

/// What `driveSubject` stores on `serverUrl`. Null when the node is
/// unreachable, the agent may not read the drive, or the drive is not there.
Future<DriveUsage?> fetchDriveUsage(
String serverUrl,
String driveSubject,
AtomicAgent agent, {
http.Client? client,
}) async {
if (serverUrl.isEmpty || driveSubject.isEmpty) {
return null;
}

final url = Uri.parse('$serverUrl/drive-usage')
.replace(queryParameters: {'subject': driveSubject});
final http.Client httpClient = client ?? http.Client();

try {
// Sign the URL being fetched, query string and all — the server rebuilds
// the signed message from the request it received.
final response = await httpClient.get(
url,
headers: {
...signedHeaders(url.toString(), agent),
'Accept': 'application/json',
},
);

if (response.statusCode != 200) {
return null;
}

final json = jsonDecode(response.body) as Map<String, dynamic>;
final name = json['name'];

return DriveUsage(
driveName: name is String && name.isNotEmpty ? name : null,
resourceCount: (json['resourceCount'] as num?)?.toInt() ?? 0,
blobBytes: (json['blobBytes'] as num?)?.toInt() ?? 0,
loroBytes: (json['loroBytes'] as num?)?.toInt() ?? 0,
);
} catch (_) {
return null;
} finally {
if (client == null) httpClient.close();
}
}

/// Bytes as a person reads them.
String formatBytes(int bytes) {
if (bytes < 1024) return '$bytes B';

const units = ['KB', 'MB', 'GB', 'TB'];
var value = bytes / 1024;
var unit = 0;

while (value >= 1024 && unit < units.length - 1) {
value /= 1024;
unit++;
}

return '${value.toStringAsFixed(value < 10 ? 1 : 0)} ${units[unit]}';
}
60 changes: 60 additions & 0 deletions flutter/lib/atomic/server_url.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/// Server URL handling, shared by every screen that takes one from a user.
///
/// Kept in step with `normalizeServerUrl` / `sameOrigin` in the data-browser's
/// SyncRoute: the same URL typed into either client should mean the same thing.
library;

/// A server URL as typed by a person, made into one that can be fetched.
///
/// Requiring a scheme is a papercut — people type `localhost:9883`. Local
/// addresses get `http` (there is no certificate on a dev box), everything
/// else `https`. An explicit scheme is always kept.
String normalizeServerUrl(String input) {
final trimmed = input.trim().replaceAll(RegExp(r'/+$'), '');

if (RegExp(r'^https?://', caseSensitive: false).hasMatch(trimmed)) {
return trimmed;
}

if (trimmed.isEmpty) {
return '';
}

final isLocal = RegExp(r'^(localhost|127\.0\.0\.1)(:\d+)?$', caseSensitive: false)
.hasMatch(trimmed);

return '${isLocal ? 'http' : 'https'}://$trimmed';
}

/// Whether two server URLs point at the same server — how "is this the one in
/// use?" is decided, tolerant of trailing slashes and paths.
bool sameOrigin(String a, String? b) {
if (b == null || b.isEmpty) {
return false;
}

try {
final ua = Uri.parse(a);
final ub = Uri.parse(b);

return ua.scheme == ub.scheme && ua.host == ub.host && ua.port == ub.port;
} catch (_) {
return false;
}
}

/// A server URL as shown to a person: the bare authority, keeping the port
/// (which distinguishes two dev servers) and dropping only the scheme.
String serverLabel(String url) {
try {
final parsed = Uri.parse(url);

if (parsed.host.isEmpty) {
return url;
}

return parsed.hasPort ? '${parsed.host}:${parsed.port}' : parsed.host;
} catch (_) {
return url;
}
}
Loading