- Swift 99.4%
- Shell 0.4%
- C++ 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| Resources | ||
| scripts | ||
| Signing | ||
| Sources/Hanasand | ||
| Tests/HanasandTests | ||
| .gitignore | ||
| main.cpp | ||
| Package.resolved | ||
| Package.swift | ||
| README.md | ||
Hanasand Desktop
Native macOS client for Hanasand account, content, security, infrastructure, organization, and developer workflows. It also runs an embedded Swift command bridge that the signed-in mobile app can discover over the local network.
Run the macOS app
From the repository root:
swift run Hanasand
The package requires macOS 14 or later. The app starts its embedded Swift HTTPS bridge on port 45731 by default. Set HANASAND_DESKTOP_AGENT_PORT to a validated port from 1024 through 65535 to run an isolated local instance. It creates a unique P-256 TLS key in the macOS Keychain and a self-signed leaf certificate for localhost plus every active advertised IPv4 address. The private key stays in Keychain. The app reuses the certificate while its address set is unchanged, reissues it when that set changes, and publishes the lowercase SHA-256 fingerprint of the certificate's DER bytes with the LAN endpoints. The bridge also accepts requests on the Mac itself. Its requests require a short-lived, nonce-protected HMAC session proof derived from the current Hanasand login; unauthenticated requests receive HTTP 401 inside the TLS connection.
The Albums section manages private-by-default CDN photo albums, including album names, contents, and invited viewer or contributor access. The Images workspace includes the private CDN file library, image previews and favorites, and local image review. CDN file uploads accept images, videos, and other files through the native upload panel. Supported video files up to 50 MiB can be previewed in Desktop. Album thumbnails are loaded through the authenticated CDN session and kept out of the shared URL cache.
Share workspaces support collaborative file editing, GitHub import and synchronization, staged commits and push, and text search and replace across workspace files. Their browser terminal connects to the Website service and currently supports only pwd, ls, clear, and help; it is not a general-purpose shell or PTY.
Embedded Swift bridge routes
The bridge exposes these HTTPS routes:
GET /health,GET /status, andGET /commandsremain available to compatible callers. The mobile client sends these as fixed-sizePOSTframes.GET /screenshotreturns the current desktop screenshot. The mobile client sends it as a fixed-sizePOSTframe; the embedded bridge accepts both methods.POST /commandaccepts a command in a JSON body; the same bridge also accepts command query forms.
Commands include status and update checks, AI actions, app-section navigation, browser actions, native dashboard panels, server logs, settings, and remote-desktop controls. Parameterized commands include codex_prompt:<url-encoded-prompt>, mac_control_type_text:<url-encoded-text>, and mac_control_pointer_click_at:<x>:<y>. Keyboard text entry and pointer clicks require macOS Accessibility permission. Use the authenticated client for requests and read GET /commands for the names accepted by the running app; unsupported commands return HTTP 400. The bridge returns HTTP 401 when the session proof is missing or invalid.
The bridge uses Network.framework TLS 1.2 or later. The mobile client pins the leaf certificate using its published SHA-256 fingerprint. The bridge also encrypts request and response bodies with AES-256-GCM, and HMAC proofs bind each request body to its method, path, timestamp, and nonce. While the signed-in app is foregrounded, the mobile client sends one request on each one-second slot when a desktop agent is available. Real requests use the next slot; otherwise it sends a status request. The bridge rounds responses to the next one-second boundary. Every request body is 64 KiB and every encrypted response body is 256 KiB. Screenshots are resized and JPEG-compressed to fit that response frame.
This cover traffic adds about 2.6 Mbps (roughly 1.2 GB or 1.1 GiB per foreground hour) while a desktop agent is available, excluding TLS overhead. It adds up to one second of slot wait plus response-boundary delay. The app stops cover traffic in the background and allows up to four outstanding requests; if all four exceed a slot, it skips that frame. Desktop presence discovery refreshes over the Hanasand API at most once every 15 seconds, outside the local frames. An observer of all device traffic can still see those API requests, the destination IP, whether the app is foregrounded, and gaps caused by network failures or a saturated request limit. TLS protects content; fixed frames and scheduled requests reduce the timing and size signals on the LAN bridge.
Package a macOS update
From the repository root, choose a semantic version and run:
HANASAND_APP_VERSION=0.1.2 scripts/package-update.sh
The script builds and verifies a release .app, signs it with HANASAND_CODESIGN_IDENTITY when set, or selects the first local Apple Development identity. It refuses a signed package if the certificate's team identifier differs from the app's signed team identifier. The app uses the com.hanasand.desktop bundle ID and the hanasand-desktop SSO callback scheme. Passkey sign-in is temporarily disabled in the UI and authentication model while a valid Associated Domains signing identity is unavailable; its implementation is retained for later re-enabling. Stable signing keeps macOS privacy permissions such as Screen Recording attached across updates. Setting HANASAND_ALLOW_ADHOC_SIGNING=1 forces an identity-free ad-hoc signature with no Apple entitlements for a throwaway local build. Such packages use the local manifest channel and cannot be assigned a release channel. By default, the script writes dist/Hanasand-<version>-macos.zip and dist/manifest.json. Set HANASAND_APP_OUTPUT_DIR to choose another output directory:
HANASAND_APP_VERSION=0.1.2 HANASAND_APP_OUTPUT_DIR=/tmp/hanasand-desktop-0.1.2 scripts/package-update.sh
The update API reads the exact archive filename recorded in the manifest. Upload the versioned archive and its manifest together, without renaming the archive. For example, from the repository root:
rsync -az dist/Hanasand-0.1.2-macos.zip dist/manifest.json ubuntu@hanasand:/srv/hanasand/app-updates/
The runner helper is intended for a Forgejo update workflow, which needs a macOS runner because SwiftUI cannot be built in the Linux Git container. This checkout includes the helper but no Forgejo workflow definition, so any trigger and automated release steps are outside this repository. Start the runner in the foreground when that external workflow requires it:
scripts/run-macos-runner-once.sh
The script builds the Forgejo runner locally if needed, registers it with macos and macos-arm64 labels, and runs until stopped with Ctrl-C.
The remote Codex worker is disabled by default. To enable it for a local checkout, set HANASAND_DESKTOP_START_CODEX_WORKER=1; set HANASAND_DESKTOP_REPO_ROOT to this repository's root if the app is launched with a different working directory.
Capture visual smoke screenshots
On macOS, run the screenshot pass from the repository root:
scripts/smoke-screenshots.sh
The script builds or launches the Desktop app, opens sections through authenticated POST /command requests, checks the screenshots, and saves a manifest and browsable HTML contact sheet under .artifacts/desktop-smoke-<timestamp>/. It prompts once for the Hanasand user ID and session token; token input is hidden. The script does not load session credentials from environment variables or files. A local CryptoKit helper reads the desktop leaf certificate from Keychain, verifies the exact DER SHA-256 fingerprint during TLS, derives request proofs, encrypts command payloads, and verifies/decrypts response bodies in memory. The helper binary is placed in the smoke output directory; neither credentials nor plaintext response bodies are written there.
Useful options:
HANASAND_DESKTOP_SMOKE_KEEP_RUNNING=1 scripts/smoke-screenshots.sh
HANASAND_DESKTOP_SMOKE_OUT=/tmp/hanasand-smoke scripts/smoke-screenshots.sh
HANASAND_DESKTOP_AGENT_URL=https://localhost:45731 scripts/smoke-screenshots.sh
HANASAND_DESKTOP_SMOKE_SCOPE=core scripts/smoke-screenshots.sh
HANASAND_DESKTOP_SMOKE_SCOPE=dashboard scripts/smoke-screenshots.sh
HANASAND_DESKTOP_SMOKE_WINDOW_WIDTHS=1080,1512 scripts/smoke-screenshots.sh
HANASAND_DESKTOP_AGENT_PORT=45732 HANASAND_DESKTOP_AGENT_URL=https://localhost:45732 HANASAND_DESKTOP_APP_BIN=/absolute/path/to/HanasandCandidate HANASAND_DESKTOP_APP_NAME="Hanasand Candidate" HANASAND_DESKTOP_SMOKE_WINDOW_WIDTHS=1080,1512 scripts/smoke-screenshots.sh
HANASAND_DESKTOP_SMOKE_CAPTURE_MODE=rect scripts/smoke-screenshots.sh
HANASAND_DESKTOP_SMOKE_CAPTURE_RECT=80,70,1340,900 scripts/smoke-screenshots.sh
The default full pass captures Control, Server, Mail, Documents, Images, Dashboard, Settings, and every registered native dashboard route. The script checks route-command coverage before capture. Use core for a short shell pass or dashboard for dashboard route coverage. Screenshots use the visible Hanasand window ID by default. HANASAND_DESKTOP_SMOKE_WINDOW_WIDTHS accepts comma-separated window widths in points; it defaults to 1340, and 1080,1512 captures each route at both widths. Screenshot labels include the width. Each manifest entry records the requested layout_points separately from dimensions, which retains the screenshot's actual pixel dimensions. For an already-built candidate executable, set HANASAND_DESKTOP_APP_BIN and choose a free HANASAND_DESKTOP_AGENT_PORT plus matching HANASAND_DESKTOP_AGENT_URL; the app honors that port so the installed instance can remain running. Give the candidate process name through HANASAND_DESKTOP_APP_NAME for AppleScript activation. Set HANASAND_DESKTOP_SMOKE_CAPTURE_MODE=rect to capture a rectangle, or use HANASAND_DESKTOP_SMOKE_CAPTURE_MODE=screen and clear HANASAND_DESKTOP_SMOKE_CAPTURE_RECT for a full-screen capture.
Legacy C++ loopback helper
main.cpp builds a separate, minimal helper for cases that do not need the macOS GUI. It is not the Swift bridge: it binds only to 127.0.0.1, exposes GET /health, GET /status, and POST /command, and returns status output when the request text contains status; other command text returns HTTP 400. It does not parse the JSON command field, and it has no LAN discovery or the Swift bridge's session-proof protocol. Its default port is also 45731; choose another port when running it alongside the Swift app.
Build it from the repository root:
clang++ -std=c++17 -O2 -Wall -Wextra main.cpp -o build/hanasand-agent
On Windows, use a C++17 compiler and link Winsock. Run it with an optional port:
./build/hanasand-agent 45732