v1 prep: multi-tenancy seam (resolve_owner) + hub-setup how-to #4

Merged
eblume merged 3 commits from feature/v1-hub-prep into main 2026-06-04 08:00:32 -07:00
Owner

The final v1.0.0 prep for the distributed/hub story — the cheap seam we agreed on (resolve the request's owner instead of a single-owner gate) plus the hub-setup how-to.

The seam (no behavior change)

Replaces Store::authorize_owner_sub(sub) -> bool with resolve_owner(sub) -> Option<owner_id>. The hub auth middleware now resolves the token's identity to the owner it may act as (Some → allow, None → 403). For the single-owner hub this is byte-for-byte the same behavior (claim-on-first; a stranger's token still 403s), but the contract no longer bakes in "one global owner" — so multi-tenancy later is additive, not a rewrite. The exact line where per-request owner scoping will wire through is marked.

Why this is the right v1 move: multi-tenancy is a code concern, not a data one (every row already carries owner_id), so deferring it carries zero data-migration risk. Full multi-tenancy (one-DB-vs-per-owner fork, provisioning, cross-tenant isolation hardening + adversarial tests) stays deferred until a 2nd user exists. Decision recorded in the Adoption + multi-tenant task's context doc.

The how-to

docs/how-to/set-up-sync-hub.md — stand up the canonical hub (indri) and connect an existing device (gilbert) as an offline-capable spoke, the data-safe way: Path A, where the hub adopts the device's identity (same owner_id + data) rather than rewriting the device. Covers the hub-and-spoke model, Tailscale-vs-Authentik (transport vs. identity), the hephd flags, and heph auth login.

It honestly flags two enabler gaps (filed as Hephaestus tasks): heph daemon doesn't yet bake hub/spoke flags into the generated service, and Path-A seeding is still manual (snapshot + origin reset).

Testing

  • cargo test --all228 passed, 0 failed (incl. the renamed/strengthened resolve_owner_claims_first_then_requires_match + the existing hub-auth adversarial battery, which still 403s strangers through the new path)
  • clippy -D warnings + fmt + prek run --all-files clean

Ready to merge ahead of cutting v1.0.0.

The final v1.0.0 prep for the distributed/hub story — the **cheap seam** we agreed on (resolve the request's owner instead of a single-owner gate) plus the **hub-setup how-to**. ## The seam (no behavior change) Replaces `Store::authorize_owner_sub(sub) -> bool` with **`resolve_owner(sub) -> Option<owner_id>`**. The hub auth middleware now resolves the token's identity to the owner it may act as (`Some` → allow, `None` → 403). For the single-owner hub this is byte-for-byte the same behavior (claim-on-first; a stranger's token still 403s), but the contract no longer bakes in "one global owner" — so multi-tenancy later is **additive, not a rewrite**. The exact line where per-request owner scoping will wire through is marked. Why this is the right v1 move: multi-tenancy is a **code** concern, not a **data** one (every row already carries `owner_id`), so deferring it carries zero data-migration risk. Full multi-tenancy (one-DB-vs-per-owner fork, provisioning, cross-tenant isolation hardening + adversarial tests) stays deferred until a 2nd *user* exists. Decision recorded in the `Adoption + multi-tenant` task's context doc. ## The how-to `docs/how-to/set-up-sync-hub.md` — stand up the canonical hub (indri) and connect an existing device (gilbert) as an offline-capable spoke, **the data-safe way**: Path A, where the hub adopts the device's identity (same `owner_id` + data) rather than rewriting the device. Covers the hub-and-spoke model, Tailscale-vs-Authentik (transport vs. identity), the `hephd` flags, and `heph auth login`. It honestly flags two enabler gaps (filed as Hephaestus tasks): `heph daemon` doesn't yet bake hub/spoke flags into the generated service, and Path-A seeding is still manual (snapshot + origin reset). ## Testing - `cargo test --all` — **228 passed, 0 failed** (incl. the renamed/strengthened `resolve_owner_claims_first_then_requires_match` + the existing hub-auth adversarial battery, which still 403s strangers through the new path) - `clippy -D warnings` + `fmt` + `prek run --all-files` clean Ready to merge ahead of cutting v1.0.0.
feat: multi-tenancy seam (resolve_owner) + hub-setup how-to (v1 prep)
All checks were successful
Build / validate (pull_request) Successful in 10m34s
a0b04eefda
The cheap "seam" that keeps the single-owner hub from calcifying, ahead of
the gilbert -> indri bring-up:

- Replace the single-tenant gate `Store::authorize_owner_sub(sub) -> bool`
  with `resolve_owner(sub) -> Option<owner_id>`. The hub auth middleware now
  resolves the token's identity to the owner it may act as (Some -> allow,
  None -> 403). Behavior is identical for the single-owner hub (claim-on-first;
  strangers still 403), but the contract no longer assumes one global owner, so
  serving N owners later is additive, not a rewrite. The per-request owner is
  marked at the exact line where downstream scoping wires through.
- New how-to docs/how-to/set-up-sync-hub.md: stand up the hub and connect an
  existing device as an offline-capable spoke, the data-safe way (Path A: the
  hub adopts the device's identity rather than rewriting the device).

The decision (cheap seam now, defer full multi-tenancy + adoption rewrite) is
recorded in the Adoption + multi-tenant task's context doc. Two enabler gaps
the how-to surfaced (heph daemon hub/spoke service flags; Path-A seeding tool)
are filed as Hephaestus tasks.

Green: 228 tests, clippy -D warnings + fmt + prek clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
infra: slim the keyring dependency (keyring meta-crate -> keyring-core + one store/OS)
Some checks failed
Build / validate (pull_request) Failing after 45s
6ba94119e4
keyring 4's `keyring` meta-crate has no feature gating and compiles every
platform credential backend for the target. On Linux that dragged in the zbus
async stack, a redundant libdbus secret-service, the keyutils store, a
sqlite/zstd db-keystore, and OpenSSL (~290 crates in its subtree) — a real cost
on the RAM/CPU-constrained CI runner building with CARGO_BUILD_JOBS=1.

Depend on keyring-core (the API) + exactly one store crate per OS instead:
- macOS  -> apple-native-keyring-store (keychain feature)
- Linux  -> dbus-secret-service-keyring-store (crypto-rust; libdbus, no openssl)

oauth.rs registers the per-target store as the keyring-core default itself
(replacing keyring::use_native_store). Runtime behavior is unchanged (tokens
still go to the macOS Keychain / Linux Secret Service).

hephd's Linux dependency graph: 401 -> 235 crates (-166), dropping the zbus
ecosystem and two C builds (zstd-sys, plus the redundant secret-service path).

macOS builds + the full suite are green here (228 tests, clippy -D warnings,
fmt, prek); the Linux store path is CI-verified (API confirmed from source).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(ci): vendor libdbus in the Linux keyring store (no system libdbus-1-dev)
All checks were successful
Build / validate (pull_request) Successful in 8m11s
b6a96013ca
The rust:1-bookworm CI image has no libdbus-1-dev, so libdbus-sys's
pkg-config build failed. Enable the dbus store's `vendored` feature to build
libdbus from bundled source (self-contained, the proven path the earlier
keyring-4 build used). `crypto-rust` keeps it OpenSSL-free; openssl-sys is only
an inert lock entry (the conditional `openssl?/vendored` reference), compiled
nowhere. Linux footprint unchanged at 235 crates; vendored libdbus is a
build-time C compile, not new crates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
eblume merged commit 80f83cbba8 into main 2026-06-04 08:00:32 -07:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
eblume/hephaestus!4
No description provided.