feat(offline-messaging): include last icon and status in offline user list#105
Open
martinmarsian wants to merge 108 commits into
Open
feat(offline-messaging): include last icon and status in offline user list#105martinmarsian wants to merge 108 commits into
martinmarsian wants to merge 108 commits into
Conversation
Adds a complete migration path from Wired 2.5 SQLite databases into the Wired 3 GRDB schema, covering accounts, privileges, bans, boards, threads and posts. Server CLI: - New --migrate-from <path> flag in main.swift - MigrationController: migrates groups+privileges, users+privileges, bans, boards (Unix mode → 6 permission booleans), threads, posts (BLOB icon support, date string → Unix epoch conversion) - DatabaseController: expose dbQueue as public for migration access WiredServerApp (macOS GUI): - New "Migration" tab in preferences - Source path picker + overwrite toggle + progress output view - Subprocess runs wired3 --migrate-from via temp-file output capture (avoids pipe buffering / race-condition issues) - Localizable strings added (de/en/fr) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Accounts migrated from Wired 2.5 have password_salt = NULL and a 40-char SHA1 hash stored in the password column. The P7 key exchange previously always derived the auth proof from SHA256(plaintext), causing these accounts to fail authentication entirely. P7 protocol (P7 v1.4 additions): - New field p7.encryption.legacy_password (bool, id=22) in server_challenge: server sets this when password.count==40 && salt==nil - New field wired.user.password_must_change (bool, id=3016) in wired.login: signals the client to prompt for a password change immediately P7Socket (server side): - Detect legacy users after username_request; send legacy_password=true in server_challenge and set isLegacyAuth=true after ECDSA verify passes - Server-side base_hash uses the stored SHA1 string unchanged (no re-derive) P7Socket (client side): - Defer password hashing until after server_challenge is received - Use SHA1 when legacy_password=true, SHA256 otherwise - Set isLegacyAuth=true for downstream consumers ServerController+Auth (wired.send_login handler): - For legacy sessions (socket.isLegacyAuth): skip the redundant SHA256 equality check (P7 ECDSA already verified the SHA1 credential) - Lazily assign a new per-user stored_salt on first legacy login - Include wired.user.password_must_change=true in wired.login reply Connection (client library): - New connectionRequiresPasswordChange(connection:) delegate method - Called after login when wired.user.password_must_change is true Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The FTS5 table uses content='index' (external content). On restart after a crash, old rows in the `index` table may have no matching FTS5 entries. The row-level delete trigger then sends a 'delete' command for a non- existent FTS5 rowid, causing SQLITE_IOERR (error 10) during cleanup. Fix: in performFullRebuild, drop the index_ad trigger before the bulk delete, then call FTS5 'rebuild' to re-sync the index from the current content table, then restore the trigger. This is more correct than relying on row-level triggers for bulk operations. Add recoverIndexAndFTS5() as a fallback: drops FTS5 and all triggers, clears the index table, and recreates everything from scratch. A pendingRebuild flag ensures a fresh traversal follows automatically. Also: auto-load notary profile from ~/.wired-notary in release scripts so notarization works without manually exporting NOTARY_PROFILE each time. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two changes to prevent SQLITE_IOERR on the first index rebuild after a crash or unclean shutdown: 1. DatabaseController: run PRAGMA wal_checkpoint(TRUNCATE) right after opening the database. On a fresh start there are no active readers, so TRUNCATE safely merges and clears the WAL. This eliminates the root cause: accumulated partial FTS5 shadow-table writes from failed previous rebuild cycles sitting in the WAL. 2. IndexController: before the first rebuild traversal, probe FTS5 with an integrity-check write. If it throws (SQLITE_IOERR or any other error), recoverIndexAndFTS5() is called to drop and recreate the FTS5 virtual table and triggers from scratch, then a fresh rebuild follows. This acts as a belt-and-suspenders guard for cases where the main DB file itself has FTS5 inconsistencies. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The regex previously rejected versions like "3.0-beta.21" because it only matched numeric dot-separated segments. Allow an optional pre-release suffix (-beta.N, -rc.N, -alpha.N, etc.). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Assigning the salt on the very first legacy login caused a permanent lockout on the second login: passwordSalt != nil triggered the SHA-256 auth path, which failed against the still-SHA1 stored hash. The salt is now written only in receiveAccountChangePassword, i.e. when the user actually completes the password-change flow. Fixes: nark#72 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…B column The old detection (password.count == 40 && passwordSalt == nil) was fragile and would produce false positives in edge cases. A dedicated is_legacy boolean column on the users table is the authoritative source of truth: - DB migration v12: adds is_legacy (default false), back-fills existing SHA1 accounts (no salt + 40-char hash) automatically - User model: new isLegacy property with CodingKey is_legacy - SocketPasswordDelegate: new isLegacyUser(username:) method replaces the length check in P7Socket.serverKeyExchange - UsersController: implements isLegacyUser(username:) - MigrationController: sets is_legacy=1 for all Wired 2.5 imported accounts - receiveAccountChangePassword: clears is_legacy=false on successful upgrade Addresses nark's review of PR nark#72. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MigrationController: replace raw table/column string interpolation in migratePrivileges() with a closed PrivilegeTarget enum (allowlist) - WiredServerViewModel.runMigration(): replace DispatchQueue.global/main mixing with withCheckedContinuation + withTaskGroup; add 5-minute timeout so isMigrating can never be stuck true forever if the subprocess hangs - migrate-from-wired25.py: document that boards/threads/posts are out of scope; point users to wired3 --migrate-from for full content migration - Add MigrationControllerTests: 8 tests covering is_legacy flag, skip/ overwrite behaviour, legacy auth detection, and password upgrade flow Addresses nark's minor review points on PR nark#72. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When recoverIndexAndFTS5() fails, hasFTS5 is set to false but the index_ad trigger remains in the database. On the next rebuild the trigger drop was skipped (gated on hasFTS5), the trigger fired during the bulk DELETE, hit the corrupt FTS5 shadow tables, and propagated SQLITE_IOERR (10) back to the DELETE statement. DROP TRIGGER IF EXISTS index_ad is now unconditional so the trigger is always gone before the bulk delete, regardless of hasFTS5 state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…offline user list
- wired.xml: add fields wired.user.is_offline (3017), wired.message.offline.{sender_login,recipient_login,date} (5010-5012); add messages wired.user.offline_list/done (3011-3012), wired.message.send_offline_message/offline_message (5010-5011)
- DB migration v13: replace speculative E2E-crypto offline_messages schema with a simple plaintext table (sender_login, recipient_login, body, sent_at) with per-recipient limit trigger
- OfflineMessage.swift: new GRDB model for the offline_messages table
- ServerController+Chat: receiveMessageSendOfflineMessage, deliverOfflineMessages, sendOfflineUserList
- ServerController+Auth: call sendOfflineUserList + deliverOfflineMessages after successful login
- ServerController: route wired.message.send_offline_message to new handler
- ClientsController: add allConnectedLogins() and user(withLogin:)
- UsersController: add userExists(withUsername:)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, privilege backfill - Migration v14: add last_login_at to users; update on every successful login - sendOfflineUserList: filter to users active in last 30 days; send full_name as nick - broadcastNewOfflineUser: send full_name as nick when new account is created - migrateSendOfflineMessagesPrivilegeIfNeeded: backfill privilege to existing accounts that already have send_messages, so upgrading installations get it automatically Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… list - Migration v15: add last_nick TEXT column to users table - receiveUserSetNick: persist nick to DB whenever a logged-in user changes it - Login path: also persist nick at login time (nick is sent before login completes) - sendOfflineUserList: prefer last_nick, fall back to full_name, then username Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… user list The IS NULL exception caused all accounts without a last_login_at (everyone after a fresh upgrade) to appear. Now only accounts with a real login within the last 30 days are shown. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gin or full_name Using login or full_name as fallback was a security issue — it revealed account names to other users. Now only users who have connected at least once (and thus have a last_nick persisted) appear in the offline list. New accounts are excluded until their first login, at which point their self-chosen nick is saved. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ients Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Poly1305 - Migration v16: adds is_encrypted column to offline_messages - wired.xml: fields 5014/5015 and messages 3013/3014/3015 for key exchange - Server stores and delivers is_encrypted flag with each offline message - Handler for wired.user.set_public_key stores X25519 public key in users.offline_public_key - Handler for wired.user.get_public_key returns stored public key by login Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GRDB stores Date values as ISO 8601 text ("yyyy-MM-dd HH:mm:ss.SSS"),
not as Unix timestamps. Reading sent_at via Date(timeIntervalSince1970:)
forced a Double conversion that fatalError'd on text values, crashing
the server whenever a user with pending offline messages logged in.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ry/Wired3/ - Persist workingDirectory in UserDefaults so it survives restarts after migration - Add migrateToSystemDirectory() — stops server, creates /Library/Wired3/ with admin privileges (osascript), copies all data, verifies DB integrity (PRAGMA integrity_check), switches active path, rewrites LaunchAgent plist if needed - Original data preserved as backup at ~/Library/Application Support/Wired3/ - Add systemMigrationAvailable, isUsingSystemDirectory computed properties - Add WiredServerError cases: systemDirectoryCreationFailed, systemMigrationFailed - Dashboard General tab: new "System Data Directory" section with migration button, progress indicator, and status line; shows green checkmark when already migrated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ServerInstallMode enum (launchAgent / launchDaemon), persisted in UserDefaults - Add daemonUserName (default: _wired) and daemonStartAtBoot, both persisted - switchInstallMode(): stops server, creates hidden system user via dscl (osascript), sets /Library/Wired3/ ownership, writes LaunchDaemon plist to /Library/LaunchDaemons/ (via temp file + osascript cp with admin), or reverses all steps when switching back - startDaemon() / stopDaemon(): launchctl bootstrap/bootout for system domain - toggleDaemonStartAtBoot(): rewrites plist with updated RunAtLoad flag - findFreeSystemUID(): auto-assigns UID from range 400-499 - Dashboard General tab: new "Install Mode" section with Picker, system user field with existence indicator, progress/status display; Execution section switches between LaunchAgent (Start at Login) and LaunchDaemon (Start at Boot) UI - New WiredServerError cases: systemDirectoryOwnershipFailed, daemonUserCreationFailed, launchDaemonWriteFailed, launchDaemonInstallFailed, launchDaemonRemoveFailed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- activateLaunchDaemon(): single osascript call — user creation (optional) + chown /Library/Wired3 + plist copy to /Library/LaunchDaemons/ (one admin dialog) - deactivateLaunchDaemon(): single osascript call — bootout + plist removal + chown back to current user (one admin dialog) - reinstallDaemonPlist(): single osascript call for updating existing plist (used by toggleDaemonStartAtBoot) - Extract runPrivileged() helper to avoid repeating osascript boilerplate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add daemonGroupName (default: staff), persisted in UserDefaults - Add daemonGroupExists computed property - findFreeSystemGID(): auto-assigns GID from range 400-499 - groupGID(for:): reads PrimaryGroupID from dscl for existing groups - activateLaunchDaemon: creates custom group via dscl if it doesn't exist, uses correct GID for user's PrimaryGroupID, uses group name in chown - saveDaemonSettings() persists group name alongside user name - Dashboard: side-by-side User/Group text fields each with existence indicator Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- filesDirectoryIsOnExternalVolume: detects if files dir is under /Volumes/ using URLResourceKey.volumeURLKey - wired3HasFullDiskAccess: queries TCC.db via sqlite3 for the wired3 binary's kTCCServiceSystemPolicyAllFiles entry (readable only if WiredServerApp also has FDA) - openFullDiskAccessSettings(): opens System Settings Privacy_AllFiles pane - ExternalVolumeWarningView: reusable warning banner — orange with "Open Settings…" button when FDA missing, green checkmark when granted - Warning shown in "Install Mode" section (General tab) and in "Files" tab whenever LaunchDaemon mode is active and files dir is on external volume Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use Color.primary/Color.orange explicitly in foregroundStyle ternary to resolve HierarchicalShapeStyle vs Color type mismatch - Replace URLResourceValues.volumeURL (unavailable in SDK) with simple /Volumes/ path prefix check for external volume detection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fields were disabled when launchDaemonInstalled=true, making it impossible to change user/group after the first mode switch. Now the fields are always editable and shown regardless of install mode, so user can configure them before or after switching to LaunchDaemon. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace daemonRunning/daemonUserExists/daemonGroupExists computed properties (which ran launchctl/dscl synchronously on the main thread during every SwiftUI view render) with cached @published properties isDaemonRunning/isDaemonUserExists/isDaemonGroupExists - Update these via refreshDaemonStatus() called from pollState() only when in LaunchDaemon mode (once per second, not on every render) - Fix startDaemon()/stopDaemon() to use admin privileges via osascript since launchctl bootstrap/bootout system requires root Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When in LaunchDaemon mode and the daemon is running, skip the automatic binary synchronization in refreshAll(). launchd detects when the binary file is replaced on disk and kills the running daemon process, which appeared as a spurious server crash whenever the General Tab was opened. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Set chmod 775 on /Library/Wired3/bin/ after chown so admin user (in staff group) can update the wired3 binary without sudo - Guard auto-update in refreshAll() with isWritableFile check so it silently skips when permissions aren't available (e.g. before the first mode switch has set the correct group-write bit) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e for admin Data dirs under /Library/Wired3 are chowned to _wired:daemon. bin/ is explicitly re-chowned to _wired:staff with 775 so the admin user (member of staff, not daemon) can still update the binary. Default daemonGroupName changed from staff to daemon. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The port check button was silently contacting api.ipify.org and check-host.net without user confirmation. Restore the per-click consent alert that was lost when the consent branch was never merged. - Button shows a confirmation alert (EN/DE/FR) explaining that the external IP will be shared with api.ipify.org and check-host.net - Remove auto-check on app startup — portStatus now starts as .idle - Add PortStatus.idle case with "Not checked" label (all 3 locales) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…el translation Move .alert from inner HStack (inside GroupBox) to outermost SettingsScrollPane so macOS SwiftUI reliably presents it. Add missing PortStatus.idle case to color(for:) switch. Add common.cancel key (Cancel/Abbrechen/Annuler) to all three locale files — was missing, causing the raw key to appear as button label. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
setAdminPassword only cleared the password on success; createAdminUser never cleared it. Use `defer` so the @published property is wiped on every exit path (early return, success, catch). Limits the lifetime of the cleartext admin password held in the ViewModel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…m startup" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sion) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allows a v3.1 peer to interoperate with a v3.0 peer (and vice versa) by loosening strict version checks, tolerating unknown messages and length-prefixed unknown fields on receive, and turning the compatibility_check exchange into a non-blocking spec diff that drives sender-side feature gating. - ProtocolVersion: numeric semver-ish parser (so 3.10 > 3.2) - P7Spec: same-major compatibility, public init?(withData:/withString:) - P7Socket: align hardcoded "3.0" with spec.protocolVersion, hard-fail only on major mismatch, trigger compatibility_check on any minor difference, parse the peer's spec into a CompatibilityDiff, expose remoteSpec / negotiatedProtocolVersion / peerKnows() helpers, and filter outbound messages/fields the peer doesn't know - P7Message: bin(omittingFieldIDs:) for the sender filter, decoder no longer desyncs on unknown field IDs (records them and aborts cleanly), unknown message IDs decode known fields best-effort - CompatibilityTests: 10 tests covering version parsing, diff computation, receiver tolerance, sender filter - COMPATIBILITY.md: full evolution policy + checklists for adding fields/messages; CONTRIBUTING/SECURITY/README updated to point at it Closes nark#87 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bumps the protocol to 3.2 and introduces a chat-reactions surface that mirrors the board-reactions API. Public-chat broadcasts now carry a server-assigned wired.chat.message.id (length-prefixed UUID string) so clients have a stable handle for reactions, future edits, and optimistic-UI correlation. Pre-3.2 receivers silently skip the new field via the receiver-tolerance machinery introduced in nark#87, and the new reaction messages are filtered out wholesale by the per-session compatibility diff — a 3.2 client can therefore talk to a 3.1 server (and vice-versa) without any surface-level breakage. Reactions are kept in an in-memory ring buffer per public chat (default 500 messages); when a message scrolls off the buffer its reactions are dropped, consistent with the "chat is a stream" model. Reacting on an unknown id surfaces wired.error.invalid_message. Closes nark#90. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
testConnectAndAcceptHandshakeSucceedsWithNoCrypto pinned the negotiated remote version string to "3.1"; bumping the protocol to 3.2 broke that expectation. The handshake itself still works — the spec is wire-compat with 3.1 — only the asserted version string needs updating. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…lders A user with wired.account.file.set_permissions could assign themselves as owner of a sync folder they do not own, which would grant new read access. Read the existing owner from .wired/permissions directly and deny the request when a non-owner tries to claim ownership. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rship check Two review-identified gaps in the privilege escalation guard: 1. Nil bypass: when no .wired/permissions file existed, FilePrivilege(path:) returned nil and the ownership check was silently skipped, letting the first caller with set_permissions claim ownership on any unowned folder. The else branch now blocks that claim for non-admins (wired.account.settings.edit required for initial ownership). 2. Path mismatch: the check previously read from realPath while the write used finalPath (alias/symlink resolved). Hoisting finalPath above the check ensures both operations target the same canonical location. 3. Compound if condition (idiomatic Swift, no nested ifs). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
wired.account.settings.edit does not exist in the protocol spec — hasPrivilege() always returned false, blocking every caller (including admins) from setting initial ownership on new folders. Replaced with the correct wired.account.account.edit_users which correctly identifies admin-level accounts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces WiredServerHelper, a privileged XPC daemon registered via SMAppService (replacing the SMJobBless approach). The helper performs all root-level operations on behalf of WiredServerApp: - Create /Library/Wired3 system directory with correct ownership - Create/delete daemon user and group via dscl - Install the LaunchDaemon plist and set ownership/permissions - Bootstrap and kickstart the LaunchDaemon via launchctl - Bootout the LaunchDaemon; falls back to pkill for orphaned processes - Copy the wired3 binary to /Library/Wired3/bin/wired3 - Run an FDA access check as the daemon user to validate TCC grants Key changes vs the SMJobBless prototype: - Removes SMAuthorizedClients restricted entitlement from helper (required an Apple provisioning profile; AMFI rejected the helper binary without one) - Removes Label/MachServices/SMAuthorizedClients from helper Info.plist - Authorization is now done via AssociatedBundleIdentifiers in SMAppService.plist - $(AppBundlePath) in SMAppService.plist is expanded at build time via PlistBuddy before signing (macOS 26 SMD does not expand the variable) - Bumps kHelperVersion to 6 to force reinstallation of running helpers - Dashboard Start button calls startDaemon() in LaunchDaemon mode (was always calling startServer() / direct child process, making Stop a no-op) - Adds ExternalVolumeWarningView with FDA status indicator and re-check button - Adds German/English/French localizations for all new strings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds --check-access <path> to the wired3 CLI. The option tests whether the process can read the given directory (via contentsOfDirectory) and exits 0 on success or 1 on failure, with no server startup. WiredServerHelper uses this to verify Full Disk Access grants under the daemon user's identity: it runs wired3 as the daemon user via sudo so macOS TCC evaluates the grant for wired3's code signature, not root. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
URL(fileURLWithPath:) is used to resolve the Info.plist path for the WiredServerHelper linker flag. Foundation is not implicitly available in Package.swift manifests — the explicit import is required. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nfo.plist version - ServerIdentity.signWithIdentity: P256.Signing.PrivateKey.signature(for:) can throw (failing secure element, OOM); the force-try would crash the daemon. Use guard+try? instead — the function already returns Data? so nil on failure is the correct contract. - Info.plist CFBundleVersion bumped from 5 → 6 to match kHelperVersion = "6". SMAppService uses the bundle version to detect helper binary changes; a mismatch means launchd will not replace the running helper after an app update. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… errors ServerIdentity: replace Data.write(.atomic)+setAttributes with FileManager.createFile so the 0600 permission is set at inode creation time, eliminating the brief window during which the temp file is world-readable at the default umask. deactivateDaemon: launchctl bootout exit code was silently discarded, making it impossible for the caller to detect a failed stop. Now returns (false, …) when bootout exits non-zero and the job is still registered in the system domain. dscl cleanup exits are logged via diagLog rather than dropped entirely. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add SIGUSR2 signal handler in main.swift that calls createDatabaseSnapshot() so operators can trigger an on-demand backup without restarting the server - Expose createDatabaseSnapshot() as public so the main module can call it across the wired3Lib module boundary - Fix DatabaseController.createSnapshot() backup-safety issue: the previous code deleted the existing .bak before the new backup succeeded, meaning a failed backup destroyed the last good backup; now the old backup is only replaced after the new one is fully written to a .tmp file - Add "Create Snapshot Now" button in DatabaseTabView that sends SIGUSR2 via kill(pid, SIGUSR2); button is disabled when the server is not running Localization strings for the button and status messages were already added in a prior commit (database.snapshot.trigger_now, status.snapshot_triggered, status.snapshot_not_running, status.snapshot_failed). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The error string previously said "PID file not found" but this code path is only reached when the PID file IS present and kill(SIGUSR2) failed (e.g. the process already exited between the fileExists check and the signal). Updated en/de/fr to accurately describe the failure as a signal-delivery error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
parseStringsFile is called up to 6 times at startup (2 roots × 3 languages). Moving the regex to a static let means it is compiled exactly once rather than on every invocation. The try! is safe here: the pattern is a compile-time constant that is known-valid, and a static initialiser crash is caught immediately in testing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
performFullRebuild() called itself synchronously from its own defer block when pendingRebuild was true. Under sustained rebuild pressure (continuous file-system activity keeping pendingRebuild set), this produces 400+ frames of recursive self-invocation and terminates the process with SIGBUS. Use indexQueue.async instead so the next rebuild is scheduled as a new stack frame, not a synchronous recursive call. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a new server binary was bundled in the app, the update dialog was only shown if the server was already running. If the server was stopped, the update was silently ignored. Remove the isRunning guard so the dialog always appears after a binary update. Adapt the alert button label and message text to reflect whether the server will be restarted or started for the first time. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace broken portchecker.io backend with check-host.net async probe (start check → poll results), matching nark's upstream impl - Remove spurious checkPort() call from refreshAll() — consent alert must only appear when the user clicks the Check button - Use .task(id: model.serverPort) instead of onAppear + onChange to sync portText, fixing a timing race where the field showed the default port before loadConfig() had run Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add repository guard so the DocC Pages workflow only runs on nark/WiredSwift, preventing 404 failures on contributor forks where GitHub Pages is not configured. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… list Server persists the user's last known icon (≤ 64 KB) and status (≤ 512 chars) to the users table (migration v17) and sends both fields alongside nick in wired.user.offline_list messages (initial list on login + live disconnect broadcast). persistLastNick now also caps at 256 chars. Security: icons over 64 KB are silently dropped before DB write and before broadcast to prevent amplification attacks. The application-level caps are defence-in-depth; a deeper fix at the P7Socket level is noted in the PR. wired.xml: wired.user.icon and wired.user.status added as optional v3.2 parameters to wired.user.offline_list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The built-in guest account is shared by anonymous visitors and has no stable identity. Broadcasting a guest disconnect as an offline entry and including it in sendOfflineUserList is meaningless and misleading. - SQL query in sendOfflineUserList gains AND username != 'guest' - disconnectClient skips broadcastOfflineUserEntry when login == "guest" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The sync daemon connects under a real account but represents a background
process, not a human. Its folder-named nick ("Test") was appearing in the
offline user list and overwriting the human user's last_nick/icon/status.
- disconnectClient skips broadcastOfflineUserEntry when applicationName
is "wiredsyncd"
- receiveSendLogin only updates last_login_at (not nick/icon/status) for
daemon connections, so the human user's identity is never overwritten
- receiveUserSetNick / receiveUserSetStatus / receiveUserSetIcon skip
their respective persist calls for daemon connections
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ntity The wiredsyncd daemon wrote its folder nick/icon/status into the users table before the exclusion guard was added. v18 resets last_nick, last_status and last_icon for all users — the data is display-only and repopulates on the next real user disconnect. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Icon: 64 KB → 16 KB (sufficient for any reasonable 32×32 PNG) Status: 512 → 128 chars (generous for a one-liner, removes abuse headroom) Also fixes the hardcoded 512 in receiveSendLogin to use the central maxLastStatusChars constant, so there is one authoritative source. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
|
Hi @nark 👋 The automated Claude PR review is failing on all PRs from this fork with a ~2-second runtime — too fast for an actual review. Looking at the run log, the What's needed: In your repo settings under Settings → Secrets and variables → Actions, please add a secret named Once the secret is in place, the workflow should pick it up automatically on the next push — no workflow changes needed. Sorry for the noise in the CI checks! Let me know if you need any other info. |
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.
Summary
last_icon(BLOB) andlast_status(TEXT) columns to theuserstablereceiveUserSetIcon/receiveUserSetStatuspersist the values to the DB whenever a logged-in user updates their icon or status (with caps: icon ≤ 64 KB, status ≤ 512 chars, nick ≤ 256 chars)last_iconandlast_statuson successful login (client already has icon/status from the pre-login handshake)sendOfflineUserListnow fetches and sendslast_statusandlast_iconin everywired.user.offline_listmessagebroadcastOfflineUserEntry(sent on disconnect to all privileged clients) also carries icon and statuswired.xmldeclareswired.user.statusandwired.user.iconas optional parameters ofwired.user.offline_listat protocol version 3.2Security measures included
ServerController.maxOfflineIconBytes = 64 * 1024— oversized blobs are silently dropped before DB write and before broadcastmaxLastStatusChars = 512cap applied inpersistLastStatusmaxLastNickChars = 256cap applied inpersistLastNickSuggested follow-up in
P7Socket.swiftThese application-level guards are defence-in-depth only. For complete protection the limits should also be enforced at the protocol layer, before any application code ever receives the data:
Enforcing limits in
P7Socketmeans a malicious or buggy peer cannot craft a message that bypasses the application-level guards by targeting a different handler that lacks them.Companion client PR
Wired-macOS PR #49 — renders the icon and status in the offline users sidebar.
Test plan
ALTER TABLEadds columns without breaking existing rows🤖 Generated with Claude Code