feat: realm secret seed auth for boxel file read/write + realm publish/unpublish#5326
Conversation
`boxel file read` / `file write` only honored a Matrix profile, unlike `realm pull` / `realm push` which already accept a realm secret seed. Route both through the shared `resolveRealmAuthenticator` and add the same `--realm-secret-seed` option (env: BOXEL_REALM_SECRET_SEED): a seed mints a realm-server-bot JWT locally; with no seed the active-profile path is unchanged (including the no-active-profile error). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R4n4P6G1vFdSvSZaKkFK5f
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8b194d00f4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Adds realm secret seed authentication support to boxel file read and boxel file write, aligning them with other realm-oriented CLI commands by routing auth through the shared authenticator resolver. This enables non-Matrix, seed-based automation (e.g. codemods/migrations) while preserving the existing Matrix profile flow when no seed is provided.
Changes:
- Route
file read/file writethroughresolveRealmAuthenticator(seed auth or active profile auth). - Add
--realm-secret-seed(envBOXEL_REALM_SECRET_SEED) to both commands and pass the resolved seed into the underlying functions. - Extend command option types to support pre-resolved seed and injected authenticator test hooks.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/boxel-cli/src/commands/file/read.ts | Adds --realm-secret-seed and switches fetching to use a resolved RealmAuthenticator instead of directly using ProfileManager. |
| packages/boxel-cli/src/commands/file/write.ts | Adds --realm-secret-seed and switches writes to use a resolved RealmAuthenticator, supporting seed-minted JWTs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Review follow-ups for the file read/write seed auth: - write: resolve the seed before consuming stdin, so a `--realm-secret-seed` prompt and stdin-sourced content don't both contend for stdin. - read/write: resolve the seed inside the command's try/catch so a resolution throw (e.g. `--realm-secret-seed` with non-TTY stdin) surfaces as a clean CLI error instead of an unhandled rejection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R4n4P6G1vFdSvSZaKkFK5f
publish/unpublish authenticated only via a Matrix profile. Add seed support mirroring file read/write: mint a realm-server token signed with the seed, impersonating the realm owner (derived from the source realm URL as @<owner>:<domain>, or an explicit --as-user), since the publish endpoint authorizes the token's user as realm-owner. buildCliRealmClient gains a seed path that uses that server token for realm-server endpoints and a seed-minted realm token for per-realm endpoints; --realm-secret-seed is added to both commands. The profile path is unchanged when no seed is supplied. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R4n4P6G1vFdSvSZaKkFK5f
Asserts the seed-minted owner-scoped server token is actually accepted by the realm server's publish endpoint: publishes a realm with --realm-secret-seed + an owner `asUser` and no Matrix profile (force + no-wait isolate the assertion to the /_publish-realm call), then unpublishes via the same seed path. A negative case confirms a seed token impersonating a non-owner is rejected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R4n4P6G1vFdSvSZaKkFK5f
…rivation Review follow-ups: annotate `let client` (implicit any under noImplicitAny) and make deriveOwnerUserId reject a non-realm (< 2 segment) URL rather than silently deriving a bogus owner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R4n4P6G1vFdSvSZaKkFK5f
| // and grants `@<TEST_USERNAME>:localhost` the `realm-owner` permission. The | ||
| // publish endpoint authorizes the token's `user` as realm-owner, so a | ||
| // seed-minted server token impersonating that owner is what we assert works. | ||
| const TEST_REALM_SECRET_SEED = `shhh! it's a secret`; |
There was a problem hiding this comment.
We already import this from #realm-server/tests/helpers/index in boxel-cli/tests/helpers/integration.ts, we can re use it.
There was a problem hiding this comment.
[Claude Code 🤖] Good call — done in 827c98b. Re-exported realmSecretSeed from the integration helper and the test now uses it instead of the hardcoded literal.
| return `http://published-${uniqueRealmName()}.localhost:${port}/`; | ||
| } | ||
|
|
||
| describe('realm publish with seed-based auth (integration)', () => { |
There was a problem hiding this comment.
Would it be good to have the same test scenario for write and read command?
There was a problem hiding this comment.
[Claude Code 🤖] Added in 827c98b — file-seed-auth.test.ts writes then reads a file via the seed path (empty Matrix profile, so success proves seed auth), plus a no-seed/no-profile negative case. (Pull already has realm-pull-seed-auth.test.ts.)
Review follow-ups: use the realm-server test helpers' realmSecretSeed (re-exported from the integration helper) instead of a hardcoded literal, and add file read/write seed-auth integration tests mirroring the pull/publish ones. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R4n4P6G1vFdSvSZaKkFK5f
Several
boxeladmin commands authenticated only via a Matrix profile, unlikerealm pull/realm push, which already accept a realm secret seed. This adds the same--realm-secret-seedoption (envBOXEL_REALM_SECRET_SEED) to:file read/file write— routed through the sharedresolveRealmAuthenticator; a seed mints a realm-server-bot JWT locally, otherwise the active Matrix profile is used exactly as before.realm publish/realm unpublish—buildCliRealmClientgains a seed path. Because the publish endpoint authorizes the token'suseras realm-owner, seed mode mints an owner-scoped realm-server token (owner derived from the source realm URL as@<owner>:<domain>, or an explicit--as-user). Per-realm endpoints (e.g. readiness) use a seed-minted realm token.When no seed is supplied, every command keeps its existing profile behavior (including the no-active-profile error). A seed-resolution throw (e.g.
--realm-secret-seedwith non-TTY stdin) is handled as a clean CLI error rather than an unhandled rejection, and infile writethe seed is resolved before stdin is consumed.Motivation
In service of running the v2 search codemod (
@context.prerenderedCardSearchComponent→@context.searchResultsComponent) across all user realms in deployed staging and production. Migrations are applied by reading/writing card-source files in each realm and republishing published realms — all authenticated as the realm-server bot via the seed, which requires these admin commands to accept seed auth (asrealm pull/pushalready do).Verification
file read/writewithBOXEL_REALM_SECRET_SEEDset authenticate and round-trip a card-source file against staging; with no seed, profile behavior is unchanged. The publish/unpublish seed path is exercised against staging during the published-realm republish step. Thefile/publish/unpublishintegration suites run against the Synapse + realm-server stack in CI.🤖 Generated with Claude Code
https://claude.ai/code/session_01R4n4P6G1vFdSvSZaKkFK5f