Index is a macOS 13+ WKWebView client with a native, credential-free request bridge. It is an optional owner-control surface: standalone Hermes connection does not require the Index app to be installed or running.
The signed app stores one Better Auth session token — this device's own session — in the Keychain. Raw credentials and authorization headers never enter browser JavaScript, WebKit storage, Application Support records, logs, callback URLs, or generated HTML.
The native indexAPI bridge accepts only the exact bundled main document and document generation. JavaScript supplies structured, allowlisted requests; Swift constructs the fixed HTTP API URLs, reads the owner credential natively, validates body/schema/resource bounds, and returns sanitized data only. It permits bounded REST, upload, and SSE operations (32 pending requests, 1 MiB ordinary request/response, 8 MiB decoded images, 64 KiB events, 256 events; 30-second ordinary and five-minute stream deadlines). It never accepts a browser-supplied URL, header, credential, or transport override.
Login opens the web /cli-auth page (the same state-bound handshake the CLI uses) with a loopback callback on http://127.0.0.1:<port>/callback. The page runs the device authorization grant against the owner's browser session and returns only a short-lived device code, which the app exchanges at /api/auth/device/token for its own 30-day session; the app verifies the Keychain write/read-back before treating login as complete. There is no approval prompt: the page mints and approves the code itself, so no externally supplied code can enter the grant, and the loopback binding is what keeps that safe.
Logout quarantines bridge work, pauses/scrubs Hermes local activity, deletes the Keychain item, then revokes the session server-side with its own token. A failed Keychain deletion retains the credential and never claims logout completed. A failed revocation still signs the app out locally; the device can also be revoked from Index web settings.
The native app may show the same owner controls as the web, but it is not required for direct Hermes use. Hermes setup writes the session into ~/.hermes/.env and installs the plugin. The plugin authenticates with INDEX_SESSION_TOKEN. The local runtime uses generation-fenced fallback and cron ownership markers: it pauses only the exact owned schedule and preserves unrelated Hermes state.
cd apps/mac
python3 assemble.py # regenerates Resources/index.html
./scripts/build.sh # macOS Swift buildGenerated HTML must be regenerated through assemble.py, never hand-edited. The production source boundary disables Web Inspector; development inspection requires the explicit development build flag.
Production distribution is direct Developer ID distribution, not the Mac App Store. It requires macOS 13+, Universal 2 artifacts, Hardened Runtime, Developer ID signing, notarization, stapling, checksums, immutable production HTTPS endpoint inputs, and a clean-account acceptance run. App Sandbox is not a production requirement for this direct-distribution model; release validation rejects unexpected sandbox/debug entitlements rather than requiring them.
Pushes to dev and main that touch apps/mac run .github/workflows/mac-app-release.yml: Developer ID sign, notarize the app, package the branded DMG, notarize that, and attach Index.dmg plus Index.zip (the same stapled app) to a rolling release.
Signed installs update themselves: on launch and every 30 minutes the app compares its IndexBuildSHA with the commit in its channel's release notes, downloads Index.zip in the background, accepts it only if it satisfies the running app's designated requirement, prompts once with Restart Now / Later, and swaps the bundle when the app quits. Ad-hoc, translocated, or read-only installs fall back to the DMG from Index ▸ Check for Updates….
| Branch | Release | Download |
|---|---|---|
dev |
prerelease mac-dev |
https://github.com/indexnetwork/mac-client/releases/download/mac-dev/Index.dmg |
main |
mac (latest) |
https://github.com/indexnetwork/mac-client/releases/download/mac/Index.dmg |
Required Actions secrets (fail closed if any are missing): MAC_CODESIGN_P12, MAC_CODESIGN_P12_PASSWORD, MAC_CODESIGN_IDENTITY, MAC_APP_IDENTIFIER_PREFIX, MAC_PROVISIONING_PROFILE, MAC_NOTARY_KEY, MAC_NOTARY_KEY_ID, MAC_NOTARY_ISSUER. Pull requests do not produce a DMG; they stay on the ad-hoc compile in mac-app-build.yml.
./scripts/notarize.sh and ./scripts/dmg.sh accept either a local NOTARYTOOL_PROFILE or CI's NOTARYTOOL_KEY / NOTARYTOOL_KEY_ID / NOTARYTOOL_ISSUER.
For rapid iteration without rebuilding the Swift binary each time:
cd apps/mac
./dev.shThis will:
- Watch
src/for changes (JSX, HTML, CSS) - Re-run
assemble.pyon each change to updateResources/index.html - Automatically open the app (if not running) or trigger a reload
The app will hot-reload as you edit files, great for UI tweaking.
dev.sh defaults INDEX_DEVELOPMENT_BUILD=1: the build enables the web
inspector and, because an ad-hoc build carries no provisioning-profile-authorized
Keychain access group, stores the owner credential in the login keychain so
sign-in works locally. Production (signed) builds are unaffected and still fail
closed without the authorized owner group. Override with
INDEX_DEVELOPMENT_BUILD=0 ./dev.sh.
To manually reload during development, press Cmd+R (standard browser reload) in the app, or close and relaunch.
"AssertionError: no @font-face url() references found"
- The CSS must reference fonts at
fonts/jetbrains-mono-latin-var.woff2etc. - Check
src/index.htmlfor correct paths.
Swift compilation fails
- Ensure you have Xcode Command Line Tools:
xcode-select --install - Try
swiftc -versionto verify.
"codesign failed"
- Ad-hoc signing is skipped for local builds. The app will still run locally.
The app opens two URL families, and all routing lives in one pure function,
parseDeepLink in api/deeplink.mjs, inlined into the bundle as
window.IndexApi.parseDeepLink. The Swift shell
only delivers URLs — it raises the window and forwards the raw string to the
page as an index-deeplink CustomEvent, queuing anything that arrives before
the web view has finished loading (cold launch).
| URL | Opens |
|---|---|
https://index.network/o/<id> · index://o/<id> |
that opportunity's card |
https://index.network/u/<id> · index://u/<id> |
that person's profile |
https://index.network/i/<id> · index://i/<id> |
that signal |
index://q/<question-id> |
the signal that owns that pending question |
index://chat/<conversation-id> |
that conversation's chat inside its signal |
The q/chat routes are minted only by the app's own desktop notifications
(no web page serves them), so they matter mostly as the toast tap target.
Query strings, fragments and trailing slashes are ignored; foreign hosts,
unknown paths and malformed URLs are ignored silently. Extra hosts (staging)
are a hosts argument, not a code change.
The https:// half only works in a Developer ID-signed, notarized build.
build.sh generates the com.apple.developer.associated-domains entitlement
for the selected INDEX_LINK_HOST and passes that generated plist to
codesign; Index.entitlements is not the signed build input.
macOS verifies the resulting signed-app entitlement (for the default profile,
applinks:index.network) against the host's apple-app-site-association,
which lists <APPLE_TEAM_ID>.network.index.system6 — so the web host also needs
APPLE_TEAM_ID set. Inspect the built artifact with
codesign -d --entitlements :- dist/Index.app. An ad-hoc dev build has no team,
so macOS never hands it a universal link and build.sh says so.
The index:// scheme (registered via CFBundleURLTypes in Info.plist) has no
such requirement and is the way to exercise deep links locally:
open "index://o/<opportunity-id>"
open "index://u/<user-id>"
# only on a signed, notarized build:
open "https://index.network/o/<opportunity-id>"This operator-only handoff runs on macOS with a Developer ID Application identity and local notarytool profile. It never places endpoint credentials in the bundle.
- Register the explicit App ID
network.index.system6, enable Associated Domains enabled, and create a Developer ID provisioning profile that authorizes exactly the owner Keychain group. - Build with all four required inputs. The prefix has a trailing period and must match the profile/Team application-identifier prefix:
cd apps/mac
INDEX_LINK_HOST=dev.index.network \
INDEX_APP_IDENTIFIER_PREFIX='TEAM123ABC.' \
CODESIGN_IDENTITY='Developer ID Application: <name> (<team-id>)' \
PROVISIONING_PROFILE='<path-to-downloaded-profile>' \
./scripts/build.sh- Verify
embedded.provisionprofile, signature/entitlements, then notarize using a local keychain profile:
NOTARYTOOL_PROFILE='<local-keychain-profile>' ./scripts/notarize.shA runtime error such as No matching profile found means the profile does not authorize the launched app; do not treat prior signing/notarization checks as success. Verify that https://dev.index.network/u/<id>/chat stays in the browser while an allowed opportunity/profile link opens the signed app.
- Package the distributable disk image from the same verified app:
NOTARYTOOL_PROFILE='<local-keychain-profile>' ./scripts/dmg.sh
xcrun stapler validate dist/Index.dmg./scripts/dmg.sh revalidates the signed, stapled bundle, lays out the branded disk image, notarizes it, and staples dist/Index.dmg. The local DMG is for debugging; GitHub Releases are the distribution path. Replace scripts/dmg-background.png (660×420) and scripts/dmg-background@2x.png (1320×840) to change the Finder window art; keep those sizes so they match the icon layout. The art carries only the card and arrow: Finder draws the real icons and names at the positions set in dmg.sh, so do not bake them into the image. The mounted-disk glyph (title bar / desktop) is .VolumeIcon.icns, copied from the app's AppIcon.icns.
Record only redacted commands and pass/fail status in PR evidence; never IDs, credentials, certificate subjects, or profile names.