Skip to content

Local testing

You can exercise the entire pairing and session flow without ever touching production HiveCast. The dev pattern is "sibling Hosts": one local Host plays the platform/cloud role, another plays the linked Device role. Both use the same code paths, the same actor names, and the same wire shapes.

Two-runtime topology

CLAUDE.md § "Two-Runtime Dev Topology" describes the canonical setup. A worker container or workstation runs:

  • matrix-web runtime as the platform (account-facing shell, OAuth surface, Devices dashboard), exposing port 5001.
  • matrix-edge runtime as the Device (local Host UI), exposing port 5002.

Both runtimes share the same Host Service, the same local NATS, the same authority root, and the same system.devices.

bash
# Start a Host without auto-launched default runtimes:
hivecast install --home /tmp/matrix-home --no-start
hivecast start --home /tmp/matrix-home --no-default-runtimes

# Bring up the platform shell on a fixed port:
matrix up @open-matrix/matrix-web --serve --port 5001 \
  --runtime-id WEB --env hivecast --startup auto --restart always

# And the Device shell on another fixed port:
matrix up @open-matrix/matrix-edge --serve --port 5002 \
  --runtime-id EDGE --env hivecast --startup auto --restart always

Browse:

  • http://127.0.0.1:5001/apps/web/ — platform-role shell
  • http://127.0.0.1:5002/apps/edge/ — Device-role shell

Both are real packages running their real code paths.

Pairing against a sibling Host

To test the pairing flow without https://hivecast.ai:

bash
# Treat the platform-role Host as "the cloud":
hivecast login --device --cloud http://127.0.0.1:5001 \
  --route-key dev-pairing-$(date -u +%s) \
  --home /tmp/another-matrix-home

The --cloud URL points at the sibling Host. The pairing protocol is identical. The Devices page on the sibling Host shows the linked Device.

projects/matrix-3/scripts/prove-fresh-device-link.ts automates this case for proof runs.

OAuth in local testing

Local-loopback provider OAuth (Anthropic, Codex) works exactly as in production — these flows do not depend on a cloud OAuth client.

HiveCast account OAuth (Google) requires a registered redirect URI. For local dev, you have three options:

Option A — Use a staging Google client

Set up a Google OAuth client whose redirect URI is http://127.0.0.1:5001/api/auth/callback/google. Note that Google permits http://127.0.0.1 (and http://localhost) as authorized redirect URIs for development.

Configure the platform-role Host with that client's clientId / clientSecret. Sign-in works.

Option B — Mock the principal directly

For dev runs that don't need real Google, the local Host's auth state store can have a principal pre-seeded:

bash
matrix invoke system.auth auth.principal.ensure \
  '{"issuer":"dev-mock","subject":"alice","email":"alice@example.com","displayName":"Alice"}'

This creates a principal record without going through OIDC. Then mint a session directly:

bash
matrix invoke system.auth auth.session.create \
  '{"principalId":"p_<sha20>","email":"alice@example.com"}'

Set the resulting JWT as the mx_session cookie. Sign-in is bypassed.

Trade-off (Option B): the entire OAuth path is untested. Anything that depends on verifyGoogleIdToken() is skipped. Use this only for tests that don't exercise sign-in itself.

Option C — auth.mode: 'public-session'

Some dev configurations set auth.mode: 'public-session' to allow unauthenticated session creation. This is appropriate for unit tests and CI smoke tests but should never be used in user-facing dev environments.

Trade-off (Option C): session creation is a no-op gate. Anyone can mint a session for any principalId. Only safe inside CI.

Verifying auth properties locally

Verify session JWT signing

bash
# Mint a session, then validate it:
TOKEN=$(matrix invoke system.auth auth.session.create \
  '{"principalId":"p_test"}' | jq -r .token)

matrix invoke system.auth auth.session.validate '{"token":"'$TOKEN'"}'
# expect: { ok: true, claims: { sub: 'p_test', iss: 'localhost-dev', ... } }

Verify revocation

bash
matrix invoke system.auth auth.session.revoke \
  '{"headers":{"cookie":"mx_session='$TOKEN'"}}'

matrix invoke system.auth auth.session.validate '{"token":"'$TOKEN'"}'
# expect: { ok: false }   (jti is now in revokedSessions[])

Verify identity from transport metadata

bash
matrix invoke system.auth auth.identity.resolve \
  '{"headers":{"cookie":"mx_session='$TOKEN'"},"loopback":true}'
# expect: { authenticated: true, principalId: 'p_test', localClient: true }

Verify pairing flow end-to-end

bash
# Start a device-code:
RESP=$(matrix invoke system.auth auth.device.start '{}')
USER_CODE=$(echo $RESP | jq -r .userCode)
DEVICE_CODE=$(echo $RESP | jq -r .deviceCode)

# Approve from a "user" session:
matrix invoke system.auth auth.device.approve \
  '{"userCode":"'$USER_CODE'","principalId":"p_test"}'

# Exchange to get the credentials:
matrix invoke system.auth auth.device.exchange \
  '{"deviceCode":"'$DEVICE_CODE'"}'

The exchange response contains the DeviceSetupExchangeResponse-shaped payload. Inspect credentials to see the device-scoped NATS user JWT and heartbeat token.

What you cannot test locally

  • Production registry behavior. The cloud's Devices page UI may rely on cloud-only state synced from federation peers; sibling Hosts don't replicate that.
  • Real DNS-verified domain claims. auth.namespace.claim for domain.* namespaces requires DNS TXT verification; that path is cloud-only.
  • Real federation between Hosts. The two-runtime dev topology runs in one Host. The full federated case (one platform Host + one separate Device Host federated as a NATS leaf) is owned by WORKSTREAMS/runtime-environment-multi-instance/.

See also

Source: CLAUDE.md two-runtime dev topology; projects/matrix-3/packages/system-auth/src/host-auth.ts (HostSessionService, HostPrincipalStore); projects/matrix-3/scripts/prove-fresh-device-link.ts.