feat(cli): add sanity new to mint unclaimed projects without logging in#1592
feat(cli): add sanity new to mint unclaimed projects without logging in#1592shapirodaniel wants to merge 11 commits into
sanity new to mint unclaimed projects without logging in#1592Conversation
📦 Bundle Stats —
|
| Metric | Value | vs main (98ae742) |
|---|---|---|
| Internal (raw) | 2.2 KB | - |
| Internal (gzip) | 838 B | - |
| Bundled (raw) | 11.20 MB | -80 B, -0.0% |
| Bundled (gzip) | 2.11 MB | +7 B, +0.0% |
| Import time | 924ms | +0ms, +0.0% |
bin:sanity
| Metric | Value | vs main (98ae742) |
|---|---|---|
| Internal (raw) | 782 B | - |
| Internal (gzip) | 423 B | - |
| Bundled (raw) | 9.90 MB | - |
| Bundled (gzip) | 1.78 MB | - |
| Import time | 2.24s | +1ms, +0.1% |
🗺️ View treemap · Artifacts
Details
- Import time regressions over 10% are flagged with
⚠️ - Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.
📦 Bundle Stats — @sanity/cli-core
Compared against main (98ae7425)
| Metric | Value | vs main (98ae742) |
|---|---|---|
| Internal (raw) | 117.2 KB | +3.1 KB, +2.7% |
| Internal (gzip) | 30.2 KB | +1017 B, +3.4% |
| Bundled (raw) | 21.78 MB | +15.1 KB, +0.1% |
| Bundled (gzip) | 3.47 MB | +3.6 KB, +0.1% |
| Import time | 819ms | -4ms, -0.5% |
🗺️ View treemap · Artifacts
Details
- Import time regressions over 10% are flagged with
⚠️ - Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.
📦 Bundle Stats — @sanity/cli-build
Compared against main (98ae7425)
@sanity/cli-build/_internal/build
| Metric | Value | vs main (98ae742) |
|---|---|---|
| Internal (raw) | 113.2 KB | - |
| Internal (gzip) | 28.7 KB | - |
| Bundled (raw) | 17.75 MB | +49 B, +0.0% |
| Bundled (gzip) | 3.55 MB | -158 B, -0.0% |
| Import time | 1.16s | -4ms, -0.3% |
@sanity/cli-build/_internal/env
| Metric | Value | vs main (98ae742) |
|---|---|---|
| Internal (raw) | 1.8 KB | - |
| Internal (gzip) | 644 B | - |
| Bundled (raw) | 1.31 MB | - |
| Bundled (gzip) | 333.8 KB | - |
| Import time | 128ms | -0ms, -0.0% |
@sanity/cli-build/_internal/extract
| Metric | Value | vs main (98ae742) |
|---|---|---|
| Internal (raw) | 8.6 KB | - |
| Internal (gzip) | 2.7 KB | - |
| Bundled (raw) | 155.0 KB | - |
| Bundled (gzip) | 39.5 KB | - |
| Import time | 253ms | -1ms, -0.2% |
🗺️ ./_internal/env · ./_internal/extract · @sanity/cli-build:./_internal/build treemap too large to embed · Artifacts
Details
- Import time regressions over 10% are flagged with
⚠️ - Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.
📦 Bundle Stats — create-sanity
Compared against main (98ae7425)
| Metric | Value | vs main (98ae742) |
|---|---|---|
| Internal (raw) | 908 B | - |
| Internal (gzip) | 483 B | - |
| Bundled (raw) | 931 B | - |
| Bundled (gzip) | 491 B | - |
| Import time | ❌ ChildProcess denied: node | - |
Details
- Import time regressions over 10% are flagged with
⚠️ - Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.
|
bugbot run |
Coverage Delta
Comparing 15 changed files against main @ Overall Coverage
|
|
bugbot run |
|
bugbot run |
|
bugbot run |
|
bugbot run |
|
bugbot run |
|
bugbot run |
|
bugbot run |
|
bugbot run |
|
bugbot run |
|
bugbot run |
|
bugbot run |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b9f1fbb. Configure here.
Resolve the robot token from the minted-project ledger (getCliToken middle tier) and add the flow-output UX helper used to narrate the mint command. The ledger parses .env with dotenv (matching readEnvValues) and any ledger write invalidates the token cache, so a dropped record stops authenticating. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The provisioning mint + claim-state service (401 reports a revoked token), the .env writer with gitignore enforcement and git-tracked detection, the shared GUARDED_ENV_KEYS, and the new-command splash + terminal hyperlinks. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Record minted projects in the local ledger and render expiry-tiered claim reminders. The cwd project is always claim-verified (so a claimed/expired/ revoked record is dropped and stops outranking the login session), and post-drop guidance points at working recovery commands. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Mint an unclaimed project without logging in. Writes credentials to .env (gitignored, with a warning when .env is already tracked or the write fails), never touches the ledger under --json, and warns if the ledger write that powers bare-directory auth fails. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Prerun hook that nudges until a minted project is claimed, and sanity init picking up minted credentials (token-only identity). The two-ways-to-start banner and the unattended guidance are suppressed wherever the remint guard would refuse (any guarded .env key present), so neither dead-ends. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Target the stored login session directly, warn when SANITY_AUTH_TOKEN is set in the environment (logout cannot end it), and surface that a minted directory keeps acting as the project robot via the ledger token. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
b9f1fbb to
62a7452
Compare
| : tier >= 4 | ||
| ? `🚨 Final reminder — project ${record.projectId} expires in ${timeLeft}` | ||
| : `${urgent ? '🚨' : '⏰'} Project ${record.projectId} expires in ${timeLeft}` | ||
| const bell = tier >= 4 ? '' : '' |
There was a problem hiding this comment.
Both branches are empty strings so the final reminder never actually rings the bell. Either \u0007 in the first branch or delete the variable.
There was a problem hiding this comment.
nice, imo let's nuke the bell entirely, it's only a human-accessible thing and it's not super differentiated, terminals ring or click for a number of reasons including user inputs
| switch (state) { | ||
| case 'claimable': { | ||
| continue | ||
| break |
There was a problem hiding this comment.
Unreachable, the continue above already exits the iteration.
| * Resolve the robot token for the minted project the current directory points at, given the | ||
| * ledger `records` (the `unclaimedProjects` config value). Lets the CLI authenticate in a freshly | ||
| * minted directory that has no config file — where env injection never runs. The token is owned | ||
| * by the ledger, not `.env`: `.env` only carries the non-secret project id. Never throws. |
There was a problem hiding this comment.
This isn't true, mint writes the full robot token into .env as SANITY_AUTH_TOKEN (mint.ts:200), which is why we gitignore it and warn when it's tracked. However this one is true: getCliToken resolves from the ledger rather than .env because .env is mutable.
Worth rewording before someone reads this and concludes .env needs no secret handling. Same wording appears in the getCliToken comment in cliUserConfig.ts.
| // exists — the gap that stops `sanity init` authenticating in a freshly minted directory. | ||
| let mintedToken: string | undefined | ||
| try { | ||
| mintedToken = resolveMintedProjectToken(getUserConfig().get(UNCLAIMED_PROJECTS_CONFIG_KEY)) |
There was a problem hiding this comment.
Unsure this is the right approach.
What happens today: inside any sanity project, .env gets loaded into the environment on every command, and an env SANITY_AUTH_TOKEN outranks your login session. That's old behaviour and this PR doesn't touch it. But today you only end up there by putting a token in .env yourself, so it's not a surprise.
What this PR adds: sanity new writes the robot token into .env for you, and this ledger tier makes it win even in a bare directory where env injection doesn't run. So in a minted directory the CLI always prefers the robot token over whoever is actually logged in. Pre-claim that's fair enough, the robot is the only identity that can access the project.
Why I'm unsure it's correct: the preference doesn't really end at claim. The ledger entry gets cleaned up once the claim check succeeds, but the same token is still sitting in .env, and once a studio is scaffolded that file loads into the environment on every command. So in the normal mint → init → claim flow you carry on acting as "Editor Token (Robot)" in that directory forever, even after running sanity login, until you hand-delete the line from .env. And nothing tells you.
Maybe we just need to encode this in docs or the claim message, there could also be issues if we wipe .env for you as well.
There was a problem hiding this comment.
Maybe we just need to encode this in docs or the claim message
totally agree, in 733d043 we're now warning if an .env token "outranks" the user's session, the same way we warn at sanity logout
in 4d3af36, the final claim "farewell" message gives similar guidance about the directory being robot auth'd and how to switch to user auth
| try { | ||
| const response = await fetch(url, { | ||
| headers: {Authorization: `Bearer ${robotToken}`}, | ||
| signal: AbortSignal.timeout(options?.timeoutMs ?? 1500), |
There was a problem hiding this comment.
Default is still 1500 here (and on lookupClaimState at line 61) but every caller now passes 500 after Wednesday's decision. Worth flipping the defaults to 500 so the next caller doesn't quietly get the old timeout.
|
|
||
| if (!response.ok) { | ||
| const body = await response.text().catch(() => '') | ||
| throw new Error(`Mint failed (HTTP ${response.status}): ${body || response.statusText}`) |
There was a problem hiding this comment.
A 404 here means the kill switch is off but it surfaces as Mint failed (HTTP 404): .... The friendly 'provisioning disabled, or rate limited' message below only fires on a 200 with no claimToken, which isn't how the kill switch responds. Suggest special-casing 404 and 429 with the human message.
There was a problem hiding this comment.
yeah def agree on special-casing, i've added human-friendly copy to reference minting as disabled / rate limit reached in d1739c2
| const url = `${getProvisionApiBase()}/${PROVISION_API_VERSION}/provision` | ||
| debug('minting unclaimed project at %s', url) | ||
|
|
||
| const response = await fetch(url, { |
There was a problem hiding this comment.
I think there's a HTTP setup in CLI to use. Bare fetch means no proxy support and no CLI user agent, unlike everything else that goes through the client stack.
Someone behind a corporate proxy will have every sanity command work except sanity new, and the server side can't tell CLI mints apart for triage. Fine to keep if it's deliberate for the unauthenticated endpoints, but deserves a comment saying so.
There was a problem hiding this comment.
great catch, and thanks for pointing me to createRequester! updated in d1739c2
| expect(mockGetMintedProjectRecord).toHaveBeenCalledWith('abc123') | ||
| }) | ||
|
|
||
| test('env token plus stored session: warns about the env token and ends the session', async () => { |
There was a problem hiding this comment.
This proves logout succeeds when both tokens are present, but the nock mock matches on URL only, so it doesn't prove it was the session token rather than SANITY_AUTH_TOKEN that hit /auth/logout. That's the exact leak the change prevents, worth asserting the Authorization header.
There was a problem hiding this comment.
great call out, yeah let's def do this -- updated to assert Authorization header content in 733d043
- after a successful sanity login, warn when SANITY_AUTH_TOKEN (or a minted ledger record) still outranks the fresh session, mirroring the logout warnings - correct the token-location comments: .env carries the token for the app runtime, the ledger is the CLI's resolution source - assert the Authorization header in the logout session test Co-Authored-By: Claude Fable 5 <[email protected]>
Collapse the tiered waterfall and the directory-scoped ambient line into a single reminder shown on every command, anywhere, from mint until claim, expiry, or revocation: - constant copy for the whole window; the headline goes red once 24 hours remain, and 'Final reminder' is gone - exact hours-and-minutes countdowns, never 'about'; no emoji - with several unclaimed projects, the soonest-to-expire leads with the full block and the rest aggregate to one compact line each - claim-state lookups run in parallel and fail open, so the blocking prerun check costs one 500ms timeout at most - the claim farewell gains the robot-token handoff guidance Co-Authored-By: Claude Fable 5 <[email protected]>
- route mint and claim-state requests through cli-core createRequester (CLI user agent), replacing bare fetch - special-case provision 404 (minting disabled) and 429 (rate limited) with plain-language errors; flip lookup timeout defaults to 500ms - state the claim deadline plainly with the exact timestamp and deletion consequence, plus a dim two-line agent handoff - expand sanity new --help into an agent brief and end with the sanity.new next-steps pointer; the splash drops its links Co-Authored-By: Claude Fable 5 <[email protected]>
- take the reviewed banner wording: log in or sign up vs create without an account, claim within 72 hours - move the agent lines below the box, unboxed and dim like every other agent instruction, in the shared 'If an agent is running this' voice - point the unattended not-logged-in error at sanity new with --json and the sanity.new reference Co-Authored-By: Claude Fable 5 <[email protected]>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 705c604. Configure here.
| export { | ||
| resolveMintedProjectToken, | ||
| UNCLAIMED_PROJECTS_CONFIG_KEY, | ||
| } from '../config/cli/unclaimedProjects.js' |
There was a problem hiding this comment.
New exports on deprecated barrel
Medium Severity
resolveMintedProjectToken and UNCLAIMED_PROJECTS_CONFIG_KEY were added to the root @sanity/cli-core barrel despite its explicit “DO NOT ADD NEW EXPORTS” rule. They already belong on @sanity/cli-core/config, and claimNudges can import them from that subpath instead.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 705c604. Configure here.
in offline sync, we agreed this is def something we want in v1 launch, and it's a good fit for an immediate fast follow, given the stealth launch of |


Description
this PR adds
sanity newas an alias ofsanity projects mint. high level, we're allowing humans and agents to create projects without an account, through public apis that produce a claim URL that allows transfer of a newly-created project to the user's desired organization within 72hrs of creationWhat's included
sanity newmints a new project and issues a robot token that can be used to drive programmatic content lake operationssanity inithints atsanity newavailability so humans and agents can discover and leverage the new behaviorsanity newFor reviewers
we initially gave a stacked diff a try, but the result was a bit unwieldy:
sanity newfunctionality is deep intertwined, and easier to review with agentic support if it's broken into commits on a single diff as per early feedback. we've reshipped as a series of commits instead, and recommend reviewing commit-by-commit or pointing a review agent at the pr to do a holistic passTesting
we added unit coverage and conducted manual uat to exercise all branching logic related to credential management
Notes for release
Add
sanity newto mint an unclaimed Sanity project without logging in, then claim it with a Sanity account later.Note
High Risk
Changes CLI authentication precedence, stores robot tokens in user config and
.env, and adds unauthenticated project provisioning—mistakes could leak credentials or let users act under the wrong identity.Overview
Introduces
sanity new(alias ofsanity projects mint) to create an unclaimed Sanity project via the provision API without logging in, returning a robot token and claim URL (72-hour claim window). Interactive runs write credentials to.env, gitignore it when possible, and record the mint in a localunclaimedProjectsledger;--jsonreturns structured data without touching the filesystem.getCliTokennow resolves auth from that ledger (keyed bySANITY_PROJECT_IDin the directory.env) between envSANITY_AUTH_TOKENand the stored login session, so commands work in a freshly minted folder before Studio config exists. Ledger writes invalidate the in-process token cache likeauthTokenchanges.Supporting pieces include provision mint/claim lookup services,
.envhelpers with remint guardrails,createFlowUX for the mint narrative, a prerunclaimRemindershook (skipped on mint commands), and claim nudges that verify state and remind users to claim.sanity initadvertisessanity newvia a “two ways to start” banner when appropriate and tailors unattended auth errors for minted vs credential-heavy directories.login/logoutwarn when env or ledger robot tokens outrank or survive a session logout; logout calls the API with the stored session token only.Reviewed by Cursor Bugbot for commit 705c604. Bugbot is set up for automated code reviews on this repo. Configure here.