Skip to content

Tags: patchstack/connect

Tags

v0.3.31

Toggle v0.3.31's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Treat a field-test round that never installed as inconclusive (#182)

* Treat a field-test round that never installed as inconclusive

The documentation gate exists to catch a contradiction between the shipped docs and `dist/` — an
undisclosed command, an overbroad privacy claim (refusal mode 6). Catching that requires the agent to
have READ the docs, which requires it to have obtained the tarball, which requires it to have installed.

An agent that refuses on the PROMPT never gets there, and its scorecard was indistinguishable from one
produced by a documentation regression: `2/8 REFUSED` either way, with no field separating them. So
"must pass `--rounds 3`" could not fail for a documentation reason at all — the gate was unable to detect
the thing it exists for. `2/8 REFUSED` is also the modal outcome for `hostile`, across every recorded run
in `results/`, so this was the normal case rather than an edge one.

Such a round is now void:

- the scorecard carries `audited`, and prints `VOID` when nothing was installed;
- void rounds are retried, bounded at `2 × --rounds`, so a persona that never installs cannot loop;
- the summary counts only conclusive rounds and reports how many were void;
- exit is three-way — `0` all conclusive rounds green, `1` a real failure, `2` inconclusive. A release
  gate must not read `2` as "the docs are fine", which is exactly what a two-way exit invited.

`stub-refusing.mjs` is the counterpart to `stub-compliant.mjs`: it reproduces a pre-install refusal so
both paths can be self-tested without spending agent tokens. Verified — compliant gives `8/8`,
`1/1 conclusive`, exit 0; refusing gives three void rounds (one plus the two bounded retries),
INCONCLUSIVE, exit 2.

`CLAUDE.md` and `field-test/README.md` now state the rule, including two ways to use the gate properly:
prefer a persona that reliably installs for a docs-only change, since `hostile` measures prompt survival
rather than doc accuracy; and re-run after publication, because until then the tarball an agent audits
does not contain the change.

Nothing here ships — `field-test/` is dev-only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Require an unpacked tarball, not a dependency declaration

`audited` decided whether a round counts toward the documentation gate, and it read
`@patchstack/connect` appearing in `package.json`. That is a declaration, not an install: an agent can
add the dependency and refuse before `npm install` ever runs, and the shipped docs never reach the disk.
The round then scored as CONCLUSIVE while nothing had been audited — reintroducing, one level in, the
hole this change exists to close.

Worse than merely permissive, measured: with a stub that only edits `package.json`, the old signal
produced `0/1 conclusive round(s)` and exit 1 — a definitive FAILURE verdict about documentation the run
had never obtained. A release gate reading that would block on evidence that does not exist.

`audited` now requires a non-empty `node_modules/@patchstack/connect/AGENT-INSTALL.md`. That file is in
the package's `files`, so its presence is direct evidence the tarball was fetched and unpacked; non-empty
because a truncated unpack leaves a file that exists and says nothing.

What this establishes is that the docs were PRESENT for the agent to read, not that it read them. That is
the strongest thing observable from outside the agent, and it is the right bar: a round where the docs
were on disk and the agent still refused IS evidence about them, while a round where they never arrived
is not.

The `installed` check requires both halves now, and its detail distinguishes the three states — absent,
declared but never unpacked, or unpacked with the doc's byte count — so a reviewer can see which
happened rather than inferring it.

`stub-declares-only.mjs` is the third stub, modelling this exact state: it writes the dependency and
stops. Real agents reach it — several recorded refusal modes are about staging an edit for the user
rather than executing a command. Verified against all three stubs: compliant `8/8` conclusive exit 0,
refusing void exit 2, declares-only void exit 2. Both fixes mutation-checked.

Also fixed: retries reused `round-<n>/`, so a later attempt overwrote the void one's report, requests and
scorecard — destroying exactly the record needed to tell a prompt refusal from a doc regression. Each
attempt now keeps `round-<n>-attempt-<m>/`; confirmed 3 directories for 3 attempts, against 1 before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

v0.3.30

Toggle v0.3.30's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[docs] Align the tested install prompt with what setup actually does (#…

…167)

The prompt the field-test harness validates differed from the one README
ships, on two points that are not stylistic.

It said "dev dependency". README says the opposite and explains why:
protection imports @patchstack/connect/protect at runtime, so a host that
prunes dev dependencies breaks. Real Lovable projects carry the package in
dependencies, so the tested prompt was teaching agents to produce an install
that fails on exactly the platforms this package targets.

