From e3266869dd14947536b525e0e28308354e0d89b0 Mon Sep 17 00:00:00 2001 From: Mario Tarosso Date: Thu, 29 Oct 2026 09:56:31 +0000 Subject: [PATCH 1/3] Tell agents in a hosted builder to recover a lost key with `login` A hosted builder's workspace may not keep the git-ignored key file, so a later scan finds no key. AGENT-INSTALL.md now says to run `login` there, never to ask the person for a key or open the builder's secrets form, to remove a wrong PATCHSTACK_API_KEY secret first, and to scan and publish again after approval because rotating retires the read-only credential the published app carries. Co-Authored-By: Claude Opus 5.5 --- AGENT-INSTALL.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/AGENT-INSTALL.md b/AGENT-INSTALL.md index 36029fa..fd11e3b 100644 --- a/AGENT-INSTALL.md +++ b/AGENT-INSTALL.md @@ -198,6 +198,8 @@ When the person asks for any of these after setup, run the command and pass on w Say only what the output says. Do not explain why setup could not finish a part, and do not add steps the output did not print. Never ask the person to copy their API key into a builder's settings or secrets: in an app that runs in the browser, a key saved there can end up in the published page. Never offer to wire protection in by hand when the output says no server was found. `go-live` prints the steps for the person's host when they are ready. +If a later `scan` says the project's API key is missing or was not accepted, run `login` as in "Recovering a lost credential — `login`". Do not ask the person for the key. + Leave out the commands you ran, settings, and anything that worked as expected; the setup output already lists which files changed. If the person asks for the detail, give it then. `--verbose` prints it. **When the person later asks you to deploy, re-read "Deploying" below before running anything.** A deploy that skips the build, or uploads the project folder, publishes the API key and leaves the live site unrecognised. @@ -1053,10 +1055,18 @@ is what fits a conversation. ## Recovering a lost credential — `login` -Use this when the project **already has a site** but its credential is gone or rejected: `.patchstackrc.local.json` was deleted, the repo was cloned without it (it is git-ignored, so a fresh clone never has it), a container was recycled, or ingest started failing with 401. The site UUID `login` needs comes from the committed `.patchstackrc.json`. +Use this when the project **already has a site** but its credential is gone or rejected: `.patchstackrc.local.json` was deleted, the repo was cloned without it (it is git-ignored, so a fresh clone never has it), a container was recycled, a hosted builder's workspace no longer has it (a builder may not keep git-ignored files), or ingest started failing with 401. The site UUID `login` needs comes from the committed `.patchstackrc.json`. > **Do not "fix" a missing credential by deleting `.patchstackrc.json` and running `scan` again.** That file holds the site UUID, and without it `scan` provisions a **second site** — the original, with all its history and its widget tag already live on the deployed page, is orphaned. `login` recovers the credential for the site you already have. +### In a hosted builder + +In Lovable, Replit, Bolt and similar builders, `login` is the fix. The missing-key message also mentions `PATCHSTACK_API_KEY`: that is for CI and for hosts the person deploys to themselves, not for a builder's workspace. + +- Do not ask the person for the key, do not open the builder's secrets or environment settings for it, and do not tell them to create or copy one in the Patchstack dashboard. Setup never shows the key, so the person has none to give you; `login` writes a new one. +- If a `PATCHSTACK_API_KEY` secret was already added in the workspace, ask the person to remove it first. It is read before `.patchstackrc.local.json`, so a wrong value keeps being used after `login`. +- After `login`, run `scan`, then ask the person to publish again. See "Consequences to tell the user about" for why. + ### What it does ``` @@ -1097,6 +1107,8 @@ You cannot complete this alone. It is deliberately a human-in-the-loop step: sta **Approving rotates the credential — the old one stops working immediately.** Anywhere it was configured needs the new value: CI secrets, hosting-platform env vars, preview environments, other developers' checkouts. Say this before they approve, not after. +A hosted builder's published app runs without the key, so there is nothing to update in the builder's settings, even though `login` says to update hosting. Rotating does retire the read-only credential that app carries (see "Package and command behavior"). So after `login`, run `scan`, which writes the new one into the guard's rules file, and ask the person to publish again. Until they do, the live app cannot fetch new rules or report what it blocks. + ### When it will not work | Situation | What happens | What to do | From b62e88b04f62233490ea175d2a095f9e03941883 Mon Sep 17 00:00:00 2001 From: Mario Tarosso Date: Thu, 29 Oct 2026 10:28:33 +0000 Subject: [PATCH 2/3] Restore a hosted builder's lost key with login, in the CLI's own messages In a Lovable or Replit project, the messages for a missing or refused key now say to run login in the builder's workspace, then scan and publish again. They no longer offer PATCHSTACK_API_KEY, which sent agents to the builder's secrets form to ask the person for a key they were never shown. login's own output says the same there, and on any machine warns when PATCHSTACK_API_KEY or PATCHSTACK_PULSE_AUTH is set, since either one is read before the key login saves. A builder's project deployed through a platform that identifies itself keeps the environment-variable advice. Co-Authored-By: Claude Opus 5.5 --- AGENT-INSTALL.md | 4 +-- src/build-hook.ts | 14 ++++++++--- src/cli.ts | 22 ++++++++++++++-- src/client.ts | 37 ++++++++++++++++++++++++--- src/config.ts | 4 ++- src/types.ts | 5 ++++ tests/auth-failure-message.test.ts | 40 ++++++++++++++++++++++++++++++ tests/build-hook.test.ts | 22 ++++++++++++++++ tests/config.test.ts | 8 ++++++ 9 files changed, 145 insertions(+), 11 deletions(-) diff --git a/AGENT-INSTALL.md b/AGENT-INSTALL.md index fd11e3b..221691a 100644 --- a/AGENT-INSTALL.md +++ b/AGENT-INSTALL.md @@ -1061,7 +1061,7 @@ Use this when the project **already has a site** but its credential is gone or r ### In a hosted builder -In Lovable, Replit, Bolt and similar builders, `login` is the fix. The missing-key message also mentions `PATCHSTACK_API_KEY`: that is for CI and for hosts the person deploys to themselves, not for a builder's workspace. +In Lovable, Replit, Bolt and similar builders, `login` is the fix. In a Lovable or Replit project the CLI's missing-key and rejected-key messages say so. Elsewhere they also name `PATCHSTACK_API_KEY`: that is for CI and for hosts the person deploys to themselves, not for a builder's workspace. - Do not ask the person for the key, do not open the builder's secrets or environment settings for it, and do not tell them to create or copy one in the Patchstack dashboard. Setup never shows the key, so the person has none to give you; `login` writes a new one. - If a `PATCHSTACK_API_KEY` secret was already added in the workspace, ask the person to remove it first. It is read before `.patchstackrc.local.json`, so a wrong value keeps being used after `login`. @@ -1107,7 +1107,7 @@ You cannot complete this alone. It is deliberately a human-in-the-loop step: sta **Approving rotates the credential — the old one stops working immediately.** Anywhere it was configured needs the new value: CI secrets, hosting-platform env vars, preview environments, other developers' checkouts. Say this before they approve, not after. -A hosted builder's published app runs without the key, so there is nothing to update in the builder's settings, even though `login` says to update hosting. Rotating does retire the read-only credential that app carries (see "Package and command behavior"). So after `login`, run `scan`, which writes the new one into the guard's rules file, and ask the person to publish again. Until they do, the live app cannot fetch new rules or report what it blocks. +A hosted builder's published app runs without the key, so there is nothing to update in the builder's settings. Rotating does retire the read-only credential that app carries (see "Package and command behavior"). So after `login`, run `scan`, which writes the new one into the guard's rules file, and ask the person to publish again. Until they do, the live app cannot fetch new rules or report what it blocks. ### When it will not work diff --git a/src/build-hook.ts b/src/build-hook.ts index 2b73a6e..77612fc 100644 --- a/src/build-hook.ts +++ b/src/build-hook.ts @@ -1,5 +1,6 @@ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; +import { restoresKeyWithLogin } from './client.js'; import { SECRET_CONFIG_FILENAME } from './config.js'; import { reportTrigger } from './report-context.js'; import type { Config, PatchstackError } from './types.js'; @@ -53,7 +54,9 @@ export function isPreBundleBuildHook(env: NodeJS.ProcessEnv = process.env, cwd = * * The error's own message names the remedy for a developer machine. A build environment needs a different * one: it never has the credential file, `login` refuses to run there, and the only fix is the platform's - * environment. Said for the credential cases only; a network or server failure has nothing to set. + * environment. A hosted builder's project is the exception: its workspace holds the key and sends the + * package list, so a key that is missing there is restored with `login`, never added as a secret (see + * restoresKeyWithLogin). Said for the credential cases only; a network or server failure has nothing to set. * * When no credential was found, the line names every source that was checked and where. Somebody who can * see the file on their own machine reads "none is configured" as "the file was not read", and the log is @@ -64,14 +67,19 @@ export function undeliveredReportLines(err: PatchstackError, config: Config, cwd if (err.code === 'UNAUTHORIZED') { const hasCredential = typeof config.pulseAuth === 'string' && config.pulseAuth.length > 0; + const builder = restoresKeyWithLogin(config); if (hasCredential) { lines.push( - 'patchstack: the credential this environment holds was rejected. If `login` rotated it, set the new value as PATCHSTACK_API_KEY here.', + builder + ? "patchstack: the credential this copy holds was rejected. Replace it with npx @patchstack/connect login in the builder's workspace, not with a new secret." + : 'patchstack: the credential this environment holds was rejected. If `login` rotated it, set the new value as PATCHSTACK_API_KEY here.', ); } else { lines.push( `patchstack: PATCHSTACK_API_KEY is not set, and neither ${SECRET_CONFIG_FILENAME} nor .patchstackrc.json in ${cwd} holds a credential.`, - `patchstack: ${SECRET_CONFIG_FILENAME} is git-ignored, so a clean checkout never has it. Set PATCHSTACK_API_KEY in the platform's environment so builds can report.`, + builder + ? `patchstack: ${SECRET_CONFIG_FILENAME} is git-ignored, so a copy of a builder's project may not have it. Restore it with npx @patchstack/connect login in the builder's workspace; do not add the key as a secret.` + : `patchstack: ${SECRET_CONFIG_FILENAME} is git-ignored, so a clean checkout never has it. Set PATCHSTACK_API_KEY in the platform's environment so builds can report.`, ); } } diff --git a/src/cli.ts b/src/cli.ts index 007afe1..3134d9b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -25,6 +25,7 @@ import { setRunCommand, hasSiteCredential, postRunNotice, + restoresKeyWithLogin, } from './client.js'; import { runWasLogged } from './report-context.js'; import { detectHostedBuilder } from './environment.js'; @@ -603,6 +604,9 @@ async function runLogin(args: ParsedArgs): Promise { } const config = await resolveCliConfig(args); + // In a builder's workspace the published app runs without the key, so there is nothing to update in the + // builder's settings. What it does carry is the read-only credential, which the rotation also retires. + const builder = restoresKeyWithLogin(config); // Checked here rather than carried up from the write: the rotation happens several layers down, and the // claim belongs to the line that prints it. @@ -611,7 +615,17 @@ async function runLogin(args: ParsedArgs): Promise { // The value itself is never printed — only that it landed, and only that it is ignored when it is. console.log(`\n ✓ Credential restored and saved to ${SECRET_CONFIG_FILENAME}.`); console.log(gitignoreOutcomeLine(ignore)); - console.log(' The old credential no longer works. Update it in CI, hosting and other checkouts.\n'); + // Both variables are read before the file, so either one left set keeps the old key in use. + for (const name of ['PATCHSTACK_API_KEY', 'PATCHSTACK_PULSE_AUTH']) { + if ((process.env[name] ?? '').trim() !== '') { + console.log(` ${name} is set here and is read before ${SECRET_CONFIG_FILENAME}, so the old key is still the one used. Remove it.`); + } + } + console.log( + builder + ? ' The old credential no longer works, and neither does the read-only one the published app carries. Run scan, then publish again.\n' + : ' The old credential no longer works. Update it in CI, hosting and other checkouts.\n', + ); return 0; }; @@ -637,7 +651,11 @@ async function runLogin(args: ParsedArgs): Promise { // Said before approval, not after: the person deciding needs to know it is // a rotation, and an assistant relaying this has to pass the warning on. console.log(" Open the link and approve as the site's owner. This replaces the current credential,"); - console.log(' so CI, deploys and other machines will need the new one.\n'); + console.log( + builder + ? ' so the app has to be published again afterwards.\n' + : ' so CI, deploys and other machines will need the new one.\n', + ); }; // Nobody is watching this stream. Blocking here would hide the link until the diff --git a/src/client.ts b/src/client.ts index c5c6a2b..fe7153e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -97,6 +97,28 @@ export function buildEndpointUrl(base: string, siteUuid?: string | null): string : trimmed; } +/** + * Whether a missing or refused key is restored with `login` rather than set as PATCHSTACK_API_KEY. + * + * True in a hosted builder's project. The person there has never been shown the key, so they have + * nothing to paste into the builder's secrets, and the builder's workspace may not keep the git-ignored + * file the key lives in. A deploy platform that identified itself is the exception, even for a project a + * builder generated: its own environment is where the key belongs. + */ +export function restoresKeyWithLogin(config: Config): boolean { + return typeof config.hostedBuilder === 'string' && config.environmentSource !== 'platform'; +} + +/** + * What restores a builder workspace's key. `login` replaces the key, which also retires the read-only + * credential the published app carries; `scan` writes the new one into the guard's rules file, and only a + * publish takes it to the live app. + */ +const BUILDER_LOGIN_STEPS = + "In the builder's workspace, run npx @patchstack/connect login: the person approves one link, and a new key " + + 'is saved there. Do not ask the person for a key; they have never been shown one. Then run scan, and publish ' + + 'again so the live app gets the new read-only credential.'; + /** * What a refusal means, in terms someone can act on. * @@ -114,12 +136,18 @@ export function buildEndpointUrl(base: string, siteUuid?: string | null): string */ export function authFailureMessage(status: number, config: Config): string | null { const hasCredential = typeof config.pulseAuth === 'string' && config.pulseAuth.length > 0; + const builder = restoresKeyWithLogin(config); if (status === 401 && !hasCredential) { - return "Patchstack needs this project's API key, and none is set here. Run npx @patchstack/connect login, or set PATCHSTACK_API_KEY."; + return builder + ? `Patchstack needs this project's API key, and none is set here. ${BUILDER_LOGIN_STEPS}` + : "Patchstack needs this project's API key, and none is set here. Run npx @patchstack/connect login, or set PATCHSTACK_API_KEY."; } if (status === 401) { - return "Patchstack did not accept this project's API key: it may have expired or been revoked, or the project may no longer exist. Run npx @patchstack/connect login to get a new one."; + return builder + ? "Patchstack did not accept this project's API key. If PATCHSTACK_API_KEY is set in the builder's workspace, " + + `remove it first: it is read before the key login saves. ${BUILDER_LOGIN_STEPS}` + : "Patchstack did not accept this project's API key: it may have expired or been revoked, or the project may no longer exist. Run npx @patchstack/connect login to get a new one."; } if (status === 403) { return 'This API key belongs to a different project. Check that siteUuid in .patchstackrc.json is the project the key was made for.'; @@ -258,7 +286,10 @@ export async function postInputMap( // The route refuses a map without the site's credential, so none is sent. A project copy that holds // the key (setup, or the development sync) uploads the same map under the same identity. if (!hasSiteCredential(config)) { - return { result: 'skipped', message: 'no API key here, so this build did not send it. The copy of the project that has the key sends it, or set PATCHSTACK_API_KEY here.' }; + const message = restoresKeyWithLogin(config) + ? 'no API key here, so this build did not send it. The workspace sends it with its key; if this is the workspace, run npx @patchstack/connect login to restore the key.' + : 'no API key here, so this build did not send it. The copy of the project that has the key sends it, or set PATCHSTACK_API_KEY here.'; + return { result: 'skipped', message }; } const url = buildInputMapUrl(config.endpoint, config.siteUuid); diff --git a/src/config.ts b/src/config.ts index fd24a7f..4aff649 100644 --- a/src/config.ts +++ b/src/config.ts @@ -191,9 +191,10 @@ export async function resolveConfig(options: ResolveConfigOptions): Promise { }); }); +describe("in a hosted builder's project", () => { + // The person there has never been shown the key, so the only fix is `login`: a message that offers + // PATCHSTACK_API_KEY sends an agent to the builder's secrets form, asking for a value nobody has. + const LOVABLE = config({ hostedBuilder: 'lovable', environmentSource: 'builder' }); + + it('restores a missing key with login, and never asks for one', () => { + const said = authFailureMessage(401, LOVABLE)!; + + expect(said).toContain('npx @patchstack/connect login'); + expect(said).toContain('publish again'); + expect(said).not.toContain('PATCHSTACK_API_KEY'); + }); + + it('replaces a refused key with login, after any secret that would shadow it is removed', () => { + const said = authFailureMessage(401, { ...LOVABLE, pulseAuth: 'a-secret-40-chars-long-enough-for-this-1' })!; + + expect(said).toContain('npx @patchstack/connect login'); + expect(said).toMatch(/PATCHSTACK_API_KEY is set .*remove it first/); + expect(said).toContain('publish again'); + }); + + it('keeps the environment variable where a deploy platform identified itself', () => { + // A builder's project deployed through Vercel or Netlify: that platform's environment is where the key belongs. + const deployed = config({ hostedBuilder: 'lovable', environmentSource: 'platform' }); + + expect(authFailureMessage(401, deployed)).toBe(authFailureMessage(401, config())); + }); + + it('a map upload without the key points the workspace at login', async () => { + const fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + + const outcome = await postInputMap(LOVABLE, { version: 3, endpoints: [] }); + + expect(fetchMock).not.toHaveBeenCalled(); + expect((outcome as { message: string }).message).toContain('npx @patchstack/connect login'); + expect((outcome as { message: string }).message).not.toContain('PATCHSTACK_API_KEY'); + }); +}); + describe('the write paths report a refusal in those terms', () => { it('map upload without a key is not sent, and says how to send it', async () => { const fetchMock = vi.fn(); diff --git a/tests/build-hook.test.ts b/tests/build-hook.test.ts index df9d03a..59ce0af 100644 --- a/tests/build-hook.test.ts +++ b/tests/build-hook.test.ts @@ -93,6 +93,28 @@ describe('undeliveredReportLines', () => { expect(said).not.toContain('/srv/app'); }); + it("points a hosted builder's copy at login, not at a secret", () => { + // The direct `scan` an agent runs in a builder's workspace prints these lines too, so "set + // PATCHSTACK_API_KEY in the platform's environment" there sends it to the builder's secrets form. + const lovable = config({ hostedBuilder: 'lovable', environmentSource: 'builder' }); + const missing = undeliveredReportLines(new PatchstackError('none configured', 'UNAUTHORIZED'), lovable, '/srv/app').join('\n'); + const refused = undeliveredReportLines( + new PatchstackError('rejected', 'UNAUTHORIZED'), + { ...lovable, apiKey: 'a-secret', pulseAuth: 'a-secret' }, + '/srv/app', + ).join('\n'); + + for (const said of [missing, refused]) { + expect(said).toContain('npx @patchstack/connect login'); + expect(said).toContain('not'); + expect(said).not.toContain("Set PATCHSTACK_API_KEY in the platform's environment"); + expect(said).not.toContain('set the new value as PATCHSTACK_API_KEY'); + expect(said).toContain('continuing the build'); + } + // Still names where it looked, so a missing file can be told from an unread one. + expect(missing).toContain('/srv/app'); + }); + it('adds no credential advice to a failure that is not about credentials', () => { // The control: a network failure with credential advice attached would send someone to set a variable // that changes nothing. diff --git a/tests/config.test.ts b/tests/config.test.ts index bbfa483..13b6082 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -126,6 +126,14 @@ describe('resolveConfig', () => { }); }); + it("names the hosted builder a project belongs to, from its own package.json", async () => { + await writeFile(path.join(cwd, 'package.json'), JSON.stringify({ devDependencies: { 'lovable-tagger': '^1.1.0' } })); + expect((await resolveConfig({ cwd, cliSiteUuid: VALID_UUID })).hostedBuilder).toBe('lovable'); + + await writeFile(path.join(cwd, 'package.json'), JSON.stringify({ dependencies: { react: '^19.0.0' } })); + expect((await resolveConfig({ cwd, cliSiteUuid: VALID_UUID })).hostedBuilder).toBeNull(); + }); + it('reads PATCHSTACK_TIMEOUT_MS from the environment', async () => { process.env.PATCHSTACK_TIMEOUT_MS = '5000'; const config = await resolveConfig({ cwd, cliSiteUuid: VALID_UUID }); From 39642e0062acdacae532c02869fd54ae52bfd6bc Mon Sep 17 00:00:00 2001 From: Mario Tarosso Date: Thu, 29 Oct 2026 10:32:41 +0000 Subject: [PATCH 3/3] Say the lost-key steps in plain words The builder messages no longer say "read-only credential", "git-ignored" or "read before": the agent passes these on to the person, so they say what happens instead (the live site keeps getting new protection rules, the secret overrides the new key). Co-Authored-By: Claude Opus 5.5 --- src/build-hook.ts | 4 ++-- src/cli.ts | 4 ++-- src/client.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/build-hook.ts b/src/build-hook.ts index 77612fc..a91c07c 100644 --- a/src/build-hook.ts +++ b/src/build-hook.ts @@ -71,14 +71,14 @@ export function undeliveredReportLines(err: PatchstackError, config: Config, cwd if (hasCredential) { lines.push( builder - ? "patchstack: the credential this copy holds was rejected. Replace it with npx @patchstack/connect login in the builder's workspace, not with a new secret." + ? "patchstack: the key here was refused. Replace it by running npx @patchstack/connect login in the builder's workspace, not by adding a new secret." : 'patchstack: the credential this environment holds was rejected. If `login` rotated it, set the new value as PATCHSTACK_API_KEY here.', ); } else { lines.push( `patchstack: PATCHSTACK_API_KEY is not set, and neither ${SECRET_CONFIG_FILENAME} nor .patchstackrc.json in ${cwd} holds a credential.`, builder - ? `patchstack: ${SECRET_CONFIG_FILENAME} is git-ignored, so a copy of a builder's project may not have it. Restore it with npx @patchstack/connect login in the builder's workspace; do not add the key as a secret.` + ? `patchstack: the key is kept in ${SECRET_CONFIG_FILENAME}, which a builder may not keep. Restore it by running npx @patchstack/connect login in the builder's workspace; do not add the key as a secret.` : `patchstack: ${SECRET_CONFIG_FILENAME} is git-ignored, so a clean checkout never has it. Set PATCHSTACK_API_KEY in the platform's environment so builds can report.`, ); } diff --git a/src/cli.ts b/src/cli.ts index 3134d9b..cb0753f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -618,12 +618,12 @@ async function runLogin(args: ParsedArgs): Promise { // Both variables are read before the file, so either one left set keeps the old key in use. for (const name of ['PATCHSTACK_API_KEY', 'PATCHSTACK_PULSE_AUTH']) { if ((process.env[name] ?? '').trim() !== '') { - console.log(` ${name} is set here and is read before ${SECRET_CONFIG_FILENAME}, so the old key is still the one used. Remove it.`); + console.log(` ${name} is set here and overrides the new key, so the old one is still used. Remove it.`); } } console.log( builder - ? ' The old credential no longer works, and neither does the read-only one the published app carries. Run scan, then publish again.\n' + ? ' The old key no longer works. Run scan, then publish again so the live site keeps getting new protection rules.\n' : ' The old credential no longer works. Update it in CI, hosting and other checkouts.\n', ); return 0; diff --git a/src/client.ts b/src/client.ts index fe7153e..4551c31 100644 --- a/src/client.ts +++ b/src/client.ts @@ -117,7 +117,7 @@ export function restoresKeyWithLogin(config: Config): boolean { const BUILDER_LOGIN_STEPS = "In the builder's workspace, run npx @patchstack/connect login: the person approves one link, and a new key " + 'is saved there. Do not ask the person for a key; they have never been shown one. Then run scan, and publish ' + - 'again so the live app gets the new read-only credential.'; + 'again so the live site keeps getting new protection rules.'; /** * What a refusal means, in terms someone can act on. @@ -146,7 +146,7 @@ export function authFailureMessage(status: number, config: Config): string | nul if (status === 401) { return builder ? "Patchstack did not accept this project's API key. If PATCHSTACK_API_KEY is set in the builder's workspace, " + - `remove it first: it is read before the key login saves. ${BUILDER_LOGIN_STEPS}` + `remove it first: it overrides the key login saves. ${BUILDER_LOGIN_STEPS}` : "Patchstack did not accept this project's API key: it may have expired or been revoked, or the project may no longer exist. Run npx @patchstack/connect login to get a new one."; } if (status === 403) {