Skip to content
Access and sessions

Access and sessions

The allowlist

Sign-in is Google-only, and only for addresses on the allowlist. There is no password path, and none should be added.

VariableBehaviour
LUNA_OWNER_EMAILA comma- or whitespace-separated list of Google accounts. A single address works as before. Empty or unset means every login is rejected.
LUNA_OWNER_EMAIL_FILEOptional path to a file holding the list, for example the mounted allowed-emails key of the luna-secrets Secret. If set, it wins over LUNA_OWNER_EMAIL and is re-read every 5 seconds, so allowlist edits need no rollout. An unreadable file means an empty list (fail closed). Not yet wired in the deployment manifest.

Accounts are added through CORE’s @add_users issue workflow, which writes luna-secrets/allowed-emails the same way it does for the other Runink apps.

One deployment, one Luna per account. Each allowlisted account has its own journal, memory, plans, avatar, streaks and level, keyed by a hash of the address.

How sign-in works

  1. The app gets a Google ID token (scopes openid email profile) and calls IdentityService.GoogleLogin.
  2. The server verifies the token locally against Google’s published keys, with LUNA_GOOGLE_CLIENT_ID as the audience. The issuer is Google unless LUNA_OIDC_ISSUER overrides it.
  3. It requires a verified email on the allowlist, then opens a session.

Sessions

Sessions are rows in LUNA’s own appfs root (run/sessions), not signed tokens. Each one is a 256-bit opaque id, stored only as its SHA-256 hash, and lives for 12 hours.

  • Every RPC validates its session against the shared table (cached for 5 seconds) and re-checks the allowlist.
  • IdentityService.Logout revokes the session on every replica. The app’s Sign out calls it.
  • Removing an address revokes access without a rollout. The first request from a removed account writes the revocation, and a reconcile loop (at boot and every 30 seconds) writes it for the rest. Adding the address back doesn’t bring an old session back.
  • Sessions need CORE_ENVELOPE_KEK. Without it there is no session store, and every authenticated RPC answers Unavailable.

Only IdentityService/GoogleLogin and gRPC reflection are public. Everything else needs authorization: Bearer <session token>.

Admin subcommands

The server binary is luna-server (the Cobra root is luna). In the deployment, run subcommands inside the pod, where the object-store credentials and the KEK are already present:

kubectl -n luna-system exec deploy/luna -- /app/luna-server <subcommand> [flags]

Every command that takes --account requires it and refuses to guess.

SubcommandWhat it does
serve [-p PORT]Runs the backend: gRPC, gRPC-Web and the static web bundle on one port. The port comes from PORT, and 0 means auto-assign. The image runs serve.
mint-session --account <email>Opens a session for an allowlisted account and prints its token, for smoke tests. It must share the server’s appfs root and CORE_ENVELOPE_KEK.
revoke-sessions --account <email>Revokes every session the account holds, on all replicas, with no rollout.
grant-level --account <email> [--level N] [--reason "…"]Writes a LEVEL_UP receipt (level 1–10, default 10) into that account’s journal. It never invents workouts or meals.
claim-legacy-data --account <email> [--dry-run]Assigns records written before per-account scoping (the default partition) to one account. Until then, those records are readable by nobody.
migrate-appfsMoves journals, owner notes and memory from the older record-store layout into the appfs root. It is idempotent, never deletes the old objects, and also runs automatically after each connect. Run it after a claim-legacy-data.
migrate-bucket --from <bucket>Copies every object from another bucket into this deployment’s bucket (OBJECTSTORE_BUCKET). It refuses if the source and destination are the same.
Never backdate WORKOUT or MEAL_ADHERENCE events to give someone points. LogEvent accepts any occurred_at, but doing this writes false health history into the journal that grounds every piece of coaching. Use grant-level instead.