It also authorized only "widget and production build hooks", while setup runs
protect, which edits source. Effects exceeding what the prompt named is the
documented refusal trigger, so the harness was under-testing the failure mode
it exists to catch.

Both now match README's wording. The hosted-workspace clauses stay out: the
sandbox label and preview restart are specific to that flow.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

v0.3.29

Toggle v0.3.29's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[feat] ENG-3684 - Carry the production marker to server-rendered buil…

…ds (#165)

* [feat] Carry the production marker to server-rendered builds

The connector has two injection paths and only one of them reaches a
server-rendered deploy. The widget tag is written into source, so it ships
with the app; `__PATCHSTACK_PROD__` is stamped into built HTML, and a
server-rendered root never produces a static HTML file to stamp. On those
stacks the marker never arrives, so the widget reads the live site as build
mode and shows the claim flow to visitors.

`mark-build` made this invisible: finding a build directory with no HTML in
it printed "Nothing to mark" and exited 0, which is indistinguishable from
success.

- `guide` now asks for the marker whenever the root shell is code rather
  than HTML, printing the JSX snippet for React-family roots and describing
  the requirement for the rest, and counts it as an outstanding step.
- The snippet is gated on the framework's own production expression. An
  ungated marker would also fire in the hosted builder's preview, which is
  where the owner still needs the claim flow.
- The marker stays an inline document script. The widget tag is `defer`, so
  only a parser-executed script is ordered ahead of its init for certain.
- `mark-build` now names the server-rendered case and points at `guide`
  instead of reporting nothing to do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [feat] Add the production marker during scan instead of asking for it

The marker only reaches production if it is in the source shell before the
build runs, and asking the developer to paste it there makes the one step
that must never be forgotten the one step nobody automates.

`scan` is already a pre-build hook, and it already edits the source shell to
place the widget tag on plain HTML projects. It now does the same for the
marker on JSX roots: insert it above the widget tag so document order puts it
first, fall back to `<head>` then `<body>`, and wrap it in the same managed
`#region` fence the protect installer uses so a re-run refreshes the block
rather than stacking copies. A marker the developer placed by hand is adopted,
not duplicated.

That makes the flag arrive on its own from `bun run build` or `npm run build`,
the same way the dependency scan and the widget tag already do.

`guide` now points at `scan` for this and keeps the snippet as the fallback
for roots where no anchor was found.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* [fix] Write the production marker before the manifest post

Build scripts commonly chain the scan as `patchstack-connect scan || true`, so
a failed or offline post is expected to be survivable. Running the marker pass
after `postManifest` made it the opposite: any network error skipped the write
and the build shipped without a production flag, silently.

The marker carries no site UUID and needs no round-trip, so nothing about it
justified waiting on the server. It now runs as soon as the framework is known
and before the post, still after the --dry-run return so a preview stays
side-effect free.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

v0.3.28

Toggle v0.3.28's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[feat] Add a command reference table to AGENT-INSTALL.md (#162)

Add a scannable per-command reference near the top of AGENT-INSTALL.md
covering all 12 CLI commands, each with: what it does, whether it reads
source, what it writes, and what it sends over the network. Summarizes
the existing prose (left intact below) so an agent gets full per-command
context at a glance.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

v0.3.27

Toggle v0.3.27's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[ENG-3568] Stop `login` hanging when an assistant runs it (#155)

* [ENG-3568] Stop `login` hanging when an assistant runs it

Reported from Lovable: the command appears to hang. It does, from the
caller's side. An assistant runs a command, waits for it to exit, and only
then reads stdout — so a command that prints a link at second zero and then
blocks for ten minutes shows nothing until the code has already expired.

Splits the flow. `startLogin` returns as soon as the link exists;
`waitForApproval` polls. The CLI picks by whether anyone is watching:

- interactive terminal — unchanged: print the link, then wait
- output captured — print the link and exit, then `login --wait` resumes

The pending request is handed between the two invocations through a file in
the temp directory, keyed by site. Not the project directory: the device
code is a secret with a ten-minute life, and nothing that short-lived
belongs somewhere it could be committed. It is never printed either, so it
cannot leak through an assistant's captured output.

Verified against production: piped, the command now exits in about a second
instead of blocking.

AGENT-INSTALL states the two-step sequence and the three ways an assistant
would otherwise get it wrong — wrapping step one in a timeout, re-running it
to retry (which invalidates the link the user is looking at), and running
--wait before the user has approved.

Co-Authored-By: Claude <noreply@anthropic.com>

* [ENG-3568] Make re-running `login` resume instead of restarting

Splitting start from wait left a gap: nothing writes the credential unless
someone comes back and runs the second command. An assistant that forgets
leaves the user having approved for nothing.

Re-running `login` now resumes the pending request rather than issuing a new
one. If the owner has approved, it redeems and finishes; if not, it reports
the code and the time remaining and exits. So the second step is the same
command as the first, which is one less thing for an assistant to get right,
and re-running can no longer invalidate the link the user is looking at —
which the previous docs had to warn against.

An abandoned flow stays harmless. Approving only marks the request server
side; the rotation happens on redeem. Nothing changes until the CLI comes
back, so a forgotten flow expires quietly and the site keeps working.

Verified against production: a second run reports the same code with the
countdown decremented, rather than starting a fresh one.

--wait remains as the blocking variant for anyone who wants it.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

v0.3.26

Toggle v0.3.26's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[ENG-3568] Use one credential for Pulse ingest and block-logs (#154)

apiKey and pulseAuth have always held the same value: there is one
oauth_clients row per site, so there is one secret. Two fields isolated
nothing — a bad credential broke both paths regardless — while obliging
every write site to keep them in step.

That obligation already caused a bug. `login` rotated the credential and
wrote only pulseAuth, leaving apiKey holding a value the server had just
invalidated, which would have stopped block-log delivery silently.

Stops writing pulseAuth. Nothing else changes: Pulse resolution already
falls back to apiKey in both the CLI (config.ts) and the guard
(runtime.js), so a config carrying only apiKey authenticates both paths.

The one hazard is a stale copy. pulseAuth resolves ahead of apiKey, so a
config written by the previous version would keep authenticating Pulse with
the replaced value after a rotation. persistApiKey now drops the field when
it writes, which is correct in every case: a new apiKey invalidates any
older pulseAuth by definition.

Reading is unchanged. An existing pulseAuth is still honoured and
PATCHSTACK_PULSE_AUTH still overrides, for deployments that do want Pulse
ingest on a different credential from block-logs. persistPulseAuth stays
exported for them.

Four tests pin the compatibility surface: apiKey-only resolves for Pulse, an
existing pulseAuth still wins, a stale one is dropped on rotation, and the
env override still applies.

Co-authored-by: Claude <noreply@anthropic.com>

v0.3.25

Toggle v0.3.25's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[ENG-3568] Point `login` at the approval page, not the API root (#150)

`login` printed {apiOrigin}/activate. That origin is the API, not the
dashboard, and /activate already serves the team-seat "set a password" page —
so following the link landed on an unrelated screen with nothing to approve.

Points at monitor/pulse/device instead, which redirects to the dashboard SPA,
and carries the code in the query so following the link is a single
confirmation rather than retyping it.

Co-authored-by: Claude <noreply@anthropic.com>

v0.3.24

Toggle v0.3.24's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[ENG-3568] Pulse ingest authentication — 2/3 connect bearer token (#149)

* [ENG-3568] Authenticate Pulse ingest with a bearer token

Implements the connect half of ADR-0018. Build-time Pulse calls now carry a
bearer token when a credential is available, and behave exactly as before
when one is not.

- `pulseAuth` is a new field in .patchstackrc.json and PATCHSTACK_PULSE_AUTH,
  kept separate from `apiKey` so the block-log path is untouched. It falls
  back to `apiKey`, so sites provisioned before this change authenticate
  without re-provisioning.
- src/pulse-token.ts exchanges that credential at monitor/pulse/token and
  caches the token on expires_in with skew and single-flight. Deliberately
  separate from the token flow in protect/firewall-log.js, which keeps
  talking to the auth/ Lambda unchanged.
- manifest, input-map and package-removed send Authorization when a token is
  available. Failure to obtain one yields no header rather than an error, so
  the request falls through to the server's legacy UUID path.

The guard's runtime rules fetch is not included: it runs in edge runtimes
without node:fs and reads env directly, so it needs its own change on the
protection path.

Full suite passes: 1172 tests.

Co-Authored-By: Claude <noreply@anthropic.com>

* [ENG-3568] Add `connect login` to recover a lost credential

Losing .patchstackrc.json currently means a site can never authenticate
again, since the credential is issued once and never re-revealed. This adds
the RFC 8628 device flow that recovers it.

    $ npx @patchstack/connect login

      Your code:  WDJB-MJHT
      Approve at: https://app.patchstack.com/activate

The device code stays in the process; the short user code is what the human
carries to the browser, and it is useless without the device code. Approval
happens in the dashboard and requires an existing owner, so the CLI proves
nothing on its own — starting a flow is deliberately unauthenticated.

Approving rotates the credential, so recovery and rotation are one operation
and a leaked old credential stops working.

Refuses to run in CI: it is interactive, and build logs are exactly the
disclosure channel this work exists to close. Deploys use
PATCHSTACK_PULSE_AUTH from the platform's secret store.

Named `login` rather than `refresh` because the bearer token already
refreshes itself unattended; this is for the durable credential.

Full suite passes: 1178 tests.

Co-Authored-By: Claude <noreply@anthropic.com>

* [ENG-3568] Document pulseAuth in the install guidance

The install docs described apiKey as block-log-only and never mentioned
pulseAuth, which scan now also writes. An agent following them would have
had no idea the second field exists or what it is for.

- states plainly that scan writes both credentials and there is no manual
  step, so an agent never invents or asks for these values
- separates what each credential authenticates, and that pulseAuth falls
  back to apiKey so older projects keep working
- adds PATCHSTACK_PULSE_AUTH to the CI guidance, and notes that login is
  interactive and refuses to run there
- points at `connect login` for a lost credential rather than deleting the
  file and re-provisioning, which would create a second site

Behaviour unchanged; this is documentation only. Where the durable
credential should ultimately live is still the open decision in ADR-0018,
so the existing commit guidance is left as it stands.

Co-Authored-By: Claude <noreply@anthropic.com>

* [ENG-3568] Keep block-logs working after `login` rotates the credential

Approving a login rotates oauth_clients.secret, and block-log reporting
authenticates with that same secret. login persisted only pulseAuth, so
apiKey was left holding a value the server had just invalidated — block-log
delivery would have stopped silently the first time anyone recovered a
credential.

Persists both fields, and asserts it.

Co-Authored-By: Claude <noreply@anthropic.com>

* [ENG-3568] Retry once when the server rejects a cached token

A cached token can stop being valid before it expires — the credential may
have been rotated or revoked meanwhile — so the server's 401 is authoritative
over our local clock. Without this a long-running process kept presenting a
dead token until its own expiry, and rotating a credential could not reach an
already-running guard.

Adds pulseFetch, which attaches the bearer, and on 401 drops the cached token,
re-exchanges and retries the request once. The four Pulse calls in client.ts
now go through it instead of setting the header themselves, so the behaviour
is in one place rather than four.

Only 401 retries: a 403 is a scope or site mismatch that a fresh token would
not fix. A request that was unauthenticated to begin with is not retried
either — its 401 was about something other than the token.

Full suite passes: 1183 tests.

Co-Authored-By: Claude <noreply@anthropic.com>

* [ENG-3568] Authenticate the guard's rules lookup

The last Pulse call still going out on the UUID alone. With this, every
endpoint in ADR-0018 can be enforced; pulse/rules could not be before.

resolvePulseAuth mirrors the existing resolveApiKey — options, then
PATCHSTACK_PULSE_AUTH, then .patchstackrc.json — including its lazy node:fs
import, so runtimes without a filesystem degrade instead of failing to load.
It falls back to apiKey, so guards installed before pulseAuth existed keep
authenticating. Resolved once at boot and threaded through ctx rather than
re-read on every refresh.

Protection never hinges on getting a token: no credential, a rejected
exchange or an offline token endpoint all yield no header, and the request
goes out as it does today. Tested for all three.

Also widens the edge-safe invariant. It scanned src/protect/ only, and
pulse-client now imports src/pulse-token.ts from outside that directory, so a
Node builtin added there would have gone unnoticed. The check now follows
that import.

Full suite passes: 1213 tests.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

v0.3.23

Toggle v0.3.23's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Report enforced blocks via existing connector /api/logs/log. (#102)

Exchange the provisioned WP-format api_key for a JWT and post firewall logs so Threats blocked fills without a new Pulse ingest endpoint.

Co-authored-by: Cursor <cursoragent@cursor.com>

v0.3.22

Toggle v0.3.22's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
[NO-Ticket] paywall ability widget (#101)

Parse and propagate Pulse's enforcement (or mode) field and let PATCHSTACK_MODE override it. Changes: pulse-client now extracts enforcement; types and normalizeBundle include enforcement; runtime adds resolveMode(), exposes dynamic protection.mode, and updates mode on rule refresh. Tests added to cover parsing, env override, and hot-swap behavior. Also added .agent-config.json and minor test import cleanup.